diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.jpg b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.jpg new file mode 100644 index 0000000000..7977396159 Binary files /dev/null and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.jpg differ diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.ktx b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.ktx new file mode 100644 index 0000000000..a5f4fae4eb Binary files /dev/null and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.ktx differ diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.texmeta.json b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.texmeta.json new file mode 100644 index 0000000000..96edd3abf0 --- /dev/null +++ b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.texmeta.json @@ -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 +} diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx new file mode 100644 index 0000000000..279042e8dd Binary files /dev/null and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx differ diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient_COMPRESSED_SRGB8_ETC2.ktx b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient_COMPRESSED_SRGB8_ETC2.ktx new file mode 100644 index 0000000000..0a36017dbe Binary files /dev/null and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient_COMPRESSED_SRGB8_ETC2.ktx differ diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.ktx b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.ktx index 4231bf7650..7d5e18f3c9 100644 Binary files a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.ktx and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.ktx differ diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.texmeta.json b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.texmeta.json index 28512662d9..729bb2d70f 100644 --- a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.texmeta.json +++ b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.texmeta.json @@ -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 } diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx index c789fa4ac5..9e821605b4 100644 Binary files a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx differ diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_SRGB8_ETC2.ktx b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_SRGB8_ETC2.ktx index deede32614..405cd9f09b 100644 Binary files a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_SRGB8_ETC2.ktx and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_SRGB8_ETC2.ktx differ diff --git a/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml b/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml index b426f2d986..d6ee593edd 100644 --- a/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml +++ b/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml @@ -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." } diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/audio/Audio.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/audio/Audio.qml index b73c07c78b..1f9aa5bcbc 100644 --- a/interface/resources/qml/hifi/simplifiedUI/settingsApp/audio/Audio.qml +++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/audio/Audio.qml @@ -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 diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml index 97799921c2..c7e3cc9fc2 100644 --- a/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml +++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml @@ -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 diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 0dba4498d5..a0cb790958 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -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()->goToEntry(); - sentTo = SENT_TO_ENTRY; - _firstRun.set(false); - + if (!_overrideEntry) { + DependencyManager::get()->goToEntry(); + sentTo = SENT_TO_ENTRY; + } else { + DependencyManager::get()->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()->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()->setAccessTokens(tokens); + Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, true).set(true); +} +void Application::setConfigFileURL(const QString& fileUrl) { + DependencyManager::get()->setConfigFileURL(fileUrl); +} #if defined(Q_OS_ANDROID) void Application::beforeEnterBackground() { diff --git a/interface/src/Application.h b/interface/src/Application.h index 210039beba..837fb8eae6 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -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 diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 9d8b733ba7..7fc4a5b651 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -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"); QCommandLineOption overrideScriptsPathOption(SCRIPTS_SWITCH, "set scripts ", "path"); + QCommandLineOption responseTokensOption("tokens", "set response tokens ", "json"); + QCommandLineOption displayNameOption("displayName", "set user display name ", "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(&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); diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 4aff76df21..81a6b100d0 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -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(sourceSize), sourcePointer); - } else { + } else +#endif + { glCompressedTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, internalFormat, static_cast(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; diff --git a/libraries/gpu/src/gpu/Format.cpp b/libraries/gpu/src/gpu/Format.cpp index 7e277ae488..013a5ac4a6 100644 --- a/libraries/gpu/src/gpu/Format.cpp +++ b/libraries/gpu/src/gpu/Format.cpp @@ -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 }; diff --git a/libraries/gpu/src/gpu/FrameReader.cpp b/libraries/gpu/src/gpu/FrameReader.cpp index 812f4db7cc..740ec2b26f 100644 --- a/libraries/gpu/src/gpu/FrameReader.cpp +++ b/libraries/gpu/src/gpu/FrameReader.cpp @@ -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; } diff --git a/libraries/graphics/src/graphics/skybox.slf b/libraries/graphics/src/graphics/skybox.slf index c20dd94bf4..231c1f4140 100755 --- a/libraries/graphics/src/graphics/skybox.slf +++ b/libraries/graphics/src/graphics/skybox.slf @@ -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); } diff --git a/libraries/image/src/image/TextureProcessing.cpp b/libraries/image/src/image/TextureProcessing.cpp index 429859d109..c144ed530a 100644 --- a/libraries/image/src/image/TextureProcessing.cpp +++ b/libraries/image/src/image/TextureProcessing.cpp @@ -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); diff --git a/libraries/image/src/image/TextureProcessing.h b/libraries/image/src/image/TextureProcessing.h index b4036ddd9f..c1817a08a6 100644 --- a/libraries/image/src/image/TextureProcessing.h +++ b/libraries/image/src/image/TextureProcessing.h @@ -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, diff --git a/libraries/ktx/src/TextureMeta.cpp b/libraries/ktx/src/TextureMeta.cpp index c8427c1f60..f97b5404e1 100644 --- a/libraries/ktx/src/TextureMeta.cpp +++ b/libraries/ktx/src/TextureMeta.cpp @@ -16,6 +16,7 @@ #include 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(); diff --git a/libraries/ktx/src/TextureMeta.h b/libraries/ktx/src/TextureMeta.h index 5450fee110..fc39db6ef1 100644 --- a/libraries/ktx/src/TextureMeta.h +++ b/libraries/ktx/src/TextureMeta.h @@ -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 { @@ -37,6 +38,7 @@ struct TextureMeta { QUrl original; QUrl uncompressed; std::unordered_map availableTextureTypes; + uint16_t version { 0 }; }; diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 226433e388..3a7d3e0a67 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -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(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(); + } + } +} \ No newline at end of file diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 8732042e93..c2187f79cb 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -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 diff --git a/libraries/procedural/src/procedural/proceduralSkybox.slf b/libraries/procedural/src/procedural/proceduralSkybox.slf index 12e8de9dc3..f938e0b9a2 100644 --- a/libraries/procedural/src/procedural/proceduralSkybox.slf +++ b/libraries/procedural/src/procedural/proceduralSkybox.slf @@ -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); } diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index b8c720e9ca..82b7f3102a 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -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()->getTexture( - defaultSkyboxURL, image::TextureUsage::AMBIENT_TEXTURE); + defaultAmbientURL, image::TextureUsage::AMBIENT_TEXTURE); } if (_defaultSkyboxNetworkTexture && _defaultSkyboxNetworkTexture->isLoaded() && _defaultSkyboxNetworkTexture->getGPUTexture()) { diff --git a/scripts/simplifiedUI/defaultScripts.js b/scripts/simplifiedUI/defaultScripts.js index 0911c6518a..3213303f2b 100644 --- a/scripts/simplifiedUI/defaultScripts.js +++ b/scripts/simplifiedUI/defaultScripts.js @@ -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) { diff --git a/scripts/simplifiedUI/modules/appUi.js b/scripts/simplifiedUI/modules/appUi.js new file mode 100644 index 0000000000..9771348377 --- /dev/null +++ b/scripts/simplifiedUI/modules/appUi.js @@ -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; diff --git a/scripts/simplifiedUI/modules/request.js b/scripts/simplifiedUI/modules/request.js new file mode 100644 index 0000000000..37f3ac0d7b --- /dev/null +++ b/scripts/simplifiedUI/modules/request.js @@ -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(' ')); +} diff --git a/scripts/simplifiedUI/modules/vec3.js b/scripts/simplifiedUI/modules/vec3.js new file mode 100644 index 0000000000..f164f01374 --- /dev/null +++ b/scripts/simplifiedUI/modules/vec3.js @@ -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)+']'); +}; + diff --git a/scripts/simplifiedUI/system/away.js b/scripts/simplifiedUI/system/away.js new file mode 100644 index 0000000000..6293c0c452 --- /dev/null +++ b/scripts/simplifiedUI/system/away.js @@ -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 diff --git a/scripts/simplifiedUI/system/controllers/+android_questInterface/controllerScripts.js b/scripts/simplifiedUI/system/controllers/+android_questInterface/controllerScripts.js new file mode 100644 index 0000000000..d313efaca1 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/+android_questInterface/controllerScripts.js @@ -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(); +} diff --git a/scripts/simplifiedUI/system/controllers/controllerDispatcher.js b/scripts/simplifiedUI/system/controllers/controllerDispatcher.js new file mode 100644 index 0000000000..0a9fa4dce1 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerDispatcher.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerDisplay.js b/scripts/simplifiedUI/system/controllers/controllerDisplay.js new file mode 100644 index 0000000000..e40b761307 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerDisplay.js @@ -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); +}; diff --git a/scripts/simplifiedUI/system/controllers/controllerDisplayManager.js b/scripts/simplifiedUI/system/controllers/controllerDisplayManager.js new file mode 100644 index 0000000000..f93f8b1624 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerDisplayManager.js @@ -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(); +}); + +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/disableOtherModule.js b/scripts/simplifiedUI/system/controllers/controllerModules/disableOtherModule.js new file mode 100644 index 0000000000..7636c56f65 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/disableOtherModule.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/equipEntity.js b/scripts/simplifiedUI/system/controllers/controllerModules/equipEntity.js new file mode 100644 index 0000000000..54b56ff271 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/equipEntity.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/simplifiedUI/system/controllers/controllerModules/farActionGrabEntity.js new file mode 100644 index 0000000000..1eaed44ce2 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/farActionGrabEntity.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/farGrabEntity.js b/scripts/simplifiedUI/system/controllers/controllerModules/farGrabEntity.js new file mode 100644 index 0000000000..ecafa3cb26 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/farGrabEntity.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/farTrigger.js b/scripts/simplifiedUI/system/controllers/controllerModules/farTrigger.js new file mode 100644 index 0000000000..c9c9d3deee --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/farTrigger.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/simplifiedUI/system/controllers/controllerModules/hudOverlayPointer.js new file mode 100644 index 0000000000..f7d5b5a2dd --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/hudOverlayPointer.js @@ -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); + +})(); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/inEditMode.js b/scripts/simplifiedUI/system/controllers/controllerModules/inEditMode.js new file mode 100644 index 0000000000..5709b19efe --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/inEditMode.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/inVREditMode.js b/scripts/simplifiedUI/system/controllers/controllerModules/inVREditMode.js new file mode 100644 index 0000000000..104e37d76c --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/inVREditMode.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/mouseHMD.js b/scripts/simplifiedUI/system/controllers/controllerModules/mouseHMD.js new file mode 100644 index 0000000000..172923a8e2 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/mouseHMD.js @@ -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); +})(); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/nearGrabEntity.js b/scripts/simplifiedUI/system/controllers/controllerModules/nearGrabEntity.js new file mode 100644 index 0000000000..763c1a1ce0 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/nearGrabEntity.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/nearGrabHyperLinkEntity.js b/scripts/simplifiedUI/system/controllers/controllerModules/nearGrabHyperLinkEntity.js new file mode 100644 index 0000000000..962ae89bb9 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/nearGrabHyperLinkEntity.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/nearParentGrabOverlay.js b/scripts/simplifiedUI/system/controllers/controllerModules/nearParentGrabOverlay.js new file mode 100644 index 0000000000..5dcfee23cb --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/nearParentGrabOverlay.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/nearTabletHighlight.js b/scripts/simplifiedUI/system/controllers/controllerModules/nearTabletHighlight.js new file mode 100644 index 0000000000..2e046f5dc6 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/nearTabletHighlight.js @@ -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); + +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/nearTrigger.js b/scripts/simplifiedUI/system/controllers/controllerModules/nearTrigger.js new file mode 100644 index 0000000000..4bff4ea3f0 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/nearTrigger.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/pushToTalk.js b/scripts/simplifiedUI/system/controllers/controllerModules/pushToTalk.js new file mode 100644 index 0000000000..11335ba2f5 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/pushToTalk.js @@ -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 diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/scaleAvatar.js b/scripts/simplifiedUI/system/controllers/controllerModules/scaleAvatar.js new file mode 100644 index 0000000000..1868b0228a --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/scaleAvatar.js @@ -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); +})(); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/scaleEntity.js b/scripts/simplifiedUI/system/controllers/controllerModules/scaleEntity.js new file mode 100644 index 0000000000..50b6c5b853 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/scaleEntity.js @@ -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); +})(); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/stylusInput.js b/scripts/simplifiedUI/system/controllers/controllerModules/stylusInput.js new file mode 100644 index 0000000000..c4aa9efd50 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/stylusInput.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/teleport.js b/scripts/simplifiedUI/system/controllers/controllerModules/teleport.js new file mode 100644 index 0000000000..5a51773930 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/teleport.js @@ -0,0 +1,1108 @@ +"use strict"; + +// Created by james b. pollack @imgntn on 7/2/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Creates a beam and target and then teleports you there. Release when its close to you to cancel. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* jslint bitwise: true */ + +/* global Script, Entities, MyAvatar, Controller, Quat, RIGHT_HAND, LEFT_HAND, + enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, makeRunningValues, Vec3, + HMD, Uuid, AvatarList, Picks, Pointers, PickType +*/ + +Script.include("/~/system/libraries/Xform.js"); +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { // BEGIN LOCAL_SCOPE + + var TARGET_MODEL_URL = Script.resolvePath("../../assets/models/teleportationSpotBasev8.fbx"); + var SEAT_MODEL_URL = Script.resolvePath("../../assets/models/teleport-seat.fbx"); + + var TARGET_MODEL_DIMENSIONS = { x: 0.6552, y: 0.3063, z: 0.6552 }; + + var COLORS_TELEPORT_SEAT = { + red: 255, + green: 0, + blue: 170 + }; + + var COLORS_TELEPORT_CAN_TELEPORT = { + red: 97, + green: 247, + blue: 255 + }; + + var COLORS_TELEPORT_CANCEL = { + red: 255, + green: 184, + blue: 73 + }; + + var handInfo = { + right: { + controllerInput: Controller.Standard.RightHand + }, + left: { + controllerInput: Controller.Standard.LeftHand + } + }; + + var cancelPath = { + color: COLORS_TELEPORT_CANCEL, + alpha: 0.3, + width: 0.025, + drawInFront: true + }; + + var teleportPath = { + color: COLORS_TELEPORT_CAN_TELEPORT, + alpha: 0.7, + width: 0.025, + drawInFront: true + }; + + var seatPath = { + color: COLORS_TELEPORT_SEAT, + alpha: 0.7, + width: 0.025, + drawInFront: true + }; + + var teleportEnd = { + type: "model", + url: TARGET_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + ignorePickIntersection: true + }; + + var seatEnd = { + type: "model", + url: SEAT_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + ignorePickIntersection: true + }; + + var collisionEnd = { + type: "shape", + shape: "box", + dimensions: { x: 1.0, y: 0.001, z: 1.0 }, + alpha: 0.0, + ignorePickIntersection: true + }; + + var teleportRenderStates = [{name: "cancel", path: cancelPath}, + {name: "teleport", path: teleportPath, end: teleportEnd}, + {name: "seat", path: seatPath, end: seatEnd}, + {name: "collision", end: collisionEnd}]; + + var DEFAULT_DISTANCE = 8.0; + var teleportDefaultRenderStates = [{name: "cancel", distance: DEFAULT_DISTANCE, path: cancelPath}]; + + var ignoredEntities = []; + + var TELEPORTER_STATES = { + IDLE: 'idle', + TARGETTING: 'targetting', + TARGETTING_INVALID: 'targetting_invalid' + }; + + var TARGET = { + NONE: 'none', // Not currently targetting anything + INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.) + COLLIDES: 'collides', // Insufficient space to accommodate the avatar capsule + DISCREPANCY: 'discrepancy', // We are not 100% sure the avatar will fit so we trigger safe landing + SURFACE: 'surface', // The current target is a valid surface + SEAT: 'seat' // The current target is a seat + }; + + var speed = 9.3; + var accelerationAxis = {x: 0.0, y: -5.0, z: 0.0}; + + function Teleporter(hand) { + var _this = this; + this.init = false; + this.hand = hand; + this.buttonValue = 0; + this.standardAxisLY = 0.0; + this.standardAxisRY = 0.0; + this.disabled = false; // used by the 'Hifi-Teleport-Disabler' message handler + this.active = false; + this.state = TELEPORTER_STATES.IDLE; + this.currentTarget = TARGET.INVALID; + this.currentResult = null; + this.capsuleThreshold = 0.05; + this.pickHeightOffset = 0.05; + + this.getOtherModule = function() { + var otherModule = this.hand === RIGHT_HAND ? leftTeleporter : rightTeleporter; + return otherModule; + }; + + this.teleportHeadCollisionPick; + this.teleportHandCollisionPick; + this.teleportParabolaHandVisuals; + this.teleportParabolaHandCollisions; + this.teleportParabolaHeadVisuals; + this.teleportParabolaHeadCollisions; + + + this.PLAY_AREA_OVERLAY_MODEL = Script.resolvePath("../../assets/models/trackingSpacev18.fbx"); + this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS = { x: 1.969, y: 0.001, z: 1.969 }; + this.PLAY_AREA_FLOAT_ABOVE_FLOOR = 0.005; + this.PLAY_AREA_OVERLAY_OFFSET = // Offset from floor. + { x: 0, y: this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y / 2 + this.PLAY_AREA_FLOAT_ABOVE_FLOOR, z: 0 }; + this.PLAY_AREA_SENSOR_OVERLAY_MODEL = Script.resolvePath("../../assets/models/oculusSensorv11.fbx"); + this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS = { x: 0.1198, y: 0.2981, z: 0.1198 }; + this.PLAY_AREA_SENSOR_OVERLAY_ROTATION = Quat.fromVec3Degrees({ x: 0, y: -90, z: 0 }); + this.PLAY_AREA_BOX_ALPHA = 1.0; + this.PLAY_AREA_SENSOR_ALPHA = 0.8; + this.playAreaSensorPositions = []; + this.playArea = { x: 0, y: 0 }; + this.playAreaCenterOffset = this.PLAY_AREA_OVERLAY_OFFSET; + this.isPlayAreaVisible = false; + this.wasPlayAreaVisible = false; + this.isPlayAreaAvailable = false; + this.targetOverlayID = null; + this.playAreaOverlay = null; + this.playAreaSensorPositionOverlays = []; + + this.TELEPORT_SCALE_DURATION = 130; + this.TELEPORT_SCALE_TIMEOUT = 25; + this.isTeleportVisible = false; + this.teleportScaleTimer = null; + this.teleportScaleStart = 0; + this.teleportScaleFactor = 0; + this.teleportScaleMode = "head"; + + this.TELEPORTED_FADE_DELAY_DURATION = 900; + this.TELEPORTED_FADE_DURATION = 200; + this.TELEPORTED_FADE_INTERVAL = 25; + this.TELEPORTED_FADE_DELAY_DELTA = this.TELEPORTED_FADE_INTERVAL / this.TELEPORTED_FADE_DELAY_DURATION; + this.TELEPORTED_FADE_DELTA = this.TELEPORTED_FADE_INTERVAL / this.TELEPORTED_FADE_DURATION; + this.teleportedFadeTimer = null; + this.teleportedFadeDelayFactor = 0; + this.teleportedFadeFactor = 0; + this.teleportedPosition = Vec3.ZERO; + this.TELEPORTED_TARGET_ALPHA = 1.0; + this.TELEPORTED_TARGET_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }); + this.teleportedTargetOverlay = null; + + this.setPlayAreaDimensions = function () { + var avatarScale = MyAvatar.sensorToWorldScale; + + var playAreaOverlayProperties = { + dimensions: + Vec3.multiply(_this.teleportScaleFactor * avatarScale, { + x: _this.playArea.width, + y: _this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y, + z: _this.playArea.height + }) + }; + + if (_this.teleportScaleFactor < 1) { + // Adjust position of playAreOverlay so that its base is at correct height. + // Always parenting to teleport target is good enough for this. + var sensorToWorldMatrix = MyAvatar.sensorToWorldMatrix; + var sensorToWorldRotation = Mat4.extractRotation(MyAvatar.sensorToWorldMatrix); + var worldToSensorMatrix = Mat4.inverse(sensorToWorldMatrix); + var avatarSensorPosition = Mat4.transformPoint(worldToSensorMatrix, MyAvatar.position); + avatarSensorPosition.y = 0; + + var targetRotation = Overlays.getProperty(_this.targetOverlayID, "rotation"); + var relativePlayAreaCenterOffset = + Vec3.sum(_this.playAreaCenterOffset, { x: 0, y: -TARGET_MODEL_DIMENSIONS.y / 2, z: 0 }); + var localPosition = Vec3.multiplyQbyV(Quat.inverse(targetRotation), + Vec3.multiplyQbyV(sensorToWorldRotation, + Vec3.multiply(avatarScale, Vec3.subtract(relativePlayAreaCenterOffset, avatarSensorPosition)))); + localPosition.y = _this.teleportScaleFactor * localPosition.y; + + playAreaOverlayProperties.parentID = _this.targetOverlayID; + playAreaOverlayProperties.localPosition = localPosition; + } + + Overlays.editOverlay(_this.playAreaOverlay, playAreaOverlayProperties); + + for (var i = 0; i < _this.playAreaSensorPositionOverlays.length; i++) { + localPosition = _this.playAreaSensorPositions[i]; + localPosition = Vec3.multiply(avatarScale, localPosition); + // Position relative to the play area. + localPosition.y = avatarScale * (_this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS.y / 2 + - _this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y / 2); + Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { + dimensions: Vec3.multiply(_this.teleportScaleFactor * avatarScale, _this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS), + parentID: _this.playAreaOverlay, + localPosition: localPosition + }); + } + }; + + this.updatePlayAreaScale = function () { + if (_this.isPlayAreaAvailable) { + _this.setPlayAreaDimensions(); + } + }; + + + this.teleporterSelectionName = "teleporterSelection" + hand.toString(); + this.TELEPORTER_SELECTION_STYLE = { + outlineUnoccludedColor: { red: 0, green: 0, blue: 0 }, + outlineUnoccludedAlpha: 0, + outlineOccludedColor: { red: 0, green: 0, blue: 0 }, + outlineOccludedAlpha: 0, + fillUnoccludedColor: { red: 0, green: 0, blue: 0 }, + fillUnoccludedAlpha: 0, + fillOccludedColor: { red: 0, green: 0, blue: 255 }, + fillOccludedAlpha: 0.84, + outlineWidth: 0, + isOutlineSmooth: false + }; + + this.addToSelectedItemsList = function (properties) { + for (var i = 0, length = teleportRenderStates.length; i < length; i++) { + var state = properties.renderStates[teleportRenderStates[i].name]; + if (state && state.end) { + Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", state.end); + } + } + }; + + + this.cleanup = function() { + Selection.removeListFromMap(_this.teleporterSelectionName); + Pointers.removePointer(_this.teleportParabolaHandVisuals); + Pointers.removePointer(_this.teleportParabolaHandCollisions); + Pointers.removePointer(_this.teleportParabolaHeadVisuals); + Pointers.removePointer(_this.teleportParabolaHeadCollisions); + Picks.removePick(_this.teleportHandCollisionPick); + Picks.removePick(_this.teleportHeadCollisionPick); + Overlays.deleteOverlay(_this.teleportedTargetOverlay); + Overlays.deleteOverlay(_this.playAreaOverlay); + for (var i = 0; i < _this.playAreaSensorPositionOverlays.length; i++) { + Overlays.deleteOverlay(_this.playAreaSensorPositionOverlays[i]); + } + _this.playAreaSensorPositionOverlays = []; + }; + + this.initPointers = function() { + if (_this.init) { + _this.cleanup(); + } + + _this.teleportParabolaHandVisuals = Pointers.createPointer(PickType.Parabola, { + joint: (_this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", + dirOffset: { x: 0, y: 1, z: 0.1 }, + posOffset: { x: (_this.hand === RIGHT_HAND) ? 0.03 : -0.03, y: 0.2, z: 0.02 }, + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + scaleWithParent: true, + centerEndY: false, + speed: speed, + accelerationAxis: accelerationAxis, + rotateAccelerationWithAvatar: true, + renderStates: teleportRenderStates, + defaultRenderStates: teleportDefaultRenderStates, + maxDistance: 8.0 + }); + + _this.teleportParabolaHandCollisions = Pointers.createPointer(PickType.Parabola, { + joint: (_this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", + dirOffset: { x: 0, y: 1, z: 0.1 }, + posOffset: { x: (_this.hand === RIGHT_HAND) ? 0.03 : -0.03, y: 0.2, z: 0.02 }, + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + scaleWithParent: true, + centerEndY: false, + speed: speed, + accelerationAxis: accelerationAxis, + rotateAccelerationWithAvatar: true, + renderStates: teleportRenderStates, + maxDistance: 8.0 + }); + + _this.teleportParabolaHeadVisuals = Pointers.createPointer(PickType.Parabola, { + joint: "Avatar", + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + scaleWithParent: true, + centerEndY: false, + speed: speed, + accelerationAxis: accelerationAxis, + rotateAccelerationWithAvatar: true, + renderStates: teleportRenderStates, + defaultRenderStates: teleportDefaultRenderStates, + maxDistance: 8.0 + }); + + _this.teleportParabolaHeadCollisions = Pointers.createPointer(PickType.Parabola, { + joint: "Avatar", + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + scaleWithParent: true, + centerEndY: false, + speed: speed, + accelerationAxis: accelerationAxis, + rotateAccelerationWithAvatar: true, + renderStates: teleportRenderStates, + maxDistance: 8.0 + }); + + _this.addToSelectedItemsList(Pointers.getPointerProperties(_this.teleportParabolaHandVisuals)); + _this.addToSelectedItemsList(Pointers.getPointerProperties(_this.teleportParabolaHeadVisuals)); + + + var capsuleData = MyAvatar.getCollisionCapsule(); + + var sensorToWorldScale = MyAvatar.getSensorToWorldScale(); + + var diameter = 2.0 * capsuleData.radius / sensorToWorldScale; + var height = (Vec3.distance(capsuleData.start, capsuleData.end) + diameter) / sensorToWorldScale; + var capsuleRatio = 5.0 * diameter / height; + var offset = _this.pickHeightOffset * capsuleRatio; + + _this.teleportHandCollisionPick = Picks.createPick(PickType.Collision, { + enabled: true, + parentID: Pointers.getPointerProperties(_this.teleportParabolaHandCollisions).renderStates["collision"].end, + filter: Picks.PICK_ENTITIES | Picks.PICK_AVATARS, + shape: { + shapeType: "capsule-y", + dimensions: { + x: diameter, + y: height, + z: diameter + } + }, + position: { x: 0, y: offset + height * 0.5, z: 0 }, + threshold: _this.capsuleThreshold + }); + + _this.teleportHeadCollisionPick = Picks.createPick(PickType.Collision, { + enabled: true, + parentID: Pointers.getPointerProperties(_this.teleportParabolaHeadCollisions).renderStates["collision"].end, + filter: Picks.PICK_ENTITIES | Picks.PICK_AVATARS, + shape: { + shapeType: "capsule-y", + dimensions: { + x: diameter, + y: height, + z: diameter + } + }, + position: { x: 0, y: offset + height * 0.5, z: 0 }, + threshold: _this.capsuleThreshold + }); + + + _this.playAreaOverlay = Overlays.addOverlay("model", { + url: _this.PLAY_AREA_OVERLAY_MODEL, + drawInFront: false, + visible: false + }); + + _this.teleportedTargetOverlay = Overlays.addOverlay("model", { + url: TARGET_MODEL_URL, + alpha: _this.TELEPORTED_TARGET_ALPHA, + visible: false + }); + + Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", _this.playAreaOverlay); + Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", _this.teleportedTargetOverlay); + + + _this.playArea = HMD.playArea; + _this.isPlayAreaAvailable = HMD.active && _this.playArea.width !== 0 && _this.playArea.height !== 0; + if (_this.isPlayAreaAvailable) { + _this.playAreaCenterOffset = Vec3.sum({ x: _this.playArea.x, y: 0, z: _this.playArea.y }, + _this.PLAY_AREA_OVERLAY_OFFSET); + _this.playAreaSensorPositions = HMD.sensorPositions; + + for (var i = 0; i < _this.playAreaSensorPositions.length; i++) { + if (i > _this.playAreaSensorPositionOverlays.length - 1) { + var overlay = Overlays.addOverlay("model", { + url: _this.PLAY_AREA_SENSOR_OVERLAY_MODEL, + dimensions: _this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS, + parentID: _this.playAreaOverlay, + localRotation: _this.PLAY_AREA_SENSOR_OVERLAY_ROTATION, + drawInFront: false, + visible: false + }); + _this.playAreaSensorPositionOverlays.push(overlay); + Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", overlay); + } + } + + _this.setPlayAreaDimensions(); + } + + _this.init = true; + }; + + _this.initPointers(); + + + this.translateXAction = Controller.findAction("TranslateX"); + this.translateYAction = Controller.findAction("TranslateY"); + this.translateZAction = Controller.findAction("TranslateZ"); + + this.setPlayAreaVisible = function (visible, targetOverlayID, fade) { + if (!_this.isPlayAreaAvailable || _this.isPlayAreaVisible === visible) { + return; + } + + _this.wasPlayAreaVisible = _this.isPlayAreaVisible; + _this.isPlayAreaVisible = visible; + _this.targetOverlayID = targetOverlayID; + + if (_this.teleportedFadeTimer !== null) { + Script.clearTimeout(_this.teleportedFadeTimer); + _this.teleportedFadeTimer = null; + } + if (visible || !fade) { + // Immediately make visible or invisible. + _this.isPlayAreaVisible = visible; + Overlays.editOverlay(_this.playAreaOverlay, { + dimensions: Vec3.ZERO, + alpha: _this.PLAY_AREA_BOX_ALPHA, + visible: visible + }); + for (var i = 0; i < _this.playAreaSensorPositionOverlays.length; i++) { + Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { + dimensions: Vec3.ZERO, + alpha: _this.PLAY_AREA_SENSOR_ALPHA, + visible: visible + }); + } + Overlays.editOverlay(_this.teleportedTargetOverlay, { visible: false }); + } else { + // Fading out of overlays is initiated in setTeleportVisible(). + } + }; + + this.updatePlayArea = function (position) { + var sensorToWorldMatrix = MyAvatar.sensorToWorldMatrix; + var sensorToWorldRotation = Mat4.extractRotation(MyAvatar.sensorToWorldMatrix); + var worldToSensorMatrix = Mat4.inverse(sensorToWorldMatrix); + var avatarSensorPosition = Mat4.transformPoint(worldToSensorMatrix, MyAvatar.position); + avatarSensorPosition.y = 0; + + var targetXZPosition = { x: position.x, y: 0, z: position.z }; + var avatarXZPosition = MyAvatar.position; + avatarXZPosition.y = 0; + var MIN_PARENTING_DISTANCE = 0.2; // Parenting under this distance results in the play area's rotation jittering. + if (Vec3.distance(targetXZPosition, avatarXZPosition) < MIN_PARENTING_DISTANCE) { + // Set play area position and rotation in world coordinates with no parenting. + Overlays.editOverlay(_this.playAreaOverlay, { + parentID: Uuid.NULL, + position: Vec3.sum(position, + Vec3.multiplyQbyV(sensorToWorldRotation, + Vec3.multiply(MyAvatar.sensorToWorldScale, + Vec3.subtract(_this.playAreaCenterOffset, avatarSensorPosition)))), + rotation: sensorToWorldRotation + }); + } else { + // Set play area position and rotation in local coordinates with parenting. + var targetRotation = Overlays.getProperty(_this.targetOverlayID, "rotation"); + var sensorToTargetRotation = Quat.multiply(Quat.inverse(targetRotation), sensorToWorldRotation); + var relativePlayAreaCenterOffset = + Vec3.sum(_this.playAreaCenterOffset, { x: 0, y: -TARGET_MODEL_DIMENSIONS.y / 2, z: 0 }); + Overlays.editOverlay(_this.playAreaOverlay, { + parentID: _this.targetOverlayID, + localPosition: Vec3.multiplyQbyV(Quat.inverse(targetRotation), + Vec3.multiplyQbyV(sensorToWorldRotation, + Vec3.multiply(MyAvatar.sensorToWorldScale, + Vec3.subtract(relativePlayAreaCenterOffset, avatarSensorPosition)))), + localRotation: sensorToTargetRotation + }); + } + }; + + + this.scaleInTeleport = function () { + _this.teleportScaleFactor = Math.min((Date.now() - _this.teleportScaleStart) / _this.TELEPORT_SCALE_DURATION, 1); + Pointers.editRenderState( + _this.teleportScaleMode === "head" ? _this.teleportParabolaHeadVisuals : _this.teleportParabolaHandVisuals, + "teleport", + { + path: teleportPath, // Teleport beam disappears if not included. + end: { dimensions: Vec3.multiply(_this.teleportScaleFactor, TARGET_MODEL_DIMENSIONS) } + } + ); + if (_this.isPlayAreaVisible) { + _this.setPlayAreaDimensions(); + } + if (_this.teleportScaleFactor < 1) { + _this.teleportScaleTimer = Script.setTimeout(_this.scaleInTeleport, _this.TELEPORT_SCALE_TIMEOUT); + } else { + _this.teleportScaleTimer = null; + } + }; + + this.fadeOutTeleport = function () { + var isAvatarMoving, + i, length; + + isAvatarMoving = Controller.getActionValue(_this.translateXAction) !== 0 + || Controller.getActionValue(_this.translateYAction) !== 0 + || Controller.getActionValue(_this.translateZAction) !== 0; + + if (_this.teleportedFadeDelayFactor > 0 && !_this.isTeleportVisible && !isAvatarMoving) { + // Delay fade. + _this.teleportedFadeDelayFactor = _this.teleportedFadeDelayFactor - _this.TELEPORTED_FADE_DELAY_DELTA; + _this.teleportedFadeTimer = Script.setTimeout(_this.fadeOutTeleport, _this.TELEPORTED_FADE_INTERVAL); + } else if (_this.teleportedFadeFactor > 0 && !_this.isTeleportVisible && !isAvatarMoving) { + // Fade. + _this.teleportedFadeFactor = _this.teleportedFadeFactor - _this.TELEPORTED_FADE_DELTA; + Overlays.editOverlay(_this.teleportedTargetOverlay, { + alpha: _this.teleportedFadeFactor * _this.TELEPORTED_TARGET_ALPHA + }); + if (_this.wasPlayAreaVisible) { + Overlays.editOverlay(_this.playAreaOverlay, { + alpha: _this.teleportedFadeFactor * _this.PLAY_AREA_BOX_ALPHA + }); + var sensorAlpha = _this.teleportedFadeFactor * _this.PLAY_AREA_SENSOR_ALPHA; + for (i = 0, length = _this.playAreaSensorPositionOverlays.length; i < length; i++) { + Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { alpha: sensorAlpha }); + } + } + _this.teleportedFadeTimer = Script.setTimeout(_this.fadeOutTeleport, _this.TELEPORTED_FADE_INTERVAL); + } else { + // Make invisible. + Overlays.editOverlay(_this.teleportedTargetOverlay, { visible: false }); + if (_this.wasPlayAreaVisible) { + Overlays.editOverlay(_this.playAreaOverlay, { visible: false }); + for (i = 0, length = _this.playAreaSensorPositionOverlays.length; i < length; i++) { + Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { visible: false }); + } + } + _this.teleportedFadeTimer = null; + Selection.disableListHighlight(_this.teleporterSelectionName); + } + }; + + this.cancelFade = function () { + // Other hand may call this to immediately hide fading overlays. + var i, length; + if (_this.teleportedFadeTimer) { + Overlays.editOverlay(_this.teleportedTargetOverlay, { visible: false }); + if (_this.wasPlayAreaVisible) { + Overlays.editOverlay(_this.playAreaOverlay, { visible: false }); + for (i = 0, length = _this.playAreaSensorPositionOverlays.length; i < length; i++) { + Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { visible: false }); + } + } + _this.teleportedFadeTimer = null; + } + }; + + this.setTeleportVisible = function (visible, mode, fade) { + // Scales in teleport target and play area when start displaying them. + if (visible === _this.isTeleportVisible) { + return; + } + + if (visible) { + _this.teleportScaleMode = mode; + Pointers.editRenderState( + mode === "head" ? _this.teleportParabolaHeadVisuals : _this.teleportParabolaHandVisuals, + "teleport", + { + path: teleportPath, // Teleport beam disappears if not included. + end: { dimensions: Vec3.ZERO } + } + ); + _this.getOtherModule().cancelFade(); + _this.teleportScaleStart = Date.now(); + _this.teleportScaleFactor = 0; + _this.scaleInTeleport(); + Selection.enableListHighlight(_this.teleporterSelectionName, _this.TELEPORTER_SELECTION_STYLE); + } else { + if (_this.teleportScaleTimer !== null) { + Script.clearTimeout(_this.teleportScaleTimer); + _this.teleportScaleTimer = null; + } + + if (fade) { + // Copy of target at teleported position for fading. + var avatarScale = MyAvatar.sensorToWorldScale; + Overlays.editOverlay(_this.teleportedTargetOverlay, { + position: Vec3.sum(_this.teleportedPosition, { + x: 0, + y: -getAvatarFootOffset() + avatarScale * TARGET_MODEL_DIMENSIONS.y / 2, + z: 0 + }), + rotation: Quat.multiply(_this.TELEPORTED_TARGET_ROTATION, MyAvatar.orientation), + dimensions: Vec3.multiply(avatarScale, TARGET_MODEL_DIMENSIONS), + alpha: _this.TELEPORTED_TARGET_ALPHA, + visible: true + }); + + // Fade out over time. + _this.teleportedFadeDelayFactor = 1.0; + _this.teleportedFadeFactor = 1.0; + _this.teleportedFadeTimer = Script.setTimeout(_this.fadeOutTeleport, _this.TELEPORTED_FADE_DELAY); + } else { + Selection.disableListHighlight(_this.teleporterSelectionName); + } + } + + _this.isTeleportVisible = visible; + }; + + + this.axisButtonStateX = 0; // Left/right axis button pressed. + this.axisButtonStateY = 0; // Up/down axis button pressed. + this.BUTTON_TRANSITION_DELAY = 100; // Allow time for transition from direction buttons to touch-pad. + + this.axisButtonChangeX = function (value) { + if (value !== 0) { + _this.axisButtonStateX = value; + } else { + // Delay direction button release until after teleport possibly pressed. + Script.setTimeout(function () { + _this.axisButtonStateX = value; + }, _this.BUTTON_TRANSITION_DELAY); + } + }; + + this.axisButtonChangeY = function (value) { + if (value !== 0) { + _this.axisButtonStateY = value; + } else { + // Delay direction button release until after teleport possibly pressed. + Script.setTimeout(function () { + _this.axisButtonStateY = value; + }, _this.BUTTON_TRANSITION_DELAY); + } + }; + + this.teleportLocked = function () { + // Lock teleport if in advanced movement mode and have just transitioned from pressing a direction button. + return Controller.getValue(Controller.Hardware.Application.AdvancedMovement) && + (_this.axisButtonStateX !== 0 || _this.axisButtonStateY !== 0); + }; + + this.buttonPress = function (value) { + if (value === 0 || !_this.teleportLocked()) { + _this.buttonValue = value; + } + }; + + this.getStandardLY = function (value) { + _this.standardAxisLY = value; + }; + + this.getStandardRY = function (value) { + _this.standardAxisRY = value; + }; + + // Return value for the getDominantY and getOffhandY functions has to be inverted. + this.getDominantY = function () { + return (MyAvatar.getDominantHand() === "left") ? -(_this.standardAxisLY) : -(_this.standardAxisRY); + }; + + this.getOffhandY = function () { + return (MyAvatar.getDominantHand() === "left") ? -(_this.standardAxisRY) : -(_this.standardAxisLY); + }; + + this.getDominantHand = function () { + return (MyAvatar.getDominantHand() === "left") ? LEFT_HAND : RIGHT_HAND; + } + + this.getOffHand = function () { + return (MyAvatar.getDominantHand() === "left") ? RIGHT_HAND : LEFT_HAND; + } + + this.showReticle = function () { + return (_this.getDominantY() > TELEPORT_DEADZONE) ? true : false; + }; + + this.shouldTeleport = function () { + return (_this.getDominantY() > TELEPORT_DEADZONE && _this.getOffhandY() > TELEPORT_DEADZONE) ? true : false; + }; + + this.shouldCancel = function () { + //return (_this.getDominantY() < -TELEPORT_DEADZONE || _this.getOffhandY() < -TELEPORT_DEADZONE) ? true : false; + return (_this.getDominantY() <= TELEPORT_DEADZONE) ? true : false; + }; + + this.parameters = makeDispatcherModuleParameters( + 80, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.enterTeleport = function() { + _this.state = TELEPORTER_STATES.TARGETTING; + }; + + this.isReady = function(controllerData, deltaTime) { + if ((Window.interstitialModeEnabled && !Window.isPhysicsEnabled()) || !MyAvatar.allowTeleporting) { + return makeRunningValues(false, [], []); + } + + var otherModule = this.getOtherModule(); + if (!this.disabled && this.showReticle() && !otherModule.active && this.hand === this.getDominantHand()) { + this.active = true; + this.enterTeleport(); + return makeRunningValues(true, [], []); + } + return makeRunningValues(false, [], []); + }; + + this.run = function(controllerData, deltaTime) { + // Kill condition: + if (_this.shouldCancel()) { + _this.disableLasers(); + this.active = false; + return makeRunningValues(false, [], []); + } + + // Get current hand pose information to see if the pose is valid + var pose = Controller.getPoseValue(handInfo[(_this.hand === RIGHT_HAND) ? 'right' : 'left'].controllerInput); + var mode = pose.valid ? _this.hand : 'head'; + if (!pose.valid) { + Pointers.disablePointer(_this.teleportParabolaHandVisuals); + Pointers.disablePointer(_this.teleportParabolaHandCollisions); + Picks.disablePick(_this.teleportHandCollisionPick); + Pointers.enablePointer(_this.teleportParabolaHeadVisuals); + Pointers.enablePointer(_this.teleportParabolaHeadCollisions); + Picks.enablePick(_this.teleportHeadCollisionPick); + } else { + Pointers.enablePointer(_this.teleportParabolaHandVisuals); + Pointers.enablePointer(_this.teleportParabolaHandCollisions); + Picks.enablePick(_this.teleportHandCollisionPick); + Pointers.disablePointer(_this.teleportParabolaHeadVisuals); + Pointers.disablePointer(_this.teleportParabolaHeadCollisions); + Picks.disablePick(_this.teleportHeadCollisionPick); + } + + // We do up to 2 picks to find a teleport location. + // There are 2 types of teleport locations we are interested in: + // + // 1. A visible floor. This can be any entity surface that points within some degree of "up" + // and where the avatar capsule can be positioned without colliding + // + // 2. A seat. The seat can be visible or invisible. + // + // The Collision Pick is currently parented to the end overlay on teleportParabolaXXXXCollisions + // + // TODO + // Parent the collision Pick directly to the teleportParabolaXXXXVisuals and get rid of teleportParabolaXXXXCollisions + // + var result, collisionResult; + if (mode === 'head') { + result = Pointers.getPrevPickResult(_this.teleportParabolaHeadCollisions); + collisionResult = Picks.getPrevPickResult(_this.teleportHeadCollisionPick); + } else { + result = Pointers.getPrevPickResult(_this.teleportParabolaHandCollisions); + collisionResult = Picks.getPrevPickResult(_this.teleportHandCollisionPick); + } + + var teleportLocationType = getTeleportTargetType(result, collisionResult); + + if (teleportLocationType === TARGET.NONE) { + // Use the cancel default state + _this.setTeleportState(mode, "cancel", ""); + } else if (teleportLocationType === TARGET.INVALID) { + _this.setTeleportState(mode, "", "cancel"); + } else if (teleportLocationType === TARGET.COLLIDES) { + _this.setTeleportState(mode, "cancel", "collision"); + } else if (teleportLocationType === TARGET.SURFACE || teleportLocationType === TARGET.DISCREPANCY) { + _this.setTeleportState(mode, "teleport", "collision"); + _this.updatePlayArea(result.intersection); + } else if (teleportLocationType === TARGET.SEAT) { + _this.setTeleportState(mode, "collision", "seat"); + } + return _this.teleport(result, teleportLocationType); + }; + + this.teleport = function(newResult, target) { + var result = newResult; + _this.teleportedPosition = newResult.intersection; + if (!_this.shouldTeleport()) { + return makeRunningValues(true, [], []); + } + + if (target === TARGET.NONE || target === TARGET.INVALID) { + // Do nothing + } else if (target === TARGET.SEAT) { + Entities.callEntityMethod(result.objectID, 'sit'); + } else if (target === TARGET.SURFACE || target === TARGET.DISCREPANCY) { + var offset = getAvatarFootOffset(); + result.intersection.y += offset; + var shouldLandSafe = target === TARGET.DISCREPANCY; + MyAvatar.goToLocation(result.intersection, true, HMD.orientation, false, shouldLandSafe); + HMD.centerUI(); + MyAvatar.centerBody(); + } + + _this.disableLasers(); + _this.active = false; + return makeRunningValues(false, [], []); + }; + + this.disableLasers = function() { + _this.setPlayAreaVisible(false, null, false); + _this.setTeleportVisible(false, null, false); + Pointers.disablePointer(_this.teleportParabolaHandVisuals); + Pointers.disablePointer(_this.teleportParabolaHandCollisions); + Pointers.disablePointer(_this.teleportParabolaHeadVisuals); + Pointers.disablePointer(_this.teleportParabolaHeadCollisions); + Picks.disablePick(_this.teleportHeadCollisionPick); + Picks.disablePick(_this.teleportHandCollisionPick); + }; + + this.teleportState = ""; + + this.setTeleportState = function (mode, visibleState, invisibleState) { + var teleportState = mode + visibleState + invisibleState; + if (teleportState === _this.teleportState) { + return; + } + _this.teleportState = teleportState; + + var pointerID; + if (mode === 'head') { + Pointers.setRenderState(_this.teleportParabolaHeadVisuals, visibleState); + Pointers.setRenderState(_this.teleportParabolaHeadCollisions, invisibleState); + pointerID = _this.teleportParabolaHeadVisuals; + } else { + Pointers.setRenderState(_this.teleportParabolaHandVisuals, visibleState); + Pointers.setRenderState(_this.teleportParabolaHandCollisions, invisibleState); + pointerID = _this.teleportParabolaHandVisuals; + } + var visible = visibleState === "teleport"; + _this.setPlayAreaVisible(visible && MyAvatar.showPlayArea, + Pointers.getPointerProperties(pointerID).renderStates.teleport.end, false); + _this.setTeleportVisible(visible, mode, false); + }; + + this.setIgnoreEntities = function(entitiesToIgnore) { + Pointers.setIgnoreItems(_this.teleportParabolaHandVisuals, entitiesToIgnore); + Pointers.setIgnoreItems(_this.teleportParabolaHandCollisions, entitiesToIgnore); + Pointers.setIgnoreItems(_this.teleportParabolaHeadVisuals, entitiesToIgnore); + Pointers.setIgnoreItems(_this.teleportParabolaHeadCollisions, entitiesToIgnore); + Picks.setIgnoreItems(_this.teleportHeadCollisionPick, entitiesToIgnore); + Picks.setIgnoreItems(_this.teleportHandCollisionPick, entitiesToIgnore); + }; + } + + // related to repositioning the avatar after you teleport + var FOOT_JOINT_NAMES = ["RightToe_End", "RightToeBase", "RightFoot"]; + var DEFAULT_ROOT_TO_FOOT_OFFSET = 0.5; + + function getAvatarFootOffset() { + + // find a valid foot jointIndex + var footJointIndex = -1; + var i, l = FOOT_JOINT_NAMES.length; + for (i = 0; i < l; i++) { + footJointIndex = MyAvatar.getJointIndex(FOOT_JOINT_NAMES[i]); + if (footJointIndex !== -1) { + break; + } + } + if (footJointIndex !== -1) { + // default vertical offset from foot to avatar root. + var footPos = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(footJointIndex); + if (footPos.x === 0 && footPos.y === 0 && footPos.z === 0.0) { + // if footPos is exactly zero, it's probably wrong because avatar is currently loading, fall back to default. + return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale; + } else { + return -footPos.y; + } + } else { + return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale; + } + } + + var mappingName, teleportMapping; + var isViveMapped = false; + + function parseJSON(json) { + try { + return JSON.parse(json); + } catch (e) { + return undefined; + } + } + // When determininig whether you can teleport to a location, the normal of the + // point that is being intersected with is looked at. If this normal is more + // than MAX_ANGLE_FROM_UP_TO_TELEPORT degrees from your avatar's up, then + // you can't teleport there. + var MAX_ANGLE_FROM_UP_TO_TELEPORT = 70; + var MAX_DISCREPANCY_DISTANCE = 1.0; + var MAX_DOT_SIGN = -0.6; + + function checkForMeshDiscrepancy(result, collisionResult) { + var intersectingObjects = collisionResult.intersectingObjects; + if (intersectingObjects.length > 0 && intersectingObjects.length < 3) { + for (var j = 0; j < collisionResult.intersectingObjects.length; j++) { + var intersectingObject = collisionResult.intersectingObjects[j]; + for (var i = 0; i < intersectingObject.collisionContacts.length; i++) { + var normal = intersectingObject.collisionContacts[i].normalOnPick; + var distanceToPick = Vec3.distance(intersectingObject.collisionContacts[i].pointOnPick, result.intersection); + var normalSign = Vec3.dot(normal, Quat.getUp(MyAvatar.orientation)); + if ((distanceToPick > MAX_DISCREPANCY_DISTANCE) || (normalSign > MAX_DOT_SIGN)) { + return false; + } + } + } + return true; + } + return false; + } + + function getTeleportTargetType(result, collisionResult) { + if (result.type === Picks.INTERSECTED_NONE) { + return TARGET.NONE; + } + + var props = Entities.getEntityProperties(result.objectID, ['userData', 'visible']); + var data = parseJSON(props.userData); + if (data !== undefined && data.seat !== undefined) { + var avatarUuid = Uuid.fromString(data.seat.user); + if (Uuid.isNull(avatarUuid) || !AvatarList.getAvatar(avatarUuid).sessionUUID) { + return TARGET.SEAT; + } else { + return TARGET.INVALID; + } + } + var isDiscrepancy = false; + if (collisionResult.collisionRegion != undefined) { + if (collisionResult.intersects) { + isDiscrepancy = checkForMeshDiscrepancy(result, collisionResult); + if (!isDiscrepancy) { + return TARGET.COLLIDES; + } + } + } + + var surfaceNormal = result.surfaceNormal; + var angle = Math.acos(Vec3.dot(surfaceNormal, Quat.getUp(MyAvatar.orientation))) * (180.0 / Math.PI); + + if (angle > MAX_ANGLE_FROM_UP_TO_TELEPORT) { + return TARGET.INVALID; + } else if (isDiscrepancy) { + return TARGET.DISCREPANCY; + } else { + return TARGET.SURFACE; + } + } + + function registerViveTeleportMapping() { + // Disable Vive teleport if touch is transitioning across touch-pad after pressing a direction button. + if (Controller.Hardware.Vive) { + var mappingName = 'Hifi-Teleporter-Dev-Vive-' + Math.random(); + var viveTeleportMapping = Controller.newMapping(mappingName); + viveTeleportMapping.from(Controller.Hardware.Vive.LSX).peek().to(leftTeleporter.axisButtonChangeX); + viveTeleportMapping.from(Controller.Hardware.Vive.LSY).peek().to(leftTeleporter.axisButtonChangeY); + viveTeleportMapping.from(Controller.Hardware.Vive.RSX).peek().to(rightTeleporter.axisButtonChangeX); + viveTeleportMapping.from(Controller.Hardware.Vive.RSY).peek().to(rightTeleporter.axisButtonChangeY); + Controller.enableMapping(mappingName); + isViveMapped = true; + } + } + + function onHardwareChanged() { + // Controller.Hardware.Vive is not immediately available at Interface start-up. + if (!isViveMapped && Controller.Hardware.Vive) { + registerViveTeleportMapping(); + } + } + + Controller.hardwareChanged.connect(onHardwareChanged); + + function registerMappings() { + mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); + teleportMapping = Controller.newMapping(mappingName); + + // Vive teleport button lock-out. + registerViveTeleportMapping(); + + // Teleport actions. + teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftTeleporter.buttonPress); + teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightTeleporter.buttonPress); + teleportMapping.from(Controller.Standard.LY).peek().to(leftTeleporter.getStandardLY); + teleportMapping.from(Controller.Standard.RY).peek().to(leftTeleporter.getStandardRY); + teleportMapping.from(Controller.Standard.LY).peek().to(rightTeleporter.getStandardLY); + teleportMapping.from(Controller.Standard.RY).peek().to(rightTeleporter.getStandardRY); + } + + var leftTeleporter = new Teleporter(LEFT_HAND); + var rightTeleporter = new Teleporter(RIGHT_HAND); + + enableDispatcherModule("LeftTeleporter", leftTeleporter); + enableDispatcherModule("RightTeleporter", rightTeleporter); + registerMappings(); + Controller.enableMapping(mappingName); + + function cleanup() { + Controller.hardwareChanged.disconnect(onHardwareChanged); + teleportMapping.disable(); + leftTeleporter.cleanup(); + rightTeleporter.cleanup(); + disableDispatcherModule("LeftTeleporter"); + disableDispatcherModule("RightTeleporter"); + } + Script.scriptEnding.connect(cleanup); + + var handleTeleportMessages = function(channel, message, sender) { + if (sender === MyAvatar.sessionUUID) { + if (channel === 'Hifi-Teleport-Disabler') { + if (message === 'both') { + leftTeleporter.disabled = true; + rightTeleporter.disabled = true; + } + if (message === 'left') { + leftTeleporter.disabled = true; + rightTeleporter.disabled = false; + } + if (message === 'right') { + leftTeleporter.disabled = false; + rightTeleporter.disabled = true; + } + if (message === 'none') { + leftTeleporter.disabled = false; + rightTeleporter.disabled = false; + } + } else if (channel === 'Hifi-Teleport-Ignore-Add' && + !Uuid.isNull(message) && + ignoredEntities.indexOf(message) === -1) { + ignoredEntities.push(message); + leftTeleporter.setIgnoreEntities(ignoredEntities); + rightTeleporter.setIgnoreEntities(ignoredEntities); + } else if (channel === 'Hifi-Teleport-Ignore-Remove' && !Uuid.isNull(message)) { + var removeIndex = ignoredEntities.indexOf(message); + if (removeIndex > -1) { + ignoredEntities.splice(removeIndex, 1); + leftTeleporter.setIgnoreEntities(ignoredEntities); + rightTeleporter.setIgnoreEntities(ignoredEntities); + } + } + } + }; + + MyAvatar.onLoadComplete.connect(function () { + Script.setTimeout(function () { + leftTeleporter.initPointers(); + rightTeleporter.initPointers(); + }, 500); + }); + + Messages.subscribe('Hifi-Teleport-Disabler'); + Messages.subscribe('Hifi-Teleport-Ignore-Add'); + Messages.subscribe('Hifi-Teleport-Ignore-Remove'); + Messages.messageReceived.connect(handleTeleportMessages); + + MyAvatar.sensorToWorldScaleChanged.connect(function () { + leftTeleporter.updatePlayAreaScale(); + rightTeleporter.updatePlayAreaScale(); + }); + +}()); // END LOCAL_SCOPE diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/simplifiedUI/system/controllers/controllerModules/webSurfaceLaserInput.js new file mode 100644 index 0000000000..cf700a8ad9 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -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); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerScripts.js b/scripts/simplifiedUI/system/controllers/controllerScripts.js new file mode 100644 index 0000000000..c9cb61b5f5 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerScripts.js @@ -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(); +} diff --git a/scripts/simplifiedUI/system/controllers/godView.js b/scripts/simplifiedUI/system/controllers/godView.js new file mode 100644 index 0000000000..4b406399fd --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/godView.js @@ -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 diff --git a/scripts/simplifiedUI/system/controllers/grab.js b/scripts/simplifiedUI/system/controllers/grab.js new file mode 100644 index 0000000000..1fb82d3843 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/grab.js @@ -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 diff --git a/scripts/simplifiedUI/system/controllers/handTouch.js b/scripts/simplifiedUI/system/controllers/handTouch.js new file mode 100644 index 0000000000..c706d054c1 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/handTouch.js @@ -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); + } + } + } + }); + }); +}()); diff --git a/scripts/simplifiedUI/system/controllers/squeezeHands.js b/scripts/simplifiedUI/system/controllers/squeezeHands.js new file mode 100644 index 0000000000..69f44f46a9 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/squeezeHands.js @@ -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 diff --git a/scripts/simplifiedUI/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/simplifiedUI/system/controllers/toggleAdvancedMovementForHandControllers.js new file mode 100644 index 0000000000..92f72f8724 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -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 diff --git a/scripts/simplifiedUI/system/controllers/touchControllerConfiguration.js b/scripts/simplifiedUI/system/controllers/touchControllerConfiguration.js new file mode 100644 index 0000000000..991b77b8af --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/touchControllerConfiguration.js @@ -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", + } + } + }, + } + } + ] +}; diff --git a/scripts/simplifiedUI/system/controllers/viveControllerConfiguration.js b/scripts/simplifiedUI/system/controllers/viveControllerConfiguration.js new file mode 100644 index 0000000000..09fd8adacc --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/viveControllerConfiguration.js @@ -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} + } + } + } + ] +}; diff --git a/scripts/simplifiedUI/system/progress.js b/scripts/simplifiedUI/system/progress.js new file mode 100644 index 0000000000..b373612790 --- /dev/null +++ b/scripts/simplifiedUI/system/progress.js @@ -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 diff --git a/scripts/simplifiedUI/system/request-service.js b/scripts/simplifiedUI/system/request-service.js new file mode 100644 index 0000000000..b57f2d4cd7 --- /dev/null +++ b/scripts/simplifiedUI/system/request-service.js @@ -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 diff --git a/scripts/simplifiedUI/images/inputDeviceMuted.svg b/scripts/simplifiedUI/ui/images/inputDeviceMuted.svg similarity index 100% rename from scripts/simplifiedUI/images/inputDeviceMuted.svg rename to scripts/simplifiedUI/ui/images/inputDeviceMuted.svg diff --git a/scripts/simplifiedUI/images/outputDeviceMuted.svg b/scripts/simplifiedUI/ui/images/outputDeviceMuted.svg similarity index 100% rename from scripts/simplifiedUI/images/outputDeviceMuted.svg rename to scripts/simplifiedUI/ui/images/outputDeviceMuted.svg diff --git a/scripts/simplifiedUI/simplifiedNametag/resources/modules/defaultLocalEntityProps.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/defaultLocalEntityProps.js similarity index 100% rename from scripts/simplifiedUI/simplifiedNametag/resources/modules/defaultLocalEntityProps.js rename to scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/defaultLocalEntityProps.js diff --git a/scripts/simplifiedUI/simplifiedNametag/resources/modules/entityMaker.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/entityMaker.js similarity index 100% rename from scripts/simplifiedUI/simplifiedNametag/resources/modules/entityMaker.js rename to scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/entityMaker.js diff --git a/scripts/simplifiedUI/simplifiedNametag/resources/modules/nameTagListManager.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/nameTagListManager.js similarity index 100% rename from scripts/simplifiedUI/simplifiedNametag/resources/modules/nameTagListManager.js rename to scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/nameTagListManager.js diff --git a/scripts/simplifiedUI/simplifiedNametag/resources/modules/objectAssign.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/objectAssign.js similarity index 100% rename from scripts/simplifiedUI/simplifiedNametag/resources/modules/objectAssign.js rename to scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/objectAssign.js diff --git a/scripts/simplifiedUI/simplifiedNametag/resources/modules/pickRayController.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/pickRayController.js similarity index 100% rename from scripts/simplifiedUI/simplifiedNametag/resources/modules/pickRayController.js rename to scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/pickRayController.js diff --git a/scripts/simplifiedUI/simplifiedNametag/resources/modules/textHelper.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/textHelper.js similarity index 100% rename from scripts/simplifiedUI/simplifiedNametag/resources/modules/textHelper.js rename to scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/textHelper.js diff --git a/scripts/simplifiedUI/simplifiedNametag/simplifiedNametag.js b/scripts/simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js similarity index 100% rename from scripts/simplifiedUI/simplifiedNametag/simplifiedNametag.js rename to scripts/simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js diff --git a/scripts/simplifiedUI/simplifiedStatusIndicator/simplifiedStatusIndicator.js b/scripts/simplifiedUI/ui/simplifiedStatusIndicator/simplifiedStatusIndicator.js similarity index 100% rename from scripts/simplifiedUI/simplifiedStatusIndicator/simplifiedStatusIndicator.js rename to scripts/simplifiedUI/ui/simplifiedStatusIndicator/simplifiedStatusIndicator.js diff --git a/scripts/simplifiedUI/simplifiedUI.js b/scripts/simplifiedUI/ui/simplifiedUI.js similarity index 100% rename from scripts/simplifiedUI/simplifiedUI.js rename to scripts/simplifiedUI/ui/simplifiedUI.js diff --git a/tools/ci-scripts/postbuild.py b/tools/ci-scripts/postbuild.py new file mode 100644 index 0000000000..a1561f6203 --- /dev/null +++ b/tools/ci-scripts/postbuild.py @@ -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)