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

This commit is contained in:
milad 2019-05-24 13:55:54 -07:00
commit 83171a7cde
78 changed files with 11105 additions and 39 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

View file

@ -0,0 +1,9 @@
{
"compressed": {
"COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT": "Default-Sky-9-cubemap-ambient_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx",
"COMPRESSED_SRGB8_ETC2": "Default-Sky-9-cubemap-ambient_COMPRESSED_SRGB8_ETC2.ktx"
},
"original": "Default-Sky-9-cubemap-ambient.jpg",
"uncompressed": "Default-Sky-9-cubemap-ambient.ktx",
"version": 1
}

View file

@ -4,5 +4,6 @@
"COMPRESSED_SRGB8_ETC2": "Default-Sky-9-cubemap_COMPRESSED_SRGB8_ETC2.ktx"
},
"original": "Default-Sky-9-cubemap.jpg",
"uncompressed": "Default-Sky-9-cubemap.ktx"
"uncompressed": "Default-Sky-9-cubemap.ktx",
"version": 1
}

View file

@ -76,7 +76,7 @@ Rectangle {
if (result.status !== "success") {
errorText.text = "There was a problem while retrieving your inventory. " +
"Please try closing and re-opening the Avatar app.\n\nInventory status: " + result.status + "\nMessage: " + result.message;
} else if (result.data && result.data.assets && result.data.assets.length === 0) {
} else if (result.data && result.data.assets && result.data.assets.length === 0 && avatarAppInventoryModel.count === 0) {
errorText.text = "You have not created any avatars yet! Create an avatar with the Avatar Creator, then close and re-open the Avatar App."
}

View file

@ -69,8 +69,8 @@ Flickable {
SimplifiedControls.Slider {
id: peopleVolume
Layout.preferredWidth: parent.width
Layout.preferredHeight: 30
Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin
height: 30
labelText: "People Volume"
from: simplifiedUI.numericConstants.mutedValue
to: 20.0
@ -96,8 +96,8 @@ Flickable {
SimplifiedControls.Slider {
id: environmentVolume
Layout.preferredWidth: parent.width
Layout.preferredHeight: 30
Layout.topMargin: 2
height: 30
labelText: "Environment Volume"
from: simplifiedUI.numericConstants.mutedValue
to: 20.0
@ -124,8 +124,8 @@ Flickable {
SimplifiedControls.Slider {
id: systemSoundVolume
Layout.preferredWidth: parent.width
Layout.preferredHeight: 30
Layout.topMargin: 2
height: 30
labelText: "System Sound Volume"
from: simplifiedUI.numericConstants.mutedValue
to: 20.0
@ -169,8 +169,8 @@ Flickable {
SimplifiedControls.Switch {
id: muteMicrophoneSwitch
width: parent.width
height: 18
Layout.preferredHeight: 18
Layout.preferredWidth: parent.width
labelTextOn: "Mute Microphone"
checked: AudioScriptingInterface.mutedDesktop
onClicked: {
@ -180,8 +180,8 @@ Flickable {
SimplifiedControls.Switch {
id: pushToTalkSwitch
width: parent.width
height: 18
Layout.preferredHeight: 18
Layout.preferredWidth: parent.width
labelTextOn: "Push to Talk - Press and Hold \"T\" to Talk"
checked: AudioScriptingInterface.pushToTalkDesktop
onClicked: {
@ -210,9 +210,9 @@ Flickable {
ListView {
id: inputDeviceListView
Layout.preferredWidth: parent.width
Layout.preferredHeight: contentItem.height
Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin
interactive: false
height: contentItem.height
spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons
clip: true
model: AudioScriptingInterface.devices.input
@ -305,9 +305,9 @@ Flickable {
ListView {
id: outputDeviceListView
Layout.preferredWidth: parent.width
Layout.preferredHeight: contentItem.height
Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin
interactive: false
height: contentItem.height
spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons
clip: true
model: AudioScriptingInterface.devices.output

View file

@ -199,9 +199,9 @@ Flickable {
ListView {
id: inputDeviceListView
Layout.preferredWidth: parent.width
Layout.preferredHeight: contentItem.height
Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin
interactive: false
height: contentItem.height
spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons
clip: true
model: AudioScriptingInterface.devices.input
@ -294,9 +294,9 @@ Flickable {
ListView {
id: outputDeviceListView
Layout.preferredWidth: parent.width
Layout.preferredHeight: contentItem.height
Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin
interactive: false
height: contentItem.height
spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons
clip: true
model: AudioScriptingInterface.devices.output

View file

@ -3800,10 +3800,14 @@ void Application::handleSandboxStatus(QNetworkReply* reply) {
// If this is a first run we short-circuit the address passed in
if (_firstRun.get()) {
DependencyManager::get<AddressManager>()->goToEntry();
sentTo = SENT_TO_ENTRY;
_firstRun.set(false);
if (!_overrideEntry) {
DependencyManager::get<AddressManager>()->goToEntry();
sentTo = SENT_TO_ENTRY;
} else {
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
sentTo = SENT_TO_PREVIOUS_LOCATION;
}
_firstRun.set(false);
} else {
QString goingTo = "";
if (addressLookupString.isEmpty()) {
@ -3819,7 +3823,7 @@ void Application::handleSandboxStatus(QNetworkReply* reply) {
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
sentTo = SENT_TO_PREVIOUS_LOCATION;
}
UserActivityLogger::getInstance().logAction("startup_sent_to", {
{ "sent_to", sentTo },
{ "sandbox_is_running", sandboxIsRunning },
@ -9354,6 +9358,19 @@ void Application::showUrlHandler(const QUrl& url) {
}
});
}
void Application::overrideEntry(){
_overrideEntry = true;
}
void Application::forceDisplayName(const QString& displayName) {
getMyAvatar()->setDisplayName(displayName);
}
void Application::forceLoginWithTokens(const QString& tokens) {
DependencyManager::get<AccountManager>()->setAccessTokens(tokens);
Setting::Handle<bool>(KEEP_ME_LOGGED_IN_SETTING_NAME, true).set(true);
}
void Application::setConfigFileURL(const QString& fileUrl) {
DependencyManager::get<AccountManager>()->setConfigFileURL(fileUrl);
}
#if defined(Q_OS_ANDROID)
void Application::beforeEnterBackground() {

View file

@ -356,6 +356,11 @@ public:
void openDirectory(const QString& path);
void overrideEntry();
void forceDisplayName(const QString& displayName);
void forceLoginWithTokens(const QString& tokens);
void setConfigFileURL(const QString& fileUrl);
signals:
void svoImportRequested(const QString& url);
@ -828,5 +833,6 @@ private:
bool _resumeAfterLoginDialogActionTaken_WasPostponed { false };
bool _resumeAfterLoginDialogActionTaken_SafeToRun { false };
bool _startUpFinished { false };
bool _overrideEntry { false };
};
#endif // hifi_Application_h

View file

@ -83,6 +83,8 @@ int main(int argc, const char* argv[]) {
QCommandLineOption allowMultipleInstancesOption("allowMultipleInstances", "Allow multiple instances to run");
QCommandLineOption overrideAppLocalDataPathOption("cache", "set test cache <dir>", "dir");
QCommandLineOption overrideScriptsPathOption(SCRIPTS_SWITCH, "set scripts <path>", "path");
QCommandLineOption responseTokensOption("tokens", "set response tokens <json>", "json");
QCommandLineOption displayNameOption("displayName", "set user display name <string>", "string");
parser.addOption(urlOption);
parser.addOption(noLauncherOption);
@ -93,6 +95,8 @@ int main(int argc, const char* argv[]) {
parser.addOption(overrideAppLocalDataPathOption);
parser.addOption(overrideScriptsPathOption);
parser.addOption(allowMultipleInstancesOption);
parser.addOption(responseTokensOption);
parser.addOption(displayNameOption);
if (!parser.parse(arguments)) {
std::cout << parser.errorText().toStdString() << std::endl; // Avoid Qt log spam
@ -120,8 +124,10 @@ int main(int argc, const char* argv[]) {
static const QString APPLICATION_CONFIG_FILENAME = "config.json";
QDir applicationDir(applicationPath);
QFile configFile(applicationDir.filePath(APPLICATION_CONFIG_FILENAME));
QString configFileName = applicationDir.filePath(APPLICATION_CONFIG_FILENAME);
QFile configFile(configFileName);
QString launcherPath;
if (configFile.exists()) {
if (!configFile.open(QIODevice::ReadOnly)) {
qWarning() << "Found application config, but could not open it";
@ -134,7 +140,7 @@ int main(int argc, const char* argv[]) {
qWarning() << "Found application config, but could not parse it: " << error.errorString();
} else {
static const QString LAUNCHER_PATH_KEY = "launcherPath";
QString launcherPath = doc.object()[LAUNCHER_PATH_KEY].toString();
launcherPath = doc.object()[LAUNCHER_PATH_KEY].toString();
if (!launcherPath.isEmpty()) {
if (!parser.isSet(noLauncherOption)) {
qDebug() << "Found a launcherPath in application config. Starting launcher.";
@ -146,6 +152,7 @@ int main(int argc, const char* argv[]) {
qDebug() << "Found a launcherPath in application config, but the launcher"
" has been suppressed. Continuing normal execution.";
}
configFile.close();
}
}
}
@ -398,6 +405,24 @@ int main(int argc, const char* argv[]) {
printSystemInformation();
auto appPointer = dynamic_cast<Application*>(&app);
if (appPointer) {
if (parser.isSet(urlOption)) {
appPointer->overrideEntry();
}
if (parser.isSet(displayNameOption)) {
QString displayName = QString(parser.value(displayNameOption));
appPointer->forceDisplayName(displayName);
}
if (!launcherPath.isEmpty()) {
appPointer->setConfigFileURL(configFileName);
}
if (parser.isSet(responseTokensOption)) {
QString tokens = QString(parser.value(responseTokensOption));
appPointer->forceLoginWithTokens(tokens);
}
}
QTranslator translator;
translator.load("i18n/interface_en");
app.installTranslator(&translator);

View file

@ -220,6 +220,10 @@ void GL45Texture::generateMips() const {
(void)CHECK_GL_ERROR();
}
// (NOTE: it seems to work now, but for posterity:) DSA ARB does not work on AMD, so use EXT
// unless EXT is not available on the driver
#define AMD_CUBE_MAP_EXT_WORKAROUND 0
Size GL45Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const {
Size amountCopied = sourceSize;
if (GL_TEXTURE_2D == _target) {
@ -267,22 +271,26 @@ Size GL45Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const
case GL_COMPRESSED_SIGNED_R11_EAC:
case GL_COMPRESSED_RG11_EAC:
case GL_COMPRESSED_SIGNED_RG11_EAC:
#if AMD_CUBE_MAP_EXT_WORKAROUND
if (glCompressedTextureSubImage2DEXT) {
auto target = GLTexture::CUBE_FACE_LAYOUT[face];
glCompressedTextureSubImage2DEXT(_id, target, mip, 0, yOffset, size.x, size.y, internalFormat,
static_cast<GLsizei>(sourceSize), sourcePointer);
} else {
} else
#endif
{
glCompressedTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, internalFormat,
static_cast<GLsizei>(sourceSize), sourcePointer);
}
break;
default:
// DSA ARB does not work on AMD, so use EXT
// unless EXT is not available on the driver
#if AMD_CUBE_MAP_EXT_WORKAROUND
if (glTextureSubImage2DEXT) {
auto target = GLTexture::CUBE_FACE_LAYOUT[face];
glTextureSubImage2DEXT(_id, target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer);
} else {
} else
#endif
{
glTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, format, type, sourcePointer);
}
break;

View file

@ -26,8 +26,8 @@ const Element Element::COLOR_COMPRESSED_BCX_SRGB { TILE4x4, COMPRESSED, COMPRESS
const Element Element::COLOR_COMPRESSED_BCX_SRGBA_MASK { TILE4x4, COMPRESSED, COMPRESSED_BC1_SRGBA };
const Element Element::COLOR_COMPRESSED_BCX_SRGBA { TILE4x4, COMPRESSED, COMPRESSED_BC3_SRGBA };
const Element Element::COLOR_COMPRESSED_BCX_XY { TILE4x4, COMPRESSED, COMPRESSED_BC5_XY };
const Element Element::COLOR_COMPRESSED_BCX_SRGBA_HIGH { TILE4x4, COMPRESSED, COMPRESSED_BC7_SRGBA };
const Element Element::COLOR_COMPRESSED_BCX_HDR_RGB { TILE4x4, COMPRESSED, COMPRESSED_BC6_RGB };
const Element Element::COLOR_COMPRESSED_BCX_SRGBA_HIGH { TILE4x4, COMPRESSED, COMPRESSED_BC7_SRGBA };
const Element Element::COLOR_COMPRESSED_ETC2_RGB { TILE4x4, COMPRESSED, COMPRESSED_ETC2_RGB };
const Element Element::COLOR_COMPRESSED_ETC2_SRGB { TILE4x4, COMPRESSED, COMPRESSED_ETC2_SRGB };

View file

@ -18,7 +18,6 @@
#include "Batch.h"
#include "TextureTable.h"
#include "FrameIOKeys.h"
namespace gpu {
@ -324,6 +323,13 @@ TexturePointer Deserializer::readTexture(const json& node, uint32_t external) {
readOptional(ktxFile, node, keys::ktxFile);
Element ktxTexelFormat, ktxMipFormat;
if (!ktxFile.empty()) {
// If we get a texture that starts with ":" we need to re-route it to the resources directory
if (ktxFile.at(0) == ':') {
QString frameReaderPath = __FILE__;
frameReaderPath.replace("\\", "/");
frameReaderPath.replace("libraries/gpu/src/gpu/framereader.cpp", "interface/resources", Qt::CaseInsensitive);
ktxFile.replace(0, 1, frameReaderPath.toStdString());
}
if (QFileInfo(ktxFile.c_str()).isRelative()) {
ktxFile = basedir + ktxFile;
}

View file

@ -26,13 +26,6 @@ layout(location=0) in vec3 _normal;
layout(location=0) out vec4 _fragColor;
void main(void) {
vec3 coord = normalize(_normal);
vec3 color = skybox.color.rgb;
// blend is only set if there is a cubemap
float check = float(skybox.color.a > 0.0);
color = mix(color, texture(cubeMap, coord).rgb, check);
color *= mix(vec3(1.0), skybox.color.rgb, check * float(skybox.color.a < 1.0));
_fragColor = vec4(color, 0.0);
vec3 skyboxColor = texture(cubeMap, normalize(_normal)).rgb;
_fragColor = vec4(mix(skybox.color.rgb, skyboxColor, skybox.color.a), 1.0);
}

View file

@ -690,6 +690,8 @@ void convertImageToLDRTexture(gpu::Texture* texture, Image&& image, BackendTarge
compressionOptions.setFormat(nvtt::Format_BC4);
} else if (mipFormat == gpu::Element::COLOR_COMPRESSED_BCX_XY) {
compressionOptions.setFormat(nvtt::Format_BC5);
} else if (mipFormat == gpu::Element::COLOR_COMPRESSED_BCX_HDR_RGB) {
compressionOptions.setFormat(nvtt::Format_BC6);
} else if (mipFormat == gpu::Element::COLOR_COMPRESSED_BCX_SRGBA_HIGH) {
alphaMode = nvtt::AlphaMode_Transparency;
compressionOptions.setFormat(nvtt::Format_BC7);

View file

@ -57,6 +57,7 @@ namespace TextureUsage {
* @typedef {number} TextureCache.TextureType
*/
enum Type {
// NOTE: add new texture types at the bottom here
DEFAULT_TEXTURE,
STRICT_TEXTURE,
ALBEDO_TEXTURE,

View file

@ -16,6 +16,7 @@
#include <QJsonObject>
const QString TEXTURE_META_EXTENSION = ".texmeta.json";
const uint16_t KTX_VERSION = 1;
bool TextureMeta::deserialize(const QByteArray& data, TextureMeta* meta) {
QJsonParseError error;
@ -46,6 +47,9 @@ bool TextureMeta::deserialize(const QByteArray& data, TextureMeta* meta) {
}
}
}
if (root.contains("version")) {
meta->version = root["version"].toInt();
}
return true;
}
@ -62,6 +66,7 @@ QByteArray TextureMeta::serialize() {
root["original"] = original.toString();
root["uncompressed"] = uncompressed.toString();
root["compressed"] = compressed;
root["version"] = KTX_VERSION;
doc.setObject(root);
return doc.toJson();

View file

@ -19,6 +19,7 @@
#include "khronos/KHR.h"
extern const QString TEXTURE_META_EXTENSION;
extern const uint16_t KTX_VERSION;
namespace std {
template<> struct hash<khronos::gl::texture::InternalFormat> {
@ -37,6 +38,7 @@ struct TextureMeta {
QUrl original;
QUrl uncompressed;
std::unordered_map<khronos::gl::texture::InternalFormat, QUrl> availableTextureTypes;
uint16_t version { 0 };
};

View file

@ -97,6 +97,7 @@ void AccountManager::logout() {
// remove this account from the account settings file
removeAccountFromFile();
saveLoginStatus(false);
emit logoutComplete();
// the username has changed to blank
@ -650,6 +651,39 @@ void AccountManager::refreshAccessToken() {
}
}
void AccountManager::setAccessTokens(const QString& response) {
QJsonDocument jsonResponse = QJsonDocument::fromJson(response.toUtf8());
const QJsonObject& rootObject = jsonResponse.object();
if (!rootObject.contains("error")) {
// construct an OAuthAccessToken from the json object
if (!rootObject.contains("access_token") || !rootObject.contains("expires_in")
|| !rootObject.contains("token_type")) {
// TODO: error handling - malformed token response
qCDebug(networking) << "Received a response for password grant that is missing one or more expected values.";
} else {
// clear the path from the response URL so we have the right root URL for this access token
QUrl rootURL = rootObject.contains("url") ? rootObject["url"].toString() : _authURL;
rootURL.setPath("");
qCDebug(networking) << "Storing an account with access-token for" << qPrintable(rootURL.toString());
_accountInfo = DataServerAccountInfo();
_accountInfo.setAccessTokenFromJSON(rootObject);
emit loginComplete(rootURL);
persistAccountToFile();
saveLoginStatus(true);
requestProfile();
}
} else {
// TODO: error handling
qCDebug(networking) << "Error in response for password grant -" << rootObject["error_description"].toString();
emit loginFailed();
}
}
void AccountManager::requestAccessTokenFinished() {
QNetworkReply* requestReply = reinterpret_cast<QNetworkReply*>(sender());
@ -895,3 +929,34 @@ void AccountManager::handleKeypairGenerationError() {
void AccountManager::setLimitedCommerce(bool isLimited) {
_limitedCommerce = isLimited;
}
void AccountManager::saveLoginStatus(bool isLoggedIn) {
if (!_configFileURL.isEmpty()) {
QFile configFile(_configFileURL);
configFile.open(QIODevice::ReadOnly | QIODevice::Text);
QJsonParseError error;
QJsonDocument jsonDocument = QJsonDocument::fromJson(configFile.readAll(), &error);
configFile.close();
QString launcherPath;
if (error.error == QJsonParseError::NoError) {
QJsonObject rootObject = jsonDocument.object();
if (rootObject.contains("launcherPath")) {
launcherPath = rootObject["launcherPath"].toString();
}
if (rootObject.contains("loggedIn")) {
rootObject["loggedIn"] = isLoggedIn;
}
jsonDocument = QJsonDocument(rootObject);
}
configFile.open(QFile::WriteOnly | QFile::Text | QFile::Truncate);
configFile.write(jsonDocument.toJson());
configFile.close();
if (!isLoggedIn && !launcherPath.isEmpty()) {
QProcess launcher;
launcher.setProgram(launcherPath);
launcher.startDetached();
qApp->quit();
}
}
}

View file

@ -102,6 +102,10 @@ public:
bool getLimitedCommerce() { return _limitedCommerce; }
void setLimitedCommerce(bool isLimited);
void setAccessTokens(const QString& response);
void setConfigFileURL(const QString& fileURL) { _configFileURL = fileURL; }
void saveLoginStatus(bool isLoggedIn);
public slots:
void requestAccessToken(const QString& login, const QString& password);
void requestAccessTokenWithSteam(QByteArray authSessionTicket);
@ -162,6 +166,7 @@ private:
QUuid _sessionID { QUuid::createUuid() };
bool _limitedCommerce { false };
QString _configFileURL;
};
#endif // hifi_AccountManager_h

View file

@ -42,5 +42,5 @@ void main(void) {
color = max(color, vec3(0));
// Procedural Shaders are expected to be Gamma corrected so let's bring back the RGB in linear space for the rest of the pipeline
color = pow(color, vec3(2.2));
_fragColor = vec4(color, 0.0);
_fragColor = vec4(color, 1.0);
}

View file

@ -648,6 +648,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) {
if (!_defaultLight || !_defaultBackground) {
auto defaultSkyboxURL = PathUtils::resourcesUrl() + "images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.texmeta.json";
auto defaultAmbientURL = PathUtils::resourcesUrl() + "images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.texmeta.json";
if (!_defaultSkyboxNetworkTexture) {
PROFILE_RANGE(render, "Process Default Skybox");
@ -658,7 +659,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) {
if (!_defaultAmbientNetworkTexture) {
PROFILE_RANGE(render, "Process Default Ambient map");
_defaultAmbientNetworkTexture = DependencyManager::get<TextureCache>()->getTexture(
defaultSkyboxURL, image::TextureUsage::AMBIENT_TEXTURE);
defaultAmbientURL, image::TextureUsage::AMBIENT_TEXTURE);
}
if (_defaultSkyboxNetworkTexture && _defaultSkyboxNetworkTexture->isLoaded() && _defaultSkyboxNetworkTexture->getGPUTexture()) {

View file

@ -16,7 +16,7 @@ var DEFAULT_SCRIPTS_PATH_PREFIX = ScriptDiscoveryService.defaultScriptsPath + "/
var DEFAULT_SCRIPTS_SEPARATE = [
DEFAULT_SCRIPTS_PATH_PREFIX + "system/controllers/controllerScripts.js",
Script.resolvePath("simplifiedUI.js")
DEFAULT_SCRIPTS_PATH_PREFIX + "ui/simplifiedUI.js"
];
function loadSeparateDefaults() {
for (var i in DEFAULT_SCRIPTS_SEPARATE) {

View file

@ -0,0 +1,387 @@
"use strict";
/* global Tablet, Script */
//
// libraries/appUi.js
//
// Created by Howard Stearns on 3/20/18.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
function AppUi(properties) {
var request = Script.require('request').request;
/* Example development order:
1. var AppUi = Script.require('appUi');
2. Put appname-i.svg, appname-a.svg in graphicsDirectory (where non-default graphicsDirectory can be added in #3).
3. ui = new AppUi({buttonName: "APPNAME", home: "qml-or-html-path"});
(And if converting an existing app,
define var tablet = ui.tablet, button = ui.button; as needed.
remove button.clicked.[dis]connect and tablet.remove(button).)
4. Define onOpened and onClosed behavior in #3, if any.
(And if converting an existing app, remove screenChanged.[dis]connect.)
5. Define onMessage and sendMessage in #3, if any. onMessage is wired/unwired on open/close. If you
want a handler to be "always on", connect it yourself at script startup.
(And if converting an existing app, remove code that [un]wires that message handling such as
fromQml/sendToQml or webEventReceived/emitScriptEvent.)
6. (If converting an existing app, cleanup stuff that is no longer necessary, like references to button, tablet,
and use isOpen, open(), and close() as needed.)
7. lint!
*/
var that = this;
function defaultButton(name, suffix) {
var base = that[name] || (that.buttonPrefix + suffix);
that[name] = (base.indexOf('/') >= 0) ? base : (that.graphicsDirectory + base); // poor man's merge
}
// Defaults:
that.tabletName = "com.highfidelity.interface.tablet.system";
that.inject = "";
that.graphicsDirectory = "icons/tablet-icons/"; // Where to look for button svgs. See below.
that.additionalAppScreens = [];
that.checkIsOpen = function checkIsOpen(type, tabletUrl) { // Are we active? Value used to set isOpen.
// Actual url may have prefix or suffix.
return that.currentVisibleUrl &&
((that.home.indexOf(that.currentVisibleUrl) > -1) ||
(that.additionalAppScreens.indexOf(that.currentVisibleUrl) > -1));
};
that.setCurrentVisibleScreenMetadata = function setCurrentVisibleScreenMetadata(type, url) {
that.currentVisibleScreenType = type;
that.currentVisibleUrl = url;
};
that.open = function open(optionalUrl, optionalInject) { // How to open the app.
var url = optionalUrl || that.home;
var inject = optionalInject || that.inject;
if (that.isQMLUrl(url)) {
that.tablet.loadQMLSource(url);
} else {
that.tablet.gotoWebScreen(url, inject);
}
};
// Opens some app on top of the current app (on desktop, opens new window)
that.openNewAppOnTop = function openNewAppOnTop(url, optionalInject) {
var inject = optionalInject || "";
if (that.isQMLUrl(url)) {
that.tablet.loadQMLOnTop(url);
} else {
that.tablet.loadWebScreenOnTop(url, inject);
}
};
that.close = function close() { // How to close the app.
that.currentVisibleUrl = "";
// for toolbar-mode: go back to home screen, this will close the window.
that.tablet.gotoHomeScreen();
};
that.buttonActive = function buttonActive(isActive) { // How to make the button active (white).
that.button.editProperties({isActive: isActive});
};
that.isQMLUrl = function isQMLUrl(url) {
var type = /.qml$/.test(url) ? 'QML' : 'Web';
return type === 'QML';
};
that.isCurrentlyOnQMLScreen = function isCurrentlyOnQMLScreen() {
return that.currentVisibleScreenType === 'QML';
};
//
// START Notification Handling Defaults
//
that.messagesWaiting = function messagesWaiting(isWaiting) { // How to indicate a message light on button.
// Note that waitingButton doesn't have to exist unless someone explicitly calls this with isWaiting true.
that.button.editProperties({
icon: isWaiting ? that.normalMessagesButton : that.normalButton,
activeIcon: isWaiting ? that.activeMessagesButton : that.activeButton
});
};
that.notificationPollTimeout = [false];
that.notificationPollTimeoutMs = [60000];
that.notificationPollEndpoint = [false];
that.notificationPollStopPaginatingConditionMet = [false];
that.notificationDataProcessPage = function (data) {
return data;
};
that.notificationPollCallback = [that.ignore];
that.notificationPollCaresAboutSince = [false];
that.notificationInitialCallbackMade = [false];
that.notificationDisplayBanner = function (message) {
if (!that.isOpen) {
Window.displayAnnouncement(message);
}
};
//
// END Notification Handling Defaults
//
// Handlers
that.onScreenChanged = function onScreenChanged(type, url) {
// Set isOpen, wireEventBridge, set buttonActive as appropriate,
// and finally call onOpened() or onClosed() IFF defined.
that.setCurrentVisibleScreenMetadata(type, url);
if (that.checkIsOpen(type, url)) {
that.wireEventBridge(true);
if (!that.isOpen) {
that.buttonActive(true);
if (that.onOpened) {
that.onOpened();
}
that.isOpen = true;
}
} else {
// A different screen is now visible, or the tablet has been closed.
// Tablet visibility is controlled separately by `tabletShownChanged()`
that.wireEventBridge(false);
if (that.isOpen) {
that.buttonActive(false);
if (that.onClosed) {
that.onClosed();
}
that.isOpen = false;
}
}
};
// Overwrite with the given properties:
Object.keys(properties).forEach(function (key) {
that[key] = properties[key];
});
//
// START Notification Handling
//
var currentDataPageToRetrieve = [];
var concatenatedServerResponse = [];
for (var i = 0; i < that.notificationPollEndpoint.length; i++) {
currentDataPageToRetrieve[i] = 1;
concatenatedServerResponse[i] = new Array();
}
var MAX_LOG_LENGTH_CHARACTERS = 300;
function requestCallback(error, response, optionalParams) {
var indexOfRequest = optionalParams.indexOfRequest;
var urlOfRequest = optionalParams.urlOfRequest;
if (error || (response.status !== 'success')) {
print("Error: unable to complete request from URL. Error:", error || response.status);
startNotificationTimer(indexOfRequest);
return;
}
if (!that.notificationPollStopPaginatingConditionMet[indexOfRequest] ||
that.notificationPollStopPaginatingConditionMet[indexOfRequest](response)) {
startNotificationTimer(indexOfRequest);
var notificationData;
if (concatenatedServerResponse[indexOfRequest].length) {
notificationData = concatenatedServerResponse[indexOfRequest];
} else {
notificationData = that.notificationDataProcessPage[indexOfRequest](response);
}
console.debug(that.buttonName,
'truncated notification data for processing:',
JSON.stringify(notificationData).substring(0, MAX_LOG_LENGTH_CHARACTERS));
that.notificationPollCallback[indexOfRequest](notificationData);
that.notificationInitialCallbackMade[indexOfRequest] = true;
currentDataPageToRetrieve[indexOfRequest] = 1;
concatenatedServerResponse[indexOfRequest] = new Array();
} else {
concatenatedServerResponse[indexOfRequest] =
concatenatedServerResponse[indexOfRequest].concat(that.notificationDataProcessPage[indexOfRequest](response));
currentDataPageToRetrieve[indexOfRequest]++;
request({
json: true,
uri: (urlOfRequest + "&page=" + currentDataPageToRetrieve[indexOfRequest])
}, requestCallback, optionalParams);
}
}
var METAVERSE_BASE = Account.metaverseServerURL;
var MS_IN_SEC = 1000;
that.notificationPoll = function (i) {
if (!that.notificationPollEndpoint[i]) {
return;
}
// User is "appearing offline" or is not logged in
if (GlobalServices.findableBy === "none" || Account.username === "Unknown user") {
// The notification polling will restart when the user changes their availability
// or when they log in, so it's not necessary to restart a timer here.
console.debug(that.buttonName + " Notifications: User is appearing offline or not logged in. " +
that.buttonName + " will poll for notifications when user logs in and has their availability " +
"set to not appear offline.");
return;
}
var url = METAVERSE_BASE + that.notificationPollEndpoint[i];
var settingsKey = "notifications/" + that.notificationPollEndpoint[i] + "/lastPoll";
var currentTimestamp = new Date().getTime();
var lastPollTimestamp = Settings.getValue(settingsKey, currentTimestamp);
if (that.notificationPollCaresAboutSince[i]) {
url = url + "&since=" + lastPollTimestamp / MS_IN_SEC;
}
Settings.setValue(settingsKey, currentTimestamp);
request({
json: true,
uri: url
},
requestCallback,
{
indexOfRequest: i,
urlOfRequest: url
});
};
// This won't do anything if there isn't a notification endpoint set
for (i = 0; i < that.notificationPollEndpoint.length; i++) {
that.notificationPoll(i);
}
function startNotificationTimer(indexOfRequest) {
that.notificationPollTimeout[indexOfRequest] = Script.setTimeout(function () {
that.notificationPoll(indexOfRequest);
}, that.notificationPollTimeoutMs[indexOfRequest]);
}
function restartNotificationPoll() {
for (var j = 0; j < that.notificationPollEndpoint.length; j++) {
that.notificationInitialCallbackMade[j] = false;
if (that.notificationPollTimeout[j]) {
Script.clearTimeout(that.notificationPollTimeout[j]);
that.notificationPollTimeout[j] = false;
}
that.notificationPoll(j);
}
}
//
// END Notification Handling
//
// Properties:
that.tablet = Tablet.getTablet(that.tabletName);
// Must be after we gather properties.
that.buttonPrefix = that.buttonPrefix || that.buttonName.toLowerCase() + "-";
defaultButton('normalButton', 'i.svg');
defaultButton('activeButton', 'a.svg');
defaultButton('normalMessagesButton', 'i-msg.svg');
defaultButton('activeMessagesButton', 'a-msg.svg');
var buttonOptions = {
icon: that.normalButton,
activeIcon: that.activeButton,
text: that.buttonName
};
// `TabletScriptingInterface` looks for the presence of a `sortOrder` key.
// What it SHOULD do is look to see if the value inside that key is defined.
// To get around the current code, we do this instead.
if (that.sortOrder) {
buttonOptions.sortOrder = that.sortOrder;
}
that.button = that.tablet.addButton(buttonOptions);
that.ignore = function ignore() { };
that.hasOutboundEventBridge = false;
that.hasInboundQmlEventBridge = false;
that.hasInboundHtmlEventBridge = false;
// HTML event bridge uses strings, not objects. Here we abstract over that.
// (Although injected javascript still has to use JSON.stringify/JSON.parse.)
that.sendToHtml = function (messageObject) {
that.tablet.emitScriptEvent(JSON.stringify(messageObject));
};
that.fromHtml = function (messageString) {
var parsedMessage = JSON.parse(messageString);
parsedMessage.messageSrc = "HTML";
that.onMessage(parsedMessage);
};
that.sendMessage = that.ignore;
that.wireEventBridge = function wireEventBridge(on) {
// Uniquivocally sets that.sendMessage(messageObject) to do the right thing.
// Sets has*EventBridge and wires onMessage to the proper event bridge as appropriate, IFF onMessage defined.
var isCurrentlyOnQMLScreen = that.isCurrentlyOnQMLScreen();
// Outbound (always, regardless of whether there is an inbound handler).
if (on) {
that.sendMessage = isCurrentlyOnQMLScreen ? that.tablet.sendToQml : that.sendToHtml;
that.hasOutboundEventBridge = true;
} else {
that.sendMessage = that.ignore;
that.hasOutboundEventBridge = false;
}
if (!that.onMessage) {
return;
}
// Inbound
if (on) {
if (isCurrentlyOnQMLScreen && !that.hasInboundQmlEventBridge) {
console.debug(that.buttonName, 'connecting', that.tablet.fromQml);
that.tablet.fromQml.connect(that.onMessage);
that.hasInboundQmlEventBridge = true;
} else if (!isCurrentlyOnQMLScreen && !that.hasInboundHtmlEventBridge) {
console.debug(that.buttonName, 'connecting', that.tablet.webEventReceived);
that.tablet.webEventReceived.connect(that.fromHtml);
that.hasInboundHtmlEventBridge = true;
}
} else {
if (that.hasInboundQmlEventBridge) {
console.debug(that.buttonName, 'disconnecting', that.tablet.fromQml);
that.tablet.fromQml.disconnect(that.onMessage);
that.hasInboundQmlEventBridge = false;
}
if (that.hasInboundHtmlEventBridge) {
console.debug(that.buttonName, 'disconnecting', that.tablet.webEventReceived);
that.tablet.webEventReceived.disconnect(that.fromHtml);
that.hasInboundHtmlEventBridge = false;
}
}
};
that.isOpen = false;
// To facilitate incremental development, only wire onClicked to do something when "home" is defined in properties.
that.onClicked = that.home
? function onClicked() {
// Call open() or close(), and reset type based on current home property.
if (that.isOpen) {
that.close();
} else {
that.open();
}
} : that.ignore;
that.onScriptEnding = function onScriptEnding() {
// Close if necessary, clean up any remaining handlers, and remove the button.
GlobalServices.myUsernameChanged.disconnect(restartNotificationPoll);
GlobalServices.findableByChanged.disconnect(restartNotificationPoll);
that.tablet.screenChanged.disconnect(that.onScreenChanged);
if (that.isOpen) {
that.close();
that.onScreenChanged("", "");
}
if (that.button) {
if (that.onClicked) {
that.button.clicked.disconnect(that.onClicked);
}
that.tablet.removeButton(that.button);
}
for (var i = 0; i < that.notificationPollTimeout.length; i++) {
if (that.notificationPollTimeout[i]) {
Script.clearInterval(that.notificationPollTimeout[i]);
that.notificationPollTimeout[i] = false;
}
}
};
// Set up the handlers.
that.tablet.screenChanged.connect(that.onScreenChanged);
that.button.clicked.connect(that.onClicked);
Script.scriptEnding.connect(that.onScriptEnding);
GlobalServices.findableByChanged.connect(restartNotificationPoll);
GlobalServices.myUsernameChanged.connect(restartNotificationPoll);
if (that.buttonName === Settings.getValue("startUpApp")) {
Settings.setValue("startUpApp", "");
Script.setTimeout(function () {
that.open();
}, 1000);
}
}
module.exports = AppUi;

View file

@ -0,0 +1,83 @@
"use strict";
// request.js
//
// Created by Cisco Fresquet on 04/24/2017.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* global module */
// @module request
//
// This module contains the `request` module implementation
// ===========================================================================================
module.exports = {
// ------------------------------------------------------------------
// cb(error, responseOfCorrectContentType, optionalCallbackParameter) of url. A subset of npm request.
request: function (options, callback, optionalCallbackParameter) {
var httpRequest = new XMLHttpRequest(), key;
// QT bug: apparently doesn't handle onload. Workaround using readyState.
httpRequest.onreadystatechange = function () {
var READY_STATE_DONE = 4;
var HTTP_OK = 200;
if (httpRequest.readyState >= READY_STATE_DONE) {
var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText,
response = !error && httpRequest.responseText,
contentType = !error && httpRequest.getResponseHeader('content-type');
if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc.
try {
response = JSON.parse(response);
} catch (e) {
error = e;
}
}
if (error) {
response = { statusCode: httpRequest.status };
}
callback(error, response, optionalCallbackParameter);
}
};
if (typeof options === 'string') {
options = { uri: options };
}
if (options.url) {
options.uri = options.url;
}
if (!options.method) {
options.method = 'GET';
}
if (options.body && (options.method === 'GET')) { // add query parameters
var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&';
for (key in options.body) {
if (options.body.hasOwnProperty(key)) {
params.push(key + '=' + options.body[key]);
}
}
options.uri += appender + params.join('&');
delete options.body;
}
if (options.json) {
options.headers = options.headers || {};
options.headers["Content-type"] = "application/json";
options.body = JSON.stringify(options.body);
}
for (key in options.headers || {}) {
if (options.headers.hasOwnProperty(key)) {
httpRequest.setRequestHeader(key, options.headers[key]);
}
}
httpRequest.open(options.method, options.uri, true);
httpRequest.send(options.body || null);
}
};
// ===========================================================================================
// @function - debug logging
function debug() {
print('RequestModule | ' + [].slice.call(arguments).join(' '));
}

View file

@ -0,0 +1,69 @@
// Example of using a "system module" to decouple Vec3's implementation details.
//
// Users would bring Vec3 support in as a module:
// var vec3 = Script.require('vec3');
//
// (this example is compatible with using as a Script.include and as a Script.require module)
try {
// Script.require
module.exports = vec3;
} catch(e) {
// Script.include
Script.registerValue("vec3", vec3);
}
vec3.fromObject = function(v) {
//return new vec3(v.x, v.y, v.z);
//... this is even faster and achieves the same effect
v.__proto__ = vec3.prototype;
return v;
};
vec3.prototype = {
multiply: function(v2) {
// later on could support overrides like so:
// if (v2 instanceof quat) { [...] }
// which of the below is faster (C++ or JS)?
// (dunno -- but could systematically find out and go with that version)
// pure JS option
// return new vec3(this.x * v2.x, this.y * v2.y, this.z * v2.z);
// hybrid C++ option
return vec3.fromObject(Vec3.multiply(this, v2));
},
// detects any NaN and Infinity values
isValid: function() {
return isFinite(this.x) && isFinite(this.y) && isFinite(this.z);
},
// format Vec3's, eg:
// var v = vec3();
// print(v); // outputs [Vec3 (0.000, 0.000, 0.000)]
toString: function() {
if (this === vec3.prototype) {
return "{Vec3 prototype}";
}
function fixed(n) { return n.toFixed(3); }
return "[Vec3 (" + [this.x, this.y, this.z].map(fixed) + ")]";
},
};
vec3.DEBUG = true;
function vec3(x, y, z) {
if (!(this instanceof vec3)) {
// if vec3 is called as a function then re-invoke as a constructor
// (so that `value instanceof vec3` holds true for created values)
return new vec3(x, y, z);
}
// unfold default arguments (vec3(), vec3(.5), vec3(0,1), etc.)
this.x = x !== undefined ? x : 0;
this.y = y !== undefined ? y : this.x;
this.z = z !== undefined ? z : this.y;
if (vec3.DEBUG && !this.isValid())
throw new Error('vec3() -- invalid initial values ['+[].slice.call(arguments)+']');
};

View file

@ -0,0 +1,387 @@
"use strict";
//
// away.js
//
// examples
//
// Created by Howard Stearns 11/3/15
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
// Goes into "paused" when the '.' key (and automatically when started in HMD), and normal when pressing any key.
// See MAIN CONTROL, below, for what "paused" actually does.
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
(function() { // BEGIN LOCAL_SCOPE
var BASIC_TIMER_INTERVAL = 50; // 50ms = 20hz
var OVERLAY_WIDTH = 1920;
var OVERLAY_HEIGHT = 1080;
var OVERLAY_DATA = {
width: OVERLAY_WIDTH,
height: OVERLAY_HEIGHT,
imageURL: Script.resolvePath("assets/images/Overlay-Viz-blank.png"),
emissive: true,
drawInFront: true,
alpha: 1
};
var AVATAR_MOVE_FOR_ACTIVE_DISTANCE = 0.8; // meters -- no longer away if avatar moves this far while away
var CAMERA_MATRIX = -7;
var OVERLAY_DATA_HMD = {
localPosition: {x: 0, y: 0, z: -1 * MyAvatar.sensorToWorldScale},
localRotation: {x: 0, y: 0, z: 0, w: 1},
width: OVERLAY_WIDTH,
height: OVERLAY_HEIGHT,
url: Script.resolvePath("assets/images/Overlay-Viz-blank.png"),
color: {red: 255, green: 255, blue: 255},
alpha: 1,
scale: 2 * MyAvatar.sensorToWorldScale,
emissive: true,
drawInFront: true,
parentID: MyAvatar.SELF_ID,
parentJointIndex: CAMERA_MATRIX,
ignorePickIntersection: true
};
var AWAY_INTRO = {
url: "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/kneel.fbx",
playbackRate: 30.0,
loopFlag: false,
startFrame: 0.0,
endFrame: 83.0
};
// MAIN CONTROL
var isEnabled = true;
var wasMuted; // unknonwn?
var isAway = false; // we start in the un-away state
var eventMappingName = "io.highfidelity.away"; // goActive on hand controller button events, too.
var eventMapping = Controller.newMapping(eventMappingName);
var avatarPosition = MyAvatar.position;
var wasHmdMounted = HMD.mounted;
var previousBubbleState = Users.getIgnoreRadiusEnabled();
var enterAwayStateWhenFocusLostInVR = HMD.enterAwayStateWhenFocusLostInVR;
// some intervals we may create/delete
var avatarMovedInterval;
// prefetch the kneel animation and hold a ref so it's always resident in memory when we need it.
var _animation = AnimationCache.prefetch(AWAY_INTRO.url);
function playAwayAnimation() {
MyAvatar.overrideAnimation(AWAY_INTRO.url,
AWAY_INTRO.playbackRate,
AWAY_INTRO.loopFlag,
AWAY_INTRO.startFrame,
AWAY_INTRO.endFrame);
}
function stopAwayAnimation() {
MyAvatar.restoreAnimation();
}
// OVERLAY
var overlay = Overlays.addOverlay("image", OVERLAY_DATA);
var overlayHMD = Overlays.addOverlay("image3d", OVERLAY_DATA_HMD);
function showOverlay() {
if (HMD.active) {
// make sure desktop version is hidden
Overlays.editOverlay(overlay, { visible: false });
Overlays.editOverlay(overlayHMD, { visible: true });
} else {
// make sure HMD is hidden
Overlays.editOverlay(overlayHMD, { visible: false });
// Update for current screen size, keeping overlay proportions constant.
var screen = Controller.getViewportDimensions();
// keep the overlay it's natural size and always center it...
Overlays.editOverlay(overlay, {
visible: true,
x: ((screen.x - OVERLAY_WIDTH) / 2),
y: ((screen.y - OVERLAY_HEIGHT) / 2)
});
}
}
function hideOverlay() {
Overlays.editOverlay(overlay, {visible: false});
Overlays.editOverlay(overlayHMD, {visible: false});
}
hideOverlay();
function maybeMoveOverlay() {
if (isAway) {
// if we switched from HMD to Desktop, make sure to hide our HUD overlay and show the
// desktop overlay
if (!HMD.active) {
showOverlay(); // this will also recenter appropriately
}
if (HMD.active) {
var sensorScaleFactor = MyAvatar.sensorToWorldScale;
var localPosition = {x: 0, y: 0, z: -1 * sensorScaleFactor};
Overlays.editOverlay(overlayHMD, { visible: true, localPosition: localPosition, scale: 2 * sensorScaleFactor });
// make sure desktop version is hidden
Overlays.editOverlay(overlay, { visible: false });
// also remember avatar position
avatarPosition = MyAvatar.position;
}
}
}
function ifAvatarMovedGoActive() {
var newAvatarPosition = MyAvatar.position;
if (Vec3.distance(newAvatarPosition, avatarPosition) > AVATAR_MOVE_FOR_ACTIVE_DISTANCE) {
goActive();
}
avatarPosition = newAvatarPosition;
}
function goAway(fromStartup) {
if (!isEnabled || isAway) {
return;
}
// If we're entering away mode from some other state than startup, then we create our move timer immediately.
// However if we're just stating up, we need to delay this process so that we don't think the initial teleport
// is actually a move.
if (fromStartup === undefined || fromStartup === false) {
avatarMovedInterval = Script.setInterval(ifAvatarMovedGoActive, BASIC_TIMER_INTERVAL);
} else {
var WAIT_FOR_MOVE_ON_STARTUP = 3000; // 3 seconds
Script.setTimeout(function() {
avatarMovedInterval = Script.setInterval(ifAvatarMovedGoActive, BASIC_TIMER_INTERVAL);
}, WAIT_FOR_MOVE_ON_STARTUP);
}
previousBubbleState = Users.getIgnoreRadiusEnabled();
if (!previousBubbleState) {
Users.toggleIgnoreRadius();
}
UserActivityLogger.privacyShieldToggled(Users.getIgnoreRadiusEnabled());
UserActivityLogger.toggledAway(true);
MyAvatar.isAway = true;
}
function goActive() {
if (!isAway) {
return;
}
UserActivityLogger.toggledAway(false);
MyAvatar.isAway = false;
if (Users.getIgnoreRadiusEnabled() !== previousBubbleState) {
Users.toggleIgnoreRadius();
UserActivityLogger.privacyShieldToggled(Users.getIgnoreRadiusEnabled());
}
if (!Window.hasFocus()) {
Window.setFocus();
}
}
MyAvatar.wentAway.connect(setAwayProperties);
MyAvatar.wentActive.connect(setActiveProperties);
function setAwayProperties() {
isAway = true;
wasMuted = Audio.muted;
if (!wasMuted) {
Audio.muted = !Audio.muted;
}
MyAvatar.setEnableMeshVisible(false); // just for our own display, without changing point of view
playAwayAnimation(); // animation is still seen by others
showOverlay();
HMD.requestShowHandControllers();
// tell the Reticle, we want to stop capturing the mouse until we come back
Reticle.allowMouseCapture = false;
// Allow users to find their way to other applications, our menus, etc.
// For desktop, that means we want the reticle visible.
// For HMD, the hmd preview will show the system mouse because of allowMouseCapture,
// but we want to turn off our Reticle so that we don't get two in preview and a stuck one in headset.
Reticle.visible = !HMD.active;
wasHmdMounted = HMD.mounted; // always remember the correct state
avatarPosition = MyAvatar.position;
}
function setActiveProperties() {
isAway = false;
if (Audio.muted && !wasMuted) {
Audio.muted = false;
}
MyAvatar.setEnableMeshVisible(true); // IWBNI we respected Developer->Avatar->Draw Mesh setting.
stopAwayAnimation();
HMD.requestHideHandControllers();
// update the UI sphere to be centered about the current HMD orientation.
HMD.centerUI();
// forget about any IK joint limits
MyAvatar.clearIKJointLimitHistory();
// update the avatar hips to point in the same direction as the HMD orientation.
MyAvatar.centerBody();
hideOverlay();
// tell the Reticle, we are ready to capture the mouse again and it should be visible
Reticle.allowMouseCapture = true;
Reticle.visible = true;
if (HMD.active) {
Reticle.position = HMD.getHUDLookAtPosition2D();
}
wasHmdMounted = HMD.mounted; // always remember the correct state
Script.clearInterval(avatarMovedInterval);
}
function maybeGoActive(event) {
if (event.isAutoRepeat) { // isAutoRepeat is true when held down (or when Windows feels like it)
return;
}
if (!isAway && (event.text === 'ESC')) {
goAway();
} else {
goActive();
}
}
var wasHmdActive = HMD.active;
var wasMouseCaptured = Reticle.mouseCaptured;
function maybeGoAway() {
// If our active state change (went to or from HMD mode), and we are now in the HMD, go into away
if (HMD.active !== wasHmdActive) {
wasHmdActive = !wasHmdActive;
if (wasHmdActive) {
goAway();
return;
}
}
// If the mouse has gone from captured, to non-captured state, then it likely means the person is still in the HMD,
// but tabbed away from the application (meaning they don't have mouse control) and they likely want to go into
// an away state
if (Reticle.mouseCaptured !== wasMouseCaptured) {
wasMouseCaptured = !wasMouseCaptured;
if (!wasMouseCaptured) {
if (enterAwayStateWhenFocusLostInVR) {
goAway();
return;
}
}
}
// If you've removed your HMD from your head, and we can detect it, we will also go away...
if (HMD.mounted !== wasHmdMounted) {
wasHmdMounted = HMD.mounted;
print("HMD mounted changed...");
// We're putting the HMD on... switch to those devices
if (HMD.mounted) {
print("NOW mounted...");
} else {
print("HMD NOW un-mounted...");
if (HMD.active) {
goAway();
return;
}
}
}
}
function setEnabled(value) {
if (!value) {
goActive();
}
isEnabled = value;
}
function checkAudioToggled() {
if (isAway && !Audio.muted) {
goActive();
}
}
var CHANNEL_AWAY_ENABLE = "Hifi-Away-Enable";
var handleMessage = function(channel, message, sender) {
if (channel === CHANNEL_AWAY_ENABLE && sender === MyAvatar.sessionUUID) {
print("away.js | Got message on Hifi-Away-Enable: ", message);
setEnabled(message === 'enable');
}
};
Messages.subscribe(CHANNEL_AWAY_ENABLE);
Messages.messageReceived.connect(handleMessage);
var maybeIntervalTimer = Script.setInterval(function() {
maybeMoveOverlay();
maybeGoAway();
checkAudioToggled();
}, BASIC_TIMER_INTERVAL);
Controller.mousePressEvent.connect(goActive);
Controller.keyPressEvent.connect(maybeGoActive);
// Note peek() so as to not interfere with other mappings.
eventMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(goActive);
eventMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(goActive);
eventMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(goActive);
eventMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(goActive);
eventMapping.from(Controller.Standard.LT).peek().to(goActive);
eventMapping.from(Controller.Standard.LB).peek().to(goActive);
eventMapping.from(Controller.Standard.LS).peek().to(goActive);
eventMapping.from(Controller.Standard.LeftGrip).peek().to(goActive);
eventMapping.from(Controller.Standard.RT).peek().to(goActive);
eventMapping.from(Controller.Standard.RB).peek().to(goActive);
eventMapping.from(Controller.Standard.RS).peek().to(goActive);
eventMapping.from(Controller.Standard.RightGrip).peek().to(goActive);
eventMapping.from(Controller.Standard.Back).peek().to(goActive);
eventMapping.from(Controller.Standard.Start).peek().to(goActive);
Controller.enableMapping(eventMappingName);
function awayStateWhenFocusLostInVRChanged(enabled) {
enterAwayStateWhenFocusLostInVR = enabled;
}
Script.scriptEnding.connect(function () {
Script.clearInterval(maybeIntervalTimer);
goActive();
HMD.awayStateWhenFocusLostInVRChanged.disconnect(awayStateWhenFocusLostInVRChanged);
Controller.disableMapping(eventMappingName);
Controller.mousePressEvent.disconnect(goActive);
Controller.keyPressEvent.disconnect(maybeGoActive);
Messages.messageReceived.disconnect(handleMessage);
Messages.unsubscribe(CHANNEL_AWAY_ENABLE);
});
HMD.awayStateWhenFocusLostInVRChanged.connect(awayStateWhenFocusLostInVRChanged);
if (HMD.active && !HMD.mounted) {
print("Starting script, while HMD is active and not mounted...");
goAway(true);
}
}()); // END LOCAL_SCOPE

View file

@ -0,0 +1,58 @@
"use strict";
// controllerScripts.js
//
// Created by David Rowe on 15 Mar 2017.
// Copyright 2017 High Fidelity, Inc.
//
// 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, Menu */
var CONTOLLER_SCRIPTS = [
"squeezeHands.js",
"controllerDisplayManager.js",
"toggleAdvancedMovementForHandControllers.js",
"controllerDispatcher.js",
"controllerModules/nearParentGrabOverlay.js",
"controllerModules/stylusInput.js",
"controllerModules/equipEntity.js",
"controllerModules/nearTrigger.js",
"controllerModules/webSurfaceLaserInput.js",
"controllerModules/inVREditMode.js",
"controllerModules/disableOtherModule.js",
"controllerModules/farTrigger.js",
"controllerModules/teleport.js",
"controllerModules/hudOverlayPointer.js",
"controllerModules/scaleEntity.js",
"controllerModules/nearGrabHyperLinkEntity.js",
"controllerModules/nearTabletHighlight.js",
"controllerModules/nearGrabEntity.js",
"controllerModules/farGrabEntity.js"
];
var DEBUG_MENU_ITEM = "Debug defaultScripts.js";
function runDefaultsTogether() {
for (var j in CONTOLLER_SCRIPTS) {
if (CONTOLLER_SCRIPTS.hasOwnProperty(j)) {
Script.include(CONTOLLER_SCRIPTS[j]);
}
}
}
function runDefaultsSeparately() {
for (var i in CONTOLLER_SCRIPTS) {
if (CONTOLLER_SCRIPTS.hasOwnProperty(i)) {
Script.load(CONTOLLER_SCRIPTS[i]);
}
}
}
if (Menu.isOptionChecked(DEBUG_MENU_ITEM)) {
runDefaultsSeparately();
} else {
runDefaultsTogether();
}

View file

@ -0,0 +1,615 @@
"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,
controllerDispatcherPlugins:true, controllerDispatcherPluginsNeedSort:true,
LEFT_HAND, RIGHT_HAND, NEAR_GRAB_PICK_RADIUS, DEFAULT_SEARCH_SPHERE_DISTANCE, DISPATCHER_PROPERTIES,
getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities, Picks, PickType, Pointers,
PointerManager, getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities, Picks, PickType, Pointers,
PointerManager, print, Keyboard
*/
controllerDispatcherPlugins = {};
controllerDispatcherPluginsNeedSort = false;
Script.include("/~/system/libraries/utils.js");
Script.include("/~/system/libraries/controllers.js");
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
(function() {
Script.include("/~/system/libraries/pointersUtils.js");
var NEAR_MAX_RADIUS = 0.1;
var NEAR_TABLET_MAX_RADIUS = 0.05;
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;
var DEBUG = false;
var SHOW_GRAB_SPHERE = 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.orderedPluginNames = [];
this.tabletID = null;
this.blacklist = [];
this.pointerManager = new PointerManager();
this.grabSphereOverlays = [null, null];
this.targetIDs = {};
// 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 = {
head: false,
leftHand: false,
rightHand: false,
rightHandTrigger: false,
leftHandTrigger: false,
rightHandEquip: false,
leftHandEquip: false,
mouse: false
};
this.laserVisibleStatus = [false, false, false, false];
this.laserLockStatus = [false, false, false, 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.setIgnorePointerItems = function() {
if (HMD.tabletID && HMD.tabletID !== this.tabletID) {
this.tabletID = HMD.tabletID;
Pointers.setIgnoreItems(_this.leftPointer, _this.blacklist);
Pointers.setIgnoreItems(_this.rightPointer, _this.blacklist);
}
};
this.update = function () {
try {
_this.updateInternal();
} catch (e) {
print(e);
}
Script.setTimeout(_this.update, BASIC_TIMER_INTERVAL_MS);
};
this.updateInternal = function () {
if (PROFILE) {
Script.beginProfileRange("dispatch.pre");
}
var sensorScaleFactor = MyAvatar.sensorToWorldScale;
var deltaTime = _this.updateTimings();
_this.setIgnorePointerItems();
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;
});
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 * sensorScaleFactor);
// Tablet and mini-tablet must be within NEAR_TABLET_MAX_RADIUS in order to be grabbed.
// Mini tablet can only be grabbed the hand it's displayed on.
var tabletIndex = nearbyOverlays.indexOf(HMD.tabletID);
var miniTabletIndex = nearbyOverlays.indexOf(HMD.miniTabletID);
if (tabletIndex !== -1 || miniTabletIndex !== -1) {
var closebyOverlays =
Overlays.findOverlays(controllerLocations[h].position, NEAR_TABLET_MAX_RADIUS * sensorScaleFactor);
// Assumes that the tablet and mini-tablet are not displayed at the same time.
if (tabletIndex !== -1 && closebyOverlays.indexOf(HMD.tabletID) === -1) {
nearbyOverlays.splice(tabletIndex, 1);
}
if (miniTabletIndex !== -1 &&
((closebyOverlays.indexOf(HMD.miniTabletID) === -1) || h !== HMD.miniTabletHand)) {
nearbyOverlays.splice(miniTabletIndex, 1);
}
}
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 findRadius = NEAR_MAX_RADIUS * sensorScaleFactor;
if (SHOW_GRAB_SPHERE) {
if (this.grabSphereOverlays[h]) {
Overlays.editOverlay(this.grabSphereOverlays[h], { position: controllerLocations[h].position });
} else {
var grabSphereSize = findRadius * 2;
this.grabSphereOverlays[h] = Overlays.addOverlay("sphere", {
position: controllerLocations[h].position,
dimensions: { x: grabSphereSize, y: grabSphereSize, z: grabSphereSize },
color: { red: 30, green: 30, blue: 255 },
alpha: 0.3,
solid: true,
visible: true,
// lineWidth: 2.0,
drawInFront: false,
grabbable: false
});
}
}
var nearbyEntityIDs = Entities.findEntities(controllerPosition, findRadius);
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 = [
Pointers.getPrevPickResult(_this.leftPointer),
Pointers.getPrevPickResult(_this.rightPointer)
];
var hudRayPicks = [
Pointers.getPrevPickResult(_this.leftHudPointer),
Pointers.getPrevPickResult(_this.rightHudPointer)
];
var mouseRayPick = Pointers.getPrevPickResult(_this.mouseRayPick);
// 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 === Picks.INTERSECTED_ENTITY) {
// XXX check to make sure this one isn't already in nearbyEntityProperties?
if (rayPicks[h].distance < NEAR_GRAB_PICK_RADIUS * sensorScaleFactor) {
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;
});
}
// sometimes, during a HMD snap-turn, an equipped or held item wont be near
// the hand when the findEntities is done. Gather up any hand-children here.
for (h = LEFT_HAND; h <= RIGHT_HAND; h++) {
var handChildrenIDs = findHandChildEntities(h);
handChildrenIDs.forEach(function (handChildID) {
if (handChildID in nearbyEntityPropertiesByID) {
return;
}
var props = Entities.getEntityProperties(handChildID, DISPATCHER_PROPERTIES);
props.id = handChildID;
nearbyEntityPropertiesByID[handChildID] = props;
});
}
// also make sure we have the properties from the current module's target
for (var tIDRunningPluginName in _this.runningPluginNames) {
if (_this.runningPluginNames.hasOwnProperty(tIDRunningPluginName)) {
var targetIDs = _this.targetIDs[tIDRunningPluginName];
if (targetIDs) {
for (var k = 0; k < targetIDs.length; k++) {
var targetID = targetIDs[k];
if (!nearbyEntityPropertiesByID[targetID]) {
var targetProps = Entities.getEntityProperties(targetID, DISPATCHER_PROPERTIES);
targetProps.id = targetID;
nearbyEntityPropertiesByID[targetID] = targetProps;
}
}
}
}
}
// 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,
mouseRayPick: mouseRayPick
};
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);
_this.pointerManager.makePointerVisible(candidatePlugin.parameters.handLaser);
if (DEBUG) {
print("controllerDispatcher running " + 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 (DEBUG) {
if (JSON.stringify(_this.targetIDs[runningPluginName]) != JSON.stringify(runningness.targets)) {
print("controllerDispatcher targetIDs[" + runningPluginName + "] = " +
JSON.stringify(runningness.targets));
}
}
_this.targetIDs[runningPluginName] = runningness.targets;
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];
delete _this.targetIDs[runningPluginName];
if (DEBUG) {
print("controllerDispatcher deleted targetIDs[" + runningPluginName + "]");
}
_this.markSlots(plugin, false);
_this.pointerManager.makePointerInvisible(plugin.parameters.handLaser);
if (DEBUG) {
print("controllerDispatcher stopping " + runningPluginName);
}
}
_this.pointerManager.lockPointerEnd(plugin.parameters.handLaser, runningness.laserLockInfo);
if (PROFILE) {
Script.endProfileRange("dispatch.run." + runningPluginName);
}
}
}
}
_this.pointerManager.updatePointersRenderState(controllerData.triggerClicks, controllerData.triggerValues);
if (PROFILE) {
Script.endProfileRange("dispatch.run");
}
};
this.leftBlacklistTabletIDs = [];
this.rightBlacklistTabletIDs = [];
this.setLeftBlacklist = function () {
Pointers.setIgnoreItems(_this.leftPointer, _this.blacklist.concat(_this.leftBlacklistTabletIDs));
};
this.setRightBlacklist = function () {
Pointers.setIgnoreItems(_this.rightPointer, _this.blacklist.concat(_this.rightBlacklistTabletIDs));
};
this.setBlacklist = function() {
_this.setLeftBlacklist();
_this.setRightBlacklist();
};
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.leftPointer = this.pointerManager.createPointer(false, PickType.Ray, {
joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND",
filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE,
triggers: [{action: Controller.Standard.LTClick, button: "Focus"}, {action: Controller.Standard.LTClick, button: "Primary"}],
posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand, true),
hover: true,
scaleWithParent: true,
distanceScaleEnd: true,
hand: LEFT_HAND
});
Keyboard.setLeftHandLaser(this.leftPointer);
this.rightPointer = this.pointerManager.createPointer(false, PickType.Ray, {
joint: "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND",
filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE,
triggers: [{action: Controller.Standard.RTClick, button: "Focus"}, {action: Controller.Standard.RTClick, button: "Primary"}],
posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand, true),
hover: true,
scaleWithParent: true,
distanceScaleEnd: true,
hand: RIGHT_HAND
});
Keyboard.setRightHandLaser(this.rightPointer);
this.leftHudPointer = this.pointerManager.createPointer(true, PickType.Ray, {
joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND",
filter: Picks.PICK_HUD,
maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE,
posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand, true),
triggers: [{action: Controller.Standard.LTClick, button: "Focus"}, {action: Controller.Standard.LTClick, button: "Primary"}],
hover: true,
scaleWithParent: true,
distanceScaleEnd: true,
hand: LEFT_HAND
});
this.rightHudPointer = this.pointerManager.createPointer(true, PickType.Ray, {
joint: "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND",
filter: Picks.PICK_HUD,
maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE,
posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand, true),
triggers: [{action: Controller.Standard.RTClick, button: "Focus"}, {action: Controller.Standard.RTClick, button: "Primary"}],
hover: true,
scaleWithParent: true,
distanceScaleEnd: true,
hand: RIGHT_HAND
});
this.mouseRayPick = Pointers.createPointer(PickType.Ray, {
joint: "Mouse",
filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE,
enabled: true
});
this.handleMessage = function (channel, data, sender) {
var message;
if (sender === MyAvatar.sessionUUID) {
try {
if (channel === 'Hifi-Hand-RayPick-Blacklist') {
message = JSON.parse(data);
var action = message.action;
var id = message.id;
var index = _this.blacklist.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();
}
}
if (action === "tablet") {
var tabletIDs = message.blacklist ?
[HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID, HMD.homeButtonHighlightID] :
[];
if (message.hand === LEFT_HAND) {
_this.leftBlacklistTabletIDs = tabletIDs;
_this.setLeftBlacklist();
} else {
_this.rightBlacklistTabletIDs = tabletIDs;
_this.setRightBlacklist();
}
}
}
} catch (e) {
print("WARNING: handControllerGrab.js -- error parsing message: " + data);
}
}
};
this.cleanup = function () {
Controller.disableMapping(MAPPING_NAME);
_this.pointerManager.removePointers();
Pointers.removePointer(this.mouseRayPick);
};
}
function mouseReleaseOnOverlay(overlayID, event) {
if (HMD.homeButtonID && overlayID === HMD.homeButtonID && event.button === "Primary") {
Messages.sendLocalMessage("home", overlayID);
}
}
var HAPTIC_STYLUS_STRENGTH = 1.0;
var HAPTIC_STYLUS_DURATION = 20.0;
function mousePress(id, event) {
if (HMD.active) {
var runningPlugins = controllerDispatcher.runningPluginNames;
if (event.id === controllerDispatcher.leftPointer && event.button === "Primary" && runningPlugins.LeftWebSurfaceLaserInput) {
Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, LEFT_HAND);
} else if (event.id === controllerDispatcher.rightPointer && event.button === "Primary" && runningPlugins.RightWebSurfaceLaserInput) {
Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, RIGHT_HAND);
}
}
}
Overlays.mouseReleaseOnOverlay.connect(mouseReleaseOnOverlay);
Overlays.mousePressOnOverlay.connect(mousePress);
Entities.mousePressOnEntity.connect(mousePress);
var controllerDispatcher = new ControllerDispatcher();
Messages.subscribe('Hifi-Hand-RayPick-Blacklist');
Messages.messageReceived.connect(controllerDispatcher.handleMessage);
Script.scriptEnding.connect(controllerDispatcher.cleanup);
Script.setTimeout(controllerDispatcher.update, BASIC_TIMER_INTERVAL_MS);
}());

View file

@ -0,0 +1,292 @@
//
// controllerDisplay.js
//
// Created by Anthony J. Thibault on 10/20/16
// Originally created by Ryan Huffman on 9/21/2016
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* globals createControllerDisplay:true, deleteControllerDisplay:true, Controller, Overlays, Vec3, MyAvatar, Quat */
function clamp(value, min, max) {
if (value < min) {
return min;
} else if (value > max) {
return max;
}
return value;
}
function resolveHardware(path) {
if (typeof path === 'string') {
var parts = path.split(".");
function resolveInner(base, path, i) {
if (i >= path.length) {
return base;
}
return resolveInner(base[path[i]], path, ++i);
}
return resolveInner(Controller.Hardware, parts, 0);
}
return path;
}
var DEBUG = true;
function debug() {
if (DEBUG) {
var args = Array.prototype.slice.call(arguments);
args.unshift("controllerDisplay.js | ");
print.apply(this, args);
}
}
createControllerDisplay = function(config) {
var controllerDisplay = {
overlays: [],
partOverlays: {},
parts: {},
mappingName: "mapping-display-" + Math.random(),
partValues: {},
setVisible: function(visible) {
for (var i = 0; i < this.overlays.length; ++i) {
Overlays.editOverlay(this.overlays[i], {
visible: visible
});
}
},
setPartVisible: function(partName, visible) {
// Disabled
/*
if (partName in this.partOverlays) {
for (var i = 0; i < this.partOverlays[partName].length; ++i) {
Overlays.editOverlay(this.partOverlays[partName][i], {
//visible: visible
});
}
}
*/
},
setLayerForPart: function(partName, layerName) {
if (partName in this.parts) {
var part = this.parts[partName];
if (part.textureLayers && layerName in part.textureLayers) {
var layer = part.textureLayers[layerName];
var textures = {};
if (layer.defaultTextureURL) {
textures[part.textureName] = layer.defaultTextureURL;
}
for (var i = 0; i < this.partOverlays[partName].length; ++i) {
Overlays.editOverlay(this.partOverlays[partName][i], {
textures: textures
});
}
}
}
},
resize: function(sensorScaleFactor) {
if (this.overlays.length >= 0) {
var controller = config.controllers[0];
var position = controller.position;
// first overlay is main body.
var overlayID = this.overlays[0];
var localPosition = Vec3.multiply(sensorScaleFactor, Vec3.sum(Vec3.multiplyQbyV(controller.rotation, controller.naturalPosition), position));
var dimensions = Vec3.multiply(sensorScaleFactor, controller.dimensions);
Overlays.editOverlay(overlayID, {
dimensions: dimensions,
localPosition: localPosition
});
if (controller.parts) {
var i = 1;
for (var partName in controller.parts) {
overlayID = this.overlays[i++];
var part = controller.parts[partName];
localPosition = Vec3.subtract(part.naturalPosition, controller.naturalPosition);
var localRotation;
var value = this.partValues[partName];
var offset, rotation;
if (value !== undefined) {
if (part.type === "linear") {
offset = Vec3.multiply(part.maxTranslation * value, part.axis);
localPosition = Vec3.sum(localPosition, offset);
localRotation = undefined;
} else if (part.type === "joystick") {
rotation = Quat.fromPitchYawRollDegrees(value.y * part.xHalfAngle, 0, value.x * part.yHalfAngle);
if (part.originOffset) {
offset = Vec3.multiplyQbyV(rotation, part.originOffset);
offset = Vec3.subtract(part.originOffset, offset);
} else {
offset = { x: 0, y: 0, z: 0 };
}
localPosition = Vec3.sum(offset, localPosition);
localRotation = rotation;
} else if (part.type === "rotational") {
value = clamp(value, part.minValue, part.maxValue);
var pct = (value - part.minValue) / part.maxValue;
var angle = pct * part.maxAngle;
rotation = Quat.angleAxis(angle, part.axis);
if (part.origin) {
offset = Vec3.multiplyQbyV(rotation, part.origin);
offset = Vec3.subtract(offset, part.origin);
} else {
offset = { x: 0, y: 0, z: 0 };
}
localPosition = Vec3.sum(offset, localPosition);
localRotation = rotation;
}
}
if (localRotation !== undefined) {
Overlays.editOverlay(overlayID, {
dimensions: Vec3.multiply(sensorScaleFactor, part.naturalDimensions),
localPosition: Vec3.multiply(sensorScaleFactor, localPosition),
localRotation: localRotation
});
} else {
Overlays.editOverlay(overlayID, {
dimensions: Vec3.multiply(sensorScaleFactor, part.naturalDimensions),
localPosition: Vec3.multiply(sensorScaleFactor, localPosition)
});
}
}
}
}
}
};
var mapping = Controller.newMapping(controllerDisplay.mappingName);
for (var i = 0; i < config.controllers.length; ++i) {
var controller = config.controllers[i];
var position = controller.position;
var sensorScaleFactor = MyAvatar.sensorToWorldScale;
if (controller.naturalPosition) {
position = Vec3.sum(Vec3.multiplyQbyV(controller.rotation, controller.naturalPosition), position);
} else {
controller.naturalPosition = { x: 0, y: 0, z: 0 };
}
var baseOverlayID = Overlays.addOverlay("model", {
url: controller.modelURL,
dimensions: Vec3.multiply(sensorScaleFactor, controller.dimensions),
localRotation: controller.rotation,
localPosition: Vec3.multiply(sensorScaleFactor, position),
parentID: MyAvatar.SELF_ID,
parentJointIndex: controller.jointIndex,
ignoreRayIntersection: true
});
controllerDisplay.overlays.push(baseOverlayID);
if (controller.parts) {
for (var partName in controller.parts) {
var part = controller.parts[partName];
var localPosition = Vec3.subtract(part.naturalPosition, controller.naturalPosition);
var localRotation = { x: 0, y: 0, z: 0, w: 1 };
controllerDisplay.parts[partName] = controller.parts[partName];
var properties = {
url: part.modelURL,
localPosition: localPosition,
localRotation: localRotation,
parentID: baseOverlayID,
ignoreRayIntersection: true
};
if (part.defaultTextureLayer) {
var textures = {};
textures[part.textureName] = part.textureLayers[part.defaultTextureLayer].defaultTextureURL;
properties.textures = textures;
}
var overlayID = Overlays.addOverlay("model", properties);
if (part.type === "rotational") {
var input = resolveHardware(part.input);
mapping.from([input]).peek().to(function(partName) {
return function(value) {
// insert the most recent controller value into controllerDisplay.partValues.
controllerDisplay.partValues[partName] = value;
controllerDisplay.resize(MyAvatar.sensorToWorldScale);
};
}(partName));
} else if (part.type === "touchpad") {
var visibleInput = resolveHardware(part.visibleInput);
var xInput = resolveHardware(part.xInput);
var yInput = resolveHardware(part.yInput);
// TODO: Touchpad inputs are currently only working for half
// of the touchpad. When that is fixed, it would be useful
// to update these to display the current finger position.
mapping.from([visibleInput]).peek().to(function(value) {
});
mapping.from([xInput]).peek().to(function(value) {
});
mapping.from([yInput]).peek().invert().to(function(value) {
});
} else if (part.type === "joystick") {
(function(part, partName) {
var xInput = resolveHardware(part.xInput);
var yInput = resolveHardware(part.yInput);
mapping.from([xInput]).peek().to(function(value) {
// insert the most recent controller value into controllerDisplay.partValues.
if (controllerDisplay.partValues[partName]) {
controllerDisplay.partValues[partName].x = value;
} else {
controllerDisplay.partValues[partName] = {x: value, y: 0};
}
controllerDisplay.resize(MyAvatar.sensorToWorldScale);
});
mapping.from([yInput]).peek().to(function(value) {
// insert the most recent controller value into controllerDisplay.partValues.
if (controllerDisplay.partValues[partName]) {
controllerDisplay.partValues[partName].y = value;
} else {
controllerDisplay.partValues[partName] = {x: 0, y: value};
}
controllerDisplay.resize(MyAvatar.sensorToWorldScale);
});
})(part, partName);
} else if (part.type === "linear") {
(function(part, partName) {
var input = resolveHardware(part.input);
mapping.from([input]).peek().to(function(value) {
// insert the most recent controller value into controllerDisplay.partValues.
controllerDisplay.partValues[partName] = value;
controllerDisplay.resize(MyAvatar.sensorToWorldScale);
});
})(part, partName);
} else if (part.type === "static") {
// do nothing
} else {
debug("TYPE NOT SUPPORTED: ", part.type);
}
controllerDisplay.overlays.push(overlayID);
if (!(partName in controllerDisplay.partOverlays)) {
controllerDisplay.partOverlays[partName] = [];
}
controllerDisplay.partOverlays[partName].push(overlayID);
}
}
}
Controller.enableMapping(controllerDisplay.mappingName);
controllerDisplay.resize(MyAvatar.sensorToWorldScale);
return controllerDisplay;
};
deleteControllerDisplay = function(controllerDisplay) {
for (var i = 0; i < controllerDisplay.overlays.length; ++i) {
Overlays.deleteOverlay(controllerDisplay.overlays[i]);
}
Controller.disableMapping(controllerDisplay.mappingName);
};

View file

@ -0,0 +1,195 @@
//
// controllerDisplayManager.js
//
// Created by Anthony J. Thibault on 10/20/16
// Originally created by Ryan Huffman on 9/21/2016
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/* globals ControllerDisplayManager:true, createControllerDisplay, deleteControllerDisplay,
VIVE_CONTROLLER_CONFIGURATION_LEFT, VIVE_CONTROLLER_CONFIGURATION_RIGHT, Script, HMD, Controller,
MyAvatar, Overlays, TOUCH_CONTROLLER_CONFIGURATION_LEFT, TOUCH_CONTROLLER_CONFIGURATION_RIGHT, Messages */
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
(function () {
Script.include("controllerDisplay.js");
Script.include("viveControllerConfiguration.js");
Script.include("touchControllerConfiguration.js");
var HIDE_CONTROLLERS_ON_EQUIP = false;
//
// Management of controller display
//
ControllerDisplayManager = function() {
var self = this;
var controllerLeft = null;
var controllerRight = null;
var controllerCheckerIntervalID = null;
this.setLeftVisible = function(visible) {
if (controllerLeft) {
controllerLeft.setVisible(visible);
}
};
this.setRightVisible = function(visible) {
if (controllerRight) {
controllerRight.setVisible(visible);
}
};
function updateControllers() {
if (HMD.active && HMD.shouldShowHandControllers()) {
var leftConfig = null;
var rightConfig = null;
if ("Vive" in Controller.Hardware) {
leftConfig = VIVE_CONTROLLER_CONFIGURATION_LEFT;
rightConfig = VIVE_CONTROLLER_CONFIGURATION_RIGHT;
}
if ("OculusTouch" in Controller.Hardware) {
leftConfig = TOUCH_CONTROLLER_CONFIGURATION_LEFT;
rightConfig = TOUCH_CONTROLLER_CONFIGURATION_RIGHT;
}
if (leftConfig !== null && rightConfig !== null) {
if (controllerLeft === null) {
controllerLeft = createControllerDisplay(leftConfig);
controllerLeft.setVisible(true);
}
if (controllerRight === null) {
controllerRight = createControllerDisplay(rightConfig);
controllerRight.setVisible(true);
}
// We've found the controllers, we no longer need to look for active controllers
if (controllerCheckerIntervalID) {
Script.clearInterval(controllerCheckerIntervalID);
controllerCheckerIntervalID = null;
}
} else {
self.deleteControllerDisplays();
if (!controllerCheckerIntervalID) {
controllerCheckerIntervalID = Script.setInterval(updateControllers, 1000);
}
}
} else {
// We aren't in HMD mode, we no longer need to look for active controllers
if (controllerCheckerIntervalID) {
Script.clearInterval(controllerCheckerIntervalID);
controllerCheckerIntervalID = null;
}
self.deleteControllerDisplays();
}
}
function resizeControllers(sensorScaleFactor) {
if (controllerLeft) {
controllerLeft.resize(sensorScaleFactor);
}
if (controllerRight) {
controllerRight.resize(sensorScaleFactor);
}
}
var handleMessages = function(channel, message, sender) {
var i, data, name, visible;
if (!controllerLeft && !controllerRight) {
return;
}
if (sender === MyAvatar.sessionUUID) {
if (channel === 'Controller-Display') {
data = JSON.parse(message);
name = data.name;
visible = data.visible;
if (controllerLeft) {
if (name in controllerLeft.annotations) {
for (i = 0; i < controllerLeft.annotations[name].length; ++i) {
Overlays.editOverlay(controllerLeft.annotations[name][i], { visible: visible });
}
}
}
if (controllerRight) {
if (name in controllerRight.annotations) {
for (i = 0; i < controllerRight.annotations[name].length; ++i) {
Overlays.editOverlay(controllerRight.annotations[name][i], { visible: visible });
}
}
}
} else if (channel === 'Controller-Display-Parts') {
data = JSON.parse(message);
for (name in data) {
visible = data[name];
if (controllerLeft) {
controllerLeft.setPartVisible(name, visible);
}
if (controllerRight) {
controllerRight.setPartVisible(name, visible);
}
}
} else if (channel === 'Controller-Set-Part-Layer') {
data = JSON.parse(message);
for (name in data) {
var layer = data[name];
if (controllerLeft) {
controllerLeft.setLayerForPart(name, layer);
}
if (controllerRight) {
controllerRight.setLayerForPart(name, layer);
}
}
} else if (channel === 'Hifi-Object-Manipulation') {
if (HIDE_CONTROLLERS_ON_EQUIP) {
data = JSON.parse(message);
visible = data.action !== 'equip';
if (data.joint === "LeftHand") {
self.setLeftVisible(visible);
} else if (data.joint === "RightHand") {
self.setRightVisible(visible);
}
}
}
}
};
Messages.messageReceived.connect(handleMessages);
this.deleteControllerDisplays = function() {
if (controllerLeft) {
deleteControllerDisplay(controllerLeft);
controllerLeft = null;
}
if (controllerRight) {
deleteControllerDisplay(controllerRight);
controllerRight = null;
}
};
this.destroy = function() {
Messages.messageReceived.disconnect(handleMessages);
HMD.displayModeChanged.disconnect(updateControllers);
HMD.shouldShowHandControllersChanged.disconnect(updateControllers);
self.deleteControllerDisplays();
};
HMD.displayModeChanged.connect(updateControllers);
HMD.shouldShowHandControllersChanged.connect(updateControllers);
MyAvatar.sensorToWorldScaleChanged.connect(resizeControllers);
updateControllers();
};
var controllerDisplayManager = new ControllerDisplayManager();
Script.scriptEnding.connect(function () {
controllerDisplayManager.destroy();
});
}());

View file

@ -0,0 +1,83 @@
"use strict";
// disableOtherModule.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, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule,
makeDispatcherModuleParameters, makeRunningValues, getEnabledModuleByName, Messages
*/
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.active) {
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);
function handleMessage(channel, message, sender) {
if (sender === MyAvatar.sessionUUID) {
if (channel === 'Hifi-Hand-Disabler') {
if (message === 'left') {
leftDisableModules.disableModules = true;
} else if (message === 'right') {
rightDisableModules.disableModules = true;
} else if (message === 'both') {
leftDisableModules.disableModules = true;
rightDisableModules.disableModules = true;
} else if (message === 'none') {
leftDisableModules.disableModules = false;
rightDisableModules.disableModules = false;
} else {
print("disableOtherModule -- unknown command: " + message);
}
}
}
}
Messages.subscribe('Hifi-Hand-Disabler');
function cleanup() {
disableDispatcherModule("LeftDisableModules");
disableDispatcherModule("RightDisableModules");
}
Messages.messageReceived.connect(handleMessage);
Script.scriptEnding.connect(cleanup);
}());

View file

@ -0,0 +1,867 @@
"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, Camera, print, getControllerJointIndex,
enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters,
makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic,
entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, isInEditMode, getGrabbableData,
entityIsEquippable, HMD
*/
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 dimensions = hotspot.radius * 2 * EQUIP_SPHERE_SCALE_FACTOR;
if (hotspot.indicatorURL) {
dimensions = hotspot.indicatorScale;
}
// override default sphere with a user specified model, if it exists.
overlayInfoSet.overlays.push(Overlays.addOverlay("model", {
name: "hotspot overlay",
url: hotspot.indicatorURL ? hotspot.indicatorURL : DEFAULT_SPHERE_MODEL_URL,
position: hotspot.worldPosition,
rotation: {
x: 0,
y: 0,
z: 0,
w: 1
},
dimensions: dimensions,
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.hotspot.indicatorURL) {
var ratio = overlayInfoSet.currentSize / overlayInfoSet.targetSize;
dimensions = {
x: overlayInfoSet.hotspot.dimensions.x * ratio,
y: overlayInfoSet.hotspot.dimensions.y * ratio,
z: overlayInfoSet.hotspot.dimensions.z * ratio
};
} 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 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;
var ATTACHPOINT_MAX_DISTANCE = 3.0;
// var EMPTY_PARENT_ID = "{00000000-0000-0000-0000-000000000000}";
var UNEQUIP_KEY = "u";
function getWearableData(props) {
if (props.grab.equippable) {
return {
joints: {
LeftHand: [ props.grab.equippableLeftPosition, props.grab.equippableLeftRotation ],
RightHand: [ props.grab.equippableRightPosition, props.grab.equippableRightRotation ]
},
indicatorURL: props.grab.equippableIndicatorURL,
indicatorScale: props.grab.equippableIndicatorScale,
indicatorOffset: props.grab.equippableIndicatorOffset
};
} else {
return null;
}
}
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 skeletonModelURL = MyAvatar.skeletonModelURL;
var attachPointSettings = getAttachPointSettings();
var avatarSettingsData = attachPointSettings[skeletonModelURL];
if (avatarSettingsData) {
var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand";
var joints = avatarSettingsData[hotspot.key];
if (joints) {
// make sure they are reasonable
if (joints[jointName] && joints[jointName][0] &&
Vec3.length(joints[jointName][0]) > ATTACHPOINT_MAX_DISTANCE) {
print("equipEntity -- Warning: rejecting settings attachPoint " + Vec3.length(joints[jointName][0]));
return undefined;
}
return joints[jointName];
}
}
return undefined;
}
function storeAttachPointForHotspotInSettings(hotspot, hand, offsetPosition, offsetRotation) {
var attachPointSettings = getAttachPointSettings();
var skeletonModelURL = MyAvatar.skeletonModelURL;
var avatarSettingsData = attachPointSettings[skeletonModelURL];
if (!avatarSettingsData) {
avatarSettingsData = {};
attachPointSettings[skeletonModelURL] = avatarSettingsData;
}
var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand";
var joints = avatarSettingsData[hotspot.key];
if (!joints) {
joints = {};
avatarSettingsData[hotspot.key] = joints;
}
joints[jointName] = [offsetPosition, offsetRotation];
setAttachPointSettings(attachPointSettings);
}
function clearAttachPoints() {
setAttachPointSettings({});
}
function EquipEntity(hand) {
this.hand = hand;
this.targetEntityID = null;
this.prevHandIsUpsideDown = false;
this.triggerValue = 0;
this.messageGrabEntity = false;
this.grabEntityProps = null;
this.shouldSendStart = false;
this.equipedWithSecondary = false;
this.handHasBeenRightsideUp = false;
this.parameters = makeDispatcherModuleParameters(
115,
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.
// * indicatorURL {string} url for model to use instead of default sphere.
// * indicatorScale {Vec3} scale factor for model
this.collectEquipHotspots = function(props) {
var result = [];
var entityID = props.id;
var entityXform = new Xform(props.rotation, props.position);
var wearableProps = getWearableData(props);
var sensorToScaleFactor = MyAvatar.sensorToWorldScale;
if (wearableProps && wearableProps.joints) {
result.push({
key: entityID.toString() + "0",
entityID: entityID,
localPosition: wearableProps.indicatorOffset,
worldPosition: entityXform.pos,
radius: ((wearableProps.indicatorScale.x +
wearableProps.indicatorScale.y +
wearableProps.indicatorScale.z) / 3) * sensorToScaleFactor,
dimensions: wearableProps.indicatorScale,
joints: wearableProps.joints,
indicatorURL: wearableProps.indicatorURL,
indicatorScale: wearableProps.indicatorScale,
});
}
return result;
};
this.hotspotIsEquippable = function(hotspot, controllerData) {
var props = controllerData.nearbyEntityPropertiesByID[hotspot.entityID];
var hasParent = true;
if (props.parentID === Uuid.NULL) {
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.secondarySmoothedSqueezed = 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 cloneID = cloneEntity(props);
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) {
var _this = this;
this.dropGestureReset();
this.clearEquipHaptics();
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
var grabbedProperties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
var grabData = getGrabbableData(grabbedProperties);
// if an object is "equipped" and has a predefined offset, use it.
if (this.grabbedHotspot) {
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 (HMD.mounted && HMD.isHandControllerAvailable() && grabData.grabFollowsController) {
handJointIndex = this.controllerJointIndex;
} else {
handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
}
var reparentProps = {
parentID: MyAvatar.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;
controllerData.nearbyEntityPropertiesByID[this.targetEntityID] = grabbedProperties;
isClone = true;
} else if (grabbedProperties.locked) {
this.grabbedHotspot = null;
this.targetEntityID = null;
return;
}
// HACK -- when
// https://highfidelity.fogbugz.com/f/cases/21767/entity-edits-shortly-after-an-add-often-fail
// is resolved, this can just be an editEntity rather than a setTimeout.
this.editDelayTimeout = Script.setTimeout(function () {
_this.editDelayTimeout = null;
Entities.editEntity(_this.targetEntityID, reparentProps);
}, 100);
// we don't want to send startEquip message until the trigger is released. otherwise,
// guns etc will fire right as they are equipped.
this.shouldSendStart = true;
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'equip',
grabbedEntity: this.targetEntityID,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
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 () {
if (this.editDelayTimeout) {
Script.clearTimeout(this.editDelayTimeout);
this.editDelayTimeout = null;
}
this.storeAttachPointInSettings();
Entities.editEntity(this.targetEntityID, {
parentID: Uuid.NULL,
parentJointIndex: -1
});
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "releaseEquip", args);
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'release',
grabbedEntity: this.targetEntityID,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
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 is not cloneable and locked return null
if (potentialEquipHotspot &&
(((this.triggerSmoothedSqueezed() || this.secondarySmoothedSqueezed()) && !this.waitForTriggerRelease) ||
this.messageGrabEntity)) {
this.grabbedHotspot = potentialEquipHotspot;
this.targetEntityID = this.grabbedHotspot.entityID;
this.startEquipEntity(controllerData);
this.equipedWithSecondary = this.secondarySmoothedSqueezed();
return makeRunningValues(true, [this.targetEntityID], []);
} else {
return makeRunningValues(false, [], []);
}
};
this.isTargetIDValid = function(controllerData) {
var entityProperties = controllerData.nearbyEntityPropertiesByID[this.targetEntityID];
return entityProperties && "type" in entityProperties;
};
this.isReady = function (controllerData, deltaTime) {
var timestamp = Date.now();
this.updateInputs(controllerData);
this.handHasBeenRightsideUp = false;
return this.checkNearbyHotspots(controllerData, deltaTime, timestamp);
};
this.run = function (controllerData, deltaTime) {
var timestamp = Date.now();
this.updateInputs(controllerData);
if (!this.messageGrabEntity && !this.isTargetIDValid(controllerData)) {
this.endEquipEntity();
return makeRunningValues(false, [], []);
}
if (!this.targetEntityID) {
return this.checkNearbyHotspots(controllerData, deltaTime, timestamp);
}
if (controllerData.secondaryValues[this.hand] && !this.equipedWithSecondary) {
// 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 handIsUpsideDown = this.dropGestureProcess(deltaTime);
var dropDetected = false;
if (this.handHasBeenRightsideUp) {
dropDetected = handIsUpsideDown;
}
if (!handIsUpsideDown) {
this.handHasBeenRightsideUp = true;
}
if (this.triggerSmoothedReleased() || this.secondaryReleased()) {
if (this.shouldSendStart) {
// we don't want to send startEquip message until the trigger is released. otherwise,
// guns etc will fire right as they are equipped.
var startArgs = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "startEquip", startArgs);
this.shouldSendStart = false;
}
this.waitForTriggerRelease = false;
if (this.secondaryReleased() && this.equipedWithSecondary) {
this.equipedWithSecondary = false;
}
}
if (dropDetected && this.prevDropDetected !== dropDetected) {
this.waitForTriggerRelease = true;
}
// highlight the grabbed hotspot when the dropGesture is detected.
if (dropDetected && this.grabbedHotspot) {
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.
this.endEquipEntity();
return makeRunningValues(false, [], []);
}
this.prevDropDetected = dropDetected;
equipHotspotBuddy.update(deltaTime, timestamp, controllerData);
if (!this.shouldSendStart) {
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "continueEquip", args);
}
return makeRunningValues(true, [this.targetEntityID], []);
};
this.storeAttachPointInSettings = function() {
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.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();
}
}
}
};
var clearGrabActions = function(entityID) {
var actionIDs = Entities.getActionIDs(entityID);
var myGrabTag = "grab-" + MyAvatar.sessionUUID;
for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) {
var actionID = actionIDs[actionIndex];
var actionArguments = Entities.getActionArguments(entityID, actionID);
var tag = actionArguments.tag;
if (tag === myGrabTag) {
Entities.deleteAction(entityID, actionID);
}
}
};
var onMousePress = function(event) {
if (isInEditMode() || !event.isLeftButton) { // don't consider any left clicks on the entity while in edit
return;
}
var pickRay = Camera.computePickRay(event.x, event.y);
var intersection = Entities.findRayIntersection(pickRay, true);
if (intersection.intersects) {
var entityID = intersection.entityID;
var entityProperties = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES);
entityProperties.id = entityID;
var hasEquipData = getWearableData(entityProperties);
if (hasEquipData && entityIsEquippable(entityProperties)) {
entityProperties.id = entityID;
var rightHandPosition = MyAvatar.getJointPosition("RightHand");
var leftHandPosition = MyAvatar.getJointPosition("LeftHand");
var distanceToRightHand = Vec3.distance(entityProperties.position, rightHandPosition);
var distanceToLeftHand = Vec3.distance(entityProperties.position, leftHandPosition);
var leftHandAvailable = leftEquipEntity.targetEntityID === null;
var rightHandAvailable = rightEquipEntity.targetEntityID === null;
if (rightHandAvailable && (distanceToRightHand < distanceToLeftHand || !leftHandAvailable)) {
// clear any existing grab actions on the entity now (their later removal could affect bootstrapping flags)
clearGrabActions(entityID);
rightEquipEntity.setMessageGrabData(entityProperties);
} else if (leftHandAvailable && (distanceToLeftHand < distanceToRightHand || !rightHandAvailable)) {
// clear any existing grab actions on the entity now (their later removal could affect bootstrapping flags)
clearGrabActions(entityID);
leftEquipEntity.setMessageGrabData(entityProperties);
}
}
}
};
var onKeyPress = function(event) {
if (event.text.toLowerCase() === UNEQUIP_KEY) {
if (rightEquipEntity.targetEntityID) {
rightEquipEntity.endEquipEntity();
}
if (leftEquipEntity.targetEntityID) {
leftEquipEntity.endEquipEntity();
}
}
};
var deleteEntity = function(entityID) {
if (rightEquipEntity.targetEntityID === entityID) {
rightEquipEntity.endEquipEntity();
}
if (leftEquipEntity.targetEntityID === entityID) {
leftEquipEntity.endEquipEntity();
}
};
var clearEntities = function() {
if (rightEquipEntity.targetEntityID) {
rightEquipEntity.endEquipEntity();
}
if (leftEquipEntity.targetEntityID) {
leftEquipEntity.endEquipEntity();
}
};
Messages.subscribe('Hifi-Hand-Grab');
Messages.subscribe('Hifi-Hand-Drop');
Messages.messageReceived.connect(handleMessage);
Controller.mousePressEvent.connect(onMousePress);
Controller.keyPressEvent.connect(onKeyPress);
Entities.deletingEntity.connect(deleteEntity);
Entities.clearingEntities.connect(clearEntities);
var leftEquipEntity = new EquipEntity(LEFT_HAND);
var rightEquipEntity = new EquipEntity(RIGHT_HAND);
enableDispatcherModule("LeftEquipEntity", leftEquipEntity);
enableDispatcherModule("RightEquipEntity", rightEquipEntity);
function cleanup() {
leftEquipEntity.cleanup();
rightEquipEntity.cleanup();
disableDispatcherModule("LeftEquipEntity");
disableDispatcherModule("RightEquipEntity");
clearAttachPoints();
Messages.messageReceived.disconnect(handleMessage);
Controller.mousePressEvent.disconnect(onMousePress);
Controller.keyPressEvent.disconnect(onKeyPress);
Entities.deletingEntity.disconnect(deleteEntity);
Entities.clearingEntities.disconnect(clearEntities);
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -0,0 +1,591 @@
"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, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Camera, Quat,
getEnabledModuleByName, makeRunningValues, Entities,
enableDispatcherModule, disableDispatcherModule, entityIsDistanceGrabbable, entityIsGrabbable,
makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION,
TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic,
getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD,
Picks, makeLaserLockInfo, makeLaserParams, AddressManager, getEntityParents, Selection, DISPATCHER_HOVERING_LIST,
worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES, Uuid, Picks
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
var MARGIN = 25;
function TargetObject(entityID, entityProps) {
this.entityID = entityID;
this.entityProps = entityProps;
this.targetEntityID = null;
this.targetEntityProps = null;
this.previousCollisionStatus = null;
this.madeDynamic = null;
this.makeDynamic = function() {
if (this.targetEntityID) {
var newProps = {
dynamic: true,
collisionless: true
};
this.previousCollisionStatus = this.targetEntityProps.collisionless;
Entities.editEntity(this.targetEntityID, newProps);
this.madeDynamic = true;
}
};
this.restoreTargetEntityOriginalProps = function() {
if (this.madeDynamic) {
var props = {};
props.dynamic = false;
props.collisionless = this.previousCollisionStatus;
var zeroVector = {x: 0, y: 0, z:0};
props.localVelocity = zeroVector;
props.localRotation = zeroVector;
Entities.editEntity(this.targetEntityID, props);
}
};
this.getTargetEntity = function() {
var parentPropsLength = this.parentProps.length;
if (parentPropsLength !== 0) {
var targetEntity = {
id: this.parentProps[parentPropsLength - 1].id,
props: this.parentProps[parentPropsLength - 1]};
this.targetEntityID = targetEntity.id;
this.targetEntityProps = targetEntity.props;
return targetEntity;
}
this.targetEntityID = this.entityID;
this.targetEntityProps = this.entityProps;
return {
id: this.entityID,
props: this.entityProps};
};
}
function FarActionGrabEntity(hand) {
this.hand = hand;
this.grabbedThingID = null;
this.targetObject = null;
this.actionID = null; // action this script created...
this.entityToLockOnto = null;
this.potentialEntityWithContextOverlay = false;
this.entityWithContextOverlay = false;
this.contextOverlayTimer = false;
this.locked = false;
this.reticleMinX = MARGIN;
this.reticleMaxX = null;
this.reticleMinY = MARGIN;
this.reticleMaxY = null;
this.ignoredEntities = [];
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,
makeLaserParams(this.hand, false));
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 === Uuid.NULL) {
this.actionID = null;
}
if (this.actionID !== null) {
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.grabbedThingID, "startDistanceGrab", args);
}
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, DISPATCHER_PROPERTIES);
var now = Date.now();
var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds
this.currentObjectTime = now;
// the action was set up when this.distanceHolding was called. update the targets.
var radius = Vec3.distance(this.currentObjectPosition, 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);
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.grabbedThingID, "continueDistanceGrab", args);
// 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);
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.endFarGrabAction = function () {
ensureDynamic(this.grabbedThingID);
this.distanceHolding = false;
this.distanceRotating = false;
Entities.deleteAction(this.grabbedThingID, this.actionID);
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.grabbedThingID, "releaseGrab", args);
if (this.targetObject) {
this.targetObject.restoreTargetEntityOriginalProps();
}
this.actionID = null;
this.grabbedThingID = null;
this.targetObject = null;
this.potentialEntityWithContextOverlay = false;
};
this.updateRecommendedArea = function() {
var dims = Controller.getViewportDimensions();
this.reticleMaxX = dims.x - MARGIN;
this.reticleMaxY = dims.y - MARGIN;
};
this.calculateNewReticlePosition = function(intersection) {
this.updateRecommendedArea();
var point2d = HMD.overlayFromWorldPoint(intersection);
point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX));
point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY));
return point2d;
};
this.restoreIgnoredEntities = function() {
for (var i = 0; i < this.ignoredEntities.length; i++) {
var data = {
action: 'remove',
id: this.ignoredEntities[i]
};
Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data));
}
this.ignoredEntities = [];
};
this.notPointingAtEntity = function(controllerData) {
var intersection = controllerData.rayPicks[this.hand];
var entityProperty = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
var entityType = entityProperty.type;
var hudRayPick = controllerData.hudRayPicks[this.hand];
var point2d = this.calculateNewReticlePosition(hudRayPick.intersection);
if ((intersection.type === Picks.INTERSECTED_ENTITY && entityType === "Web") ||
intersection.type === Picks.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) {
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);
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, DISPATCHER_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.potentialEntityWithContextOverlay = false;
}
};
this.targetIsNull = function() {
var properties = Entities.getEntityProperties(this.grabbedThingID, DISPATCHER_PROPERTIES);
if (Object.keys(properties).length === 0 && this.distanceHolding) {
return true;
}
return false;
};
this.isReady = function (controllerData) {
if (HMD.active) {
if (this.notPointingAtEntity(controllerData)) {
return makeRunningValues(false, [], []);
}
this.distanceHolding = false;
this.distanceRotating = false;
if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
this.prepareDistanceRotatingData(controllerData);
return makeRunningValues(true, [], []);
} else {
this.destroyContextOverlay();
return makeRunningValues(false, [], []);
}
}
return makeRunningValues(false, [], []);
};
this.run = function (controllerData) {
var intersection = controllerData.rayPicks[this.hand];
if (intersection.type === Picks.INTERSECTED_ENTITY && !Window.isPhysicsEnabled()) {
// add to ignored items.
if (this.ignoredEntities.indexOf(intersection.objectID) === -1) {
var data = {
action: 'add',
id: intersection.objectID
};
Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data));
this.ignoredEntities.push(intersection.objectID);
}
}
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE ||
(this.notPointingAtEntity(controllerData) && Window.isPhysicsEnabled()) || this.targetIsNull()) {
this.endFarGrabAction();
this.restoreIgnoredEntities();
return makeRunningValues(false, [], []);
}
this.intersectionDistance = controllerData.rayPicks[this.hand].distance;
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",
this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight"
];
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 or tablet 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 ||
HMD.tabletID && nearGrabReadiness[k].targets[0] === HMD.tabletID)) {
this.endFarGrabAction();
this.restoreIgnoredEntities();
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.endFarGrabAction();
this.restoreIgnoredEntities();
return makeRunningValues(false, [], []);
}
}
var rayPickInfo = controllerData.rayPicks[this.hand];
if (rayPickInfo.type === Picks.INTERSECTED_ENTITY) {
if (controllerData.triggerClicks[this.hand]) {
var entityID = rayPickInfo.objectID;
var targetProps = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES);
if (targetProps.href !== "") {
AddressManager.handleLookupString(targetProps.href);
this.restoreIgnoredEntities();
return makeRunningValues(false, [], []);
}
this.targetObject = new TargetObject(entityID, targetProps);
this.targetObject.parentProps = getEntityParents(targetProps);
if (this.contextOverlayTimer) {
Script.clearTimeout(this.contextOverlayTimer);
}
this.contextOverlayTimer = false;
if (entityID === this.entityWithContextOverlay) {
this.destroyContextOverlay();
} else {
Selection.removeFromSelectedItemsList("contextOverlayHighlightList", "entity", entityID);
}
var targetEntity = this.targetObject.getTargetEntity();
entityID = targetEntity.id;
targetProps = targetEntity.props;
if (entityIsGrabbable(targetProps) || entityIsGrabbable(this.targetObject.entityProps)) {
if (!entityIsDistanceGrabbable(targetProps)) {
this.targetObject.makeDynamic();
}
if (!this.distanceRotating) {
this.grabbedThingID = entityID;
this.grabbedDistance = rayPickInfo.distance;
}
if (otherFarGrabModule.grabbedThingID === this.grabbedThingID &&
otherFarGrabModule.distanceHolding) {
this.prepareDistanceRotatingData(controllerData);
this.distanceRotate(otherFarGrabModule);
} else {
this.distanceHolding = true;
this.distanceRotating = false;
this.startFarGrabAction(controllerData, targetProps);
}
}
} else if (!this.entityWithContextOverlay) {
var _this = this;
if (_this.potentialEntityWithContextOverlay !== rayPickInfo.objectID) {
if (_this.contextOverlayTimer) {
Script.clearTimeout(_this.contextOverlayTimer);
}
_this.contextOverlayTimer = false;
_this.potentialEntityWithContextOverlay = rayPickInfo.objectID;
}
if (!_this.contextOverlayTimer) {
_this.contextOverlayTimer = Script.setTimeout(function () {
if (!_this.entityWithContextOverlay &&
_this.contextOverlayTimer &&
_this.potentialEntityWithContextOverlay === rayPickInfo.objectID) {
var props = Entities.getEntityProperties(rayPickInfo.objectID, DISPATCHER_PROPERTIES);
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(controllerData);
};
this.exitIfDisabled = function(controllerData) {
var moduleName = this.hand === RIGHT_HAND ? "RightDisableModules" : "LeftDisableModules";
var disableModule = getEnabledModuleByName(moduleName);
if (disableModule) {
if (disableModule.disableModules) {
this.endFarGrabAction();
Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity",
this.highlightedEntity);
this.highlightedEntity = null;
this.restoreIgnoredEntities();
return makeRunningValues(false, [], []);
}
}
var grabbedThing = (this.distanceHolding || this.distanceRotating) ? this.targetObject.entityID : null;
var offset = this.calculateOffset(controllerData);
var laserLockInfo = makeLaserLockInfo(grabbedThing, false, this.hand, offset);
return makeRunningValues(true, [], [], laserLockInfo);
};
this.calculateOffset = function(controllerData) {
if (this.distanceHolding || this.distanceRotating) {
var targetProps = Entities.getEntityProperties(this.targetObject.entityID,
[ "position", "rotation", "registrationPoint", "dimensions" ]);
return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection);
}
return undefined;
};
}
var leftFarActionGrabEntity = new FarActionGrabEntity(LEFT_HAND);
var rightFarActionGrabEntity = new FarActionGrabEntity(RIGHT_HAND);
enableDispatcherModule("LeftFarActionGrabEntity", leftFarActionGrabEntity);
enableDispatcherModule("RightFarActionGrabEntity", rightFarActionGrabEntity);
function cleanup() {
disableDispatcherModule("LeftFarActionGrabEntity");
disableDispatcherModule("RightFarActionGrabEntity");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -0,0 +1,585 @@
"use strict";
// farGrabEntity.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, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Quat, getEnabledModuleByName, makeRunningValues,
Entities, enableDispatcherModule, disableDispatcherModule, entityIsGrabbable, makeDispatcherModuleParameters, MSECS_PER_SEC,
HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC,
projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, makeLaserParams, AddressManager,
getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, findGrabbableGroupParent,
worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function () {
var MARGIN = 25;
function TargetObject(entityID, entityProps) {
this.entityID = entityID;
this.entityProps = entityProps;
this.targetEntityID = null;
this.targetEntityProps = null;
this.getTargetEntity = function () {
var parentPropsLength = this.parentProps.length;
if (parentPropsLength !== 0) {
var targetEntity = {
id: this.parentProps[parentPropsLength - 1].id,
props: this.parentProps[parentPropsLength - 1]
};
this.targetEntityID = targetEntity.id;
this.targetEntityProps = targetEntity.props;
return targetEntity;
}
this.targetEntityID = this.entityID;
this.targetEntityProps = this.entityProps;
return {
id: this.entityID,
props: this.entityProps
};
};
}
function FarGrabEntity(hand) {
this.hand = hand;
this.grabbing = false;
this.targetEntityID = null;
this.targetObject = null;
this.previouslyUnhooked = {};
this.potentialEntityWithContextOverlay = false;
this.entityWithContextOverlay = false;
this.contextOverlayTimer = false;
this.reticleMinX = MARGIN;
this.reticleMaxX = 0;
this.reticleMinY = MARGIN;
this.reticleMaxY = 0;
this.endedGrab = 0;
this.MIN_HAPTIC_PULSE_INTERVAL = 500; // ms
this.disabled = false;
var _this = this;
this.initialControllerRotation = Quat.IDENTITY;
this.currentControllerRotation = Quat.IDENTITY;
this.manipulating = false;
this.wasManipulating = false;
var FAR_GRAB_JOINTS = [65527, 65528]; // FARGRAB_LEFTHAND_INDEX, FARGRAB_RIGHTHAND_INDEX
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(
540,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100,
makeLaserParams(this.hand, false));
this.getOtherModule = function () {
return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("LeftFarGrabEntity") : ("RightFarGrabEntity"));
};
// Get the rotation of the fargrabbed entity.
this.getTargetRotation = function () {
if (this.targetIsNull()) {
return null;
} else {
var props = Entities.getEntityProperties(this.targetEntityID, ["rotation"]);
return props.rotation;
}
};
this.getOffhand = function () {
return (this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND);
}
// Activation criteria for rotating a fargrabbed entity. If we're changing the mapping, this is where to do it.
this.shouldManipulateTarget = function (controllerData) {
return (controllerData.triggerValues[this.getOffhand()] > TRIGGER_ON_VALUE || controllerData.secondaryValues[this.getOffhand()] > TRIGGER_ON_VALUE) ? true : false;
};
// Get the delta between the current rotation and where the controller was when manipulation started.
this.calculateEntityRotationManipulation = function (controllerRotation) {
return Quat.multiply(controllerRotation, Quat.inverse(this.initialControllerRotation));
};
this.setJointTranslation = function (newTargetPosLocal) {
MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal);
};
this.setJointRotation = function (newTargetRotLocal) {
MyAvatar.setJointRotation(FAR_GRAB_JOINTS[this.hand], newTargetRotLocal);
};
this.setJointRotation = function (newTargetRotLocal) {
MyAvatar.setJointRotation(FAR_GRAB_JOINTS[this.hand], newTargetRotLocal);
};
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.startFarGrabEntity = function (controllerData, targetProps) {
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 = targetProps.position;
this.currentObjectRotation = targetProps.rotation;
this.currentObjectTime = now;
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(targetProps.dimensions, targetProps.density);
// Debounce haptic pules. Can occur as near grab controller module vacillates between being ready or not due to
// changing positions and floating point rounding.
if (Date.now() - this.endedGrab > this.MIN_HAPTIC_PULSE_INTERVAL) {
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
}
unhighlightTargetEntity(this.targetEntityID);
var message = {
hand: this.hand,
entityID: this.targetEntityID
};
Messages.sendLocalMessage('Hifi-unhighlight-entity', JSON.stringify(message));
var newTargetPosLocal = MyAvatar.worldToJointPoint(targetProps.position);
var newTargetRotLocal = targetProps.rotation;
this.setJointTranslation(newTargetPosLocal);
this.setJointRotation(newTargetRotLocal);
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(targetProps.id, "startDistanceGrab", args);
this.targetEntityID = targetProps.id;
if (this.grabID) {
MyAvatar.releaseGrab(this.grabID);
}
var farJointIndex = FAR_GRAB_JOINTS[this.hand];
this.grabID = MyAvatar.grab(targetProps.id, farJointIndex,
Entities.worldToLocalPosition(targetProps.position, MyAvatar.SELF_ID, farJointIndex),
Entities.worldToLocalRotation(targetProps.rotation, MyAvatar.SELF_ID, farJointIndex));
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'grab',
grabbedEntity: targetProps.id,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
this.grabbing = true;
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 targetProps = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
var now = Date.now();
var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds
this.currentObjectTime = now;
// the action was set up when this.distanceHolding was called. update the targets.
var radius = Vec3.distance(this.currentObjectPosition, 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);
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "continueDistanceGrab", args);
// Update radialVelocity
var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime);
var delta = Vec3.normalize(Vec3.subtract(targetProps.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);
var newTargetPosLocal = MyAvatar.worldToJointPoint(newTargetPosition);
// This block handles the user's ability to rotate the object they're FarGrabbing
if (this.shouldManipulateTarget(controllerData)) {
// Get the pose of the controller that is not grabbing.
var pose = Controller.getPoseValue((this.getOffhand() ? Controller.Standard.RightHand : Controller.Standard.LeftHand));
if (pose.valid) {
// If we weren't manipulating the object yet, initialize the entity's original position.
if (!this.manipulating) {
// This will only be triggered if we've let go of the off-hand trigger and pulled it again without ending a grab.
// Need to poll the entity's rotation again here.
if (!this.wasManipulating) {
this.initialEntityRotation = this.getTargetRotation();
}
// Save the original controller orientation, we only care about the delta between this rotation and wherever
// the controller rotates, so that we can apply it to the entity's rotation.
this.initialControllerRotation = Quat.multiply(pose.rotation, MyAvatar.orientation);
this.manipulating = true;
}
}
var rot = Quat.multiply(pose.rotation, MyAvatar.orientation);
var rotBetween = this.calculateEntityRotationManipulation(rot);
var doubleRot = Quat.multiply(rotBetween, rotBetween);
this.lastJointRotation = Quat.multiply(doubleRot, this.initialEntityRotation);
this.setJointRotation(this.lastJointRotation);
} else {
// If we were manipulating but the user isn't currently expressing this intent, we want to know so we preserve the rotation
// between manipulations without ending the fargrab.
if (this.manipulating) {
this.initialEntityRotation = this.lastJointRotation;
this.wasManipulating = true;
}
this.manipulating = false;
// Reset the inital controller position.
this.initialControllerRotation = Quat.IDENTITY;
}
this.setJointTranslation(newTargetPosLocal);
this.previousRoomControllerPosition = roomControllerPosition;
};
this.endFarGrabEntity = function (controllerData) {
if (this.grabID) {
MyAvatar.releaseGrab(this.grabID);
this.grabID = null;
}
this.endedGrab = Date.now();
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args);
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'release',
grabbedEntity: this.targetEntityID,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
unhighlightTargetEntity(this.targetEntityID);
this.grabbing = false;
this.potentialEntityWithContextOverlay = false;
MyAvatar.clearJointData(FAR_GRAB_JOINTS[this.hand]);
this.initialEntityRotation = Quat.IDENTITY;
this.initialControllerRotation = Quat.IDENTITY;
this.targetEntityID = null;
this.manipulating = false;
this.wasManipulating = false;
var otherModule = this.getOtherModule();
otherModule.disabled = false;
};
this.updateRecommendedArea = function () {
var dims = Controller.getViewportDimensions();
this.reticleMaxX = dims.x - MARGIN;
this.reticleMaxY = dims.y - MARGIN;
};
this.calculateNewReticlePosition = function (intersection) {
this.updateRecommendedArea();
var point2d = HMD.overlayFromWorldPoint(intersection);
point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX));
point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY));
return point2d;
};
this.notPointingAtEntity = function (controllerData) {
var intersection = controllerData.rayPicks[this.hand];
var entityProperty = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
var entityType = entityProperty.type;
var hudRayPick = controllerData.hudRayPicks[this.hand];
var point2d = this.calculateNewReticlePosition(hudRayPick.intersection);
if ((intersection.type === Picks.INTERSECTED_ENTITY && entityType === "Web") ||
intersection.type === Picks.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) {
return true;
}
return false;
};
this.destroyContextOverlay = function (controllerData) {
if (this.entityWithContextOverlay) {
ContextOverlay.destroyContextOverlay(this.entityWithContextOverlay);
this.entityWithContextOverlay = false;
this.potentialEntityWithContextOverlay = false;
}
};
this.targetIsNull = function () {
var properties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
if (Object.keys(properties).length === 0 && this.distanceHolding) {
return true;
}
return false;
};
this.getTargetProps = function (controllerData) {
var targetEntity = controllerData.rayPicks[this.hand].objectID;
if (targetEntity) {
var gtProps = Entities.getEntityProperties(targetEntity, DISPATCHER_PROPERTIES);
if (entityIsGrabbable(gtProps)) {
// if we've attempted to grab a child, roll up to the root of the tree
var groupRootProps = findGrabbableGroupParent(controllerData, gtProps);
if (entityIsGrabbable(groupRootProps)) {
return groupRootProps;
}
return gtProps;
}
}
return null;
};
this.isReady = function (controllerData) {
if (HMD.active) {
if (this.notPointingAtEntity(controllerData)) {
return makeRunningValues(false, [], []);
}
this.distanceHolding = false;
if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE && !this.disabled) {
var otherModule = this.getOtherModule();
otherModule.disabled = true;
return makeRunningValues(true, [], []);
} else {
this.destroyContextOverlay();
}
}
return makeRunningValues(false, [], []);
};
this.run = function (controllerData) {
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.targetIsNull()) {
this.endFarGrabEntity(controllerData);
return makeRunningValues(false, [], []);
}
this.intersectionDistance = controllerData.rayPicks[this.hand].distance;
// 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 ? "RightNearGrabEntity" : "LeftNearGrabEntity"
];
if (!this.grabbing) {
nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay");
nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight");
}
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.targetEntityID) {
// 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.targetEntityID)) {
this.endFarGrabEntity(controllerData);
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.endFarGrabEntity(controllerData);
return makeRunningValues(false, [], []);
}
}
var rayPickInfo = controllerData.rayPicks[this.hand];
if (rayPickInfo.type === Picks.INTERSECTED_ENTITY) {
if (controllerData.triggerClicks[this.hand]) {
var entityID = rayPickInfo.objectID;
var targetProps = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES);
if (targetProps.href !== "") {
AddressManager.handleLookupString(targetProps.href);
return makeRunningValues(false, [], []);
}
this.targetObject = new TargetObject(entityID, targetProps);
this.targetObject.parentProps = getEntityParents(targetProps);
if (this.contextOverlayTimer) {
Script.clearTimeout(this.contextOverlayTimer);
}
this.contextOverlayTimer = false;
if (entityID === this.entityWithContextOverlay) {
this.destroyContextOverlay();
} else {
Selection.removeFromSelectedItemsList("contextOverlayHighlightList", "entity", entityID);
}
var targetEntity = this.targetObject.getTargetEntity();
entityID = targetEntity.id;
targetProps = targetEntity.props;
if (entityIsGrabbable(targetProps) || entityIsGrabbable(this.targetObject.entityProps)) {
this.targetEntityID = entityID;
this.grabbedDistance = rayPickInfo.distance;
this.distanceHolding = true;
this.startFarGrabEntity(controllerData, targetProps);
}
} else if (!this.entityWithContextOverlay) {
var _this = this;
if (_this.potentialEntityWithContextOverlay !== rayPickInfo.objectID) {
if (_this.contextOverlayTimer) {
Script.clearTimeout(_this.contextOverlayTimer);
}
_this.contextOverlayTimer = false;
_this.potentialEntityWithContextOverlay = rayPickInfo.objectID;
}
if (!_this.contextOverlayTimer) {
_this.contextOverlayTimer = Script.setTimeout(function () {
if (!_this.entityWithContextOverlay &&
_this.contextOverlayTimer &&
_this.potentialEntityWithContextOverlay === rayPickInfo.objectID) {
var cotProps = Entities.getEntityProperties(rayPickInfo.objectID,
DISPATCHER_PROPERTIES);
var pointerEvent = {
type: "Move",
id: _this.hand + 1, // 0 is reserved for hardware mouse
pos2D: projectOntoEntityXYPlane(rayPickInfo.objectID,
rayPickInfo.intersection, cotProps),
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);
}
}
}
}
return this.exitIfDisabled(controllerData);
};
this.exitIfDisabled = function (controllerData) {
var moduleName = this.hand === RIGHT_HAND ? "RightDisableModules" : "LeftDisableModules";
var disableModule = getEnabledModuleByName(moduleName);
if (disableModule) {
if (disableModule.disableModules) {
this.endFarGrabEntity(controllerData);
Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
this.highlightedEntity = null;
return makeRunningValues(false, [], []);
}
}
var grabbedThing = this.distanceHolding ? this.targetObject.entityID : null;
var offset = this.calculateOffset(controllerData);
var laserLockInfo = makeLaserLockInfo(grabbedThing, false, this.hand, offset);
return makeRunningValues(true, [], [], laserLockInfo);
};
this.calculateOffset = function (controllerData) {
if (this.distanceHolding) {
var targetProps = Entities.getEntityProperties(this.targetObject.entityID,
["position", "rotation", "registrationPoint", "dimensions"]);
return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection);
}
return undefined;
};
}
var leftFarGrabEntity = new FarGrabEntity(LEFT_HAND);
var rightFarGrabEntity = new FarGrabEntity(RIGHT_HAND);
enableDispatcherModule("LeftFarGrabEntity", leftFarGrabEntity);
enableDispatcherModule("RightFarGrabEntity", rightFarGrabEntity);
function cleanup() {
disableDispatcherModule("LeftFarGrabEntity");
disableDispatcherModule("RightFarGrabEntity");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -0,0 +1,102 @@
"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, RIGHT_HAND, LEFT_HAND, MyAvatar,
makeRunningValues, Entities, enableDispatcherModule, disableDispatcherModule, makeDispatcherModuleParameters,
getGrabbableData, makeLaserParams, DISPATCHER_PROPERTIES
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
function entityWantsFarTrigger(props) {
var grabbableData = getGrabbableData(props);
return grabbableData.triggerable;
}
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,
makeLaserParams(this.hand, false));
this.getTargetProps = function (controllerData) {
var targetEntity = controllerData.rayPicks[this.hand].objectID;
if (targetEntity && controllerData.rayPicks[this.hand].type === RayPick.INTERSECTED_ENTITY) {
var targetProperties = Entities.getEntityProperties(targetEntity, DISPATCHER_PROPERTIES);
if (entityWantsFarTrigger(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.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.continueFarTrigger(controllerData);
return makeRunningValues(true, [this.targetEntityID], []);
};
}
var leftFarTriggerEntity = new FarTriggerEntity(LEFT_HAND);
var rightFarTriggerEntity = new FarTriggerEntity(RIGHT_HAND);
enableDispatcherModule("LeftFarTriggerEntity", leftFarTriggerEntity);
enableDispatcherModule("RightFarTriggerEntity", rightFarTriggerEntity);
function cleanup() {
disableDispatcherModule("LeftFarTriggerEntity");
disableDispatcherModule("RightFarTriggerEntity");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -0,0 +1,126 @@
//
// hudOverlayPointer.js
//
// scripts/system/controllers/controllerModules/
//
// Created by Dante Ruiz 2017-9-21
// Copyright 2017 High Fidelity, Inc.
//
// 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, RIGHT_HAND, LEFT_HAND, HMD, makeLaserParams */
(function() {
Script.include("/~/system/libraries/controllers.js");
var ControllerDispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js");
var MARGIN = 25;
var HUD_LASER_OFFSET = 2;
function HudOverlayPointer(hand) {
this.hand = hand;
this.running = false;
this.reticleMinX = MARGIN;
this.reticleMaxX;
this.reticleMinY = MARGIN;
this.reticleMaxY;
this.parameters = ControllerDispatcherUtils.makeDispatcherModuleParameters(
160, // Same as webSurfaceLaserInput.
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100,
makeLaserParams((this.hand + HUD_LASER_OFFSET), false));
this.getFarGrab = function () {
return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("RightFarGrabEntity") : ("LeftFarGrabEntity"));
}
this.farGrabActive = function () {
var farGrab = this.getFarGrab();
// farGrab will be null if module isn't loaded.
if (farGrab) {
return farGrab.targetIsNull();
} else {
return false;
}
};
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.updateRecommendedArea = function() {
var dims = Controller.getViewportDimensions();
this.reticleMaxX = dims.x - MARGIN;
this.reticleMaxY = dims.y - MARGIN;
};
this.calculateNewReticlePosition = function(intersection) {
this.updateRecommendedArea();
var point2d = HMD.overlayFromWorldPoint(intersection);
point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX));
point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY));
return point2d;
};
this.pointingAtTablet = function(controllerData) {
var rayPick = controllerData.rayPicks[this.hand];
return (HMD.tabletScreenID && HMD.homeButtonID && (rayPick.objectID === HMD.tabletScreenID || rayPick.objectID === HMD.homeButtonID));
};
this.getOtherModule = function() {
return this.hand === RIGHT_HAND ? leftHudOverlayPointer : rightHudOverlayPointer;
};
this.processLaser = function(controllerData) {
var controllerLocation = controllerData.controllerLocations[this.hand];
if ((controllerData.triggerValues[this.hand] < ControllerDispatcherUtils.TRIGGER_ON_VALUE || !controllerLocation.valid) ||
this.pointingAtTablet(controllerData)) {
return false;
}
var hudRayPick = controllerData.hudRayPicks[this.hand];
var point2d = this.calculateNewReticlePosition(hudRayPick.intersection);
if (!Window.isPointOnDesktopWindow(point2d) && !this.triggerClicked) {
return false;
}
this.triggerClicked = controllerData.triggerClicks[this.hand];
return true;
};
this.isReady = function (controllerData) {
var otherModuleRunning = this.getOtherModule().running;
if (!otherModuleRunning && HMD.active && !this.farGrabActive()) {
if (this.processLaser(controllerData)) {
this.running = true;
return ControllerDispatcherUtils.makeRunningValues(true, [], []);
} else {
this.running = false;
return ControllerDispatcherUtils.makeRunningValues(false, [], []);
}
}
return ControllerDispatcherUtils.makeRunningValues(false, [], []);
};
this.run = function (controllerData, deltaTime) {
return this.isReady(controllerData);
};
}
var leftHudOverlayPointer = new HudOverlayPointer(LEFT_HAND);
var rightHudOverlayPointer = new HudOverlayPointer(RIGHT_HAND);
ControllerDispatcherUtils.enableDispatcherModule("LeftHudOverlayPointer", leftHudOverlayPointer);
ControllerDispatcherUtils.enableDispatcherModule("RightHudOverlayPointer", rightHudOverlayPointer);
function cleanup() {
ControllerDispatcherUtils.disableDispatcherModule("LeftHudOverlayPointer");
ControllerDispatcherUtils.disableDispatcherModule("RightHudOverlayPointer");
}
Script.scriptEnding.connect(cleanup);
})();

View file

@ -0,0 +1,253 @@
"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
/* jslint bitwise: true */
/* global Script, Controller, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, makeRunningValues,
Messages, makeDispatcherModuleParameters, HMD, getEnabledModuleByName, TRIGGER_ON_VALUE, isInEditMode, Picks,
makeLaserParams
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
Script.include("/~/system/libraries/utils.js");
(function () {
var MARGIN = 25;
function InEditMode(hand) {
this.hand = hand;
this.isEditing = false;
this.triggerClicked = false;
this.selectedTarget = null;
this.reticleMinX = MARGIN;
this.reticleMaxX = null;
this.reticleMinY = MARGIN;
this.reticleMaxY = null;
this.parameters = makeDispatcherModuleParameters(
165, // Lower priority than webSurfaceLaserInput and hudOverlayPointer.
this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"],
[],
100,
makeLaserParams(this.hand, false));
this.nearTablet = function(overlays) {
for (var i = 0; i < overlays.length; i++) {
if (HMD.tabletID && overlays[i] === HMD.tabletID) {
return true;
}
}
return false;
};
this.handToController = function() {
return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
};
this.pointingAtTablet = function(objectID) {
return (HMD.tabletScreenID && objectID === HMD.tabletScreenID) ||
(HMD.homeButtonID && objectID === HMD.homeButtonID);
};
this.calculateNewReticlePosition = function(intersection) {
var dims = Controller.getViewportDimensions();
this.reticleMaxX = dims.x - MARGIN;
this.reticleMaxY = dims.y - MARGIN;
var point2d = HMD.overlayFromWorldPoint(intersection);
point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX));
point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY));
return point2d;
};
this.ENTITY_TOOL_UPDATES_CHANNEL = "entityToolUpdates";
this.sendPickData = function(controllerData) {
if (controllerData.triggerClicks[this.hand]) {
var hand = this.hand === RIGHT_HAND ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
if (!this.triggerClicked) {
this.selectedTarget = controllerData.rayPicks[this.hand];
if (!this.selectedTarget.intersects) {
Messages.sendLocalMessage(this.ENTITY_TOOL_UPDATES_CHANNEL, JSON.stringify({
method: "clearSelection",
hand: hand
}));
} else {
if (this.selectedTarget.type === Picks.INTERSECTED_ENTITY) {
Messages.sendLocalMessage(this.ENTITY_TOOL_UPDATES_CHANNEL, JSON.stringify({
method: "selectEntity",
entityID: this.selectedTarget.objectID,
hand: hand
}));
} else if (this.selectedTarget.type === Picks.INTERSECTED_OVERLAY) {
Messages.sendLocalMessage(this.ENTITY_TOOL_UPDATES_CHANNEL, JSON.stringify({
method: "selectOverlay",
overlayID: this.selectedTarget.objectID,
hand: hand
}));
}
}
}
this.triggerClicked = true;
}
this.sendPointingAtData(controllerData);
};
this.sendPointingAtData = function(controllerData) {
var rayPick = controllerData.rayPicks[this.hand];
var hudRayPick = controllerData.hudRayPicks[this.hand];
var point2d = this.calculateNewReticlePosition(hudRayPick.intersection);
var desktopWindow = Window.isPointOnDesktopWindow(point2d);
var tablet = this.pointingAtTablet(rayPick.objectID);
var rightHand = this.hand === RIGHT_HAND;
Messages.sendLocalMessage(this.ENTITY_TOOL_UPDATES_CHANNEL, JSON.stringify({
method: "pointingAt",
desktopWindow: desktopWindow,
tablet: tablet,
rightHand: rightHand
}));
};
this.runModule = function() {
return makeRunningValues(true, [], []);
};
this.exitModule = function() {
return makeRunningValues(false, [], []);
};
this.isReady = function(controllerData) {
if (isInEditMode()) {
if (controllerData.triggerValues[this.hand] < TRIGGER_ON_VALUE) {
this.triggerClicked = false;
}
Messages.sendLocalMessage('Hifi-unhighlight-all', '');
return this.runModule();
}
this.triggerClicked = false;
return this.exitModule();
};
this.run = function(controllerData) {
// Tablet stylus.
var tabletStylusInput = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightTabletStylusInput" : "LeftTabletStylusInput");
if (tabletStylusInput) {
var tabletReady = tabletStylusInput.isReady(controllerData);
if (tabletReady.active) {
return this.exitModule();
}
}
// Tablet surface.
var webLaser = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightWebSurfaceLaserInput" : "LeftWebSurfaceLaserInput");
if (webLaser) {
var webLaserReady = webLaser.isReady(controllerData);
var target = controllerData.rayPicks[this.hand].objectID;
this.sendPointingAtData(controllerData);
if (webLaserReady.active && this.pointingAtTablet(target)) {
return this.exitModule();
}
}
// HUD overlay.
if (!controllerData.triggerClicks[this.hand]) { // Don't grab if trigger pressed when laser starts intersecting.
var hudLaser = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightHudOverlayPointer" : "LeftHudOverlayPointer");
if (hudLaser) {
var hudLaserReady = hudLaser.isReady(controllerData);
if (hudLaserReady.active) {
return this.exitModule();
}
}
}
// Tablet highlight and grabbing.
var tabletHighlight = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightNearTabletHighlight" : "LeftNearTabletHighlight");
if (tabletHighlight) {
var tabletHighlightReady = tabletHighlight.isReady(controllerData);
if (tabletHighlightReady.active) {
return this.exitModule();
}
}
// Teleport.
var teleport = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightTeleporter" : "LeftTeleporter");
if (teleport) {
var teleportReady = teleport.isReady(controllerData);
if (teleportReady.active) {
return this.exitModule();
}
}
if ((controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0)) {
var stopRunning = false;
controllerData.nearbyOverlayIDs[this.hand].forEach(function(overlayID) {
var overlayName = Overlays.getProperty(overlayID, "name");
if (overlayName === "KeyboardAnchor") {
stopRunning = true;
}
});
if (stopRunning) {
return this.exitModule();
}
}
this.sendPickData(controllerData);
return this.isReady(controllerData);
};
}
var leftHandInEditMode = new InEditMode(LEFT_HAND);
var rightHandInEditMode = new InEditMode(RIGHT_HAND);
enableDispatcherModule("LeftHandInEditMode", leftHandInEditMode);
enableDispatcherModule("RightHandInEditMode", rightHandInEditMode);
var INEDIT_STATUS_CHANNEL = "Hifi-InEdit-Status";
var HAND_RAYPICK_BLACKLIST_CHANNEL = "Hifi-Hand-RayPick-Blacklist";
this.handleMessage = function (channel, data, sender) {
if (channel === INEDIT_STATUS_CHANNEL && sender === MyAvatar.sessionUUID) {
var message;
try {
message = JSON.parse(data);
} catch (e) {
return;
}
switch (message.method) {
case "editing":
if (message.hand === LEFT_HAND) {
leftHandInEditMode.isEditing = message.editing;
} else {
rightHandInEditMode.isEditing = message.editing;
}
Messages.sendLocalMessage(HAND_RAYPICK_BLACKLIST_CHANNEL, JSON.stringify({
action: "tablet",
hand: message.hand,
blacklist: message.editing
}));
break;
}
}
};
Messages.subscribe(INEDIT_STATUS_CHANNEL);
Messages.messageReceived.connect(this.handleMessage);
function cleanup() {
disableDispatcherModule("LeftHandInEditMode");
disableDispatcherModule("RightHandInEditMode");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -0,0 +1,185 @@
"use strict";
// inVREditMode.js
//
// Created by David Rowe on 16 Sep 2017.
// Copyright 2017 High Fidelity, Inc.
//
// 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, HMD, Messages, MyAvatar, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule,
makeDispatcherModuleParameters, makeRunningValues, getEnabledModuleByName, makeLaserParams
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
(function () {
function InVREditMode(hand) {
this.hand = hand;
this.isAppActive = false;
this.isEditing = false;
this.running = false;
var NO_HAND_LASER = -1; // Invalid hand parameter so that standard laser is not displayed.
this.parameters = makeDispatcherModuleParameters(
166, // Slightly lower priority than inEditMode.
this.hand === RIGHT_HAND
? ["rightHand", "rightHandEquip", "rightHandTrigger"]
: ["leftHand", "leftHandEquip", "leftHandTrigger"],
[],
100,
makeLaserParams(NO_HAND_LASER, false)
);
this.pointingAtTablet = function (objectID) {
return (HMD.tabletScreenID && objectID === HMD.tabletScreenID) ||
(HMD.homeButtonID && objectID === HMD.homeButtonID);
};
// The Shapes app has a non-standard laser: in particular, the laser end dot displays on its own when the laser is
// pointing at the Shapes UI. The laser on/off is controlled by this module but the laser is implemented in the Shapes
// app.
// If, in the future, the Shapes app laser interaction is adopted as a standard UI style then the laser could be
// implemented in the controller modules along side the other laser styles.
var INVREDIT_MODULE_RUNNING = "Hifi-InVREdit-Module-Running";
this.runModule = function () {
if (!this.running) {
Messages.sendLocalMessage(INVREDIT_MODULE_RUNNING, JSON.stringify({
hand: this.hand,
running: true
}));
this.running = true;
}
return makeRunningValues(true, [], []);
};
this.exitModule = function () {
if (this.running) {
Messages.sendLocalMessage(INVREDIT_MODULE_RUNNING, JSON.stringify({
hand: this.hand,
running: false
}));
this.running = false;
}
return makeRunningValues(false, [], []);
};
this.isReady = function (controllerData) {
if (this.isAppActive) {
return makeRunningValues(true, [], []);
}
return makeRunningValues(false, [], []);
};
this.run = function (controllerData) {
// Default behavior if disabling is not enabled.
if (!this.isAppActive) {
return this.exitModule();
}
// Tablet stylus.
var tabletStylusInput = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightTabletStylusInput" : "LeftTabletStylusInput");
if (tabletStylusInput) {
var tabletReady = tabletStylusInput.isReady(controllerData);
if (tabletReady.active) {
return this.exitModule();
}
}
// Tablet surface.
var overlayLaser = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightWebSurfaceLaserInput" : "LeftWebSurfaceLaserInput");
if (overlayLaser) {
var overlayLaserReady = overlayLaser.isReady(controllerData);
var target = controllerData.rayPicks[this.hand].objectID;
if (overlayLaserReady.active && this.pointingAtTablet(target)) {
return this.exitModule();
}
}
// Tablet highlight and grabbing.
var tabletHighlight = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightNearTabletHighlight" : "LeftNearTabletHighlight");
if (tabletHighlight) {
var tabletHighlightReady = tabletHighlight.isReady(controllerData);
if (tabletHighlightReady.active) {
return this.exitModule();
}
}
// HUD overlay.
if (!controllerData.triggerClicks[this.hand]) {
var hudLaser = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightHudOverlayPointer" : "LeftHudOverlayPointer");
if (hudLaser) {
var hudLaserReady = hudLaser.isReady(controllerData);
if (hudLaserReady.active) {
return this.exitModule();
}
}
}
// Teleport.
var teleporter = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightTeleporter" : "LeftTeleporter");
if (teleporter) {
var teleporterReady = teleporter.isReady(controllerData);
if (teleporterReady.active) {
return this.exitModule();
}
}
// Other behaviors are disabled.
return this.runModule();
};
}
var leftHandInVREditMode = new InVREditMode(LEFT_HAND);
var rightHandInVREditMode = new InVREditMode(RIGHT_HAND);
enableDispatcherModule("LeftHandInVREditMode", leftHandInVREditMode);
enableDispatcherModule("RightHandInVREditMode", rightHandInVREditMode);
var INVREDIT_STATUS_CHANNEL = "Hifi-InVREdit-Status";
var HAND_RAYPICK_BLACKLIST_CHANNEL = "Hifi-Hand-RayPick-Blacklist";
this.handleMessage = function (channel, data, sender) {
if (channel === INVREDIT_STATUS_CHANNEL && sender === MyAvatar.sessionUUID) {
var message;
try {
message = JSON.parse(data);
} catch (e) {
return;
}
switch (message.method) {
case "active":
leftHandInVREditMode.isAppActive = message.active;
rightHandInVREditMode.isAppActive = message.active;
break;
case "editing":
if (message.hand === LEFT_HAND) {
leftHandInVREditMode.isEditing = message.editing;
} else {
rightHandInVREditMode.isEditing = message.editing;
}
Messages.sendLocalMessage(HAND_RAYPICK_BLACKLIST_CHANNEL, JSON.stringify({
action: "tablet",
hand: message.hand,
blacklist: message.editing
}));
break;
}
}
};
Messages.subscribe(INVREDIT_STATUS_CHANNEL);
Messages.messageReceived.connect(this.handleMessage);
this.cleanup = function () {
disableDispatcherModule("LeftHandInVREditMode");
disableDispatcherModule("RightHandInVREditMode");
};
Script.scriptEnding.connect(this.cleanup);
}());

View file

@ -0,0 +1,151 @@
//
// mouseHMD.js
//
// scripts/system/controllers/controllerModules/
//
// Created by Dante Ruiz 2017-9-22
// Copyright 2017 High Fidelity, Inc.
//
// 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, HMD, Reticle, Vec3, Controller */
(function() {
var ControllerDispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js");
function TimeLock(experation) {
this.experation = experation;
this.last = 0;
this.update = function(time) {
this.last = time || Date.now();
};
this.expired = function(time) {
return ((time || Date.now()) - this.last) > this.experation;
};
}
function MouseHMD() {
var _this = this;
this.hmdWasActive = HMD.active;
this.mouseMoved = false;
this.mouseActivity = new TimeLock(5000);
this.handControllerActivity = new TimeLock(4000);
this.parameters = ControllerDispatcherUtils.makeDispatcherModuleParameters(
10,
["mouse"],
[],
100);
this.onMouseMove = function() {
_this.updateMouseActivity();
};
this.onMouseClick = function() {
_this.updateMouseActivity();
};
this.updateMouseActivity = function(isClick) {
if (_this.ignoreMouseActivity()) {
return;
}
if (HMD.active) {
var now = Date.now();
_this.mouseActivity.update(now);
}
};
this.adjustReticleDepth = function(controllerData) {
if (Reticle.isPointingAtSystemOverlay(Reticle.position)) {
var reticlePositionOnHUD = HMD.worldPointFromOverlay(Reticle.position);
Reticle.depth = Vec3.distance(reticlePositionOnHUD, HMD.position);
} else {
var APPARENT_MAXIMUM_DEPTH = 100.0;
var result = controllerData.mouseRayPick;
Reticle.depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH;
}
};
this.ignoreMouseActivity = function() {
if (!Reticle.allowMouseCapture) {
return true;
}
var pos = Reticle.position;
if (!pos || (pos.x === -1 && pos.y === -1)) {
return true;
}
if (!_this.handControllerActivity.expired()) {
return true;
}
return false;
};
this.triggersPressed = function(controllerData, now) {
var onValue = ControllerDispatcherUtils.TRIGGER_ON_VALUE;
var rightHand = ControllerDispatcherUtils.RIGHT_HAND;
var leftHand = ControllerDispatcherUtils.LEFT_HAND;
var leftTriggerValue = controllerData.triggerValues[leftHand];
var rightTriggerValue = controllerData.triggerValues[rightHand];
if (leftTriggerValue > onValue || rightTriggerValue > onValue) {
this.handControllerActivity.update(now);
return true;
}
return false;
};
this.isReady = function(controllerData, deltaTime) {
var now = Date.now();
var hmdChanged = this.hmdWasActive !== HMD.active;
this.hmdWasActive = HMD.active;
this.triggersPressed(controllerData, now);
if (HMD.active) {
if (!this.mouseActivity.expired(now) && _this.handControllerActivity.expired()) {
Reticle.visible = true;
return ControllerDispatcherUtils.makeRunningValues(true, [], []);
} else {
Reticle.visible = false;
}
} else if (hmdChanged && !Reticle.visible) {
Reticle.visible = true;
}
return ControllerDispatcherUtils.makeRunningValues(false, [], []);
};
this.run = function(controllerData, deltaTime) {
var now = Date.now();
var hmdActive = HMD.active;
if (this.mouseActivity.expired(now) || this.triggersPressed(controllerData, now) || !hmdActive) {
if (!hmdActive) {
Reticle.visible = true;
} else {
Reticle.visible = false;
}
return ControllerDispatcherUtils.makeRunningValues(false, [], []);
}
this.adjustReticleDepth(controllerData);
return ControllerDispatcherUtils.makeRunningValues(true, [], []);
};
}
var mouseHMD = new MouseHMD();
ControllerDispatcherUtils.enableDispatcherModule("MouseHMD", mouseHMD);
Controller.mouseMoveEvent.connect(mouseHMD.onMouseMove);
Controller.mousePressEvent.connect(mouseHMD.onMouseClick);
function cleanup() {
ControllerDispatcherUtils.disableDispatcherModule("MouseHMD");
}
Script.scriptEnding.connect(cleanup);
})();

View file

@ -0,0 +1,226 @@
"use strict";
// nearGrabEntity.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, enableDispatcherModule,
disableDispatcherModule, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE,
makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, findGrabbableGroupParent, Vec3,
cloneEntity, entityIsCloneable, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE,
distanceBetweenPointAndEntityBoundingBox, getGrabbableData, getEnabledModuleByName, DISPATCHER_PROPERTIES, HMD,
NEAR_GRAB_DISTANCE
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/cloneEntityUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
function NearGrabEntity(hand) {
this.hand = hand;
this.targetEntityID = null;
this.grabbing = false;
this.cloneAllowed = true;
this.grabID = null;
this.parameters = makeDispatcherModuleParameters(
500,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
this.startGrab = function (targetProps) {
if (this.grabID) {
MyAvatar.releaseGrab(this.grabID);
}
var grabData = getGrabbableData(targetProps);
var handJointIndex;
if (HMD.mounted && HMD.isHandControllerAvailable() && grabData.grabFollowsController) {
handJointIndex = getControllerJointIndex(this.hand);
} else {
handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
}
this.targetEntityID = targetProps.id;
var relativePosition = Entities.worldToLocalPosition(targetProps.position, MyAvatar.SELF_ID, handJointIndex);
var relativeRotation = Entities.worldToLocalRotation(targetProps.rotation, MyAvatar.SELF_ID, handJointIndex);
this.grabID = MyAvatar.grab(targetProps.id, handJointIndex, relativePosition, relativeRotation);
};
this.startNearGrabEntity = function (targetProps) {
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
this.startGrab(targetProps);
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(targetProps.id, "startNearGrab", args);
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'grab',
grabbedEntity: targetProps.id,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
this.grabbing = true;
};
this.endGrab = function () {
if (this.grabID) {
MyAvatar.releaseGrab(this.grabID);
this.grabID = null;
}
};
this.endNearGrabEntity = function () {
this.endGrab();
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args);
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'release',
grabbedEntity: this.targetEntityID,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
this.grabbing = false;
this.targetEntityID = null;
};
this.getTargetProps = function (controllerData) {
// nearbyEntityProperties is already sorted by length from controller
var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand];
var sensorScaleFactor = MyAvatar.sensorToWorldScale;
var nearGrabDistance = NEAR_GRAB_DISTANCE * sensorScaleFactor;
var nearGrabRadius = NEAR_GRAB_RADIUS * sensorScaleFactor;
for (var i = 0; i < nearbyEntityProperties.length; i++) {
var props = nearbyEntityProperties[i];
var grabPosition = controllerData.controllerLocations[this.hand].position; // Is offset from hand position.
var dist = distanceBetweenPointAndEntityBoundingBox(grabPosition, props);
var distance = Vec3.distance(grabPosition, props.position);
if ((dist > nearGrabDistance) ||
(distance > nearGrabRadius)) { // Only smallish entities can be near grabbed.
continue;
}
if (entityIsGrabbable(props) || entityIsCloneable(props)) {
if (!entityIsCloneable(props)) {
// if we've attempted to grab a non-cloneable child, roll up to the root of the tree
var groupRootProps = findGrabbableGroupParent(controllerData, props);
if (entityIsGrabbable(groupRootProps)) {
return groupRootProps;
}
}
return props;
}
}
return null;
};
this.isReady = function (controllerData, deltaTime) {
this.targetEntityID = null;
this.grabbing = false;
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE &&
controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
this.cloneAllowed = true;
return makeRunningValues(false, [], []);
}
var scaleModuleName = this.hand === RIGHT_HAND ? "RightScaleEntity" : "LeftScaleEntity";
var scaleModule = getEnabledModuleByName(scaleModuleName);
if (scaleModule && (scaleModule.grabbedThingID || scaleModule.isReady(controllerData).active)) {
// we're rescaling -- don't start a grab.
return makeRunningValues(false, [], []);
}
var targetProps = this.getTargetProps(controllerData);
if (targetProps) {
this.targetEntityID = targetProps.id;
return makeRunningValues(true, [this.targetEntityID], []);
} else {
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.endNearGrabEntity();
return makeRunningValues(false, [], []);
}
var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID];
if (!props) {
props = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
if (!props) {
// entity was deleted
this.grabbing = false;
this.targetEntityID = null;
return makeRunningValues(false, [], []);
}
}
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args);
} else {
// still searching
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) {
if (this.cloneAllowed) {
var cloneID = cloneEntity(targetProps);
if (cloneID !== null) {
var cloneProps = Entities.getEntityProperties(cloneID, DISPATCHER_PROPERTIES);
cloneProps.id = cloneID;
this.grabbing = true;
this.targetEntityID = cloneID;
this.startNearGrabEntity(cloneProps);
this.cloneAllowed = false; // prevent another clone call until inputs released
}
}
} else if (targetProps) {
this.grabbing = true;
this.startNearGrabEntity(targetProps);
}
}
}
return makeRunningValues(true, [this.targetEntityID], []);
};
this.cleanup = function () {
if (this.targetEntityID) {
this.endNearGrabEntity();
}
};
}
var leftNearGrabEntity = new NearGrabEntity(LEFT_HAND);
var rightNearGrabEntity = new NearGrabEntity(RIGHT_HAND);
enableDispatcherModule("LeftNearGrabEntity", leftNearGrabEntity);
enableDispatcherModule("RightNearGrabEntity", rightNearGrabEntity);
function cleanup() {
leftNearGrabEntity.cleanup();
rightNearGrabEntity.cleanup();
disableDispatcherModule("LeftNearGrabEntity");
disableDispatcherModule("RightNearGrabEntity");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -0,0 +1,91 @@
"use strict";
// nearGrabHyperLinkEntity.js
//
// Created by Dante Ruiz on 03/02/2018
//
// 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, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule,
makeDispatcherModuleParameters, makeRunningValues, TRIGGER_OFF_VALUE, NEAR_GRAB_RADIUS, BUMPER_ON_VALUE, AddressManager
*/
(function() {
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
function NearGrabHyperLinkEntity(hand) {
this.hand = hand;
this.targetEntityID = null;
this.hyperlink = "";
this.parameters = makeDispatcherModuleParameters(
485,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
this.getTargetProps = function(controllerData) {
var nearbyEntitiesProperties = controllerData.nearbyEntityProperties[this.hand];
var sensorScaleFactor = MyAvatar.sensorToWorldScale;
for (var i = 0; i < nearbyEntitiesProperties.length; i++) {
var props = nearbyEntitiesProperties[i];
if (props.distance > NEAR_GRAB_RADIUS * sensorScaleFactor) {
continue;
}
if (props.href !== "" && props.href !== undefined) {
return props;
}
}
return null;
};
this.isReady = function(controllerData) {
this.targetEntityID = null;
if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE &&
controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
return makeRunningValues(false, [], []);
}
var targetProps = this.getTargetProps(controllerData);
if (targetProps) {
this.hyperlink = targetProps.href;
this.targetEntityID = targetProps.id;
return makeRunningValues(true, [], []);
}
return makeRunningValues(false, [], []);
};
this.run = function(controllerData) {
if ((controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE &&
controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) || this.hyperlink === "") {
return makeRunningValues(false, [], []);
}
if (controllerData.triggerClicks[this.hand] ||
controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) {
AddressManager.handleLookupString(this.hyperlink);
return makeRunningValues(false, [], []);
}
return makeRunningValues(true, [], []);
};
}
var leftNearGrabHyperLinkEntity = new NearGrabHyperLinkEntity(LEFT_HAND);
var rightNearGrabHyperLinkEntity = new NearGrabHyperLinkEntity(RIGHT_HAND);
enableDispatcherModule("LeftNearGrabHyperLink", leftNearGrabHyperLinkEntity);
enableDispatcherModule("RightNearGrabHyperLink", rightNearGrabHyperLinkEntity);
function cleanup() {
disableDispatcherModule("LeftNearGrabHyperLink");
disableDispatcherModule("RightNearGrabHyperLink");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -0,0 +1,255 @@
"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, getControllerJointIndex,
enableDispatcherModule, disableDispatcherModule, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION,
makeDispatcherModuleParameters, Overlays, makeRunningValues, Vec3, resizeTablet, getTabletWidthFromSettings,
NEAR_GRAB_RADIUS, HMD, Uuid, getEnabledModuleByName
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/utils.js");
(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.robbed = false;
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.getOtherModule = function() {
return (this.hand === RIGHT_HAND) ? leftNearParentingGrabOverlay : rightNearParentingGrabOverlay;
};
this.otherHandIsParent = function(props) {
return this.getOtherModule().thisHandIsParent(props);
};
this.isGrabbedThingVisible = function() {
return Overlays.getProperty(this.grabbedThingID, "visible");
};
this.thisHandIsParent = function(props) {
if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== MyAvatar.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);
this.controllerJointIndex = getControllerJointIndex(this.hand);
var handJointIndex = this.controllerJointIndex;
var grabbedProperties = this.getGrabbedProperties();
var reparentProps = {
parentID: MyAvatar.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.grabbedThingID];
this.previousParentJointIndex[this.grabbedThingID] = otherModule.previousParentJointIndex[this.grabbedThingID];
otherModule.robbed = true;
} else {
this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID;
this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex;
}
// resizeTablet to counter adjust offsets to account for change of scale from sensorToWorldMatrix
if (HMD.tabletID && this.grabbedThingID === HMD.tabletID) {
reparentAndScaleTablet(getTabletWidthFromSettings(), reparentProps);
} else {
Entities.editEntity(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 === Uuid.NULL || previousParentID === null) && !this.robbed) {
Overlays.editOverlay(this.grabbedThingID, {
parentID: Uuid.NULL,
parentJointIndex: -1
});
} else if (!this.robbed){
// before we grabbed it, overlay was a child of something; put it back.
Entities.editEntity(this.grabbedThingID, {
parentID: this.previousParentID[this.grabbedThingID],
parentJointIndex: this.previousParentJointIndex[this.grabbedThingID]
});
// resizeTablet to counter adjust offsets to account for change of scale from sensorToWorldMatrix
if (HMD.tabletID && this.grabbedThingID === HMD.tabletID) {
resizeTablet(getTabletWidthFromSettings(), this.previousParentJointIndex[this.grabbedThingID]);
}
}
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'release',
grabbedEntity: this.grabbedThingID,
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
}));
this.grabbedThingID = null;
};
this.getTargetID = function(overlays, controllerData) {
var sensorScaleFactor = MyAvatar.sensorToWorldScale;
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 <= NEAR_GRAB_RADIUS * sensorScaleFactor) {
if (overlays[i] !== HMD.miniTabletID || controllerData.secondaryValues[this.hand] === 0) {
// Don't grab mini tablet with grip.
return overlays[i];
}
}
}
return null;
};
this.isEditing = function () {
var inEditModeModule = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightHandInEditMode" : "LeftHandInEditMode");
if (inEditModeModule && inEditModeModule.isEditing) {
return true;
}
var inVREditModeModule = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightHandInVREditMode" : "LeftHandInVREditMode");
if (inVREditModeModule && inVREditModeModule.isEditing) {
return true;
}
return false;
};
this.isReady = function (controllerData) {
if ((controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0)
|| this.isEditing()) {
this.robbed = false;
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.robbed) {
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.isEditing() || !this.isGrabbedThingVisible()) {
this.endNearParentingGrabOverlay();
this.robbed = false;
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);
function cleanup() {
leftNearParentingGrabOverlay.cleanup();
rightNearParentingGrabOverlay.cleanup();
disableDispatcherModule("LeftNearParentingGrabOverlay");
disableDispatcherModule("RightNearParentingGrabOverlay");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -0,0 +1,135 @@
//
// nearTabletHighlight.js
//
// Highlight the tablet if a hand is near enough to grab it and it isn't grabbed.
//
// Created by David Rowe on 28 Aug 2018.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* global LEFT_HAND, RIGHT_HAND, makeDispatcherModuleParameters, makeRunningValues, enableDispatcherModule,
* disableDispatcherModule, getEnabledModuleByName */
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
(function () {
"use strict";
var TABLET_GRABBABLE_SELECTION_NAME = "tabletGrabbableSelection";
var TABLET_GRABBABLE_SELECTION_STYLE = {
outlineUnoccludedColor: { red: 0, green: 180, blue: 239 }, // #00b4ef
outlineUnoccludedAlpha: 1,
outlineOccludedColor: { red: 0, green: 0, blue: 0 },
outlineOccludedAlpha: 0,
fillUnoccludedColor: { red: 0, green: 0, blue: 0 },
fillUnoccludedAlpha: 0,
fillOccludedColor: { red: 0, green: 0, blue: 0 },
fillOccludedAlpha: 0,
outlineWidth: 4,
isOutlineSmooth: false
};
var isTabletNearGrabbable = [false, false];
var isTabletHighlighted = false;
function setTabletNearGrabbable(hand, enabled) {
if (enabled === isTabletNearGrabbable[hand]) {
return;
}
isTabletNearGrabbable[hand] = enabled;
if (isTabletNearGrabbable[LEFT_HAND] || isTabletNearGrabbable[RIGHT_HAND]) {
if (!isTabletHighlighted) {
Selection.addToSelectedItemsList(TABLET_GRABBABLE_SELECTION_NAME, "overlay", HMD.tabletID);
isTabletHighlighted = true;
}
} else {
if (isTabletHighlighted) {
Selection.removeFromSelectedItemsList(TABLET_GRABBABLE_SELECTION_NAME, "overlay", HMD.tabletID);
isTabletHighlighted = false;
}
}
}
function NearTabletHighlight(hand) {
this.hand = hand;
this.parameters = makeDispatcherModuleParameters(
95,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100
);
this.isEditing = function () {
var inEditModeModule = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightHandInEditMode" : "LeftHandInEditMode");
if (inEditModeModule && inEditModeModule.isEditing) {
return true;
}
var inVREditModeModule = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightHandInVREditMode" : "LeftHandInVREditMode");
if (inVREditModeModule && inVREditModeModule.isEditing) {
return true;
}
return false;
};
this.isNearTablet = function (controllerData) {
return HMD.tabletID && controllerData.nearbyOverlayIDs[this.hand].indexOf(HMD.tabletID) !== -1;
};
this.isReady = function (controllerData) {
if (!this.isEditing() && this.isNearTablet(controllerData)) {
return makeRunningValues(true, [], []);
}
setTabletNearGrabbable(this.hand, false);
return makeRunningValues(false, [], []);
};
this.run = function (controllerData) {
if (this.isEditing() || !this.isNearTablet(controllerData)) {
setTabletNearGrabbable(this.hand, false);
return makeRunningValues(false, [], []);
}
if (controllerData.triggerClicks[this.hand] || controllerData.secondaryValues[this.hand]) {
setTabletNearGrabbable(this.hand, false);
return makeRunningValues(false, [], []);
}
setTabletNearGrabbable(this.hand, true);
return makeRunningValues(true, [], []);
};
}
var leftNearTabletHighlight = new NearTabletHighlight(LEFT_HAND);
var rightNearTabletHighlight = new NearTabletHighlight(RIGHT_HAND);
enableDispatcherModule("LeftNearTabletHighlight", leftNearTabletHighlight);
enableDispatcherModule("RightNearTabletHighlight", rightNearTabletHighlight);
function onDisplayModeChanged() {
if (HMD.active) {
Selection.enableListHighlight(TABLET_GRABBABLE_SELECTION_NAME, TABLET_GRABBABLE_SELECTION_STYLE);
} else {
Selection.disableListHighlight(TABLET_GRABBABLE_SELECTION_NAME);
Selection.clearSelectedItemsList(TABLET_GRABBABLE_SELECTION_NAME);
}
}
HMD.displayModeChanged.connect(onDisplayModeChanged);
HMD.mountedChanged.connect(onDisplayModeChanged);
onDisplayModeChanged();
function cleanUp() {
disableDispatcherModule("LeftNearTabletHighlight");
disableDispatcherModule("RightNearTabletHighlight");
Selection.disableListHighlight(TABLET_GRABBABLE_SELECTION_NAME);
}
Script.scriptEnding.connect(cleanUp);
}());

View file

@ -0,0 +1,120 @@
"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, 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;
}
function NearTriggerEntity(hand) {
this.hand = hand;
this.targetEntityID = null;
this.grabbing = false;
this.previousParentID = {};
this.previousParentJointIndex = {};
this.previouslyUnhooked = {};
this.startSent = false;
this.parameters = makeDispatcherModuleParameters(
480,
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];
var sensorScaleFactor = MyAvatar.sensorToWorldScale;
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 * sensorScaleFactor) {
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, "stopNearTrigger", 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;
return makeRunningValues(true, [this.targetEntityID], []);
} else {
return makeRunningValues(false, [], []);
}
};
this.run = function (controllerData) {
if (!this.startSent) {
this.startNearTrigger(controllerData);
this.startSent = true;
} else if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) {
this.endNearTrigger(controllerData);
this.startSent = false;
return makeRunningValues(false, [], []);
} else {
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);
function cleanup() {
leftNearTriggerEntity.cleanup();
rightNearTriggerEntity.cleanup();
disableDispatcherModule("LeftNearTriggerEntity");
disableDispatcherModule("RightNearTriggerEntity");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -0,0 +1,64 @@
"use strict";
// Created by Jason C. Najera on 3/7/2019
// Copyright 2019 High Fidelity, Inc.
//
// Handles Push-to-Talk functionality for HMD mode.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() { // BEGIN LOCAL_SCOPE
function PushToTalkHandler() {
var _this = this;
this.active = false;
this.shouldTalk = function (controllerData) {
// Set up test against controllerData here...
var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND];
return (gripVal) ? true : false;
};
this.shouldStopTalking = function (controllerData) {
var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND];
return (gripVal) ? false : true;
};
this.isReady = function (controllerData, deltaTime) {
if (HMD.active && Audio.pushToTalk && this.shouldTalk(controllerData)) {
Audio.pushingToTalk = true;
return makeRunningValues(true, [], []);
}
return makeRunningValues(false, [], []);
};
this.run = function (controllerData, deltaTime) {
if (this.shouldStopTalking(controllerData) || !Audio.pushToTalk) {
Audio.pushingToTalk = false;
print("Stop pushing to talk.");
return makeRunningValues(false, [], []);
}
return makeRunningValues(true, [], []);
};
this.parameters = makeDispatcherModuleParameters(
950,
["head"],
[],
100);
}
var pushToTalk = new PushToTalkHandler();
enableDispatcherModule("PushToTalk", pushToTalk);
function cleanup() {
disableDispatcherModule("PushToTalk");
};
Script.scriptEnding.connect(cleanup);
}()); // END LOCAL_SCOPE

View file

@ -0,0 +1,88 @@
// scaleAvatar.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 Script, Vec3, MyAvatar, RIGHT_HAND */
(function () {
var dispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js");
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
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 = clamp(newAvatarScale, MyAvatar.getDomainMinScale(), MyAvatar.getDomainMaxScale());
MyAvatar.scaleChanged();
}
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);
function cleanup() {
dispatcherUtils.disableDispatcherModule("LeftScaleAvatar");
dispatcherUtils.disableDispatcherModule("RightScaleAvatar");
}
Script.scriptEnding.connect(cleanup);
})();

View file

@ -0,0 +1,110 @@
// scaleEntity.js
//
// Created by Dante Ruiz on 9/18/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 Script, Vec3, MyAvatar, Entities, RIGHT_HAND, entityIsGrabbable */
(function() {
var dispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js");
function ScaleEntity(hand) {
this.hand = hand;
this.grabbedThingID = false;
this.scalingStartDistance = false;
this.scalingStartDimensions = false;
this.parameters = dispatcherUtils.makeDispatcherModuleParameters(
120,
this.hand === RIGHT_HAND ? ["rightHandTrigger"] : ["leftHandTrigger"],
[],
100
);
this.otherHand = function() {
return this.hand === dispatcherUtils.RIGHT_HAND ? dispatcherUtils.LEFT_HAND : dispatcherUtils.RIGHT_HAND;
};
this.otherModule = function() {
return this.hand === dispatcherUtils.RIGHT_HAND ? leftScaleEntity : rightScaleEntity;
};
this.bumperPressed = function(controllerData) {
return ( controllerData.secondaryValues[this.hand] > dispatcherUtils.BUMPER_ON_VALUE);
};
this.getTargetProps = function(controllerData) {
// nearbyEntityProperties is already sorted by length from controller
var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand];
var sensorScaleFactor = MyAvatar.sensorToWorldScale;
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 > dispatcherUtils.NEAR_GRAB_RADIUS * sensorScaleFactor) {
continue;
}
if ((dispatcherUtils.entityIsGrabbable(props) ||
dispatcherUtils.propsArePhysical(props)) && !props.locked) {
return props;
}
}
return null;
};
this.isReady = function(controllerData) {
var otherModule = this.otherModule();
if (this.bumperPressed(controllerData) && otherModule.bumperPressed(controllerData)) {
var thisHandTargetProps = this.getTargetProps(controllerData);
var otherHandTargetProps = otherModule.getTargetProps(controllerData);
if (thisHandTargetProps && otherHandTargetProps) {
if (thisHandTargetProps.id === otherHandTargetProps.id) {
if (!entityIsGrabbable(thisHandTargetProps)) {
return dispatcherUtils.makeRunningValues(false, [], []);
}
this.grabbedThingID = thisHandTargetProps.id;
this.scalingStartDistance =
Vec3.length(Vec3.subtract(controllerData.controllerLocations[this.hand].position,
controllerData.controllerLocations[this.otherHand()].position));
this.scalingStartDimensions = thisHandTargetProps.dimensions;
return dispatcherUtils.makeRunningValues(true, [], []);
}
}
}
this.grabbedThingID = false;
return dispatcherUtils.makeRunningValues(false, [], []);
};
this.run = function(controllerData) {
var otherModule = this.otherModule();
if (this.bumperPressed(controllerData) && otherModule.bumperPressed(controllerData)) {
if (this.hand === dispatcherUtils.RIGHT_HAND) {
var scalingCurrentDistance =
Vec3.length(Vec3.subtract(controllerData.controllerLocations[this.hand].position,
controllerData.controllerLocations[this.otherHand()].position));
var currentRescale = scalingCurrentDistance / this.scalingStartDistance;
var newDimensions = Vec3.multiply(currentRescale, this.scalingStartDimensions);
Entities.editEntity(this.grabbedThingID, { localDimensions: newDimensions });
}
return dispatcherUtils.makeRunningValues(true, [], []);
}
this.grabbedThingID = false;
return dispatcherUtils.makeRunningValues(false, [], []);
};
}
var leftScaleEntity = new ScaleEntity(dispatcherUtils.LEFT_HAND);
var rightScaleEntity = new ScaleEntity(dispatcherUtils.RIGHT_HAND);
dispatcherUtils.enableDispatcherModule("LeftScaleEntity", leftScaleEntity);
dispatcherUtils.enableDispatcherModule("RightScaleEntity", rightScaleEntity);
function cleanup() {
dispatcherUtils.disableDispatcherModule("LeftScaleEntity");
dispatcherUtils.disableDispatcherModule("RightScaleEntity");
}
Script.scriptEnding.connect(cleanup);
})();

View file

@ -0,0 +1,220 @@
"use strict";
// stylusInput.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, Uuid, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule,
makeRunningValues, Vec3, makeDispatcherModuleParameters, Overlays, HMD, Settings, getEnabledModuleByName, Pointers,
Picks, PickType
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
function isNearStylusTarget(stylusTargets, maxNormalDistance) {
var stylusTargetIDs = [];
for (var index = 0; index < stylusTargets.length; index++) {
var stylusTarget = stylusTargets[index];
if (stylusTarget.distance <= maxNormalDistance && !(HMD.tabletID && stylusTarget.id === HMD.tabletID)) {
stylusTargetIDs.push(stylusTarget.id);
}
}
return stylusTargetIDs;
}
function getOverlayDistance(controllerPosition, overlayID) {
var position = Overlays.getProperty(overlayID, "position");
return {
id: overlayID,
distance: Vec3.distance(position, controllerPosition)
};
}
function StylusInput(hand) {
this.hand = hand;
this.parameters = makeDispatcherModuleParameters(
100,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100);
this.pointer = Pointers.createPointer(PickType.Stylus, {
hand: this.hand,
filter: Picks.PICK_OVERLAYS,
hover: true,
enabled: true
});
this.disable = false;
this.otherModuleNeedsToRun = function(controllerData) {
var grabOverlayModuleName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay";
var grabOverlayModule = getEnabledModuleByName(grabOverlayModuleName);
var grabEntityModuleName = this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity";
var grabEntityModule = getEnabledModuleByName(grabEntityModuleName);
var grabOverlayModuleReady = grabOverlayModule ? grabOverlayModule.isReady(controllerData) : makeRunningValues(false, [], []);
var grabEntityModuleReady = grabEntityModule ? grabEntityModule.isReady(controllerData) : makeRunningValues(false, [], []);
var farGrabModuleName = this.hand === RIGHT_HAND ? "RightFarActionGrabEntity" : "LeftFarActionGrabEntity";
var farGrabModule = getEnabledModuleByName(farGrabModuleName);
var farGrabModuleReady = farGrabModule ? farGrabModule.isReady(controllerData) : makeRunningValues(false, [], []);
var nearTabletHighlightModuleName =
this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight";
var nearTabletHighlightModule = getEnabledModuleByName(nearTabletHighlightModuleName);
var nearTabletHighlightModuleReady = nearTabletHighlightModule
? nearTabletHighlightModule.isReady(controllerData) : makeRunningValues(false, [], []);
return grabOverlayModuleReady.active || farGrabModuleReady.active || grabEntityModuleReady.active
|| nearTabletHighlightModuleReady.active;
};
this.overlayLaserActive = function(controllerData) {
var rightOverlayLaserModule = getEnabledModuleByName("RightWebSurfaceLaserInput");
var leftOverlayLaserModule = getEnabledModuleByName("LeftWebSurfaceLaserInput");
var rightModuleRunning = rightOverlayLaserModule ? rightOverlayLaserModule.isReady(controllerData).active : false;
var leftModuleRunning = leftOverlayLaserModule ? leftOverlayLaserModule.isReady(controllerData).active : false;
return leftModuleRunning || rightModuleRunning;
};
this.processStylus = function(controllerData) {
if (this.overlayLaserActive(controllerData) || this.otherModuleNeedsToRun(controllerData)) {
Pointers.setRenderState(this.pointer, "disabled");
return false;
}
var sensorScaleFactor = MyAvatar.sensorToWorldScale;
// build list of stylus targets, near the stylusTip
var stylusTargets = [];
var candidateOverlays = controllerData.nearbyOverlayIDs;
var controllerPosition = controllerData.controllerLocations[this.hand].position;
var i, stylusTarget;
for (i = 0; i < candidateOverlays.length; i++) {
if (!(HMD.tabletID && candidateOverlays[i] === HMD.tabletID) &&
Overlays.getProperty(candidateOverlays[i], "visible")) {
stylusTarget = getOverlayDistance(controllerPosition, candidateOverlays[i]);
if (stylusTarget) {
stylusTargets.push(stylusTarget);
}
}
}
// add the tabletScreen, if it is valid
if (HMD.tabletScreenID && HMD.tabletScreenID !== Uuid.NULL &&
Overlays.getProperty(HMD.tabletScreenID, "visible")) {
stylusTarget = getOverlayDistance(controllerPosition, HMD.tabletScreenID);
if (stylusTarget) {
stylusTargets.push(stylusTarget);
}
}
// add the tablet home button.
if (HMD.homeButtonID && HMD.homeButtonID !== Uuid.NULL &&
Overlays.getProperty(HMD.homeButtonID, "visible")) {
stylusTarget = getOverlayDistance(controllerPosition, HMD.homeButtonID);
if (stylusTarget) {
stylusTargets.push(stylusTarget);
}
}
// Add the mini tablet.
if (HMD.miniTabletScreenID && Overlays.getProperty(HMD.miniTabletScreenID, "visible")) {
stylusTarget = getOverlayDistance(controllerPosition, HMD.miniTabletScreenID);
if (stylusTarget) {
stylusTargets.push(stylusTarget);
}
}
const WEB_DISPLAY_STYLUS_DISTANCE = (Keyboard.raised && Keyboard.preferMalletsOverLasers) ? 0.2 : 0.5;
var nearStylusTarget = isNearStylusTarget(stylusTargets, WEB_DISPLAY_STYLUS_DISTANCE * sensorScaleFactor);
if (nearStylusTarget.length !== 0) {
if (!this.disable) {
Pointers.setRenderState(this.pointer,"events on");
Pointers.setIncludeItems(this.pointer, nearStylusTarget);
} else {
Pointers.setRenderState(this.pointer,"events off");
}
return true;
} else {
Pointers.setRenderState(this.pointer, "disabled");
Pointers.setIncludeItems(this.pointer, []);
return false;
}
};
this.isReady = function (controllerData) {
var PREFER_STYLUS_OVER_LASER = "preferStylusOverLaser";
var isUsingStylus = Settings.getValue(PREFER_STYLUS_OVER_LASER, false);
if (isUsingStylus && this.processStylus(controllerData)) {
Pointers.enablePointer(this.pointer);
this.hand === RIGHT_HAND ? Keyboard.disableRightMallet() : Keyboard.disableLeftMallet();
return makeRunningValues(true, [], []);
} else {
Pointers.disablePointer(this.pointer);
if (Keyboard.raised && Keyboard.preferMalletsOverLasers) {
this.hand === RIGHT_HAND ? Keyboard.enableRightMallet() : Keyboard.enableLeftMallet();
}
return makeRunningValues(false, [], []);
}
};
this.run = function (controllerData, deltaTime) {
return this.isReady(controllerData);
};
this.cleanup = function () {
Pointers.removePointer(this.pointer);
};
}
function mouseHoverEnter(overlayID, event) {
if (event.id === leftTabletStylusInput.pointer && !rightTabletStylusInput.disable && !leftTabletStylusInput.disable) {
rightTabletStylusInput.disable = true;
} else if (event.id === rightTabletStylusInput.pointer && !leftTabletStylusInput.disable && !rightTabletStylusInput.disable) {
leftTabletStylusInput.disable = true;
}
}
function mouseHoverLeave(overlayID, event) {
if (event.id === leftTabletStylusInput.pointer) {
rightTabletStylusInput.disable = false;
} else if (event.id === rightTabletStylusInput.pointer) {
leftTabletStylusInput.disable = false;
}
}
var HAPTIC_STYLUS_STRENGTH = 1.0;
var HAPTIC_STYLUS_DURATION = 20.0;
function mousePress(overlayID, event) {
if (HMD.active) {
if (event.id === leftTabletStylusInput.pointer && event.button === "Primary") {
Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, LEFT_HAND);
} else if (event.id === rightTabletStylusInput.pointer && event.button === "Primary") {
Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, RIGHT_HAND);
}
}
}
var leftTabletStylusInput = new StylusInput(LEFT_HAND);
var rightTabletStylusInput = new StylusInput(RIGHT_HAND);
enableDispatcherModule("LeftTabletStylusInput", leftTabletStylusInput);
enableDispatcherModule("RightTabletStylusInput", rightTabletStylusInput);
Overlays.hoverEnterOverlay.connect(mouseHoverEnter);
Overlays.hoverLeaveOverlay.connect(mouseHoverLeave);
Overlays.mousePressOnOverlay.connect(mousePress);
this.cleanup = function () {
leftTabletStylusInput.cleanup();
rightTabletStylusInput.cleanup();
disableDispatcherModule("LeftTabletStylusInput");
disableDispatcherModule("RightTabletStylusInput");
};
Script.scriptEnding.connect(this.cleanup);
}());

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,288 @@
"use strict";
// webSurfaceLaserInput.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, enableDispatcherModule, disableDispatcherModule, makeRunningValues,
makeDispatcherModuleParameters, Overlays, HMD, TRIGGER_ON_VALUE, TRIGGER_OFF_VALUE, getEnabledModuleByName,
ContextOverlay, Picks, makeLaserParams, Settings, MyAvatar, RIGHT_HAND, LEFT_HAND, DISPATCHER_PROPERTIES
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
Script.include("/~/system/libraries/controllers.js");
(function() {
const intersectionType = {
None: 0,
WebOverlay: 1,
WebEntity: 2,
HifiKeyboard: 3,
Overlay: 4,
HifiTablet: 5,
};
function WebSurfaceLaserInput(hand) {
this.hand = hand;
this.otherHand = this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND;
this.running = false;
this.ignoredObjects = [];
this.intersectedType = intersectionType["None"];
this.parameters = makeDispatcherModuleParameters(
160,
this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
[],
100,
makeLaserParams(hand, true));
this.getFarGrab = function () {
return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("RightFarGrabEntity") : ("LeftFarGrabEntity"));
};
this.farGrabActive = function () {
var farGrab = this.getFarGrab();
// farGrab will be null if module isn't loaded.
if (farGrab) {
return farGrab.targetIsNull();
} else {
return false;
}
};
this.grabModuleWantsNearbyOverlay = function(controllerData) {
if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) {
var nearGrabName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay";
var nearGrabModule = getEnabledModuleByName(nearGrabName);
if (nearGrabModule) {
var candidateOverlays = controllerData.nearbyOverlayIDs[this.hand];
var grabbableOverlays = candidateOverlays.filter(function(overlayID) {
return Overlays.getProperty(overlayID, "grabbable");
});
var target = nearGrabModule.getTargetID(grabbableOverlays, controllerData);
if (target) {
return true;
}
}
nearGrabName = this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity";
nearGrabModule = getEnabledModuleByName(nearGrabName);
if (nearGrabModule && nearGrabModule.isReady(controllerData)) {
// check for if near parent module is active.
var isNearGrabModuleActive = nearGrabModule.isReady(controllerData).active;
if (isNearGrabModuleActive) {
// if true, return true.
return isNearGrabModuleActive;
} else {
// check near action grab entity as a second pass.
nearGrabName = this.hand === RIGHT_HAND ? "RightNearActionGrabEntity" : "LeftNearActionGrabEntity";
nearGrabModule = getEnabledModuleByName(nearGrabName);
if (nearGrabModule && nearGrabModule.isReady(controllerData)) {
return nearGrabModule.isReady(controllerData).active;
}
}
}
}
var nearTabletHighlightModule = getEnabledModuleByName(this.hand === RIGHT_HAND
? "RightNearTabletHighlight" : "LeftNearTabletHighlight");
if (nearTabletHighlightModule) {
return nearTabletHighlightModule.isNearTablet(controllerData);
}
return false;
};
this.getOtherModule = function() {
return this.hand === RIGHT_HAND ? leftOverlayLaserInput : rightOverlayLaserInput;
};
this.addObjectToIgnoreList = function(controllerData) {
if (Window.interstitialModeEnabled && !Window.isPhysicsEnabled()) {
var intersection = controllerData.rayPicks[this.hand];
var objectID = intersection.objectID;
if (intersection.type === Picks.INTERSECTED_OVERLAY) {
var overlayIndex = this.ignoredObjects.indexOf(objectID);
var overlayName = Overlays.getProperty(objectID, "name");
if (overlayName !== "Loading-Destination-Card-Text" && overlayName !== "Loading-Destination-Card-GoTo-Image" &&
overlayName !== "Loading-Destination-Card-GoTo-Image-Hover") {
var data = {
action: 'add',
id: objectID
};
Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data));
this.ignoredObjects.push(objectID);
}
} else if (intersection.type === Picks.INTERSECTED_ENTITY) {
var entityIndex = this.ignoredObjects.indexOf(objectID);
var data = {
action: 'add',
id: objectID
};
Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data));
this.ignoredObjects.push(objectID);
}
}
};
this.restoreIgnoredObjects = function() {
for (var index = 0; index < this.ignoredObjects.length; index++) {
var data = {
action: 'remove',
id: this.ignoredObjects[index]
};
Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data));
}
this.ignoredObjects = [];
};
this.getInteractableType = function(controllerData, triggerPressed, checkEntitiesOnly) {
// allow pointing at tablet, unlocked web entities, or web overlays automatically without pressing trigger,
// but for pointing at locked web entities or non-web overlays user must be pressing trigger
var intersection = controllerData.rayPicks[this.hand];
var objectID = intersection.objectID;
if (intersection.type === Picks.INTERSECTED_OVERLAY && !checkEntitiesOnly) {
if ((HMD.tabletID && objectID === HMD.tabletID) ||
(HMD.tabletScreenID && objectID === HMD.tabletScreenID) ||
(HMD.homeButtonID && objectID === HMD.homeButtonID)) {
return intersectionType["HifiTablet"];
} else {
var overlayType = Overlays.getOverlayType(objectID);
var type = intersectionType["None"];
if (Keyboard.containsID(objectID) && !Keyboard.preferMalletsOverLasers) {
type = intersectionType["HifiKeyboard"];
} else if (overlayType === "web3d") {
type = intersectionType["WebOverlay"];
} else if (triggerPressed) {
type = intersectionType["Overlay"];
}
return type;
}
} else if (intersection.type === Picks.INTERSECTED_ENTITY) {
var entityProperties = Entities.getEntityProperties(objectID, DISPATCHER_PROPERTIES);
var entityType = entityProperties.type;
var isLocked = entityProperties.locked;
if (entityType === "Web" && (!isLocked || triggerPressed)) {
return intersectionType["WebEntity"];
}
}
return intersectionType["None"];
};
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.updateAlwaysOn = function(type) {
var PREFER_STYLUS_OVER_LASER = "preferStylusOverLaser";
this.parameters.handLaser.alwaysOn = (!Settings.getValue(PREFER_STYLUS_OVER_LASER, false) || type === intersectionType["HifiKeyboard"]);
};
this.getDominantHand = function() {
return MyAvatar.getDominantHand() === "right" ? 1 : 0;
};
this.dominantHandOverride = false;
this.isReady = function (controllerData) {
// Trivial rejection for when FarGrab is active.
if (this.farGrabActive()) {
return makeRunningValues(false, [], []);
}
var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE &&
controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE;
var type = this.getInteractableType(controllerData, isTriggerPressed, false);
if (type !== intersectionType["None"] && !this.grabModuleWantsNearbyOverlay(controllerData)) {
if (type === intersectionType["WebOverlay"] || type === intersectionType["WebEntity"] || type === intersectionType["HifiTablet"]) {
var otherModuleRunning = this.getOtherModule().running;
otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand.
var allowThisModule = !otherModuleRunning || isTriggerPressed;
if (!allowThisModule) {
return makeRunningValues(true, [], []);
}
if (isTriggerPressed) {
this.dominantHandOverride = true; // Override dominant hand.
this.getOtherModule().dominantHandOverride = false;
}
}
this.updateAlwaysOn(type);
if (this.parameters.handLaser.alwaysOn || isTriggerPressed) {
return makeRunningValues(true, [], []);
}
}
if (Window.interstitialModeEnabled && Window.isPhysicsEnabled()) {
this.restoreIgnoredObjects();
}
return makeRunningValues(false, [], []);
};
this.shouldThisModuleRun = function(controllerData) {
var otherModuleRunning = this.getOtherModule().running;
otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand.
otherModuleRunning = otherModuleRunning || this.getOtherModule().dominantHandOverride; // Override dominant hand.
var grabModuleNeedsToRun = this.grabModuleWantsNearbyOverlay(controllerData);
// only allow for non-near grab
return !otherModuleRunning && !grabModuleNeedsToRun;
};
this.run = function(controllerData, deltaTime) {
this.addObjectToIgnoreList(controllerData);
var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE;
var type = this.getInteractableType(controllerData, isTriggerPressed, false);
var laserOn = isTriggerPressed || this.parameters.handLaser.alwaysOn;
this.addObjectToIgnoreList(controllerData);
if (type === intersectionType["HifiTablet"] && laserOn) {
if (this.shouldThisModuleRun(controllerData)) {
this.running = true;
return makeRunningValues(true, [], []);
}
} else if ((type === intersectionType["WebOverlay"] || type === intersectionType["WebEntity"]) && laserOn) { // auto laser on WebEntities andWebOverlays
if (this.shouldThisModuleRun(controllerData)) {
this.running = true;
return makeRunningValues(true, [], []);
}
} else if ((type === intersectionType["HifiKeyboard"] && laserOn) || type === intersectionType["Overlay"]) {
this.running = true;
return makeRunningValues(true, [], []);
}
this.deleteContextOverlay();
this.running = false;
this.dominantHandOverride = false;
return makeRunningValues(false, [], []);
};
}
var leftOverlayLaserInput = new WebSurfaceLaserInput(LEFT_HAND);
var rightOverlayLaserInput = new WebSurfaceLaserInput(RIGHT_HAND);
enableDispatcherModule("LeftWebSurfaceLaserInput", leftOverlayLaserInput);
enableDispatcherModule("RightWebSurfaceLaserInput", rightOverlayLaserInput);
function cleanup() {
disableDispatcherModule("LeftWebSurfaceLaserInput");
disableDispatcherModule("RightWebSurfaceLaserInput");
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -0,0 +1,62 @@
"use strict";
// controllerScripts.js
//
// Created by David Rowe on 15 Mar 2017.
// Copyright 2017 High Fidelity, Inc.
//
// 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, Menu */
var CONTOLLER_SCRIPTS = [
"squeezeHands.js",
"controllerDisplayManager.js",
"grab.js",
//"toggleAdvancedMovementForHandControllers.js",
"handTouch.js",
"controllerDispatcher.js",
"controllerModules/nearParentGrabOverlay.js",
"controllerModules/stylusInput.js",
"controllerModules/equipEntity.js",
"controllerModules/nearTrigger.js",
"controllerModules/webSurfaceLaserInput.js",
"controllerModules/inEditMode.js",
"controllerModules/inVREditMode.js",
"controllerModules/disableOtherModule.js",
"controllerModules/farTrigger.js",
"controllerModules/teleport.js",
"controllerModules/hudOverlayPointer.js",
"controllerModules/mouseHMD.js",
"controllerModules/nearGrabHyperLinkEntity.js",
"controllerModules/nearTabletHighlight.js",
"controllerModules/nearGrabEntity.js",
"controllerModules/farGrabEntity.js",
"controllerModules/pushToTalk.js"
];
var DEBUG_MENU_ITEM = "Debug defaultScripts.js";
function runDefaultsTogether() {
for (var j in CONTOLLER_SCRIPTS) {
if (CONTOLLER_SCRIPTS.hasOwnProperty(j)) {
Script.include(CONTOLLER_SCRIPTS[j]);
}
}
}
function runDefaultsSeparately() {
for (var i in CONTOLLER_SCRIPTS) {
if (CONTOLLER_SCRIPTS.hasOwnProperty(i)) {
Script.load(CONTOLLER_SCRIPTS[i]);
}
}
}
if (Menu.isOptionChecked(DEBUG_MENU_ITEM)) {
runDefaultsSeparately();
} else {
runDefaultsTogether();
}

View file

@ -0,0 +1,116 @@
"use strict";
//
// godView.js
// scripts/system/
//
// Created by Brad Hefta-Gaub on 1 Jun 2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals HMD, Script, Menu, Tablet, Camera */
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
(function() { // BEGIN LOCAL_SCOPE
var godView = false;
var GOD_CAMERA_OFFSET = -1; // 1 meter below the avatar
var GOD_VIEW_HEIGHT = 300; // 300 meter above the ground
var ABOVE_GROUND_DROP = 2;
var MOVE_BY = 1;
function moveTo(position) {
if (godView) {
MyAvatar.position = position;
Camera.position = Vec3.sum(MyAvatar.position, {x:0, y: GOD_CAMERA_OFFSET, z: 0});
} else {
MyAvatar.position = position;
}
}
function keyPressEvent(event) {
if (godView) {
switch(event.text) {
case "UP":
moveTo(Vec3.sum(MyAvatar.position, {x:0.0, y: 0, z: -1 * MOVE_BY}));
break;
case "DOWN":
moveTo(Vec3.sum(MyAvatar.position, {x:0, y: 0, z: MOVE_BY}));
break;
case "LEFT":
moveTo(Vec3.sum(MyAvatar.position, {x:-1 * MOVE_BY, y: 0, z: 0}));
break;
case "RIGHT":
moveTo(Vec3.sum(MyAvatar.position, {x:MOVE_BY, y: 0, z: 0}));
break;
}
}
}
function mousePress(event) {
if (godView) {
var pickRay = Camera.computePickRay(event.x, event.y);
var pointingAt = Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction,300));
var moveToPosition = { x: pointingAt.x, y: MyAvatar.position.y, z: pointingAt.z };
moveTo(moveToPosition);
}
}
var oldCameraMode = Camera.mode;
function startGodView() {
if (!godView) {
oldCameraMode = Camera.mode;
MyAvatar.position = Vec3.sum(MyAvatar.position, {x:0, y: GOD_VIEW_HEIGHT, z: 0});
Camera.mode = "independent";
Camera.position = Vec3.sum(MyAvatar.position, {x:0, y: GOD_CAMERA_OFFSET, z: 0});
Camera.orientation = Quat.fromPitchYawRollDegrees(-90,0,0);
godView = true;
}
}
function endGodView() {
if (godView) {
Camera.mode = oldCameraMode;
MyAvatar.position = Vec3.sum(MyAvatar.position, {x:0, y: (-1 * GOD_VIEW_HEIGHT) + ABOVE_GROUND_DROP, z: 0});
godView = false;
}
}
var button;
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
function onClicked() {
if (godView) {
endGodView();
} else {
startGodView();
}
}
button = tablet.addButton({
icon: "icons/tablet-icons/switch-desk-i.svg", // FIXME - consider a better icon from Alan
text: "God View"
});
button.clicked.connect(onClicked);
Controller.keyPressEvent.connect(keyPressEvent);
Controller.mousePressEvent.connect(mousePress);
Script.scriptEnding.connect(function () {
if (godView) {
endGodView();
}
button.clicked.disconnect(onClicked);
if (tablet) {
tablet.removeButton(button);
}
Controller.keyPressEvent.disconnect(keyPressEvent);
Controller.mousePressEvent.disconnect(mousePress);
});
}()); // END LOCAL_SCOPE

View file

@ -0,0 +1,522 @@
"use strict";
// grab.js
// examples
//
// Created by Eric Levin on May 1, 2015
// Copyright 2015 High Fidelity, Inc.
//
// Grab's physically moveable entities with the mouse, by applying a spring force.
//
// Updated November 22, 2016 by Philip Rosedale: Add distance attenuation of grab effect
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* global MyAvatar, Entities, Script, HMD, Camera, Vec3, Reticle, Overlays, Messages, Quat, Controller,
isInEditMode, entityIsGrabbable, Picks, PickType, Pointers, unhighlightTargetEntity, DISPATCHER_PROPERTIES,
entityIsGrabbable, getMainTabletIDs
*/
/* jslint bitwise: true */
(function() { // BEGIN LOCAL_SCOPE
Script.include("/~/system/libraries/utils.js");
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
var MOUSE_GRAB_JOINT = 65526; // FARGRAB_MOUSE_INDEX
var MAX_SOLID_ANGLE = 0.01; // objects that appear smaller than this can't be grabbed
var DELAY_FOR_30HZ = 33; // milliseconds
var ZERO_VEC3 = { x: 0, y: 0, z: 0 };
var IDENTITY_QUAT = { x: 0, y: 0, z: 0, w: 1 };
// helper function
function mouseIntersectionWithPlane(pointOnPlane, planeNormal, event, maxDistance) {
var cameraPosition = Camera.getPosition();
var localPointOnPlane = Vec3.subtract(pointOnPlane, cameraPosition);
var distanceFromPlane = Vec3.dot(localPointOnPlane, planeNormal);
var MIN_DISTANCE_FROM_PLANE = 0.001;
if (Math.abs(distanceFromPlane) < MIN_DISTANCE_FROM_PLANE) {
// camera is touching the plane
return pointOnPlane;
}
var pickRay = Camera.computePickRay(event.x, event.y);
var dirDotNorm = Vec3.dot(pickRay.direction, planeNormal);
var MIN_RAY_PLANE_DOT = 0.00001;
var localIntersection;
var useMaxForwardGrab = false;
if (Math.abs(dirDotNorm) > MIN_RAY_PLANE_DOT) {
var distanceToIntersection = distanceFromPlane / dirDotNorm;
if (distanceToIntersection > 0 && distanceToIntersection < maxDistance) {
// ray points into the plane
localIntersection = Vec3.multiply(pickRay.direction, distanceFromPlane / dirDotNorm);
} else {
// ray intersects BEHIND the camera or else very far away
// so we clamp the grab point to be the maximum forward position
useMaxForwardGrab = true;
}
} else {
// ray points perpendicular to grab plane
// so we map the grab point to the maximum forward position
useMaxForwardGrab = true;
}
if (useMaxForwardGrab) {
// we re-route the intersection to be in front at max distance.
var rayDirection = Vec3.subtract(pickRay.direction, Vec3.multiply(planeNormal, dirDotNorm));
rayDirection = Vec3.normalize(rayDirection);
localIntersection = Vec3.multiply(rayDirection, maxDistance);
localIntersection = Vec3.sum(localIntersection, Vec3.multiply(planeNormal, distanceFromPlane));
}
var worldIntersection = Vec3.sum(cameraPosition, localIntersection);
return worldIntersection;
}
// Mouse class stores mouse click and drag info
function Mouse() {
this.current = {
x: 0,
y: 0
};
this.previous = {
x: 0,
y: 0
};
this.rotateStart = {
x: 0,
y: 0
};
this.cursorRestore = {
x: 0,
y: 0
};
}
Mouse.prototype.startDrag = function(position) {
this.current = {
x: position.x,
y: position.y
};
this.startRotateDrag();
};
Mouse.prototype.updateDrag = function(position) {
this.current = {
x: position.x,
y: position.y
};
};
Mouse.prototype.startRotateDrag = function() {
this.previous = {
x: this.current.x,
y: this.current.y
};
this.rotateStart = {
x: this.current.x,
y: this.current.y
};
this.cursorRestore = Reticle.getPosition();
};
Mouse.prototype.getDrag = function() {
var delta = {
x: this.current.x - this.previous.x,
y: this.current.y - this.previous.y
};
this.previous = {
x: this.current.x,
y: this.current.y
};
return delta;
};
Mouse.prototype.restoreRotateCursor = function() {
Reticle.setPosition(this.cursorRestore);
this.current = {
x: this.rotateStart.x,
y: this.rotateStart.y
};
};
var mouse = new Mouse();
var beacon = {
type: "cube",
dimensions: {
x: 0.01,
y: 0,
z: 0.01
},
color: {
red: 200,
green: 200,
blue: 200
},
alpha: 1,
solid: true,
ignoreRayIntersection: true,
visible: true
};
// TODO: play sounds again when we aren't leaking AudioInjector threads
// var grabSound = SoundCache.getSound("https://hifi-public.s3.amazonaws.com/eric/sounds/CloseClamp.wav");
// var releaseSound = SoundCache.getSound("https://hifi-public.s3.amazonaws.com/eric/sounds/ReleaseClamp.wav");
// var VOLUME = 0.0;
// Grabber class stores and computes info for grab behavior
function Grabber() {
this.isGrabbing = false;
this.entityID = null;
this.startPosition = ZERO_VEC3;
this.lastRotation = IDENTITY_QUAT;
this.currentPosition = ZERO_VEC3;
this.planeNormal = ZERO_VEC3;
// maxDistance is a function of the size of the object.
this.maxDistance = 0;
// mode defines the degrees of freedom of the grab target positions
// relative to startPosition options include:
// xzPlane (default)
// verticalCylinder (SHIFT)
// rotate (CONTROL)
this.mode = "xzplane";
// offset allows the user to grab an object off-center. It points from the object's center
// to the point where the ray intersects the grab plane (at the moment the grab is initiated).
// Future target positions of the ray intersection are on the same plane, and the offset is subtracted
// to compute the target position of the object's center.
this.offset = {
x: 0,
y: 0,
z: 0
};
this.liftKey = false; // SHIFT
this.rotateKey = false; // CONTROL
this.mouseRayOverlays = Picks.createPick(PickType.Ray, {
joint: "Mouse",
filter: Picks.PICK_OVERLAYS | Picks.PICK_INCLUDE_NONCOLLIDABLE,
enabled: true
});
var tabletItems = getMainTabletIDs();
if (tabletItems.length > 0) {
Picks.setIncludeItems(this.mouseRayOverlays, tabletItems);
}
var renderStates = [{name: "grabbed", end: beacon}];
this.mouseRayEntities = Pointers.createPointer(PickType.Ray, {
joint: "Mouse",
filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE,
faceAvatar: true,
scaleWithParent: true,
enabled: true,
renderStates: renderStates
});
}
Grabber.prototype.computeNewGrabPlane = function() {
if (!this.isGrabbing) {
return;
}
var modeWasRotate = (this.mode == "rotate");
this.mode = "xzPlane";
this.planeNormal = {
x: 0,
y: 1,
z: 0
};
if (this.rotateKey) {
this.mode = "rotate";
mouse.startRotateDrag();
} else {
if (modeWasRotate) {
// we reset the mouse screen position whenever we stop rotating
mouse.restoreRotateCursor();
}
if (this.liftKey) {
this.mode = "verticalCylinder";
// NOTE: during verticalCylinder mode a new planeNormal will be computed each move
}
}
this.pointOnPlane = Vec3.subtract(this.currentPosition, this.offset);
var xzOffset = Vec3.subtract(this.pointOnPlane, Camera.getPosition());
xzOffset.y = 0;
this.xzDistanceToGrab = Vec3.length(xzOffset);
};
Grabber.prototype.pressEvent = function(event) {
if (isInEditMode() || HMD.active) {
return;
}
if (event.button !== "LEFT") {
return;
}
if (event.isAlt || event.isMeta) {
return;
}
if (Overlays.getOverlayAtPoint(Reticle.position) > 0) {
// the mouse is pointing at an overlay; don't look for entities underneath the overlay.
return;
}
var overlayResult = Picks.getPrevPickResult(this.mouseRayOverlays);
if (overlayResult.type != Picks.INTERSECTED_NONE) {
return;
}
var pickResults = Pointers.getPrevPickResult(this.mouseRayEntities);
if (pickResults.type == Picks.INTERSECTED_NONE) {
Pointers.setRenderState(this.mouseRayEntities, "");
return;
}
var props = Entities.getEntityProperties(pickResults.objectID, DISPATCHER_PROPERTIES);
if (!entityIsGrabbable(props)) {
// only grab grabbable objects
return;
}
if (props.grab.equippable) {
// don't mouse-grab click-to-equip entities (let equipEntity.js handle these)
return;
}
Pointers.setRenderState(this.mouseRayEntities, "grabbed");
Pointers.setLockEndUUID(this.mouseRayEntities, pickResults.objectID, false);
unhighlightTargetEntity(pickResults.objectID);
mouse.startDrag(event);
var clickedEntity = pickResults.objectID;
var entityProperties = Entities.getEntityProperties(clickedEntity, DISPATCHER_PROPERTIES);
this.startPosition = entityProperties.position;
this.lastRotation = entityProperties.rotation;
var cameraPosition = Camera.getPosition();
var objectBoundingDiameter = Vec3.length(entityProperties.dimensions);
beacon.dimensions.y = objectBoundingDiameter;
Pointers.editRenderState(this.mouseRayEntities, "grabbed", {end: beacon});
this.maxDistance = objectBoundingDiameter / MAX_SOLID_ANGLE;
if (Vec3.distance(this.startPosition, cameraPosition) > this.maxDistance) {
// don't allow grabs of things far away
return;
}
this.isGrabbing = true;
this.entityID = clickedEntity;
this.currentPosition = entityProperties.position;
// compute the grab point
var pickRay = Camera.computePickRay(event.x, event.y);
var nearestPoint = Vec3.subtract(this.startPosition, cameraPosition);
var distanceToGrab = Vec3.dot(nearestPoint, pickRay.direction);
nearestPoint = Vec3.multiply(distanceToGrab, pickRay.direction);
this.pointOnPlane = Vec3.sum(cameraPosition, nearestPoint);
// compute the grab offset (points from point of grab to object center)
this.offset = Vec3.subtract(this.startPosition, this.pointOnPlane); // offset in world-space
MyAvatar.setJointTranslation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointPoint(this.startPosition));
MyAvatar.setJointRotation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointRotation(this.lastRotation));
this.computeNewGrabPlane();
this.moveEvent(event);
var args = "mouse";
Entities.callEntityMethod(this.entityID, "startDistanceGrab", args);
Messages.sendLocalMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'grab',
grabbedEntity: this.entityID
}));
if (this.grabID) {
MyAvatar.releaseGrab(this.grabID);
this.grabID = null;
}
this.grabID = MyAvatar.grab(this.entityID, MOUSE_GRAB_JOINT, ZERO_VEC3, IDENTITY_QUAT);
// TODO: play sounds again when we aren't leaking AudioInjector threads
//Audio.playSound(grabSound, { position: entityProperties.position, volume: VOLUME });
};
Grabber.prototype.releaseEvent = function(event) {
if (event.button !== "LEFT" && !HMD.active) {
return;
}
if (this.moveEventTimer) {
Script.clearTimeout(this.moveEventTimer);
this.moveEventTimer = null;
}
if (this.isGrabbing) {
this.isGrabbing = false;
Pointers.setRenderState(this.mouseRayEntities, "");
Pointers.setLockEndUUID(this.mouseRayEntities, null, false);
var args = "mouse";
Entities.callEntityMethod(this.entityID, "releaseGrab", args);
Messages.sendLocalMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'release',
grabbedEntity: this.entityID,
joint: "mouse"
}));
if (this.grabID) {
MyAvatar.releaseGrab(this.grabID);
this.grabID = null;
}
MyAvatar.clearJointData(MOUSE_GRAB_JOINT);
// TODO: play sounds again when we aren't leaking AudioInjector threads
//Audio.playSound(releaseSound, { position: entityProperties.position, volume: VOLUME });
}
};
Grabber.prototype.scheduleMouseMoveProcessor = function(event) {
var _this = this;
if (!this.moveEventTimer) {
this.moveEventTimer = Script.setTimeout(function() {
_this.moveEventProcess();
}, DELAY_FOR_30HZ);
}
};
Grabber.prototype.moveEvent = function(event) {
// during the handling of the event, do as little as possible. We save the updated mouse position,
// and start a timer to react to the change. If more changes arrive before the timer fires, only
// the last update will be considered. This is done to avoid backing-up Qt's event queue.
if (!this.isGrabbing || HMD.active) {
return;
}
mouse.updateDrag(event);
this.scheduleMouseMoveProcessor();
};
Grabber.prototype.moveEventProcess = function() {
this.moveEventTimer = null;
var entityProperties = Entities.getEntityProperties(this.entityID, DISPATCHER_PROPERTIES);
if (!entityProperties || HMD.active) {
return;
}
this.currentPosition = entityProperties.position;
if (this.mode === "rotate") {
var drag = mouse.getDrag();
var orientation = Camera.getOrientation();
var dragOffset = Vec3.multiply(drag.x, Quat.getRight(orientation));
dragOffset = Vec3.sum(dragOffset, Vec3.multiply(-drag.y, Quat.getUp(orientation)));
var axis = Vec3.cross(dragOffset, Quat.getForward(orientation));
axis = Vec3.normalize(axis);
var ROTATE_STRENGTH = 0.4; // magic number tuned by hand
var angle = ROTATE_STRENGTH * Math.sqrt((drag.x * drag.x) + (drag.y * drag.y));
var deltaQ = Quat.angleAxis(angle, axis);
this.lastRotation = Quat.multiply(deltaQ, this.lastRotation);
MyAvatar.setJointRotation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointRotation(this.lastRotation));
} else {
var newPointOnPlane;
if (this.mode === "verticalCylinder") {
// for this mode we recompute the plane based on current Camera
var planeNormal = Quat.getForward(Camera.getOrientation());
planeNormal.y = 0;
planeNormal = Vec3.normalize(planeNormal);
var pointOnCylinder = Vec3.multiply(planeNormal, this.xzDistanceToGrab);
pointOnCylinder = Vec3.sum(Camera.getPosition(), pointOnCylinder);
newPointOnPlane = mouseIntersectionWithPlane(pointOnCylinder, planeNormal, mouse.current, this.maxDistance);
} else {
var cameraPosition = Camera.getPosition();
newPointOnPlane = mouseIntersectionWithPlane(this.pointOnPlane, this.planeNormal, mouse.current, this.maxDistance);
var relativePosition = Vec3.subtract(newPointOnPlane, cameraPosition);
var distance = Vec3.length(relativePosition);
if (distance > this.maxDistance) {
// clamp distance
relativePosition = Vec3.multiply(relativePosition, this.maxDistance / distance);
newPointOnPlane = Vec3.sum(relativePosition, cameraPosition);
}
}
MyAvatar.setJointTranslation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointPoint(Vec3.sum(newPointOnPlane, this.offset)));
}
this.scheduleMouseMoveProcessor();
};
Grabber.prototype.keyReleaseEvent = function(event) {
if (event.text === "SHIFT") {
this.liftKey = false;
}
if (event.text === "CONTROL") {
this.rotateKey = false;
}
this.computeNewGrabPlane();
};
Grabber.prototype.keyPressEvent = function(event) {
if (event.text === "SHIFT") {
this.liftKey = true;
}
if (event.text === "CONTROL") {
this.rotateKey = true;
}
this.computeNewGrabPlane();
};
Grabber.prototype.cleanup = function() {
Pointers.removePointer(this.mouseRayEntities);
Picks.removePick(this.mouseRayOverlays);
if (this.grabID) {
MyAvatar.releaseGrab(this.grabID);
this.grabID = null;
}
};
var grabber = new Grabber();
function pressEvent(event) {
grabber.pressEvent(event);
}
function moveEvent(event) {
grabber.moveEvent(event);
}
function releaseEvent(event) {
grabber.releaseEvent(event);
}
function keyPressEvent(event) {
grabber.keyPressEvent(event);
}
function keyReleaseEvent(event) {
grabber.keyReleaseEvent(event);
}
function cleanup() {
grabber.cleanup();
}
Controller.mousePressEvent.connect(pressEvent);
Controller.mouseMoveEvent.connect(moveEvent);
Controller.mouseReleaseEvent.connect(releaseEvent);
Controller.keyPressEvent.connect(keyPressEvent);
Controller.keyReleaseEvent.connect(keyReleaseEvent);
Script.scriptEnding.connect(cleanup);
}()); // END LOCAL_SCOPE

View file

@ -0,0 +1,958 @@
//
// scripts/system/libraries/handTouch.js
//
// Created by Luis Cuenca on 12/29/17
// Copyright 2017 High Fidelity, Inc.
//
//
// 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, Overlays, Controller, Vec3, MyAvatar, Entities, RayPick
*/
(function () {
var LEAP_MOTION_NAME = "LeapMotion";
var handTouchEnabled = true;
var leapMotionEnabled = Controller.getRunningInputDeviceNames().indexOf(LEAP_MOTION_NAME) >= 0;
var MSECONDS_AFTER_LOAD = 2000;
var updateFingerWithIndex = 0;
var untouchableEntities = [];
// Keys to access finger data
var fingerKeys = ["pinky", "ring", "middle", "index", "thumb"];
// Additionally close the hands to achieve a grabbing effect
var grabPercent = { left: 0, right: 0 };
var Palm = function() {
this.position = {x: 0, y: 0, z: 0};
this.perpendicular = {x: 0, y: 0, z: 0};
this.distance = 0;
this.fingers = {
pinky: {x: 0, y: 0, z: 0},
middle: {x: 0, y: 0, z: 0},
ring: {x: 0, y: 0, z: 0},
thumb: {x: 0, y: 0, z: 0},
index: {x: 0, y: 0, z: 0}
};
this.set = false;
};
var palmData = {
left: new Palm(),
right: new Palm()
};
var handJointNames = {left: "LeftHand", right: "RightHand"};
// Store which fingers are touching - if all false restate the default poses
var isTouching = {
left: {
pinky: false,
middle: false,
ring: false,
thumb: false,
index: false
}, right: {
pinky: false,
middle: false,
ring: false,
thumb: false,
index: false
}
};
// frame count for transition to default pose
var countToDefault = {
left: 0,
right: 0
};
// joint data for open pose
var dataOpen = {
left: {
pinky: [
{x: -0.0066, y: -0.0224, z: -0.2174, w: 0.9758},
{x: 0.0112, y: 0.0001, z: 0.0093, w: 0.9999},
{x: -0.0346, y: 0.0003, z: -0.0073, w: 0.9994}
],
ring: [
{x: -0.0029, y: -0.0094, z: -0.1413, w: 0.9899},
{x: 0.0112, y: 0.0001, z: 0.0059, w: 0.9999},
{x: -0.0346, y: 0.0002, z: -0.006, w: 0.9994}
],
middle: [
{x: -0.0016, y: 0, z: -0.0286, w: 0.9996},
{x: 0.0112, y: -0.0001, z: -0.0063, w: 0.9999},
{x: -0.0346, y: -0.0003, z: 0.0073, w: 0.9994}
],
index: [
{x: -0.0016, y: 0.0001, z: 0.0199, w: 0.9998},
{x: 0.0112, y: 0, z: 0.0081, w: 0.9999},
{x: -0.0346, y: 0.0008, z: -0.023, w: 0.9991}
],
thumb: [
{x: 0.0354, y: 0.0363, z: 0.3275, w: 0.9435},
{x: -0.0945, y: 0.0938, z: 0.0995, w: 0.9861},
{x: -0.0952, y: 0.0718, z: 0.1382, w: 0.9832}
]
}, right: {
pinky: [
{x: -0.0034, y: 0.023, z: 0.1051, w: 0.9942},
{x: 0.0106, y: -0.0001, z: -0.0091, w: 0.9999},
{x: -0.0346, y: -0.0003, z: 0.0075, w: 0.9994}
],
ring: [
{x: -0.0013, y: 0.0097, z: 0.0311, w: 0.9995},
{x: 0.0106, y: -0.0001, z: -0.0056, w: 0.9999},
{x: -0.0346, y: -0.0002, z: 0.0061, w: 0.9994}
],
middle: [
{x: -0.001, y: 0, z: 0.0285, w: 0.9996},
{x: 0.0106, y: 0.0001, z: 0.0062, w: 0.9999},
{x: -0.0346, y: 0.0003, z: -0.0074, w: 0.9994}
],
index: [
{x: -0.001, y: 0, z: -0.0199, w: 0.9998},
{x: 0.0106, y: -0.0001, z: -0.0079, w: 0.9999},
{x: -0.0346, y: -0.0008, z: 0.0229, w: 0.9991}
],
thumb: [
{x: 0.0355, y: -0.0363, z: -0.3263, w: 0.9439},
{x: -0.0946, y: -0.0938, z: -0.0996, w: 0.9861},
{x: -0.0952, y: -0.0719, z: -0.1376, w: 0.9833}
]
}
};
// joint data for close pose
var dataClose = {
left: {
pinky: [
{x: 0.5878, y: -0.1735, z: -0.1123, w: 0.7821},
{x: 0.5704, y: 0.0053, z: 0.0076, w: 0.8213},
{x: 0.6069, y: -0.0044, z: -0.0058, w: 0.7947}
],
ring: [
{x: 0.5761, y: -0.0989, z: -0.1025, w: 0.8048},
{x: 0.5332, y: 0.0032, z: 0.005, w: 0.846},
{x: 0.5773, y: -0.0035, z: -0.0049, w: 0.8165}
],
middle: [
{x: 0.543, y: -0.0469, z: -0.0333, w: 0.8378},
{x: 0.5419, y: -0.0034, z: -0.0053, w: 0.8404},
{x: 0.5015, y: 0.0037, z: 0.0063, w: 0.8651}
],
index: [
{x: 0.3051, y: -0.0156, z: -0.014, w: 0.9521},
{x: 0.6414, y: 0.0051, z: 0.0063, w: 0.7671},
{x: 0.5646, y: -0.013, z: -0.019, w: 0.8251}
],
thumb: [
{x: 0.313, y: -0.0348, z: 0.3192, w: 0.8938},
{x: 0, y: 0, z: -0.37, w: 0.929},
{x: 0, y: 0, z: -0.2604, w: 0.9655}
]
}, right: {
pinky: [
{x: 0.5881, y: 0.1728, z: 0.1114, w: 0.7823},
{x: 0.5704, y: -0.0052, z: -0.0075, w: 0.8213},
{x: 0.6069, y: 0.0046, z: 0.006, w: 0.7947}
],
ring: [
{x: 0.5729, y: 0.1181, z: 0.0898, w: 0.8061},
{x: 0.5332, y: -0.003, z: -0.0048, w: 0.846},
{x: 0.5773, y: 0.0035, z: 0.005, w: 0.8165}
],
middle: [
{x: 0.543, y: 0.0468, z: 0.0332, w: 0.8378},
{x: 0.5419, y: 0.0034, z: 0.0052, w: 0.8404},
{x: 0.5047, y: -0.0037, z: -0.0064, w: 0.8632}
],
index: [
{x: 0.306, y: -0.0076, z: -0.0584, w: 0.9502},
{x: 0.6409, y: -0.005, z: -0.006, w: 0.7675},
{x: 0.5646, y: 0.0129, z: 0.0189, w: 0.8251}
],
thumb: [
{x: 0.313, y: 0.0352, z: -0.3181, w: 0.8942},
{x: 0, y: 0, z: 0.3698, w: 0.9291},
{x: 0, y: 0, z: 0.2609, w: 0.9654}
]
}
};
// snapshot for the default pose
var dataDefault = {
left: {
pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
set: false
},
right: {
pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
set: false
}
};
// joint data for the current frame
var dataCurrent = {
left: {
pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}]
},
right: {
pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}]
}
};
// interpolated values on joint data to smooth movement
var dataDelta = {
left: {
pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}]
},
right: {
pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}],
index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}]
}
};
// Acquire an updated value per hand every 5 frames when finger is touching (faster in)
var touchAnimationSteps = 5;
// Acquire an updated value per hand every 20 frames when finger is returning to default position (slower out)
var defaultAnimationSteps = 10;
// Debugging info
var showSphere = false;
var showLines = false;
// This get setup on creation
var linesCreated = false;
var sphereCreated = false;
// Register object with API Debugger
var varsToDebug = {
scriptLoaded: false,
toggleDebugSphere: function() {
showSphere = !showSphere;
if (showSphere && !sphereCreated) {
createDebugSphere();
sphereCreated = true;
}
},
toggleDebugLines: function() {
showLines = !showLines;
if (showLines && !linesCreated) {
createDebugLines();
linesCreated = true;
}
},
fingerPercent: {
left: {
pinky: 0.38,
middle: 0.38,
ring: 0.38,
thumb: 0.38,
index: 0.38
} ,
right: {
pinky: 0.38,
middle: 0.38,
ring: 0.38,
thumb: 0.38,
index: 0.38
}
},
triggerValues: {
leftTriggerValue: 0,
leftTriggerClicked: 0,
rightTriggerValue: 0,
rightTriggerClicked: 0,
leftSecondaryValue: 0,
rightSecondaryValue: 0
},
palmData: {
left: new Palm(),
right: new Palm()
},
offset: {x: 0, y: 0, z: 0},
avatarLoaded: false
};
// Add/Subtract the joint data - per finger joint
function addVals(val1, val2, sign) {
var val = [];
if (val1.length !== val2.length) {
return;
}
for (var i = 0; i < val1.length; i++) {
val.push({x: 0, y: 0, z: 0, w: 0});
val[i].x = val1[i].x + sign*val2[i].x;
val[i].y = val1[i].y + sign*val2[i].y;
val[i].z = val1[i].z + sign*val2[i].z;
val[i].w = val1[i].w + sign*val2[i].w;
}
return val;
}
// Multiply/Divide the joint data - per finger joint
function multiplyValsBy(val1, num) {
var val = [];
for (var i = 0; i < val1.length; i++) {
val.push({x: 0, y: 0, z: 0, w: 0});
val[i].x = val1[i].x * num;
val[i].y = val1[i].y * num;
val[i].z = val1[i].z * num;
val[i].w = val1[i].w * num;
}
return val;
}
// Calculate the finger lengths by adding its joint lengths
function getJointDistances(jointNamesArray) {
var result = {distances: [], totalDistance: 0};
for (var i = 1; i < jointNamesArray.length; i++) {
var index0 = MyAvatar.getJointIndex(jointNamesArray[i-1]);
var index1 = MyAvatar.getJointIndex(jointNamesArray[i]);
var pos0 = MyAvatar.getJointPosition(index0);
var pos1 = MyAvatar.getJointPosition(index1);
var distance = Vec3.distance(pos0, pos1);
result.distances.push(distance);
result.totalDistance += distance;
}
return result;
}
function dataRelativeToWorld(side, dataIn, dataOut) {
var handJoint = handJointNames[side];
var jointIndex = MyAvatar.getJointIndex(handJoint);
var worldPosHand = MyAvatar.jointToWorldPoint({x: 0, y: 0, z: 0}, jointIndex);
dataOut.position = MyAvatar.jointToWorldPoint(dataIn.position, jointIndex);
var localPerpendicular = side === "right" ? {x: 0.2, y: 0, z: 1} : {x: -0.2, y: 0, z: 1};
dataOut.perpendicular = Vec3.normalize(
Vec3.subtract(MyAvatar.jointToWorldPoint(localPerpendicular, jointIndex), worldPosHand)
);
dataOut.distance = dataIn.distance;
for (var i = 0; i < fingerKeys.length; i++) {
var finger = fingerKeys[i];
dataOut.fingers[finger] = MyAvatar.jointToWorldPoint(dataIn.fingers[finger], jointIndex);
}
}
function dataRelativeToHandJoint(side, dataIn, dataOut) {
var handJoint = handJointNames[side];
var jointIndex = MyAvatar.getJointIndex(handJoint);
var worldPosHand = MyAvatar.jointToWorldPoint({x: 0, y: 0, z: 0}, jointIndex);
dataOut.position = MyAvatar.worldToJointPoint(dataIn.position, jointIndex);
dataOut.perpendicular = MyAvatar.worldToJointPoint(Vec3.sum(worldPosHand, dataIn.perpendicular), jointIndex);
dataOut.distance = dataIn.distance;
for (var i = 0; i < fingerKeys.length; i++) {
var finger = fingerKeys[i];
dataOut.fingers[finger] = MyAvatar.worldToJointPoint(dataIn.fingers[finger], jointIndex);
}
}
// Calculate touch field; Sphere at the center of the palm,
// perpendicular vector from the palm plane and origin of the the finger rays
function estimatePalmData(side) {
// Return data object
var data = new Palm();
var jointOffset = { x: 0, y: 0, z: 0 };
var upperSide = side[0].toUpperCase() + side.substring(1);
var jointIndexHand = MyAvatar.getJointIndex(upperSide + "Hand");
// Store position of the hand joint
var worldPosHand = MyAvatar.jointToWorldPoint(jointOffset, jointIndexHand);
var minusWorldPosHand = {x: -worldPosHand.x, y: -worldPosHand.y, z: -worldPosHand.z};
// Data for finger rays
var directions = {pinky: undefined, middle: undefined, ring: undefined, thumb: undefined, index: undefined};
var positions = {pinky: undefined, middle: undefined, ring: undefined, thumb: undefined, index: undefined};
var thumbLength = 0;
var weightCount = 0;
// Calculate palm center
var handJointWeight = 1;
var fingerJointWeight = 2;
var palmCenter = {x: 0, y: 0, z: 0};
palmCenter = Vec3.sum(worldPosHand, palmCenter);
weightCount += handJointWeight;
for (var i = 0; i < fingerKeys.length; i++) {
var finger = fingerKeys[i];
var jointSuffixes = 4; // Get 4 joint names with suffix numbers (0, 1, 2, 3)
var jointNames = getJointNames(side, finger, jointSuffixes);
var fingerLength = getJointDistances(jointNames).totalDistance;
var jointIndex = MyAvatar.getJointIndex(jointNames[0]);
positions[finger] = MyAvatar.jointToWorldPoint(jointOffset, jointIndex);
directions[finger] = Vec3.normalize(Vec3.sum(positions[finger], minusWorldPosHand));
data.fingers[finger] = Vec3.sum(positions[finger], Vec3.multiply(fingerLength, directions[finger]));
if (finger !== "thumb") {
// finger joints have double the weight than the hand joint
// This would better position the palm estimation
palmCenter = Vec3.sum(Vec3.multiply(fingerJointWeight, positions[finger]), palmCenter);
weightCount += fingerJointWeight;
} else {
thumbLength = fingerLength;
}
}
// perpendicular change direction depending on the side
data.perpendicular = (side === "right") ?
Vec3.normalize(Vec3.cross(directions.index, directions.pinky)):
Vec3.normalize(Vec3.cross(directions.pinky, directions.index));
data.position = Vec3.multiply(1.0/weightCount, palmCenter);
if (side === "right") {
varsToDebug.offset = MyAvatar.worldToJointPoint(worldPosHand, jointIndexHand);
}
var palmDistanceMultiplier = 1.55; // 1.55 based on test/error for the sphere radius that best fits the hand
data.distance = palmDistanceMultiplier*Vec3.distance(data.position, positions.index);
// move back thumb ray origin
var thumbBackMultiplier = 0.2;
data.fingers.thumb = Vec3.sum(
data.fingers.thumb, Vec3.multiply( -thumbBackMultiplier * thumbLength, data.perpendicular));
// return getDataRelativeToHandJoint(side, data);
dataRelativeToHandJoint(side, data, palmData[side]);
palmData[side].set = true;
}
// Register GlobalDebugger for API Debugger
Script.registerValue("GlobalDebugger", varsToDebug);
// store the rays for the fingers - only for debug purposes
var fingerRays = {
left: {
pinky: undefined,
middle: undefined,
ring: undefined,
thumb: undefined,
index: undefined
},
right: {
pinky: undefined,
middle: undefined,
ring: undefined,
thumb: undefined,
index: undefined
}
};
// Create debug overlays - finger rays + palm rays + spheres
var palmRay, sphereHand;
function createDebugLines() {
for (var i = 0; i < fingerKeys.length; i++) {
fingerRays.left[fingerKeys[i]] = Overlays.addOverlay("line3d", {
color: { red: 0, green: 0, blue: 255 },
start: { x: 0, y: 0, z: 0 },
end: { x: 0, y: 1, z: 0 },
visible: showLines
});
fingerRays.right[fingerKeys[i]] = Overlays.addOverlay("line3d", {
color: { red: 0, green: 0, blue: 255 },
start: { x: 0, y: 0, z: 0 },
end: { x: 0, y: 1, z: 0 },
visible: showLines
});
}
palmRay = {
left: Overlays.addOverlay("line3d", {
color: { red: 255, green: 0, blue: 0 },
start: { x: 0, y: 0, z: 0 },
end: { x: 0, y: 1, z: 0 },
visible: showLines
}),
right: Overlays.addOverlay("line3d", {
color: { red: 255, green: 0, blue: 0 },
start: { x: 0, y: 0, z: 0 },
end: { x: 0, y: 1, z: 0 },
visible: showLines
})
};
linesCreated = true;
}
function createDebugSphere() {
sphereHand = {
right: Overlays.addOverlay("sphere", {
position: MyAvatar.position,
color: { red: 0, green: 255, blue: 0 },
scale: { x: 0.01, y: 0.01, z: 0.01 },
visible: showSphere
}),
left: Overlays.addOverlay("sphere", {
position: MyAvatar.position,
color: { red: 0, green: 255, blue: 0 },
scale: { x: 0.01, y: 0.01, z: 0.01 },
visible: showSphere
})
};
sphereCreated = true;
}
function acquireDefaultPose(side) {
for (var i = 0; i < fingerKeys.length; i++) {
var finger = fingerKeys[i];
var jointSuffixes = 3; // We need rotation of the 0, 1 and 2 joints
var names = getJointNames(side, finger, jointSuffixes);
for (var j = 0; j < names.length; j++) {
var index = MyAvatar.getJointIndex(names[j]);
var rotation = MyAvatar.getJointRotation(index);
dataDefault[side][finger][j] = dataCurrent[side][finger][j] = rotation;
}
}
dataDefault[side].set = true;
}
var rayPicks = {
left: {
pinky: undefined,
middle: undefined,
ring: undefined,
thumb: undefined,
index: undefined
},
right: {
pinky: undefined,
middle: undefined,
ring: undefined,
thumb: undefined,
index: undefined
}
};
var dataFailed = {
left: {
pinky: 0,
middle: 0,
ring: 0,
thumb: 0,
index: 0
},
right: {
pinky: 0,
middle: 0,
ring: 0,
thumb: 0,
index: 0
}
};
function clearRayPicks(side) {
for (var i = 0; i < fingerKeys.length; i++) {
var finger = fingerKeys[i];
if (rayPicks[side][finger] !== undefined) {
RayPick.removeRayPick(rayPicks[side][finger]);
rayPicks[side][finger] = undefined;
}
}
}
function createRayPicks(side) {
var data = palmData[side];
clearRayPicks(side);
for (var i = 0; i < fingerKeys.length; i++) {
var finger = fingerKeys[i];
var LOOKUP_DISTANCE_MULTIPLIER = 1.5;
var dist = LOOKUP_DISTANCE_MULTIPLIER*data.distance;
var checkOffset = {
x: data.perpendicular.x * dist,
y: data.perpendicular.y * dist,
z: data.perpendicular.z * dist
};
var checkPoint = Vec3.sum(data.position, Vec3.multiply(2, checkOffset));
var sensorToWorldScale = MyAvatar.getSensorToWorldScale();
var origin = data.fingers[finger];
var direction = Vec3.normalize(Vec3.subtract(checkPoint, origin));
origin = Vec3.multiply(1/sensorToWorldScale, origin);
rayPicks[side][finger] = RayPick.createRayPick(
{
"enabled": false,
"joint": handJointNames[side],
"posOffset": origin,
"dirOffset": direction,
"filter": RayPick.PICK_ENTITIES
}
);
RayPick.setPrecisionPicking(rayPicks[side][finger], true);
}
}
function activateNextRay(side, index) {
var nextIndex = (index < fingerKeys.length-1) ? index + 1 : 0;
for (var i = 0; i < fingerKeys.length; i++) {
var finger = fingerKeys[i];
if (i === nextIndex) {
RayPick.enableRayPick(rayPicks[side][finger]);
} else {
RayPick.disableRayPick(rayPicks[side][finger]);
}
}
}
function updateSphereHand(side) {
var data = new Palm();
dataRelativeToWorld(side, palmData[side], data);
varsToDebug.palmData[side] = palmData[side];
var palmPoint = data.position;
var LOOKUP_DISTANCE_MULTIPLIER = 1.5;
var dist = LOOKUP_DISTANCE_MULTIPLIER*data.distance;
// Situate the debugging overlays
var checkOffset = {
x: data.perpendicular.x * dist,
y: data.perpendicular.y * dist,
z: data.perpendicular.z * dist
};
var spherePos = Vec3.sum(palmPoint, checkOffset);
var checkPoint = Vec3.sum(palmPoint, Vec3.multiply(2, checkOffset));
if (showLines) {
Overlays.editOverlay(palmRay[side], {
start: palmPoint,
end: checkPoint,
visible: showLines
});
for (var i = 0; i < fingerKeys.length; i++) {
Overlays.editOverlay(fingerRays[side][fingerKeys[i]], {
start: data.fingers[fingerKeys[i]],
end: checkPoint,
visible: showLines
});
}
}
if (showSphere) {
Overlays.editOverlay(sphereHand[side], {
position: spherePos,
scale: {
x: 2*dist,
y: 2*dist,
z: 2*dist
},
visible: showSphere
});
}
// Update the intersection of only one finger at a time
var finger = fingerKeys[updateFingerWithIndex];
var nearbyEntities = Entities.findEntities(spherePos, dist);
// Filter the entities that are allowed to be touched
var touchableEntities = nearbyEntities.filter(function (id) {
return untouchableEntities.indexOf(id) == -1;
});
var intersection;
if (rayPicks[side][finger] !== undefined) {
intersection = RayPick.getPrevRayPickResult(rayPicks[side][finger]);
}
var animationSteps = defaultAnimationSteps;
var newFingerData = dataDefault[side][finger];
var isAbleToGrab = false;
if (touchableEntities.length > 0) {
RayPick.setIncludeItems(rayPicks[side][finger], touchableEntities);
if (intersection === undefined) {
return;
}
var percent = 0; // Initialize
isAbleToGrab = intersection.intersects && intersection.distance < LOOKUP_DISTANCE_MULTIPLIER*dist;
if (isAbleToGrab && !getTouching(side)) {
acquireDefaultPose(side); // take a snapshot of the default pose before touch starts
newFingerData = dataDefault[side][finger]; // assign default pose to finger data
}
// Store if this finger is touching something
isTouching[side][finger] = isAbleToGrab;
if (isAbleToGrab) {
// update the open/close percentage for this finger
var FINGER_REACT_MULTIPLIER = 2.8;
percent = intersection.distance/(FINGER_REACT_MULTIPLIER*dist);
var THUMB_FACTOR = 0.2;
var FINGER_FACTOR = 0.05;
// Amount of grab coefficient added to the fingers - thumb is higher
var grabMultiplier = finger === "thumb" ? THUMB_FACTOR : FINGER_FACTOR;
percent += grabMultiplier * grabPercent[side];
// Calculate new interpolation data
var totalDistance = addVals(dataClose[side][finger], dataOpen[side][finger], -1);
// Assign close/open ratio to finger to simulate touch
newFingerData = addVals(dataOpen[side][finger], multiplyValsBy(totalDistance, percent), 1);
animationSteps = touchAnimationSteps;
}
varsToDebug.fingerPercent[side][finger] = percent;
}
if (!isAbleToGrab) {
dataFailed[side][finger] = dataFailed[side][finger] === 0 ? 1 : 2;
} else {
dataFailed[side][finger] = 0;
}
// If it only fails once it will not update increments
if (dataFailed[side][finger] !== 1) {
// Calculate animation increments
dataDelta[side][finger] =
multiplyValsBy(addVals(newFingerData, dataCurrent[side][finger], -1), 1.0/animationSteps);
}
}
// Recreate the finger joint names
function getJointNames(side, finger, count) {
var names = [];
for (var i = 1; i < count+1; i++) {
var name = side[0].toUpperCase()+side.substring(1)+"Hand"+finger[0].toUpperCase()+finger.substring(1)+(i);
names.push(name);
}
return names;
}
// Capture the controller values
var leftTriggerPress = function (value) {
varsToDebug.triggerValues.leftTriggerValue = value;
// the value for the trigger increments the hand-close percentage
grabPercent.left = value;
};
var leftTriggerClick = function (value) {
varsToDebug.triggerValues.leftTriggerClicked = value;
};
var rightTriggerPress = function (value) {
varsToDebug.triggerValues.rightTriggerValue = value;
// the value for the trigger increments the hand-close percentage
grabPercent.right = value;
};
var rightTriggerClick = function (value) {
varsToDebug.triggerValues.rightTriggerClicked = value;
};
var leftSecondaryPress = function (value) {
varsToDebug.triggerValues.leftSecondaryValue = value;
};
var rightSecondaryPress = function (value) {
varsToDebug.triggerValues.rightSecondaryValue = value;
};
var MAPPING_NAME = "com.highfidelity.handTouch";
var mapping = Controller.newMapping(MAPPING_NAME);
mapping.from([Controller.Standard.RT]).peek().to(rightTriggerPress);
mapping.from([Controller.Standard.RTClick]).peek().to(rightTriggerClick);
mapping.from([Controller.Standard.LT]).peek().to(leftTriggerPress);
mapping.from([Controller.Standard.LTClick]).peek().to(leftTriggerClick);
mapping.from([Controller.Standard.RB]).peek().to(rightSecondaryPress);
mapping.from([Controller.Standard.LB]).peek().to(leftSecondaryPress);
mapping.from([Controller.Standard.LeftGrip]).peek().to(leftSecondaryPress);
mapping.from([Controller.Standard.RightGrip]).peek().to(rightSecondaryPress);
Controller.enableMapping(MAPPING_NAME);
if (showLines && !linesCreated) {
createDebugLines();
linesCreated = true;
}
if (showSphere && !sphereCreated) {
createDebugSphere();
sphereCreated = true;
}
function getTouching(side) {
var animating = false;
for (var i = 0; i < fingerKeys.length; i++) {
var finger = fingerKeys[i];
animating = animating || isTouching[side][finger];
}
return animating; // return false only if none of the fingers are touching
}
function reEstimatePalmData() {
["right", "left"].forEach(function(side) {
estimatePalmData(side);
});
}
function recreateRayPicks() {
["right", "left"].forEach(function(side) {
createRayPicks(side);
});
}
function cleanUp() {
["right", "left"].forEach(function (side) {
if (linesCreated) {
Overlays.deleteOverlay(palmRay[side]);
}
if (sphereCreated) {
Overlays.deleteOverlay(sphereHand[side]);
}
clearRayPicks(side);
for (var i = 0; i < fingerKeys.length; i++) {
var finger = fingerKeys[i];
var jointSuffixes = 3; // We need to clear the joints 0, 1 and 2 joints
var names = getJointNames(side, finger, jointSuffixes);
for (var j = 0; j < names.length; j++) {
var index = MyAvatar.getJointIndex(names[j]);
MyAvatar.clearJointData(index);
}
if (linesCreated) {
Overlays.deleteOverlay(fingerRays[side][finger]);
}
}
});
}
MyAvatar.shouldDisableHandTouchChanged.connect(function (shouldDisable) {
if (shouldDisable) {
if (handTouchEnabled) {
cleanUp();
}
} else {
if (!handTouchEnabled) {
reEstimatePalmData();
recreateRayPicks();
}
}
handTouchEnabled = !shouldDisable;
});
Controller.inputDeviceRunningChanged.connect(function (deviceName, isEnabled) {
if (deviceName == LEAP_MOTION_NAME) {
leapMotionEnabled = isEnabled;
}
});
MyAvatar.disableHandTouchForIDChanged.connect(function (entityID, disable) {
var entityIndex = untouchableEntities.indexOf(entityID);
if (disable) {
if (entityIndex == -1) {
untouchableEntities.push(entityID);
}
} else {
if (entityIndex != -1) {
untouchableEntities.splice(entityIndex, 1);
}
}
});
MyAvatar.onLoadComplete.connect(function () {
// Sometimes the rig is not ready when this signal is trigger
console.log("avatar loaded");
Script.setTimeout(function() {
reEstimatePalmData();
recreateRayPicks();
}, MSECONDS_AFTER_LOAD);
});
MyAvatar.sensorToWorldScaleChanged.connect(function() {
reEstimatePalmData();
});
Script.scriptEnding.connect(function () {
cleanUp();
});
Script.update.connect(function () {
if (!handTouchEnabled || leapMotionEnabled) {
return;
}
// index of the finger that needs to be updated this frame
updateFingerWithIndex = (updateFingerWithIndex < fingerKeys.length-1) ? updateFingerWithIndex + 1 : 0;
["right", "left"].forEach(function(side) {
if (!palmData[side].set) {
reEstimatePalmData();
recreateRayPicks();
}
// recalculate the base data
updateSphereHand(side);
activateNextRay(side, updateFingerWithIndex);
// this vars manage the transition to default pose
var isHandTouching = getTouching(side);
countToDefault[side] = isHandTouching ? 0 : countToDefault[side] + 1;
for (var i = 0; i < fingerKeys.length; i++) {
var finger = fingerKeys[i];
var jointSuffixes = 3; // We need to update rotation of the 0, 1 and 2 joints
var names = getJointNames(side, finger, jointSuffixes);
// Add the animation increments
dataCurrent[side][finger] = addVals(dataCurrent[side][finger], dataDelta[side][finger], 1);
// update every finger joint
for (var j = 0; j < names.length; j++) {
var index = MyAvatar.getJointIndex(names[j]);
// if no finger is touching restate the default poses
if (isHandTouching || (dataDefault[side].set &&
countToDefault[side] < fingerKeys.length*touchAnimationSteps)) {
var quatRot = dataCurrent[side][finger][j];
MyAvatar.setJointRotation(index, quatRot);
} else {
MyAvatar.clearJointData(index);
}
}
}
});
});
}());

View file

@ -0,0 +1,184 @@
"use strict";
//
// controllers/squeezeHands.js
//
// Created by Anthony J. Thibault
// Copyright 2015 High Fidelity, Inc.
//
// Default script to drive the animation of the hands based on hand controllers.
//
// 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, Messages, Controller */
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
(function() { // BEGIN LOCAL_SCOPE
var lastLeftTrigger = 0;
var lastRightTrigger = 0;
var leftHandOverlayAlpha = 0;
var rightHandOverlayAlpha = 0;
// var CONTROLLER_DEAD_SPOT = 0.25;
var TRIGGER_SMOOTH_TIMESCALE = 0.1;
var OVERLAY_RAMP_RATE = 8.0;
var animStateHandlerID;
var leftIndexPointingOverride = 0;
var rightIndexPointingOverride = 0;
var leftThumbRaisedOverride = 0;
var rightThumbRaisedOverride = 0;
var HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index";
var isLeftIndexPointing = false;
var isRightIndexPointing = false;
var isLeftThumbRaised = false;
var isRightThumbRaised = false;
function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}
// function normalizeControllerValue(val) {
// return clamp((val - CONTROLLER_DEAD_SPOT) / (1 - CONTROLLER_DEAD_SPOT), 0, 1);
// }
function lerp(a, b, alpha) {
return a * (1 - alpha) + b * alpha;
}
function init() {
Script.update.connect(update);
animStateHandlerID = MyAvatar.addAnimationStateHandler(
animStateHandler,
[
"leftHandOverlayAlpha", "leftHandGraspAlpha",
"rightHandOverlayAlpha", "rightHandGraspAlpha",
"isLeftHandGrasp", "isLeftIndexPoint", "isLeftThumbRaise", "isLeftIndexPointAndThumbRaise",
"isRightHandGrasp", "isRightIndexPoint", "isRightThumbRaise", "isRightIndexPointAndThumbRaise"
]
);
Messages.subscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL);
Messages.messageReceived.connect(handleMessages);
}
function animStateHandler(props) {
return {
leftHandOverlayAlpha: leftHandOverlayAlpha,
leftHandGraspAlpha: lastLeftTrigger,
rightHandOverlayAlpha: rightHandOverlayAlpha,
rightHandGraspAlpha: lastRightTrigger,
isLeftHandGrasp: !isLeftIndexPointing && !isLeftThumbRaised,
isLeftIndexPoint: isLeftIndexPointing && !isLeftThumbRaised,
isLeftThumbRaise: !isLeftIndexPointing && isLeftThumbRaised,
isLeftIndexPointAndThumbRaise: isLeftIndexPointing && isLeftThumbRaised,
isRightHandGrasp: !isRightIndexPointing && !isRightThumbRaised,
isRightIndexPoint: isRightIndexPointing && !isRightThumbRaised,
isRightThumbRaise: !isRightIndexPointing && isRightThumbRaised,
isRightIndexPointAndThumbRaise: isRightIndexPointing && isRightThumbRaised
};
}
function update(dt) {
var leftTrigger = clamp(Controller.getValue(Controller.Standard.LT) + Controller.getValue(Controller.Standard.LeftGrip), 0, 1);
var rightTrigger = clamp(Controller.getValue(Controller.Standard.RT) + Controller.getValue(Controller.Standard.RightGrip), 0, 1);
// Average last few trigger values together for a bit of smoothing
var tau = clamp(dt / TRIGGER_SMOOTH_TIMESCALE, 0, 1);
lastLeftTrigger = lerp(leftTrigger, lastLeftTrigger, tau);
lastRightTrigger = lerp(rightTrigger, lastRightTrigger, tau);
// ramp on/off left hand overlay
var leftHandPose = Controller.getPoseValue(Controller.Standard.LeftHand);
if (leftHandPose.valid) {
leftHandOverlayAlpha = clamp(leftHandOverlayAlpha + OVERLAY_RAMP_RATE * dt, 0, 1);
} else {
leftHandOverlayAlpha = clamp(leftHandOverlayAlpha - OVERLAY_RAMP_RATE * dt, 0, 1);
}
// ramp on/off right hand overlay
var rightHandPose = Controller.getPoseValue(Controller.Standard.RightHand);
if (rightHandPose.valid) {
rightHandOverlayAlpha = clamp(rightHandOverlayAlpha + OVERLAY_RAMP_RATE * dt, 0, 1);
} else {
rightHandOverlayAlpha = clamp(rightHandOverlayAlpha - OVERLAY_RAMP_RATE * dt, 0, 1);
}
// Pointing index fingers and raising thumbs
isLeftIndexPointing = (leftIndexPointingOverride > 0) || (leftHandPose.valid && Controller.getValue(Controller.Standard.LeftIndexPoint) === 1);
isRightIndexPointing = (rightIndexPointingOverride > 0) || (rightHandPose.valid && Controller.getValue(Controller.Standard.RightIndexPoint) === 1);
isLeftThumbRaised = (leftThumbRaisedOverride > 0) || (leftHandPose.valid && Controller.getValue(Controller.Standard.LeftThumbUp) === 1);
isRightThumbRaised = (rightThumbRaisedOverride > 0) || (rightHandPose.valid && Controller.getValue(Controller.Standard.RightThumbUp) === 1);
}
function handleMessages(channel, message, sender) {
if (sender === MyAvatar.sessionUUID && channel === HIFI_POINT_INDEX_MESSAGE_CHANNEL) {
var data = JSON.parse(message);
if (data.pointIndex !== undefined) {
if (data.pointIndex) {
leftIndexPointingOverride++;
rightIndexPointingOverride++;
} else {
leftIndexPointingOverride--;
rightIndexPointingOverride--;
}
}
if (data.pointLeftIndex !== undefined) {
if (data.pointLeftIndex) {
leftIndexPointingOverride++;
} else {
leftIndexPointingOverride--;
}
}
if (data.pointRightIndex !== undefined) {
if (data.pointRightIndex) {
rightIndexPointingOverride++;
} else {
rightIndexPointingOverride--;
}
}
if (data.raiseThumbs !== undefined) {
if (data.raiseThumbs) {
leftThumbRaisedOverride++;
rightThumbRaisedOverride++;
} else {
leftThumbRaisedOverride--;
rightThumbRaisedOverride--;
}
}
if (data.raiseLeftThumb !== undefined) {
if (data.raiseLeftThumb) {
leftThumbRaisedOverride++;
} else {
leftThumbRaisedOverride--;
}
}
if (data.raiseRightThumb !== undefined) {
if (data.raiseRightThumb) {
rightThumbRaisedOverride++;
} else {
rightThumbRaisedOverride--;
}
}
}
}
function shutdown() {
Script.update.disconnect(update);
MyAvatar.removeAnimationStateHandler(animStateHandlerID);
Messages.unsubscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL);
Messages.messageReceived.disconnect(handleMessages);
}
Script.scriptEnding.connect(shutdown);
init();
}()); // END LOCAL_SCOPE

View file

@ -0,0 +1,174 @@
"use strict";
// Created by james b. pollack @imgntn on 8/18/2016
// Copyright 2016 High Fidelity, Inc.
//
// advanced movements settings are in individual controller json files
// what we do is check the status of the 'advance movement' checkbox when you enter HMD mode
// if 'advanced movement' is checked...we give you the defaults that are in the json.
// if 'advanced movement' is not checked... we override the advanced controls with basic ones.
//
// 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, Quat, MyAvatar, HMD, Controller, Messages*/
(function() { // BEGIN LOCAL_SCOPE
var TWO_SECONDS_INTERVAL = 2000;
var FLYING_MAPPING_NAME = 'Hifi-Flying-Dev-' + Math.random();
var DRIVING_MAPPING_NAME = 'Hifi-Driving-Dev-' + Math.random();
var flyingMapping = null;
var drivingMapping = null;
var TURN_RATE = 1000;
var isDisabled = false;
var previousFlyingState = MyAvatar.getFlyingEnabled();
var previousDrivingState = false;
function rotate180() {
var newOrientation = Quat.multiply(MyAvatar.orientation, Quat.angleAxis(180, {
x: 0,
y: 1,
z: 0
}));
MyAvatar.orientation = newOrientation;
}
var inFlipTurn = false;
function registerBasicMapping() {
drivingMapping = Controller.newMapping(DRIVING_MAPPING_NAME);
drivingMapping.from(Controller.Standard.LY).to(function(value) {
if (isDisabled) {
return;
}
if (value === 1 && Controller.Hardware.OculusTouch !== undefined) {
rotate180();
} else if (Controller.Hardware.Vive !== undefined) {
if (value > 0.75 && inFlipTurn === false) {
inFlipTurn = true;
rotate180();
Script.setTimeout(function() {
inFlipTurn = false;
}, TURN_RATE);
}
}
return;
});
flyingMapping = Controller.newMapping(FLYING_MAPPING_NAME);
flyingMapping.from(Controller.Standard.RY).to(function(value) {
if (isDisabled) {
return;
}
if (value === 1 && Controller.Hardware.OculusTouch !== undefined) {
rotate180();
} else if (Controller.Hardware.Vive !== undefined) {
if (value > 0.75 && inFlipTurn === false) {
inFlipTurn = true;
rotate180();
Script.setTimeout(function() {
inFlipTurn = false;
}, TURN_RATE);
}
}
return;
});
}
function scriptEnding() {
Controller.disableMapping(FLYING_MAPPING_NAME);
Controller.disableMapping(DRIVING_MAPPING_NAME);
}
Script.scriptEnding.connect(scriptEnding);
registerBasicMapping();
Script.setTimeout(function() {
if (MyAvatar.useAdvanceMovementControls) {
Controller.disableMapping(DRIVING_MAPPING_NAME);
} else {
Controller.enableMapping(DRIVING_MAPPING_NAME);
}
if (MyAvatar.getFlyingEnabled()) {
Controller.disableMapping(FLYING_MAPPING_NAME);
} else {
Controller.enableMapping(FLYING_MAPPING_NAME);
}
}, 100);
HMD.displayModeChanged.connect(function(isHMDMode) {
if (isHMDMode) {
if (Controller.Hardware.Vive !== undefined || Controller.Hardware.OculusTouch !== undefined) {
if (MyAvatar.useAdvancedMovementControls) {
Controller.disableMapping(DRIVING_MAPPING_NAME);
} else {
Controller.enableMapping(DRIVING_MAPPING_NAME);
}
if (MyAvatar.getFlyingEnabled()) {
Controller.disableMapping(FLYING_MAPPING_NAME);
} else {
Controller.enableMapping(FLYING_MAPPING_NAME);
}
}
}
});
function update() {
if ((Controller.Hardware.Vive !== undefined || Controller.Hardware.OculusTouch !== undefined) && HMD.active) {
var flying = MyAvatar.getFlyingEnabled();
var driving = MyAvatar.useAdvancedMovementControls;
if (flying !== previousFlyingState) {
if (flying) {
Controller.disableMapping(FLYING_MAPPING_NAME);
} else {
Controller.enableMapping(FLYING_MAPPING_NAME);
}
previousFlyingState = flying;
}
if (driving !== previousDrivingState) {
if (driving) {
Controller.disableMapping(DRIVING_MAPPING_NAME);
} else {
Controller.enableMapping(DRIVING_MAPPING_NAME);
}
previousDrivingState = driving;
}
}
Script.setTimeout(update, TWO_SECONDS_INTERVAL);
}
Script.setTimeout(update, TWO_SECONDS_INTERVAL);
var HIFI_ADVANCED_MOVEMENT_DISABLER_CHANNEL = 'Hifi-Advanced-Movement-Disabler';
function handleMessage(channel, message, sender) {
if (channel === HIFI_ADVANCED_MOVEMENT_DISABLER_CHANNEL) {
if (message === 'disable') {
isDisabled = true;
} else if (message === 'enable') {
isDisabled = false;
}
}
}
Messages.subscribe(HIFI_ADVANCED_MOVEMENT_DISABLER_CHANNEL);
Messages.messageReceived.connect(handleMessage);
}()); // END LOCAL_SCOPE

View file

@ -0,0 +1,372 @@
//
// touchControllerConfiguration.js
//
// Created by Ryan Huffman on 12/06/16
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals TOUCH_CONTROLLER_CONFIGURATION_LEFT:true, TOUCH_CONTROLLER_CONFIGURATION_RIGHT:true,
Quat, Vec3, Script, MyAvatar, Controller */
/* eslint camelcase: ["error", { "properties": "never" }] */
var leftBaseRotation = Quat.multiply(
Quat.fromPitchYawRollDegrees(-90, 0, 0),
Quat.fromPitchYawRollDegrees(0, 0, 90)
);
var rightBaseRotation = Quat.multiply(
Quat.fromPitchYawRollDegrees(-90, 0, 0),
Quat.fromPitchYawRollDegrees(0, 0, -90)
);
// keep these in sync with the values from OculusHelpers.cpp
var CONTROLLER_LENGTH_OFFSET = 0.0762;
// var CONTROLLER_LATERAL_OFFSET = 0.0381;
// var CONTROLLER_VERTICAL_OFFSET = 0.0381;
// var CONTROLLER_FORWARD_OFFSET = 0.1524;
var leftBasePosition = Vec3.multiplyQbyV(leftBaseRotation, {
x: -CONTROLLER_LENGTH_OFFSET / 2.0,
y: CONTROLLER_LENGTH_OFFSET / 2.0,
z: CONTROLLER_LENGTH_OFFSET * 1.5
});
var rightBasePosition = Vec3.multiplyQbyV(rightBaseRotation, {
x: CONTROLLER_LENGTH_OFFSET / 2.0,
y: CONTROLLER_LENGTH_OFFSET / 2.0,
z: CONTROLLER_LENGTH_OFFSET * 1.5
});
var BASE_URL = Script.resourcesPath() + "meshes/controller/touch/";
TOUCH_CONTROLLER_CONFIGURATION_LEFT = {
name: "Touch",
controllers: [
{
modelURL: BASE_URL + "touch_l_body.fbx",
jointIndex: MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"),
naturalPosition: { x: 0.01648625358939171, y: -0.03551870584487915, z: -0.018527675420045853 },
dimensions: { x: 0.11053799837827682, y: 0.0995776429772377, z: 0.10139888525009155 },
rotation: leftBaseRotation,
position: leftBasePosition,
parts: {
tips: {
type: "static",
modelURL: BASE_URL + "Oculus-Labels-L.fbx",
naturalPosition: { x: -0.022335469722747803, y: 0.00022516027092933655, z: 0.020340695977211 },
naturalDimensions: { x: 0.132063, y: 0.0856, z: 0.130282 },
textureName: "blank",
defaultTextureLayer: "blank",
textureLayers: {
blank: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Blank.png"
},
trigger: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Trigger.png"
},
arrows: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Rotate.png"
},
grip: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Grip-oculus.png"
},
teleport: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Teleport.png"
},
both_triggers: {
defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Grip-Trigger.png"
},
}
},
trigger: {
type: "rotational",
modelURL: BASE_URL + "touch_l_trigger.fbx",
naturalPosition: { x: 0.0008544912561774254, y: -0.019867943599820137, z: 0.018800459802150726 },
naturalDimensions: { x: 0.027509, y: 0.025211, z: 0.018443 },
// rotational
input: Controller.Standard.LT,
origin: { x: 0, y: -0.015, z: -0.00 },
minValue: 0.0,
maxValue: 1.0,
axis: { x: 1, y: 0, z: 0 },
maxAngle: 17,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_l_trigger.fbx/touch_l_trigger.fbm/L_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_l_trigger.fbx/touch_l_trigger.fbm/L_controller-highlight_DIF.jpg",
}
}
},
grip: {
type: "linear",
modelURL: BASE_URL + "touch_l_bumper.fbx",
naturalPosition: { x: 0.00008066371083259583, y: -0.02715788595378399, z: -0.02448512241244316 },
naturalDimensions: { x: 0.017444, y: 0.020297, z: 0.026003 },
// linear properties
// Offset from origin = 0.36470, 0.11048, 0.11066
input: "OculusTouch.LeftGrip",
axis: { x: 1, y: 0.302933918, z: 0.302933918 },
maxTranslation: 0.003967,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_l_bumper.fbx/touch_l_bumper.fbm/L_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_l_bumper.fbx/touch_l_bumper.fbm/L_controller-highlight_DIF.jpg",
}
}
},
joystick: {
type: "joystick",
modelURL: BASE_URL + "touch_l_joystick.fbx",
naturalPosition: { x: 0.0075613949447870255, y: -0.008225866593420506, z: 0.004792703315615654 },
naturalDimensions: { x: 0.027386, y: 0.033254, z: 0.027272 },
// joystick
xInput: "OculusTouch.LX",
yInput: "OculusTouch.LY",
originOffset: { x: 0, y: -0.0028564, z: -0.00 },
xHalfAngle: 20,
yHalfAngle: 20,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_l_joystick.fbx/touch_l_joystick.fbm/L_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_l_joystick.fbx/touch_l_joystick.fbm/L_controller-highlight_DIF.jpg",
}
}
},
button_a: {
type: "linear",
modelURL: BASE_URL + "touch_l_button_x.fbx",
naturalPosition: { x: -0.009307309985160828, y: -0.00005015172064304352, z: -0.012594521045684814 },
naturalDimensions: { x: 0.009861, y: 0.004345, z: 0.00982 },
input: "OculusTouch.X",
axis: { x: 0, y: -1, z: 0 },
maxTranslation: 0.001,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_l_button_x.fbx/touch_l_button_x.fbm/L_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_l_button_x.fbx/touch_l_button_x.fbm/L_controller-highlight_DIF.jpg",
}
}
},
button_b: {
type: "linear",
modelURL: BASE_URL + "touch_l_button_y.fbx",
naturalPosition: { x: -0.01616849936544895, y: -0.000050364527851343155, z: 0.0017703399062156677 },
naturalDimensions: { x: 0.010014, y: 0.004412, z: 0.009972 },
input: "OculusTouch.Y",
axis: { x: 0, y: -1, z: 0 },
maxTranslation: 0.001,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_l_button_y.fbx/touch_l_button_y.fbm/L_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_l_button_y.fbx/touch_l_button_y.fbm/L_controller-highlight_DIF.jpg",
}
}
},
}
}
]
};
TOUCH_CONTROLLER_CONFIGURATION_RIGHT = {
name: "Touch",
controllers: [
{
modelURL: BASE_URL + "touch_r_body.fbx",
jointIndex: MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"),
naturalPosition: { x: -0.016486231237649918, y: -0.03551865369081497, z: -0.018527653068304062 },
dimensions: { x: 0.11053784191608429, y: 0.09957750141620636, z: 0.10139875113964081 },
rotation: rightBaseRotation,
position: rightBasePosition,
parts: {
tips: {
type: "static",
modelURL: BASE_URL + "Oculus-Labels-R.fbx",
naturalPosition: { x: 0.009739525616168976, y: -0.0017818436026573181, z: 0.016794726252555847 },
naturalDimensions: { x: 0.129049, y: 0.078297, z: 0.139492 },
textureName: "blank",
defaultTextureLayer: "blank",
textureLayers: {
blank: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Blank.png"
},
trigger: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Trigger.png"
},
arrows: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Rotate.png"
},
grip: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Grip-oculus.png"
},
teleport: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Teleport.png"
},
both_triggers: {
defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Grip-Trigger.png"
},
}
},
trigger: {
type: "rotational",
modelURL: BASE_URL + "touch_r_trigger.fbx",
naturalPosition: { x: -0.0008544912561774254, y: -0.019867943599820137, z: 0.018800459802150726 },
naturalDimensions: { x: 0.027384, y: 0.025201, z: 0.018425 },
// rotational
input: "OculusTouch.RT",
origin: { x: 0, y: -0.015, z: 0 },
minValue: 0.0,
maxValue: 1.0,
axis: { x: 1, y: 0, z: 0 },
maxAngle: 17,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_r_trigger.fbx/touch_r_trigger.fbm/R_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_r_trigger.fbx/touch_r_trigger.fbm/R_controller-highlight_DIF.jpg",
}
}
},
grip: {
type: "linear",
modelURL: BASE_URL + "touch_r_bumper.fbx",
naturalPosition: { x: -0.0000806618481874466, y: -0.027157839387655258, z: -0.024485092610120773 },
naturalDimensions: { x: 0.017268, y: 0.020366, z: 0.02599 },
// linear properties
// Offset from origin = 0.36470, 0.11048, 0.11066
input: "OculusTouch.RightGrip",
axis: { x: -1, y: 0.302933918, z: 0.302933918 },
maxTranslation: 0.003967,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_r_bumper.fbx/touch_r_bumper.fbm/R_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_r_bumper.fbx/touch_r_bumper.fbm/R_controller-highlight_DIF.jpg",
}
}
},
joystick: {
type: "joystick",
modelURL: BASE_URL + "touch_r_joystick.fbx",
naturalPosition: { x: -0.007561382371932268, y: -0.008225853554904461, z: 0.00479268841445446 },
naturalDimensions: { x: 0.027272, y: 0.033254, z: 0.027272 },
// joystick
xInput: "OculusTouch.RX",
yInput: "OculusTouch.RY",
originOffset: { x: 0, y: -0.0028564, z: 0 },
xHalfAngle: 20,
yHalfAngle: 20,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_r_joystick.fbx/touch_r_joystick.fbm/R_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_r_joystick.fbx/touch_r_joystick.fbm/R_controller-highlight_DIF.jpg",
}
}
},
button_a: {
type: "linear",
modelURL: BASE_URL + "touch_r_button_a.fbx",
naturalPosition: { x: 0.009307296946644783, y: -0.00005015172064304352, z: -0.012594504281878471 },
naturalDimensions: { x: 0.00982, y: 0.004345, z: 0.00982 },
input: "OculusTouch.A",
axis: { x: 0, y: -1, z: 0 },
maxTranslation: 0.001,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_r_button_a.fbx/touch_r_button_a.fbm/R_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_r_button_a.fbx/touch_r_button_a.fbm/R_controller-highlight_DIF.jpg",
}
}
},
button_b: {
type: "linear",
modelURL: BASE_URL + "touch_r_button_b.fbx",
naturalPosition: { x: 0.01616847701370716, y: -0.000050364527851343155, z: 0.0017703361809253693 },
naturalDimensions: { x: 0.009972, y: 0.004412, z: 0.009972 },
input: "OculusTouch.B",
axis: { x: 0, y: -1, z: 0 },
maxTranslation: 0.001,
textureName: "tex-highlight",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + "touch_r_button_b.fbx/touch_r_button_b.fbm/R_controller_DIF.jpg",
},
highlight: {
defaultTextureURL: BASE_URL + "touch_r_button_b.fbx/touch_r_button_b.fbm/R_controller-highlight_DIF.jpg",
}
}
},
}
}
]
};

View file

@ -0,0 +1,343 @@
//
// viveControllerConfiguration.js
//
// Created by Anthony J. Thibault on 10/20/16
// Originally created by Ryan Huffman on 9/21/2016
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* globals VIVE_CONTROLLER_CONFIGURATION_LEFT:true, VIVE_CONTROLLER_CONFIGURATION_RIGHT:true,
MyAvatar, Quat, Script, Vec3, Controller */
/* eslint camelcase: ["error", { "properties": "never" }] */
// var LEFT_JOINT_INDEX = MyAvatar.getJointIndex("_CONTROLLER_LEFTHAND");
// var RIGHT_JOINT_INDEX = MyAvatar.getJointIndex("_CONTROLLER_RIGHTHAND");
var leftBaseRotation = Quat.multiply(
Quat.fromPitchYawRollDegrees(0, 0, 45),
Quat.multiply(
Quat.fromPitchYawRollDegrees(90, 0, 0),
Quat.fromPitchYawRollDegrees(0, 0, 90)
)
);
var rightBaseRotation = Quat.multiply(
Quat.fromPitchYawRollDegrees(0, 0, -45),
Quat.multiply(
Quat.fromPitchYawRollDegrees(90, 0, 0),
Quat.fromPitchYawRollDegrees(0, 0, -90)
)
);
// keep these in sync with the values from plugins/openvr/src/OpenVrHelpers.cpp:303
var CONTROLLER_LATERAL_OFFSET = 0.0381;
var CONTROLLER_VERTICAL_OFFSET = 0.0495;
var CONTROLLER_FORWARD_OFFSET = 0.1371;
var leftBasePosition = {
x: CONTROLLER_VERTICAL_OFFSET,
y: CONTROLLER_FORWARD_OFFSET,
z: CONTROLLER_LATERAL_OFFSET
};
var rightBasePosition = {
x: -CONTROLLER_VERTICAL_OFFSET,
y: CONTROLLER_FORWARD_OFFSET,
z: CONTROLLER_LATERAL_OFFSET
};
var viveNaturalDimensions = {
x: 0.1174320001155138,
y: 0.08361100335605443,
z: 0.21942697931081057
};
var viveNaturalPosition = {
x: 0,
y: -0.034076502197422087,
z: 0.06380049744620919
};
var BASE_URL = Script.resourcesPath();
// var TIP_TEXTURE_BASE_URL = BASE_URL + "meshes/controller/vive_tips.fbm/";
var viveModelURL = BASE_URL + "meshes/controller/vive_body.fbx";
// var viveTipsModelURL = BASE_URL + "meshes/controller/vive_tips.fbx";
var viveTriggerModelURL = "meshes/controller/vive_trigger.fbx";
VIVE_CONTROLLER_CONFIGURATION_LEFT = {
name: "Vive",
controllers: [
{
modelURL: viveModelURL,
jointIndex: MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"),
naturalPosition: viveNaturalPosition,
rotation: leftBaseRotation,
position: Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, 0, 45), leftBasePosition),
dimensions: viveNaturalDimensions,
parts: {
// DISABLED FOR NOW
/*
tips: {
type: "static",
modelURL: viveTipsModelURL,
naturalPosition: {"x":-0.004377640783786774,"y":-0.034371938556432724,"z":0.06769277155399323},
naturalDimensions: {x: 0.191437, y: 0.094095, z: 0.085656},
textureName: "Tex.Blank",
defaultTextureLayer: "blank",
textureLayers: {
blank: {
defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Blank.png"
},
trigger: {
defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Trigger.png"
},
arrows: {
defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Rotate.png"
},
grip: {
defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Grip.png"
},
teleport: {
defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Teleport.png"
}
}
},
*/
// The touchpad type draws a dot indicating the current touch/thumb position
// and swaps in textures based on the thumb position.
touchpad: {
type: "touchpad",
modelURL: BASE_URL + "meshes/controller/vive_trackpad.fbx",
visibleInput: "Vive.RSTouch",
xInput: "Vive.LX",
yInput: "Vive.LY",
naturalPosition: {"x":0,"y":0.000979491975158453,"z":0.04872849956154823},
naturalDimensions: {x: 0.042824, y: 0.012537, z: 0.043115},
minValue: 0.0,
maxValue: 1.0,
minPosition: { x: -0.035, y: 0.004, z: -0.005 },
maxPosition: { x: -0.035, y: 0.004, z: -0.005 },
disable_textureName: "Tex.touchpad-blank",
disable_defaultTextureLayer: "blank",
disable_textureLayers: {
blank: {
defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-blank.jpg"
},
teleport: {
defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-teleport-active-LG.jpg"
},
arrows: {
defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-look-arrows.jpg"
}
}
},
trigger: {
type: "rotational",
modelURL: BASE_URL + "meshes/controller/vive_trigger.fbx",
input: Controller.Standard.LT,
naturalPosition: {"x":0.000004500150680541992,"y":-0.027690507471561432,"z":0.04830199480056763},
naturalDimensions: {x: 0.019105, y: 0.022189, z: 0.01909},
origin: { x: 0, y: -0.015, z: -0.00 },
minValue: 0.0,
maxValue: 1.0,
axis: { x: -1, y: 0, z: 0 },
maxAngle: 25,
textureName: "Tex.black-trigger",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/black.jpg"
},
highlight: {
defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/yellow.jpg"
}
}
},
l_grip: {
type: "static",
modelURL: BASE_URL + "meshes/controller/vive_l_grip.fbx",
naturalPosition: {"x":-0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226},
naturalDimensions: {x: 0.010094, y: 0.015064, z: 0.029552}
},
r_grip: {
type: "static",
modelURL: BASE_URL + "meshes/controller/vive_r_grip.fbx",
naturalPosition: {"x":0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226},
naturalDimensions: {x: 0.010083, y: 0.015064, z: 0.029552}
},
sys_button: {
type: "static",
modelURL: BASE_URL + "meshes/controller/vive_sys_button.fbx",
naturalPosition: {"x":0,"y":0.0020399854984134436,"z":0.08825899660587311},
naturalDimensions: {x: 0.009986, y: 0.004282, z: 0.010264}
},
button: {
type: "static",
modelURL: BASE_URL + "meshes/controller/vive_button.fbx",
naturalPosition: {"x":0,"y":0.005480996798723936,"z":0.019918499514460564},
naturalDimensions: {x: 0.009986, y: 0.004496, z: 0.010121}
},
button2: {
type: "static",
modelURL: BASE_URL + "meshes/controller/vive_button.fbx",
naturalPosition: {"x":0,"y":0.005480996798723936,"z":0.019918499514460564},
naturalDimensions: {x: 0.009986, y: 0.004496, z: 0.010121}
}
}
}
]
};
VIVE_CONTROLLER_CONFIGURATION_RIGHT = {
name: "Vive Right",
controllers: [
{
modelURL: viveModelURL,
jointIndex: MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"),
rotation: rightBaseRotation,
position: Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, 0, -45), rightBasePosition),
dimensions: viveNaturalDimensions,
naturalPosition: {
x: 0,
y: -0.034076502197422087,
z: 0.06380049744620919
},
parts: {
// DISABLED FOR NOW
/*
tips: {
type: "static",
modelURL: viveTipsModelURL,
naturalPosition: {"x":-0.004377640783786774,"y":-0.034371938556432724,"z":0.06769277155399323},
naturalDimensions: {x: 0.191437, y: 0.094095, z: 0.085656},
textureName: "Tex.Blank",
defaultTextureLayer: "blank",
textureLayers: {
blank: {
defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Blank.png"
},
trigger: {
defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Trigger.png"
},
arrows: {
defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Rotate.png"
},
grip: {
defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Grip.png"
},
teleport: {
defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Teleport.png"
}
}
},
*/
// The touchpad type draws a dot indicating the current touch/thumb position
// and swaps in textures based on the thumb position.
touchpad: {
type: "touchpad",
modelURL: BASE_URL + "meshes/controller/vive_trackpad.fbx",
visibleInput: "Vive.RSTouch",
xInput: "Vive.RX",
yInput: "Vive.RY",
naturalPosition: { x: 0, y: 0.000979491975158453, z: 0.04872849956154823 },
naturalDimensions: {x: 0.042824, y: 0.012537, z: 0.043115},
minValue: 0.0,
maxValue: 1.0,
minPosition: { x: -0.035, y: 0.004, z: -0.005 },
maxPosition: { x: -0.035, y: 0.004, z: -0.005 },
disable_textureName: "Tex.touchpad-blank",
disable_defaultTextureLayer: "blank",
disable_textureLayers: {
blank: {
defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-blank.jpg"
},
teleport: {
defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-teleport-active-LG.jpg"
},
arrows: {
defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-look-arrows-active.jpg"
}
}
},
trigger: {
type: "rotational",
modelURL: BASE_URL + "meshes/controller/vive_trigger.fbx",
input: Controller.Standard.RT,
naturalPosition: {"x":0.000004500150680541992,"y":-0.027690507471561432,"z":0.04830199480056763},
naturalDimensions: {x: 0.019105, y: 0.022189, z: 0.01909},
origin: { x: 0, y: -0.015, z: -0.00 },
minValue: 0.0,
maxValue: 1.0,
axis: { x: -1, y: 0, z: 0 },
maxAngle: 25,
textureName: "Tex.black-trigger",
defaultTextureLayer: "normal",
textureLayers: {
normal: {
defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/black.jpg"
},
highlight: {
defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/yellow.jpg"
}
}
},
l_grip: {
type: "static",
modelURL: BASE_URL + "meshes/controller/vive_l_grip.fbx",
naturalPosition: {"x":-0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226},
naturalDimensions: {x: 0.010094, y: 0.015064, z: 0.029552}
},
r_grip: {
type: "static",
modelURL: BASE_URL + "meshes/controller/vive_r_grip.fbx",
naturalPosition: {"x":0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226},
naturalDimensions: {x: 0.010083, y: 0.015064, z: 0.029552}
},
sys_button: {
type: "static",
modelURL: BASE_URL + "meshes/controller/vive_sys_button.fbx",
naturalPosition: {"x":0,"y":0.0020399854984134436,"z":0.08825899660587311},
naturalDimensions: {x: 0.009986, y: 0.004282, z: 0.010264}
},
button: {
type: "static",
modelURL: BASE_URL + "meshes/controller/vive_button.fbx",
naturalPosition: {"x":0,"y":0.005480996798723936,"z":0.019918499514460564},
naturalDimensions: {x: 0.009986, y: 0.004496, z: 0.010121}
},
button2: {
type: "static",
modelURL: BASE_URL + "meshes/controller/vive_button.fbx",
naturalPosition: {"x":0,"y":0.005480996798723936,"z":0.019918499514460564},
naturalDimensions: {x: 0.009986, y: 0.004496, z: 0.010121}
}
}
}
]
};

View file

@ -0,0 +1,387 @@
"use strict";
//
// progress.js
// examples
//
// Created by David Rowe on 29 Jan 2015.
// Copyright 2015 High Fidelity, Inc.
//
// This script displays a progress download indicator when downloads are in progress.
//
// 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
function debug() {
//print.apply(null, arguments);
}
Script.include("/~/system/libraries/globals.js");
var rawProgress = 100, // % raw value.
displayProgress = 100, // % smoothed value to display.
alpha = 0.0,
alphaDelta = 0.0, // > 0 if fading in; < 0 if fading out.
ALPHA_DELTA_IN = 0.15,
ALPHA_DELTA_OUT = -0.02,
fadeTimer = null,
FADE_INTERVAL = 30, // ms between changes in alpha.
fadeWaitTimer = null,
FADE_OUT_WAIT = 1000, // Wait before starting to fade out after progress 100%.
visible = false,
BAR_DESKTOP_2K_WIDTH = 2240, // Width of SVG image in pixels. Sized for 1920 x 1080 display with 6 visible repeats.
BAR_DESKTOP_2K_REPEAT = 320, // Length of repeat in bar = 2240 / 7.
BAR_DESKTOP_2K_HEIGHT = 3, // Display height of SVG
BAR_DESKTOP_2K_URL = Script.resolvePath("assets/images/progress-bar-2k.svg"),
BAR_DESKTOP_4K_WIDTH = 4480, // Width of SVG image in pixels. Sized for 4096 x 1920 display with 6 visible repeats.
BAR_DESKTOP_4K_REPEAT = 640, // Length of repeat in bar = 2240 / 7.
BAR_DESKTOP_4K_HEIGHT = 6, // Display height of SVG
BAR_DESKTOP_4K_URL = Script.resolvePath("assets/images/progress-bar-4k.svg"),
BAR_HMD_WIDTH = 2240, // Desktop image works with HMD well.
BAR_HMD_REPEAT = 320,
BAR_HMD_HEIGHT = 3,
BAR_HMD_URL = Script.resolvePath("assets/images/progress-bar-2k.svg"),
BAR_Y_OFFSET_DESKTOP = 0, // Offset of progress bar while in desktop mode
BAR_Y_OFFSET_HMD = -100, // Offset of progress bar while in HMD
ANIMATION_SECONDS_PER_REPEAT = 4, // Speed of bar animation
TEXT_HEIGHT = 32,
TEXT_WIDTH = 256,
TEXT_URL = Script.resolvePath("assets/images/progress-bar-text.svg"),
windowWidth = 0,
windowHeight = 0,
barDesktop = {},
barHMD = {},
textDesktop = {}, // Separate desktop and HMD overlays because can't change text size after overlay created.
textHMD = {},
SCALE_TEXT_DESKTOP = 0.6,
SCALE_TEXT_HMD = 1.0,
isHMD = false,
// Max seen since downloads started. This is reset when all downloads have completed.
maxSeen = 0,
// Progress is defined as: (pending_downloads + active_downloads) / max_seen
// We keep track of both the current progress (rawProgress) and the
// best progress we've seen (bestRawProgress). As you are downloading, you may
// encounter new assets that require downloads, increasing the number of
// pending downloads and thus decreasing your overall progress.
bestRawProgress = 0,
// True if we have known active downloads
isDownloading = false,
// Entities are streamed to users, so you don't receive them all at once; instead, you
// receive them over a period of time. In many cases we end up in a situation where
//
// The initial delay cooldown keeps us from tracking progress before the allotted time
// has passed.
INITIAL_DELAY_COOLDOWN_TIME = 1000,
initialDelayCooldown = 0,
isInInterstitialMode = false;
function fade() {
alpha = alpha + alphaDelta;
if (alpha < 0) {
alpha = 0;
} else if (alpha > 1) {
alpha = 1;
}
if (alpha === 0 || alpha === 1) { // Finished fading in or out
alphaDelta = 0;
Script.clearInterval(fadeTimer);
}
if (alpha === 0) { // Finished fading out
visible = false;
}
Overlays.editOverlay(barDesktop.overlay, {
alpha: alpha,
visible: visible && !isHMD
});
Overlays.editOverlay(barHMD.overlay, {
alpha: alpha,
visible: visible && isHMD
});
Overlays.editOverlay(textDesktop.overlay, {
alpha: alpha,
visible: visible && !isHMD
});
Overlays.editOverlay(textHMD.overlay, {
alpha: alpha,
visible: visible && isHMD
});
}
Window.domainChanged.connect(function () {
isDownloading = false;
bestRawProgress = 100;
rawProgress = 100;
displayProgress = 100;
});
function onDownloadInfoChanged(info) {
debug("PROGRESS: Download info changed ", info.downloading.length, info.pending, maxSeen);
// Update raw progress value
if (info.downloading.length + info.pending === 0) {
isDownloading = false;
rawProgress = 100;
bestRawProgress = 100;
initialDelayCooldown = INITIAL_DELAY_COOLDOWN_TIME;
} else {
var count = info.downloading.length + info.pending;
if (!isDownloading) {
isDownloading = true;
bestRawProgress = 0;
rawProgress = 0;
initialDelayCooldown = INITIAL_DELAY_COOLDOWN_TIME;
displayProgress = 0;
maxSeen = count;
}
if (count > maxSeen) {
maxSeen = count;
}
if (initialDelayCooldown <= 0) {
rawProgress = ((maxSeen - count) / maxSeen) * 100;
if (rawProgress > bestRawProgress) {
bestRawProgress = rawProgress;
}
}
}
debug("PROGRESS:", rawProgress, bestRawProgress, maxSeen);
}
function createOverlays() {
barDesktop.overlay = Overlays.addOverlay("image", {
imageURL: barDesktop.url,
subImage: {
x: 0,
y: 0,
width: barDesktop.width - barDesktop.repeat,
height: barDesktop.height
},
width: barDesktop.width,
height: barDesktop.height,
visible: false,
alpha: 0.0
});
barHMD.overlay = Overlays.addOverlay("image", {
imageURL: BAR_HMD_URL,
subImage: {
x: 0,
y: 0,
width: BAR_HMD_WIDTH - BAR_HMD_REPEAT,
height: BAR_HMD_HEIGHT
},
width: barHMD.width,
height: barHMD.height,
visible: false,
alpha: 0.0
});
textDesktop.overlay = Overlays.addOverlay("image", {
imageURL: TEXT_URL,
width: textDesktop.width,
height: textDesktop.height,
visible: false,
alpha: 0.0
});
textHMD.overlay = Overlays.addOverlay("image", {
imageURL: TEXT_URL,
width: textHMD.width,
height: textHMD.height,
visible: false,
alpha: 0.0
});
}
function deleteOverlays() {
Overlays.deleteOverlay(barDesktop.overlay);
Overlays.deleteOverlay(barHMD.overlay);
Overlays.deleteOverlay(textDesktop.overlay);
Overlays.deleteOverlay(textHMD.overlay);
}
function updateProgressBarLocation() {
var viewport = Controller.getViewportDimensions();
windowWidth = viewport.x;
windowHeight = viewport.y;
isHMD = HMD.active;
if (isHMD) {
Overlays.editOverlay(barHMD.overlay, {
x: windowWidth / 2 - barHMD.width / 2,
y: windowHeight - 2 * barHMD.height + BAR_Y_OFFSET_HMD
});
Overlays.editOverlay(textHMD.overlay, {
x: windowWidth / 2 - textHMD.width / 2,
y: windowHeight - 2 * barHMD.height - textHMD.height + BAR_Y_OFFSET_HMD
});
} else {
Overlays.editOverlay(barDesktop.overlay, {
x: windowWidth / 2 - barDesktop.width / 2,
y: windowHeight - 2 * barDesktop.height + BAR_Y_OFFSET_DESKTOP,
width: barDesktop.width
});
Overlays.editOverlay(textDesktop.overlay, {
x: windowWidth / 2 - textDesktop.width / 2,
y: windowHeight - 2 * barDesktop.height - textDesktop.height + BAR_Y_OFFSET_DESKTOP
});
}
}
function update() {
var viewport, diff, x, gpuTextures;
initialDelayCooldown -= 30;
if (displayProgress < rawProgress) {
diff = rawProgress - displayProgress;
if (diff < 0.5) {
displayProgress = rawProgress;
} else {
displayProgress += diff * 0.05;
}
}
gpuTextures = Render.getConfig("Stats").texturePendingGPUTransferCount;
// Update state
if (!visible) { // Not visible because no recent downloads
if ((displayProgress < 100 || gpuTextures > 0) && !isInInterstitialMode && !isInterstitialOverlaysVisible) { // Have started downloading so fade in
visible = true;
alphaDelta = ALPHA_DELTA_IN;
fadeTimer = Script.setInterval(fade, FADE_INTERVAL);
}
} else if (alphaDelta !== 0.0) { // Fading in or out
if (alphaDelta > 0) {
if (rawProgress === 100 && gpuTextures === 0) { // Was downloading but now have finished so fade out
alphaDelta = ALPHA_DELTA_OUT;
}
} else {
if (displayProgress < 100 || gpuTextures > 0) { // Was finished downloading but have resumed so fade in
alphaDelta = ALPHA_DELTA_IN;
}
}
} else { // Fully visible because downloading or recently so
if (fadeWaitTimer === null) {
if (rawProgress === 100 && gpuTextures === 0) { // Was downloading but have finished so fade out soon
fadeWaitTimer = Script.setTimeout(function () {
alphaDelta = ALPHA_DELTA_OUT;
fadeTimer = Script.setInterval(fade, FADE_INTERVAL);
fadeWaitTimer = null;
}, FADE_OUT_WAIT);
}
} else {
if (displayProgress < 100 || gpuTextures > 0) { // Was finished and waiting to fade out but have resumed so
// don't fade out
Script.clearInterval(fadeWaitTimer);
fadeWaitTimer = null;
}
}
}
if (visible) {
x = ((Date.now() / 1000) % ANIMATION_SECONDS_PER_REPEAT) / ANIMATION_SECONDS_PER_REPEAT;
if (!isHMD) {
x = x * barDesktop.repeat;
} else {
x = x * BAR_HMD_REPEAT;
}
if (isInInterstitialMode || isInterstitialOverlaysVisible) {
visible = false;
}
// Update progress bar
Overlays.editOverlay(barDesktop.overlay, {
visible: !isHMD && visible,
bounds: {
x: barDesktop.repeat - x,
y: windowHeight - barDesktop.height,
width: barDesktop.width - barDesktop.repeat,
height: barDesktop.height
}
});
Overlays.editOverlay(barHMD.overlay, {
visible: isHMD && visible,
bounds: {
x: BAR_HMD_REPEAT - x,
y: windowHeight - BAR_HMD_HEIGHT,
width: BAR_HMD_WIDTH - BAR_HMD_REPEAT,
height: BAR_HMD_HEIGHT
}
});
Overlays.editOverlay(textDesktop.overlay, {
visible: !isHMD && visible
});
Overlays.editOverlay(textHMD.overlay, {
visible: isHMD && visible
});
// Update 2D overlays to maintain positions at bottom middle of window
viewport = Controller.getViewportDimensions();
if (viewport.x !== windowWidth || viewport.y !== windowHeight || isHMD !== HMD.active) {
updateProgressBarLocation();
}
}
}
function interstitialModeChanged(inMode) {
isInInterstitialMode = inMode;
}
function setUp() {
var is4k = Window.innerWidth > 3000;
isHMD = HMD.active;
barDesktop.width = is4k ? BAR_DESKTOP_4K_WIDTH - BAR_DESKTOP_4K_REPEAT : BAR_DESKTOP_2K_WIDTH - BAR_DESKTOP_2K_REPEAT;
barDesktop.height = is4k ? BAR_DESKTOP_4K_HEIGHT : BAR_DESKTOP_2K_HEIGHT;
barDesktop.repeat = is4k ? BAR_DESKTOP_4K_REPEAT : BAR_DESKTOP_2K_REPEAT;
barDesktop.url = is4k ? BAR_DESKTOP_4K_URL : BAR_DESKTOP_2K_URL;
barHMD.width = BAR_HMD_WIDTH - BAR_HMD_REPEAT;
barHMD.height = BAR_HMD_HEIGHT;
textDesktop.width = SCALE_TEXT_DESKTOP * TEXT_WIDTH;
textDesktop.height = SCALE_TEXT_DESKTOP * TEXT_HEIGHT;
textHMD.width = SCALE_TEXT_HMD * TEXT_WIDTH;
textHMD.height = SCALE_TEXT_HMD * TEXT_HEIGHT;
createOverlays();
}
function tearDown() {
deleteOverlays();
}
setUp();
Window.interstitialModeChanged.connect(interstitialModeChanged);
GlobalServices.downloadInfoChanged.connect(onDownloadInfoChanged);
GlobalServices.updateDownloadInfo();
Script.setInterval(update, 1000 / 60);
Script.scriptEnding.connect(tearDown);
}()); // END LOCAL_SCOPE

View file

@ -0,0 +1,48 @@
"use strict";
//
// request-service.js
//
// Created by Howard Stearns on May 22, 2018
// Copyright 2018 High Fidelity, Inc
//
// 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
// QML has its own XMLHttpRequest, but:
// - npm request is easier to use.
// - It is not easy to hack QML's XMLHttpRequest to use our MetaverseServer, and to supply the user's auth when contacting it.
// a. Our custom XMLHttpRequestClass object only works with QScriptEngine, not QML's javascript.
// b. We have hacked profiles that intercept requests to our MetavserseServer (providing the correct auth), but those
// only work in QML WebEngineView. Setting up communication between ordinary QML and a hiddent WebEngineView is
// tantamount to the following anyway, and would still have to duplicate the code from request.js.
// So, this script does two things:
// 1. Allows any root .qml to signal sendToScript({id: aString, method: 'http.request', params: byNameOptions})
// We will then asynchonously call fromScript({id: theSameString, method: 'http.response', error: errorOrFalsey, response: body})
// on that root object.
// RootHttpRequest.qml does this.
// 2. If the uri used (computed from byNameOptions, see request.js) is to our metaverse, we will use the appropriate auth.
var request = Script.require('request').request;
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
function fromQml(message) { // messages are {id, method, params}, like json-rpc. See also sendToQml.
switch (message.method) {
case 'http.request':
request(message.params, function (error, response) {
tablet.sendToQml({
id: message.id,
method: 'http.response',
error: error, // Alas, this isn't always a JSON-RPC conforming error object.
response: response,
jsonrpc: '2.0'
});
});
break;
}
}
tablet.fromQml.connect(fromQml);
Script.scriptEnding.connect(function () { tablet.fromQml.disconnect(fromQml); });
}()); // END LOCAL_SCOPE

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -0,0 +1,20 @@
# Post build script
import os
import sys
SOURCE_PATH = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
BUILD_PATH = os.path.join(SOURCE_PATH, 'build')
# FIXME move the helper python modules somewher other than the root of the repo
sys.path.append(SOURCE_PATH)
import hifi_utils
#for var in sys.argv:
# print("{}".format(var))
#for var in os.environ:
# print("{} = {}".format(var, os.environ[var]))
print("Create ZIP version of installer archive")
hifi_utils.executeSubprocess(['cpack', '-G', 'ZIP'], folder=BUILD_PATH)