From ec7fcb60f3ebaa8ce1a7b05d0065f083d3f4be4b Mon Sep 17 00:00:00 2001 From: Marko Kudjerski Date: Mon, 30 Apr 2018 18:28:21 -0700 Subject: [PATCH 001/362] Updated the license date --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 60e86a1cc7..deb80afc19 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2016, High Fidelity, Inc. +Copyright (c) 2013-2018, High Fidelity, Inc. All rights reserved. licensing@highfidelity.io From 9267919d7fbe6e2daa3a3631b7d7f7146ba092ed Mon Sep 17 00:00:00 2001 From: Alexia Mandeville Date: Wed, 29 Aug 2018 16:49:27 -0700 Subject: [PATCH 002/362] Resorting Developer menu organiztion --- interface/src/Menu.cpp | 85 +++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index a7d7dcf8b3..62acb08d44 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -285,7 +285,50 @@ Menu::Menu() { // Developer menu ---------------------------------- MenuWrapper* developerMenu = addMenu("Developer", "Developer"); + + // Developer > Scripting >>> + MenuWrapper* scriptingOptionsMenu = developerMenu->addMenu("Scripting"); + + // Developer > Scripting > Console... + addActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::Console, Qt::CTRL | Qt::ALT | Qt::Key_J, + DependencyManager::get().data(), + SLOT(toggleConsole()), + QAction::NoRole, + UNSPECIFIED_POSITION); + // Developer > Scripting > API Debugger + action = addActionToQMenuAndActionHash(scriptingOptionsMenu, "API Debugger"); + connect(action, &QAction::triggered, [] { + QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); + defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/utilities/tools/currentAPI.js"); + DependencyManager::get()->loadScript(defaultScriptsLoc.toString()); + }); + + // Developer > Scripting > Log... + addActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::Log, Qt::CTRL | Qt::SHIFT | Qt::Key_L, + qApp, SLOT(toggleLogDialog())); + + // Developer > Scripting > Entity Script Server Log + auto essLogAction = addActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::EntityScriptServerLog, 0, + qApp, SLOT(toggleEntityScriptServerLogDialog())); + { + auto nodeList = DependencyManager::get(); + QObject::connect(nodeList.data(), &NodeList::canRezChanged, essLogAction, [essLogAction] { + auto nodeList = DependencyManager::get(); + essLogAction->setEnabled(nodeList->getThisNodeCanRez()); + }); + essLogAction->setEnabled(nodeList->getThisNodeCanRez()); + } + + // Developer > Scripting > Script Log (HMD friendly)... + addActionToQMenuAndActionHash(scriptingOptionsMenu, "Script Log (HMD friendly)...", Qt::NoButton, + qApp, SLOT(showScriptLogs())); + + // Developer > Scripting > Verbose Logging + addCheckableActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::VerboseLogging, 0, false, + qApp, SLOT(updateVerboseLogging())); + + // Developer > UI >>> MenuWrapper* uiOptionsMenu = developerMenu->addMenu("UI"); action = addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::DesktopTabletToToolbar, 0, @@ -691,10 +734,11 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(physicsOptionsMenu, MenuOption::PhysicsShowBulletConstraints, 0, false, qApp, SLOT(setShowBulletConstraints(bool))); addCheckableActionToQMenuAndActionHash(physicsOptionsMenu, MenuOption::PhysicsShowBulletConstraintLimits, 0, false, qApp, SLOT(setShowBulletConstraintLimits(bool))); - // Developer > Display Crash Options - addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::DisplayCrashOptions, 0, true); // Developer > Crash >>> MenuWrapper* crashMenu = developerMenu->addMenu("Crash"); + + // Developer > Crash > Display Crash Options + addCheckableActionToQMenuAndActionHash(crashMenu, MenuOption::DisplayCrashOptions, 0, true); addActionToQMenuAndActionHash(crashMenu, MenuOption::DeadlockInterface, 0, qApp, SLOT(deadlockApplication())); addActionToQMenuAndActionHash(crashMenu, MenuOption::UnresponsiveInterface, 0, qApp, SLOT(unresponsiveApplication())); @@ -729,7 +773,7 @@ Menu::Menu() { action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFaultThreaded); connect(action, &QAction::triggered, qApp, []() { std::thread(crash::newFault).join(); }); - // Developer > Stats + // Developer > Show Statistics addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats); // Settings > Enable Speech Control API @@ -744,41 +788,6 @@ Menu::Menu() { connect(speechRecognizer.data(), SIGNAL(enabledUpdated(bool)), speechRecognizerAction, SLOT(setChecked(bool))); #endif - // console - addActionToQMenuAndActionHash(developerMenu, MenuOption::Console, Qt::CTRL | Qt::ALT | Qt::Key_J, - DependencyManager::get().data(), - SLOT(toggleConsole()), - QAction::NoRole, - UNSPECIFIED_POSITION); - - // Developer > API Debugger - action = addActionToQMenuAndActionHash(developerMenu, "API Debugger"); - connect(action, &QAction::triggered, [] { - QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); - defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/utilities/tools/currentAPI.js"); - DependencyManager::get()->loadScript(defaultScriptsLoc.toString()); - }); - - // Developer > Log... - addActionToQMenuAndActionHash(developerMenu, MenuOption::Log, Qt::CTRL | Qt::SHIFT | Qt::Key_L, - qApp, SLOT(toggleLogDialog())); - auto essLogAction = addActionToQMenuAndActionHash(developerMenu, MenuOption::EntityScriptServerLog, 0, - qApp, SLOT(toggleEntityScriptServerLogDialog())); - { - auto nodeList = DependencyManager::get(); - QObject::connect(nodeList.data(), &NodeList::canRezChanged, essLogAction, [essLogAction] { - auto nodeList = DependencyManager::get(); - essLogAction->setEnabled(nodeList->getThisNodeCanRez()); - }); - essLogAction->setEnabled(nodeList->getThisNodeCanRez()); - } - - addActionToQMenuAndActionHash(developerMenu, "Script Log (HMD friendly)...", Qt::NoButton, - qApp, SLOT(showScriptLogs())); - - addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::VerboseLogging, 0, false, - qApp, SLOT(updateVerboseLogging())); - // Developer > Show Overlays addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Overlays, 0, true); From 7bc860cbc9806f87c38cdfd51307df112831e9b3 Mon Sep 17 00:00:00 2001 From: Alexia Mandeville Date: Thu, 30 Aug 2018 11:42:26 -0700 Subject: [PATCH 003/362] Move Debug defaultscripts.js to Scripting menu --- scripts/defaultScripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index b275660c0f..1ac84cd29e 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -39,7 +39,7 @@ var DEFAULT_SCRIPTS_SEPARATE = [ ]; // add a menu item for debugging -var MENU_CATEGORY = "Developer"; +var MENU_CATEGORY = "Developer > Scripting"; var MENU_ITEM = "Debug defaultScripts.js"; var SETTINGS_KEY = '_debugDefaultScriptsIsChecked'; From 1233c2ec0d8ed90709f81d992921e9b107dbf3e1 Mon Sep 17 00:00:00 2001 From: Alexia Mandeville Date: Thu, 30 Aug 2018 14:46:53 -0700 Subject: [PATCH 004/362] Moving root settings into preferred submenus --- interface/src/Menu.cpp | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 62acb08d44..e0e16f449f 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -328,15 +328,32 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::VerboseLogging, 0, false, qApp, SLOT(updateVerboseLogging())); + // Developer > Scripting > Enable Speech Control API +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + auto speechRecognizer = DependencyManager::get(); + QAction* speechRecognizerAction = addCheckableActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::ControlWithSpeech, + Qt::CTRL | Qt::SHIFT | Qt::Key_C, + speechRecognizer->getEnabled(), + speechRecognizer.data(), + SLOT(setEnabled(bool)), + UNSPECIFIED_POSITION); + connect(speechRecognizer.data(), SIGNAL(enabledUpdated(bool)), speechRecognizerAction, SLOT(setChecked(bool))); +#endif // Developer > UI >>> MenuWrapper* uiOptionsMenu = developerMenu->addMenu("UI"); action = addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::DesktopTabletToToolbar, 0, qApp->getDesktopTabletBecomesToolbarSetting()); + + // Developer > UI > Show Overlays + addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::Overlays, 0, true); + + // Developer > UI > Desktop Tablet Becomes Toolbar connect(action, &QAction::triggered, [action] { qApp->setDesktopTabletBecomesToolbarSetting(action->isChecked()); }); - + + // Developer > UI > HMD Tablet Becomes Toolbar action = addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::HMDTabletToToolbar, 0, qApp->getHmdTabletBecomesToolbarSetting()); connect(action, &QAction::triggered, [action] { @@ -776,21 +793,6 @@ Menu::Menu() { // Developer > Show Statistics addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats); - // Settings > Enable Speech Control API -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) - auto speechRecognizer = DependencyManager::get(); - QAction* speechRecognizerAction = addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::ControlWithSpeech, - Qt::CTRL | Qt::SHIFT | Qt::Key_C, - speechRecognizer->getEnabled(), - speechRecognizer.data(), - SLOT(setEnabled(bool)), - UNSPECIFIED_POSITION); - connect(speechRecognizer.data(), SIGNAL(enabledUpdated(bool)), speechRecognizerAction, SLOT(setChecked(bool))); -#endif - - // Developer > Show Overlays - addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Overlays, 0, true); - #if 0 /// -------------- REMOVED FOR NOW -------------- addDisabledActionAndSeparator(navigateMenu, "History"); QAction* backAction = addActionToQMenuAndActionHash(navigateMenu, MenuOption::Back, 0, addressManager.data(), SLOT(goBack())); From 90e98f1608e1a2bad82ada56738fabcfbc28c2a9 Mon Sep 17 00:00:00 2001 From: Alexia Mandeville Date: Fri, 31 Aug 2018 09:55:46 -0700 Subject: [PATCH 005/362] Moving Log to root Developer menu --- interface/src/Menu.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index e0e16f449f..915ef9e400 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -303,11 +303,7 @@ Menu::Menu() { defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/utilities/tools/currentAPI.js"); DependencyManager::get()->loadScript(defaultScriptsLoc.toString()); }); - - // Developer > Scripting > Log... - addActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::Log, Qt::CTRL | Qt::SHIFT | Qt::Key_L, - qApp, SLOT(toggleLogDialog())); - + // Developer > Scripting > Entity Script Server Log auto essLogAction = addActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::EntityScriptServerLog, 0, qApp, SLOT(toggleEntityScriptServerLogDialog())); @@ -792,6 +788,10 @@ Menu::Menu() { // Developer > Show Statistics addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats); + + // Developer > Log + addActionToQMenuAndActionHash(developerMenu, MenuOption::Log, Qt::CTRL | Qt::SHIFT | Qt::Key_L, + qApp, SLOT(toggleLogDialog())); #if 0 /// -------------- REMOVED FOR NOW -------------- addDisabledActionAndSeparator(navigateMenu, "History"); From 6a1c76d14d7d5a189f70c7fc66ef313b8bca7054 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 14 Sep 2018 12:19:55 -0700 Subject: [PATCH 006/362] Only sort an estimated number of avatars --- assignment-client/src/avatars/AvatarMixerSlave.cpp | 10 +++++++++- libraries/shared/src/PrioritySortUtil.h | 10 ++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 7368db0c31..e17c108775 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -244,6 +244,9 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // reset the internal state for correct random number distribution distribution.reset(); + // Base number to sort on number previously sent. + const int numToSendEst = std::max(nodeData->getNumAvatarsSentLastFrame() * 2, 20); + // reset the number of sent avatars nodeData->resetNumAvatarsSentLastFrame(); @@ -399,7 +402,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) int remainingAvatars = (int)sortedAvatars.size(); auto traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true); - const auto& sortedAvatarVector = sortedAvatars.getSortedVector(); + const auto& sortedAvatarVector = sortedAvatars.getSortedVector(numToSendEst); for (const auto& sortedAvatar : sortedAvatarVector) { const Node* otherNode = sortedAvatar.getNode(); auto lastEncodeForOther = sortedAvatar.getTimestamp(); @@ -524,6 +527,11 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) remainingAvatars--; } + if (nodeData->getNumAvatarsSentLastFrame() > numToSendEst) { + qCWarning(avatars) << "More avatars sent than upper estimate" << nodeData->getNumAvatarsSentLastFrame() + << " / " << numToSendEst; + } + quint64 startPacketSending = usecTimestampNow(); // close the current packet so that we're always sending something diff --git a/libraries/shared/src/PrioritySortUtil.h b/libraries/shared/src/PrioritySortUtil.h index 27f6b193ba..3a40fead71 100644 --- a/libraries/shared/src/PrioritySortUtil.h +++ b/libraries/shared/src/PrioritySortUtil.h @@ -66,8 +66,14 @@ namespace PrioritySortUtil { void reserve(size_t num) { _vector.reserve(num); } - const std::vector& getSortedVector() { - std::sort(_vector.begin(), _vector.end(), [](const T& left, const T& right) { return left.getPriority() > right.getPriority(); }); + const std::vector& getSortedVector(int numToSort = 0) { + if (numToSort == 0 || numToSort >= (int)_vector.size()) { + std::sort(_vector.begin(), _vector.end(), + [](const T& left, const T& right) { return left.getPriority() > right.getPriority(); }); + } else { + std::partial_sort(_vector.begin(), _vector.begin() + numToSort, _vector.end(), + [](const T& left, const T& right) { return left.getPriority() > right.getPriority(); }); + } return _vector; } From ca300db410eae5a8fef6ab6143ea415e0a099dae Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 14 Sep 2018 18:24:08 -0700 Subject: [PATCH 007/362] Send only avatar data that will fit in packet (WIP) --- .../src/avatars/AvatarMixerSlave.cpp | 16 +- libraries/avatars/src/AvatarData.cpp | 222 ++++++++++-------- libraries/avatars/src/AvatarData.h | 2 +- 3 files changed, 137 insertions(+), 103 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index e17c108775..20d85a3d4d 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -460,13 +460,13 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) bool distanceAdjust = true; glm::vec3 viewerPosition = myPosition; - AvatarDataPacket::HasFlags hasFlagsOut; // the result of the toByteArray + AvatarDataPacket::HasFlags hasFlagsOut = 0; // the result of the toByteArray bool dropFaceTracking = false; auto startSerialize = chrono::high_resolution_clock::now(); QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, - &lastSentJointsForOther); + &lastSentJointsForOther, maxAvatarDataBytes); auto endSerialize = chrono::high_resolution_clock::now(); _stats.toByteArrayElapsedTime += (quint64) chrono::duration_cast(endSerialize - startSerialize).count(); @@ -477,16 +477,16 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) dropFaceTracking = true; // first try dropping the facial data bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, - hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther); + hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther, maxAvatarDataBytes); if (bytes.size() > maxAvatarDataBytes) { qCWarning(avatars) << "otherAvatar.toByteArray() for" << otherNode->getUUID() << "without facial data resulted in very large buffer of" << bytes.size() << "bytes - reducing to MinimumData"; bytes = otherAvatar->toByteArray(AvatarData::MinimumData, lastEncodeForOther, lastSentJointsForOther, - hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther); + hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther, maxAvatarDataBytes); - if (bytes.size() > maxAvatarDataBytes) { + if (bytes.size() > maxAvatarDataBytes, maxAvatarDataBytes) { qCWarning(avatars) << "otherAvatar.toByteArray() for" << otherNode->getUUID() << "MinimumData resulted in very large buffer of" << bytes.size() << "bytes - refusing to send avatar"; @@ -604,7 +604,7 @@ void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePoin QVector emptyLastJointSendData { otherAvatar->getJointCount() }; QByteArray avatarByteArray = otherAvatar->toByteArray(AvatarData::SendAllData, 0, emptyLastJointSendData, - flagsOut, false, false, glm::vec3(0), nullptr); + flagsOut, false, false, glm::vec3(0), nullptr, 0); quint64 end = usecTimestampNow(); _stats.toByteArrayElapsedTime += (end - start); @@ -630,14 +630,14 @@ void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePoin << "-" << avatarByteArray.size() << "bytes"; avatarByteArray = otherAvatar->toByteArray(AvatarData::SendAllData, 0, emptyLastJointSendData, - flagsOut, true, false, glm::vec3(0), nullptr); + flagsOut, true, false, glm::vec3(0), nullptr, 0); if (avatarByteArray.size() > maxAvatarByteArraySize) { qCWarning(avatars) << "Replicated avatar data without facial data still too large for" << otherAvatar->getSessionUUID() << "-" << avatarByteArray.size() << "bytes"; avatarByteArray = otherAvatar->toByteArray(AvatarData::MinimumData, 0, emptyLastJointSendData, - flagsOut, true, false, glm::vec3(0), nullptr); + flagsOut, true, false, glm::vec3(0), nullptr, 0); } } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 834754e228..fdd5317bce 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -227,13 +227,13 @@ QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail, bool dro _lastToByteArray = usecTimestampNow(); return AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(), hasFlagsOut, dropFaceTracking, false, glm::vec3(0), nullptr, - &_outboundDataRate); + 0, &_outboundDataRate); } QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, - AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust, - glm::vec3 viewerPosition, QVector* sentJointDataOut, AvatarDataRate* outboundDataRateOut) const { + AvatarDataPacket::HasFlags& itemFlags, bool dropFaceTracking, bool distanceAdjust, + glm::vec3 viewerPosition, QVector* sentJointDataOut, int maxDataSize, AvatarDataRate* outboundDataRateOut) const { bool cullSmallChanges = (dataDetail == CullSmallData); bool sendAll = (dataDetail == SendAllData); @@ -270,22 +270,6 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent auto parentID = getParentID(); - bool hasAvatarGlobalPosition = true; // always include global position - bool hasAvatarOrientation = false; - bool hasAvatarBoundingBox = false; - bool hasAvatarScale = false; - bool hasLookAtPosition = false; - bool hasAudioLoudness = false; - bool hasSensorToWorldMatrix = false; - bool hasAdditionalFlags = false; - - // local position, and parent info only apply to avatars that are parented. The local position - // and the parent info can change independently though, so we track their "changed since" - // separately - bool hasParentInfo = false; - bool hasAvatarLocalPosition = false; - - bool hasFaceTrackerInfo = false; bool hasJointData = false; bool hasJointDefaultPoseFlags = false; bool hasGrabJoints = false; @@ -294,80 +278,121 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent glm::mat4 rightFarGrabMatrix; glm::mat4 mouseFarGrabMatrix; - if (sendPALMinimum) { - hasAudioLoudness = true; - } else { - hasAvatarOrientation = sendAll || rotationChangedSince(lastSentTime); - hasAvatarBoundingBox = sendAll || avatarBoundingBoxChangedSince(lastSentTime); - hasAvatarScale = sendAll || avatarScaleChangedSince(lastSentTime); - hasLookAtPosition = sendAll || lookAtPositionChangedSince(lastSentTime); - hasAudioLoudness = sendAll || audioLoudnessChangedSince(lastSentTime); - hasSensorToWorldMatrix = sendAll || sensorToWorldMatrixChangedSince(lastSentTime); - hasAdditionalFlags = sendAll || additionalFlagsChangedSince(lastSentTime); - hasParentInfo = sendAll || parentInfoChangedSince(lastSentTime); - hasAvatarLocalPosition = hasParent() && (sendAll || - tranlationChangedSince(lastSentTime) || - parentInfoChangedSince(lastSentTime)); + // Leading flags, to indicate how much data is actually included in the packet... + AvatarDataPacket::HasFlags packetStateFlags = 0; - hasFaceTrackerInfo = !dropFaceTracking && (hasFaceTracker() || getHasScriptedBlendshapes()) && - (sendAll || faceTrackerInfoChangedSince(lastSentTime)); - hasJointData = sendAll || !sendMinimum; - hasJointDefaultPoseFlags = hasJointData; - if (hasJointData) { - bool leftValid; - leftFarGrabMatrix = _farGrabLeftMatrixCache.get(leftValid); - if (!leftValid) { - leftFarGrabMatrix = glm::mat4(); + if (itemFlags == 0) { + bool hasAvatarGlobalPosition = true; // always include global position + bool hasAvatarOrientation = false; + bool hasAvatarBoundingBox = false; + bool hasAvatarScale = false; + bool hasLookAtPosition = false; + bool hasAudioLoudness = false; + bool hasSensorToWorldMatrix = false; + bool hasAdditionalFlags = false; + + // local position, and parent info only apply to avatars that are parented. The local position + // and the parent info can change independently though, so we track their "changed since" + // separately + bool hasParentInfo = false; + bool hasAvatarLocalPosition = false; + + bool hasFaceTrackerInfo = false; + + if (sendPALMinimum) { + hasAudioLoudness = true; + } else { + hasAvatarOrientation = sendAll || rotationChangedSince(lastSentTime); + hasAvatarBoundingBox = sendAll || avatarBoundingBoxChangedSince(lastSentTime); + hasAvatarScale = sendAll || avatarScaleChangedSince(lastSentTime); + hasLookAtPosition = sendAll || lookAtPositionChangedSince(lastSentTime); + hasAudioLoudness = sendAll || audioLoudnessChangedSince(lastSentTime); + hasSensorToWorldMatrix = sendAll || sensorToWorldMatrixChangedSince(lastSentTime); + hasAdditionalFlags = sendAll || additionalFlagsChangedSince(lastSentTime); + hasParentInfo = sendAll || parentInfoChangedSince(lastSentTime); + hasAvatarLocalPosition = hasParent() && (sendAll || + tranlationChangedSince(lastSentTime) || + parentInfoChangedSince(lastSentTime)); + + hasFaceTrackerInfo = !dropFaceTracking && (hasFaceTracker() || getHasScriptedBlendshapes()) && + (sendAll || faceTrackerInfoChangedSince(lastSentTime)); + hasJointData = sendAll || !sendMinimum; + hasJointDefaultPoseFlags = hasJointData; + if (hasJointData) { + bool leftValid; + leftFarGrabMatrix = _farGrabLeftMatrixCache.get(leftValid); + if (!leftValid) { + leftFarGrabMatrix = glm::mat4(); + } + bool rightValid; + rightFarGrabMatrix = _farGrabRightMatrixCache.get(rightValid); + if (!rightValid) { + rightFarGrabMatrix = glm::mat4(); + } + bool mouseValid; + mouseFarGrabMatrix = _farGrabMouseMatrixCache.get(mouseValid); + if (!mouseValid) { + mouseFarGrabMatrix = glm::mat4(); + } + hasGrabJoints = (leftValid || rightValid || mouseValid); } - bool rightValid; - rightFarGrabMatrix = _farGrabRightMatrixCache.get(rightValid); - if (!rightValid) { - rightFarGrabMatrix = glm::mat4(); - } - bool mouseValid; - mouseFarGrabMatrix = _farGrabMouseMatrixCache.get(mouseValid); - if (!mouseValid) { - mouseFarGrabMatrix = glm::mat4(); - } - hasGrabJoints = (leftValid || rightValid || mouseValid); } + + packetStateFlags = + (hasAvatarGlobalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_GLOBAL_POSITION : 0) + | (hasAvatarBoundingBox ? AvatarDataPacket::PACKET_HAS_AVATAR_BOUNDING_BOX : 0) + | (hasAvatarOrientation ? AvatarDataPacket::PACKET_HAS_AVATAR_ORIENTATION : 0) + | (hasAvatarScale ? AvatarDataPacket::PACKET_HAS_AVATAR_SCALE : 0) + | (hasLookAtPosition ? AvatarDataPacket::PACKET_HAS_LOOK_AT_POSITION : 0) + | (hasAudioLoudness ? AvatarDataPacket::PACKET_HAS_AUDIO_LOUDNESS : 0) + | (hasSensorToWorldMatrix ? AvatarDataPacket::PACKET_HAS_SENSOR_TO_WORLD_MATRIX : 0) + | (hasAdditionalFlags ? AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS : 0) + | (hasParentInfo ? AvatarDataPacket::PACKET_HAS_PARENT_INFO : 0) + | (hasAvatarLocalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_LOCAL_POSITION : 0) + | (hasFaceTrackerInfo ? AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO : 0) + | (hasJointData ? AvatarDataPacket::PACKET_HAS_JOINT_DATA : 0) + | (hasJointDefaultPoseFlags ? AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS : 0) + | (hasGrabJoints ? AvatarDataPacket::PACKET_HAS_GRAB_JOINTS : 0); + + } else { + packetStateFlags = itemFlags; } const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE + - (hasFaceTrackerInfo ? AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getBlendshapeCoefficients().size()) : 0) + - (hasJointData ? AvatarDataPacket::maxJointDataSize(_jointData.size(), hasGrabJoints) : 0) + - (hasJointDefaultPoseFlags ? AvatarDataPacket::maxJointDefaultPoseFlagsSize(_jointData.size()) : 0); + (packetStateFlags & AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO ? AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getBlendshapeCoefficients().size()) : 0) + + (packetStateFlags & AvatarDataPacket::PACKET_HAS_JOINT_DATA ? AvatarDataPacket::maxJointDataSize(_jointData.size(), true) : 0) + + (packetStateFlags & AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS ? AvatarDataPacket::maxJointDefaultPoseFlagsSize(_jointData.size()) : 0); + + if (maxDataSize == 0) { + maxDataSize = (int)byteArraySize; + } QByteArray avatarDataByteArray((int)byteArraySize, 0); unsigned char* destinationBuffer = reinterpret_cast(avatarDataByteArray.data()); unsigned char* startPosition = destinationBuffer; - // Leading flags, to indicate how much data is actually included in the packet... - AvatarDataPacket::HasFlags packetStateFlags = - (hasAvatarGlobalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_GLOBAL_POSITION : 0) - | (hasAvatarBoundingBox ? AvatarDataPacket::PACKET_HAS_AVATAR_BOUNDING_BOX : 0) - | (hasAvatarOrientation ? AvatarDataPacket::PACKET_HAS_AVATAR_ORIENTATION : 0) - | (hasAvatarScale ? AvatarDataPacket::PACKET_HAS_AVATAR_SCALE : 0) - | (hasLookAtPosition ? AvatarDataPacket::PACKET_HAS_LOOK_AT_POSITION : 0) - | (hasAudioLoudness ? AvatarDataPacket::PACKET_HAS_AUDIO_LOUDNESS : 0) - | (hasSensorToWorldMatrix ? AvatarDataPacket::PACKET_HAS_SENSOR_TO_WORLD_MATRIX : 0) - | (hasAdditionalFlags ? AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS : 0) - | (hasParentInfo ? AvatarDataPacket::PACKET_HAS_PARENT_INFO : 0) - | (hasAvatarLocalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_LOCAL_POSITION : 0) - | (hasFaceTrackerInfo ? AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO : 0) - | (hasJointData ? AvatarDataPacket::PACKET_HAS_JOINT_DATA : 0) - | (hasJointDefaultPoseFlags ? AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS : 0) - | (hasGrabJoints ? AvatarDataPacket::PACKET_HAS_GRAB_JOINTS : 0); - - memcpy(destinationBuffer, &packetStateFlags, sizeof(packetStateFlags)); + unsigned char * packetFlagsLocation = destinationBuffer; destinationBuffer += sizeof(packetStateFlags); + if (itemFlags == 0) { + itemFlags = packetStateFlags; + } + + AvatarDataPacket::HasFlags includedFlags = 0; + + const unsigned char * packetEnd = destinationBuffer + maxDataSize; + #define AVATAR_MEMCPY(src) \ memcpy(destinationBuffer, &(src), sizeof(src)); \ destinationBuffer += sizeof(src); - if (hasAvatarGlobalPosition) { +// If we want an item and there's sufficient space: +#define IF_AVATAR_SPACE(flag, space) \ + if ((itemFlags & AvatarDataPacket::flag) && (packetEnd - destinationBuffer) >= (space) \ + && (includedFlags |= AvatarDataPacket::flag)) + + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_GLOBAL_POSITION, sizeof _globalPosition) { auto startSection = destinationBuffer; AVATAR_MEMCPY(_globalPosition); @@ -378,7 +403,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } - if (hasAvatarBoundingBox) { + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_BOUNDING_BOX, sizeof _globalBoundingBoxDimensions + sizeof _globalBoundingBoxOffset) { auto startSection = destinationBuffer; AVATAR_MEMCPY(_globalBoundingBoxDimensions); AVATAR_MEMCPY(_globalBoundingBoxOffset); @@ -389,7 +414,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } - if (hasAvatarOrientation) { + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_ORIENTATION, 6) { auto startSection = destinationBuffer; auto localOrientation = getOrientationOutbound(); destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, localOrientation); @@ -400,7 +425,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } - if (hasAvatarScale) { + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_SCALE, sizeof(AvatarDataPacket::AvatarScale)) { auto startSection = destinationBuffer; auto data = reinterpret_cast(destinationBuffer); auto scale = getDomainLimitedScale(); @@ -413,7 +438,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } - if (hasLookAtPosition) { + IF_AVATAR_SPACE(PACKET_HAS_LOOK_AT_POSITION, sizeof(_headData->getLookAtPosition()) ) { auto startSection = destinationBuffer; AVATAR_MEMCPY(_headData->getLookAtPosition()); int numBytes = destinationBuffer - startSection; @@ -422,7 +447,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } - if (hasAudioLoudness) { + IF_AVATAR_SPACE(PACKET_HAS_AUDIO_LOUDNESS, sizeof(AvatarDataPacket::AudioLoudness)) { auto startSection = destinationBuffer; auto data = reinterpret_cast(destinationBuffer); data->audioLoudness = packFloatGainToByte(getAudioLoudness() / AUDIO_LOUDNESS_SCALE); @@ -434,7 +459,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } - if (hasSensorToWorldMatrix) { + IF_AVATAR_SPACE(PACKET_HAS_SENSOR_TO_WORLD_MATRIX, sizeof(AvatarDataPacket::SensorToWorldMatrix)) { auto startSection = destinationBuffer; auto data = reinterpret_cast(destinationBuffer); glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix(); @@ -452,7 +477,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } - if (hasAdditionalFlags) { + IF_AVATAR_SPACE(PACKET_HAS_ADDITIONAL_FLAGS, sizeof (uint16_t)) { auto startSection = destinationBuffer; auto data = reinterpret_cast(destinationBuffer); @@ -500,7 +525,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } - if (hasParentInfo) { + IF_AVATAR_SPACE(PACKET_HAS_PARENT_INFO, sizeof(AvatarDataPacket::ParentInfo)) { auto startSection = destinationBuffer; auto parentInfo = reinterpret_cast(destinationBuffer); QByteArray referentialAsBytes = parentID.toRfc4122(); @@ -514,7 +539,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } - if (hasAvatarLocalPosition) { + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_LOCAL_POSITION, sizeof(getLocalPosition()) ) { auto startSection = destinationBuffer; const auto localPosition = getLocalPosition(); AVATAR_MEMCPY(localPosition); @@ -525,11 +550,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } + const auto& blendshapeCoefficients = _headData->getBlendshapeCoefficients(); // If it is connected, pack up the data - if (hasFaceTrackerInfo) { + IF_AVATAR_SPACE(PACKET_HAS_FACE_TRACKER_INFO, blendshapeCoefficients.size() * (int)sizeof(float)) { auto startSection = destinationBuffer; auto faceTrackerInfo = reinterpret_cast(destinationBuffer); - const auto& blendshapeCoefficients = _headData->getBlendshapeCoefficients(); // note: we don't use the blink and average loudness, we just use the numBlendShapes and // compute the procedural info on the client side. faceTrackerInfo->leftEyeBlink = _headData->_leftEyeBlink; @@ -553,26 +578,26 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent QReadLocker readLock(&_jointDataLock); jointData = _jointData; } + const int numJoints = jointData.size(); + const int jointBitVectorSize = calcBitVectorSize(numJoints); // If it is connected, pack up the data - if (hasJointData) { + if (packetStateFlags & AvatarDataPacket::PACKET_HAS_JOINT_DATA) { auto startSection = destinationBuffer; // joint rotation data - int numJoints = jointData.size(); *destinationBuffer++ = (uint8_t)numJoints; unsigned char* validityPosition = destinationBuffer; unsigned char validity = 0; int validityBit = 0; - int numValidityBytes = calcBitVectorSize(numJoints); #ifdef WANT_DEBUG int rotationSentCount = 0; unsigned char* beforeRotations = destinationBuffer; #endif - destinationBuffer += numValidityBytes; // Move pointer past the validity bytes + destinationBuffer += jointBitVectorSize; // Move pointer past the validity bytes // sentJointDataOut and lastSentJointData might be the same vector if (sentJointDataOut) { @@ -625,7 +650,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent unsigned char* beforeTranslations = destinationBuffer; #endif - destinationBuffer += numValidityBytes; // Move pointer past the validity bytes + destinationBuffer += jointBitVectorSize; // Move pointer past the validity bytes float minTranslation = (distanceAdjust && cullSmallChanges) ? getDistanceBasedMinTranslationDistance(viewerPosition) : AVATAR_MIN_TRANSLATION; @@ -718,6 +743,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent if (outboundDataRateOut) { outboundDataRateOut->farGrabJointRate.increment(numBytes); } + + includedFlags |= AvatarDataPacket::PACKET_HAS_GRAB_JOINTS; } #ifdef WANT_DEBUG @@ -738,13 +765,13 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent outboundDataRateOut->jointDataRate.increment(numBytes); } + includedFlags |= AvatarDataPacket::PACKET_HAS_JOINT_DATA; } - - if (hasJointDefaultPoseFlags) { + + IF_AVATAR_SPACE(PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS, 1 + 2 * jointBitVectorSize) { auto startSection = destinationBuffer; // write numJoints - int numJoints = jointData.size(); *destinationBuffer++ = (uint8_t)numJoints; // write rotationIsDefaultPose bits @@ -763,6 +790,10 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } + memcpy(packetFlagsLocation, &includedFlags, sizeof(includedFlags)); + // Return dropped items. + itemFlags = packetStateFlags & ~includedFlags; + int avatarDataSize = destinationBuffer - startPosition; if (avatarDataSize > (int)byteArraySize) { @@ -771,6 +802,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } return avatarDataByteArray.left(avatarDataSize); + +#undef AVATAR_MEMCPY +#undef IF_AVATAR_SPACE } // NOTE: This is never used in a "distanceAdjust" mode, so it's ok that it doesn't use a variable minimum rotation/translation diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 056e745370..057b351670 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -450,7 +450,7 @@ public: virtual QByteArray toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, - QVector* sentJointDataOut, AvatarDataRate* outboundDataRateOut = nullptr) const; + QVector* sentJointDataOut, int maxDataSize = 0, AvatarDataRate* outboundDataRateOut = nullptr) const; virtual void doneEncoding(bool cullSmallChanges); From ff0d938d3793e80d9e5c699115981ea96af453ba Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 17 Sep 2018 16:53:15 -0700 Subject: [PATCH 008/362] initial changes to stop the squatty potty from happening when users are sitting down in vr mode --- interface/resources/qml/AnimStats.qml | 2 +- interface/src/avatar/MyAvatar.cpp | 84 +++++++++++++++++++++--- interface/src/avatar/MyAvatar.h | 6 +- interface/src/avatar/MySkeletonModel.cpp | 2 +- 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/interface/resources/qml/AnimStats.qml b/interface/resources/qml/AnimStats.qml index 35ed3799a6..fd0a280dad 100644 --- a/interface/resources/qml/AnimStats.qml +++ b/interface/resources/qml/AnimStats.qml @@ -53,7 +53,7 @@ Item { ListView { width: firstCol.width height: root.animStateMachines.length * 15 - visible: root.animStateMchines.length > 0; + visible: root.animStateMachines.length > 0; model: root.animStateMachines delegate: StatText { text: { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c47cfdb383..b626fb6c2a 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3589,9 +3589,11 @@ glm::vec3 MyAvatar::computeCounterBalance() { // if the height is higher than default hips, clamp to default hips counterBalancedCg.y = tposeHips.y + 0.05f; } else if (counterBalancedCg.y < sitSquatThreshold) { - //do a height reset + // do a height reset setResetMode(true); _follow.activate(FollowHelper::Vertical); + // disable cg behaviour in this case. + _isInSittingState = true; } return counterBalancedCg; } @@ -3832,6 +3834,10 @@ bool MyAvatar::getIsInWalkingState() const { return _isInWalkingState; } +bool MyAvatar::getIsInSittingState() const { + return _isInSittingState; +} + float MyAvatar::getWalkSpeed() const { return _walkSpeed.get() * _walkSpeedScalar; } @@ -3852,6 +3858,10 @@ void MyAvatar::setIsInWalkingState(bool isWalking) { _isInWalkingState = isWalking; } +void MyAvatar::setIsInSittingState(bool isSitting) { + _isInSittingState = isSitting; +} + void MyAvatar::setWalkSpeed(float value) { _walkSpeed.set(value); } @@ -4029,6 +4039,33 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, return fabs(lateralLeanAmount) > MAX_LATERAL_LEAN; } +bool MyAvatar::FollowHelper::shouldActivateHorizontalSitting(MyAvatar& myAvatar) const { + + // get the current readings + controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD); + controller::Pose currentLeftHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND); + controller::Pose currentRightHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND); + + bool stepDetected = false; + float myScale = myAvatar.getAvatarScale(); + + if (!withinBaseOfSupport(currentHeadPose)) { + // a step is detected + stepDetected = true; + } else { + glm::vec3 defaultHipsPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); + glm::vec3 defaultHeadPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Head")); + glm::vec3 currentHeadPosition = currentHeadPose.getTranslation(); + float anatomicalHeadToHipsDistance = glm::length(defaultHeadPosition - defaultHipsPosition); + if (!isActive(Horizontal) && + (glm::length(currentHeadPosition - defaultHipsPosition) > (anatomicalHeadToHipsDistance + (DEFAULT_AVATAR_SPINE_STRETCH_LIMIT * anatomicalHeadToHipsDistance)))) { + myAvatar.setResetMode(true); + stepDetected = true; + } + } + return stepDetected; +} + bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) const { // get the current readings @@ -4072,33 +4109,60 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons return stepDetected; } -bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { +bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { const float CYLINDER_TOP = 0.1f; const float CYLINDER_BOTTOM = -1.5f; + const float SITTING_BOTTOM = -0.02f; glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); - return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + if (myAvatar.getIsInSittingState()) { + if (offset.y < SITTING_BOTTOM) { + // we recenter when sitting. + return true; + } else if (offset.y > CYLINDER_TOP) { + // if we recenter upwards then no longer in sitting state + myAvatar.setIsInSittingState(false); + return true; + } else { + return false; + } + } else { + return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + } } void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput) { - if (myAvatar.getHMDLeanRecenterEnabled() && - qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { + qCDebug(interfaceapp) << "in sitting state: " << myAvatar.getIsInSittingState(); + + if (myAvatar.getHMDLeanRecenterEnabled() && qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); } if (myAvatar.getCenterOfGravityModelEnabled()) { - if (!isActive(Horizontal) && (shouldActivateHorizontalCG(myAvatar) || hasDriveInput)) { - activate(Horizontal); - if (myAvatar.getEnableStepResetRotation()) { - activate(Rotation); - myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); + if (!(myAvatar.getIsInSittingState())) { + if (!isActive(Horizontal) && (shouldActivateHorizontalCG(myAvatar) || hasDriveInput)) { + activate(Horizontal); + if (myAvatar.getEnableStepResetRotation()) { + activate(Rotation); + myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); + } + } + } else { + // you are in the sitting state with cg model enabled + if (!isActive(Horizontal) && (shouldActivateHorizontalSitting(myAvatar) || hasDriveInput)) { + activate(Horizontal); + if (myAvatar.getEnableStepResetRotation()) { + activate(Rotation); + myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); + } } } } else { + // center of gravity model is not enabled if (!isActive(Horizontal) && (shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Horizontal); if (myAvatar.getEnableStepResetRotation()) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 139f1f6ea2..a7fdf964d0 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1094,6 +1094,8 @@ public: void setIsInWalkingState(bool isWalking); bool getIsInWalkingState() const; + void setIsInSittingState(bool isSitting); + bool getIsInSittingState() const; void setWalkSpeed(float value); float getWalkSpeed() const; void setWalkBackwardSpeed(float value); @@ -1708,9 +1710,10 @@ private: float getMaxTimeRemaining() const; void decrementTimeRemaining(float dt); bool shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; - bool shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; + bool shouldActivateVertical(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateHorizontalCG(MyAvatar& myAvatar) const; + bool shouldActivateHorizontalSitting(MyAvatar& myAvatar) const; void prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& bodySensorMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput); glm::mat4 postPhysicsUpdate(const MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix); bool getForceActivateRotation() const; @@ -1800,6 +1803,7 @@ private: ThreadSafeValueCache _sprintSpeed { AVATAR_SPRINT_SPEED_SCALAR }; float _walkSpeedScalar { AVATAR_WALK_SPEED_SCALAR }; bool _isInWalkingState { false }; + bool _isInSittingState{ false }; // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 3084542472..42ec582c47 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -46,7 +46,7 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { } glm::mat4 hipsMat; - if (myAvatar->getCenterOfGravityModelEnabled() && !isFlying && !(myAvatar->getIsInWalkingState())) { + if (myAvatar->getCenterOfGravityModelEnabled() && !isFlying && !(myAvatar->getIsInWalkingState()) && !(myAvatar->getIsInSittingState())) { // then we use center of gravity model hipsMat = myAvatar->deriveBodyUsingCgModel(); } else { From 14fb7e1d44f8e52fbc91f8810f48d78b776b3005 Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 17 Sep 2018 17:53:08 -0700 Subject: [PATCH 009/362] removed redundant code --- interface/src/avatar/MyAvatar.cpp | 52 ++++++------------------------- 1 file changed, 9 insertions(+), 43 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b626fb6c2a..d4168616c3 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -4039,33 +4039,6 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, return fabs(lateralLeanAmount) > MAX_LATERAL_LEAN; } -bool MyAvatar::FollowHelper::shouldActivateHorizontalSitting(MyAvatar& myAvatar) const { - - // get the current readings - controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD); - controller::Pose currentLeftHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND); - controller::Pose currentRightHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND); - - bool stepDetected = false; - float myScale = myAvatar.getAvatarScale(); - - if (!withinBaseOfSupport(currentHeadPose)) { - // a step is detected - stepDetected = true; - } else { - glm::vec3 defaultHipsPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); - glm::vec3 defaultHeadPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Head")); - glm::vec3 currentHeadPosition = currentHeadPose.getTranslation(); - float anatomicalHeadToHipsDistance = glm::length(defaultHeadPosition - defaultHipsPosition); - if (!isActive(Horizontal) && - (glm::length(currentHeadPosition - defaultHipsPosition) > (anatomicalHeadToHipsDistance + (DEFAULT_AVATAR_SPINE_STRETCH_LIMIT * anatomicalHeadToHipsDistance)))) { - myAvatar.setResetMode(true); - stepDetected = true; - } - } - return stepDetected; -} - bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) const { // get the current readings @@ -4078,6 +4051,10 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons if (myAvatar.getIsInWalkingState()) { stepDetected = true; + } else if (myAvatar.getIsInSittingState()) { + if (!withinBaseOfSupport(currentHeadPose)) { + stepDetected = true; + } } else { if (!withinBaseOfSupport(currentHeadPose) && headAngularVelocityBelowThreshold(currentHeadPose) && @@ -4143,22 +4120,11 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); } if (myAvatar.getCenterOfGravityModelEnabled()) { - if (!(myAvatar.getIsInSittingState())) { - if (!isActive(Horizontal) && (shouldActivateHorizontalCG(myAvatar) || hasDriveInput)) { - activate(Horizontal); - if (myAvatar.getEnableStepResetRotation()) { - activate(Rotation); - myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); - } - } - } else { - // you are in the sitting state with cg model enabled - if (!isActive(Horizontal) && (shouldActivateHorizontalSitting(myAvatar) || hasDriveInput)) { - activate(Horizontal); - if (myAvatar.getEnableStepResetRotation()) { - activate(Rotation); - myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); - } + if (!isActive(Horizontal) && (shouldActivateHorizontalCG(myAvatar) || hasDriveInput)) { + activate(Horizontal); + if (myAvatar.getEnableStepResetRotation()) { + activate(Rotation); + myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); } } } else { From 0c7dee730c60452f262ef507aa89a3d6609d6011 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 18 Sep 2018 10:25:59 -0700 Subject: [PATCH 010/362] Pack partial avatar data in bulk packets using space available Keep a single packet in-hand and send when full rather than a packet list. --- .../src/avatars/AvatarMixerSlave.cpp | 60 +++++++++++----- libraries/avatars/src/AvatarData.cpp | 72 ++++++++++--------- 2 files changed, 79 insertions(+), 53 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 20d85a3d4d..c6d8c7f495 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -279,10 +279,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) int minimumBytesPerAvatar = PALIsOpen ? AvatarDataPacket::AVATAR_HAS_FLAGS_SIZE + NUM_BYTES_RFC4122_UUID + sizeof(AvatarDataPacket::AvatarGlobalPosition) + sizeof(AvatarDataPacket::AudioLoudness) : 0; - // setup a PacketList for the avatarPackets - auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData); - static auto maxAvatarDataBytes = avatarPacketList->getMaxSegmentSize() - NUM_BYTES_RFC4122_UUID; - // compute node bounding box const float MY_AVATAR_BUBBLE_EXPANSION_FACTOR = 4.0f; // magic number determined emperically AABox nodeBox = computeBubbleBox(avatar, MY_AVATAR_BUBBLE_EXPANSION_FACTOR); @@ -401,6 +397,11 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) int remainingAvatars = (int)sortedAvatars.size(); auto traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true); + auto avatarPacket = NLPacket::create(PacketType::BulkAvatarData); + const int avatarPacketCapacity = avatarPacket->getPayloadCapacity(); + int avatarSpaceAvailable = avatarPacketCapacity; + //avatarSpaceAvailable = 100; + int numPacketsSent = 0; const auto& sortedAvatarVector = sortedAvatars.getSortedVector(numToSendEst); for (const auto& sortedAvatar : sortedAvatarVector) { @@ -460,17 +461,40 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) bool distanceAdjust = true; glm::vec3 viewerPosition = myPosition; - AvatarDataPacket::HasFlags hasFlagsOut = 0; // the result of the toByteArray + AvatarDataPacket::HasFlags includeFlags = 0; // the result of the toByteArray bool dropFaceTracking = false; - auto startSerialize = chrono::high_resolution_clock::now(); - QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, - hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, - &lastSentJointsForOther, maxAvatarDataBytes); - auto endSerialize = chrono::high_resolution_clock::now(); - _stats.toByteArrayElapsedTime += - (quint64) chrono::duration_cast(endSerialize - startSerialize).count(); + do { + auto startSerialize = chrono::high_resolution_clock::now(); + QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, + includeFlags, dropFaceTracking, distanceAdjust, viewerPosition, + &lastSentJointsForOther, avatarSpaceAvailable); + auto endSerialize = chrono::high_resolution_clock::now(); + _stats.toByteArrayElapsedTime += + (quint64)chrono::duration_cast(endSerialize - startSerialize).count(); + avatarPacket->write(bytes); + avatarSpaceAvailable -= bytes.size(); + if (includeFlags != 0) { + // Weren't able to fit everything. + nodeList->sendPacket(std::move(avatarPacket), *destinationNode); + ++numPacketsSent; + avatarPacket = NLPacket::create(PacketType::BulkAvatarData); + avatarSpaceAvailable = avatarPacketCapacity; + } + } while (includeFlags != 0); + if (detail != AvatarData::NoData) { + _stats.numOthersIncluded++; + + // increment the number of avatars sent to this reciever + nodeData->incrementNumAvatarsSentLastFrame(); + + // set the last sent sequence number for this sender on the receiver + nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(), + otherNodeData->getLastReceivedSequenceNumber()); + nodeData->setLastOtherAvatarEncodeTime(otherNode->getUUID(), usecTimestampNow()); + } +#if 0 if (bytes.size() > maxAvatarDataBytes) { qCWarning(avatars) << "otherAvatar.toByteArray() for" << otherNode->getUUID() << "resulted in very large buffer of" << bytes.size() << "bytes - dropping facial data"; @@ -517,6 +541,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // TODO? this avatar is not included now, and will probably not be included next frame. // It would be nice if we could tweak its future sort priority to put it at the back of the list. } +#endif auto endAvatarDataPacking = chrono::high_resolution_clock::now(); _stats.avatarDataPackingElapsedTime += @@ -534,15 +559,14 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) quint64 startPacketSending = usecTimestampNow(); - // close the current packet so that we're always sending something - avatarPacketList->closeCurrentPacket(true); + if (avatarPacket->getPayloadSize() != 0) { + nodeList->sendPacket(std::move(avatarPacket), *destinationNode); + ++numPacketsSent; + } - _stats.numPacketsSent += (int)avatarPacketList->getNumPackets(); + _stats.numPacketsSent += numPacketsSent; _stats.numBytesSent += numAvatarDataBytes; - // send the avatar data PacketList - nodeList->sendPacketList(std::move(avatarPacketList), *destinationNode); - // record the bytes sent for other avatar data in the AvatarMixerClientData nodeData->recordSentAvatarData(numAvatarDataBytes); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index fdd5317bce..6e5dc3fc6a 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -65,7 +65,7 @@ size_t AvatarDataPacket::maxFaceTrackerInfoSize(size_t numBlendshapeCoefficients } size_t AvatarDataPacket::maxJointDataSize(size_t numJoints, bool hasGrabJoints) { - const size_t validityBitsSize = (size_t)std::ceil(numJoints / (float)BITS_IN_BYTE); + const size_t validityBitsSize = (size_t) calcBitVectorSize(numJoints); size_t totalSize = sizeof(uint8_t); // numJoints @@ -270,10 +270,6 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent auto parentID = getParentID(); - bool hasJointData = false; - bool hasJointDefaultPoseFlags = false; - bool hasGrabJoints = false; - glm::mat4 leftFarGrabMatrix; glm::mat4 rightFarGrabMatrix; glm::mat4 mouseFarGrabMatrix; @@ -289,6 +285,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent bool hasLookAtPosition = false; bool hasAudioLoudness = false; bool hasSensorToWorldMatrix = false; + bool hasJointData = false; + bool hasJointDefaultPoseFlags = false; + bool hasGrabJoints = false; bool hasAdditionalFlags = false; // local position, and parent info only apply to avatars that are parented. The local position @@ -354,13 +353,13 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent | (hasJointDefaultPoseFlags ? AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS : 0) | (hasGrabJoints ? AvatarDataPacket::PACKET_HAS_GRAB_JOINTS : 0); + itemFlags = packetStateFlags; } else { packetStateFlags = itemFlags; } - - const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE + - (packetStateFlags & AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO ? AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getBlendshapeCoefficients().size()) : 0) + + const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE + NUM_BYTES_RFC4122_UUID + + AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getBlendshapeCoefficients().size()) + (packetStateFlags & AvatarDataPacket::PACKET_HAS_JOINT_DATA ? AvatarDataPacket::maxJointDataSize(_jointData.size(), true) : 0) + (packetStateFlags & AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS ? AvatarDataPacket::maxJointDefaultPoseFlagsSize(_jointData.size()) : 0); @@ -371,25 +370,27 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent QByteArray avatarDataByteArray((int)byteArraySize, 0); unsigned char* destinationBuffer = reinterpret_cast(avatarDataByteArray.data()); unsigned char* startPosition = destinationBuffer; + const unsigned char * packetEnd = destinationBuffer + maxDataSize; + + AvatarDataPacket::HasFlags includedFlags = 0; + + // Packets always have UUID. + memcpy(destinationBuffer, getSessionUUID().toRfc4122(), NUM_BYTES_RFC4122_UUID); + destinationBuffer += NUM_BYTES_RFC4122_UUID; unsigned char * packetFlagsLocation = destinationBuffer; destinationBuffer += sizeof(packetStateFlags); if (itemFlags == 0) { - itemFlags = packetStateFlags; } - AvatarDataPacket::HasFlags includedFlags = 0; - - const unsigned char * packetEnd = destinationBuffer + maxDataSize; - #define AVATAR_MEMCPY(src) \ memcpy(destinationBuffer, &(src), sizeof(src)); \ destinationBuffer += sizeof(src); // If we want an item and there's sufficient space: -#define IF_AVATAR_SPACE(flag, space) \ - if ((itemFlags & AvatarDataPacket::flag) && (packetEnd - destinationBuffer) >= (space) \ +#define IF_AVATAR_SPACE(flag, space) \ + if ((itemFlags & AvatarDataPacket::flag) && (int)(packetEnd - destinationBuffer) >= (space) \ && (includedFlags |= AvatarDataPacket::flag)) IF_AVATAR_SPACE(PACKET_HAS_AVATAR_GLOBAL_POSITION, sizeof _globalPosition) { @@ -414,7 +415,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } - IF_AVATAR_SPACE(PACKET_HAS_AVATAR_ORIENTATION, 6) { + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_ORIENTATION, sizeof(AvatarDataPacket::SixByteQuat)) { auto startSection = destinationBuffer; auto localOrientation = getOrientationOutbound(); destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, localOrientation); @@ -552,7 +553,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent const auto& blendshapeCoefficients = _headData->getBlendshapeCoefficients(); // If it is connected, pack up the data - IF_AVATAR_SPACE(PACKET_HAS_FACE_TRACKER_INFO, blendshapeCoefficients.size() * (int)sizeof(float)) { + IF_AVATAR_SPACE(PACKET_HAS_FACE_TRACKER_INFO, sizeof(AvatarDataPacket::FaceTrackerInfo) + blendshapeCoefficients.size() * sizeof(float)) { auto startSection = destinationBuffer; auto faceTrackerInfo = reinterpret_cast(destinationBuffer); // note: we don't use the blink and average loudness, we just use the numBlendShapes and @@ -574,15 +575,16 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } QVector jointData; - if (hasJointData || hasJointDefaultPoseFlags) { + if (packetStateFlags & (AvatarDataPacket::PACKET_HAS_JOINT_DATA | AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS)) { QReadLocker readLock(&_jointDataLock); jointData = _jointData; } const int numJoints = jointData.size(); const int jointBitVectorSize = calcBitVectorSize(numJoints); - // If it is connected, pack up the data - if (packetStateFlags & AvatarDataPacket::PACKET_HAS_JOINT_DATA) { + // Check against full size or minimum size: count + two bit-vectors + two controllers + const int approxJointSpace = sendAll ? AvatarDataPacket::maxJointDataSize(numJoints, true) : 1 + 2 * jointBitVectorSize + 2 * (6 + 2); + IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, approxJointSpace) { auto startSection = destinationBuffer; // joint rotation data @@ -611,8 +613,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent const JointData& last = lastSentJointData[i]; if (!data.rotationIsDefaultPose) { - // The dot product for larger rotations is a lower number. - // So if the dot() is less than the value, then the rotation is a larger angle of rotation + // The dot product for larger rotations is a lower number, + // so if the dot() is less than the value, then the rotation is a larger angle of rotation if (sendAll || last.rotationIsDefaultPose || (!cullSmallChanges && last.rotation != data.rotation) || (cullSmallChanges && glm::dot(last.rotation, data.rotation) < minRotationDOT) ) { validity |= (1 << validityBit); @@ -705,7 +707,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerRightHandTransform.getTranslation(), TRANSLATION_COMPRESSION_RADIX); - if (hasGrabJoints) { + int numGrabJointBytes = 0; + IF_AVATAR_SPACE(PACKET_HAS_GRAB_JOINTS, sizeof (AvatarDataPacket::FarGrabJoints)) { // the far-grab joints may range further than 3 meters, so we can't use packFloatVec3ToSignedTwoByteFixed etc auto startSection = destinationBuffer; auto data = reinterpret_cast(destinationBuffer); @@ -738,13 +741,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent data->mouseFarGrabRotation[3] = mouseFarGrabRotation.z; destinationBuffer += sizeof(data->mouseFarGrabRotation); - int numBytes = destinationBuffer - startSection; - - if (outboundDataRateOut) { - outboundDataRateOut->farGrabJointRate.increment(numBytes); - } - - includedFlags |= AvatarDataPacket::PACKET_HAS_GRAB_JOINTS; + int numGrabJointBytes = destinationBuffer - startSection; } #ifdef WANT_DEBUG @@ -760,12 +757,17 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } #endif - int numBytes = destinationBuffer - startSection; - if (outboundDataRateOut) { - outboundDataRateOut->jointDataRate.increment(numBytes); + if (destinationBuffer >= packetEnd) { + // Joint data too large - revert + destinationBuffer = startSection; + includedFlags &= ~(AvatarDataPacket::PACKET_HAS_GRAB_JOINTS | AvatarDataPacket::PACKET_HAS_JOINT_DATA); + } else { + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->jointDataRate.increment(numBytes); + outboundDataRateOut->farGrabJointRate.increment(numGrabJointBytes); + } } - - includedFlags |= AvatarDataPacket::PACKET_HAS_JOINT_DATA; } IF_AVATAR_SPACE(PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS, 1 + 2 * jointBitVectorSize) { From 96da9b7e54ddaec7e976527059e878d0e3e27b89 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 18 Sep 2018 10:31:19 -0700 Subject: [PATCH 011/362] cleaned up code for squatty fix --- interface/resources/qml/AnimStats.qml | 2 +- interface/src/avatar/MyAvatar.cpp | 6 +++--- interface/src/avatar/MyAvatar.h | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/AnimStats.qml b/interface/resources/qml/AnimStats.qml index fd0a280dad..35ed3799a6 100644 --- a/interface/resources/qml/AnimStats.qml +++ b/interface/resources/qml/AnimStats.qml @@ -53,7 +53,7 @@ Item { ListView { width: firstCol.width height: root.animStateMachines.length * 15 - visible: root.animStateMachines.length > 0; + visible: root.animStateMchines.length > 0; model: root.animStateMachines delegate: StatText { text: { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index d4168616c3..8eceb19e09 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -4074,6 +4074,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons glm::vec3 currentHeadPosition = currentHeadPose.getTranslation(); float anatomicalHeadToHipsDistance = glm::length(defaultHeadPosition - defaultHipsPosition); if (!isActive(Horizontal) && + (!isActive(Vertical)) && (glm::length(currentHeadPosition - defaultHipsPosition) > (anatomicalHeadToHipsDistance + (DEFAULT_AVATAR_SPINE_STRETCH_LIMIT * anatomicalHeadToHipsDistance)))) { myAvatar.setResetMode(true); stepDetected = true; @@ -4112,9 +4113,8 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput) { - qCDebug(interfaceapp) << "in sitting state: " << myAvatar.getIsInSittingState(); - - if (myAvatar.getHMDLeanRecenterEnabled() && qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { + if (myAvatar.getHMDLeanRecenterEnabled() && + qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index a7fdf964d0..6f3858c5bf 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1713,7 +1713,6 @@ private: bool shouldActivateVertical(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateHorizontalCG(MyAvatar& myAvatar) const; - bool shouldActivateHorizontalSitting(MyAvatar& myAvatar) const; void prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& bodySensorMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput); glm::mat4 postPhysicsUpdate(const MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix); bool getForceActivateRotation() const; @@ -1803,7 +1802,7 @@ private: ThreadSafeValueCache _sprintSpeed { AVATAR_SPRINT_SPEED_SCALAR }; float _walkSpeedScalar { AVATAR_WALK_SPEED_SCALAR }; bool _isInWalkingState { false }; - bool _isInSittingState{ false }; + bool _isInSittingState { false }; // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; From 42e248ef84ca8207ca92885f04768935e8457062 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 18 Sep 2018 17:37:44 -0700 Subject: [PATCH 012/362] maded more changes to support sitting state to do: add option for stuck pose reset --- interface/src/avatar/MyAvatar.cpp | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 8eceb19e09..bf684ccd9a 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -453,6 +453,16 @@ void MyAvatar::update(float deltaTime) { float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); + // qCDebug(interfaceapp) << "sitting state is " << getIsInSittingState(); + // debug setting for sitting state if you start in the chair. + // if the head is close to the floor in sensor space + // and the up of the head is close to sensor up then try switching us to sitting state. + auto sensorHeadPoseDebug = getControllerPoseInSensorFrame(controller::Action::HEAD); + glm::vec3 upHead = transformVectorFast(sensorHeadPoseDebug.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); + float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); + if ((acosHead > 0.98f) && !getIsInSittingState() && (sensorHeadPoseDebug.getTranslation().y < -0.5f)) { + qCDebug(interfaceapp) << "we are going to sitting state because it looks like we should" << sensorHeadPoseDebug.getTranslation().y; + } // put the average hand azimuth into sensor space. // then mix it with head facing direction to determine rotation recenter if (getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid()) { @@ -3593,7 +3603,7 @@ glm::vec3 MyAvatar::computeCounterBalance() { setResetMode(true); _follow.activate(FollowHelper::Vertical); // disable cg behaviour in this case. - _isInSittingState = true; + setIsInSittingState(true); } return counterBalancedCg; } @@ -4049,6 +4059,12 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons bool stepDetected = false; float myScale = myAvatar.getAvatarScale(); + + // debug head hips angle + //glm::vec3 hipsPos = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); + //glm::vec3 headHipsBody = currentHeadPose.getTranslation() - hipsPos; + //qCDebug(interfaceapp) << "head in sensor space " << withinBaseOfSupport(currentHeadPose); + if (myAvatar.getIsInWalkingState()) { stepDetected = true; } else if (myAvatar.getIsInSittingState()) { @@ -4094,6 +4110,12 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); + auto sensorHeadPose = myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD); + glm::vec3 headWorldSpace = myAvatar.getHead()->getPosition(); + glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar.getSensorToWorldMatrix()); + glm::vec3 headSensorSpace = transformVectorFast(myAvatar.getSensorToWorldMatrix(), headWorldSpace); + //qCDebug(interfaceapp) << "sensor space position " << extractTranslation(currentBodyMatrix) << " head position sensor " << sensorHeadPose.getTranslation(); + if (myAvatar.getIsInSittingState()) { if (offset.y < SITTING_BOTTOM) { // we recenter when sitting. From 5d91396e91320b354fc24b5e1be30cd2ade97755 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 18 Sep 2018 17:49:23 -0700 Subject: [PATCH 013/362] Fixes for client & other clean-up Handle grab-joints better; fix packet size calc; remove dead code; other improvements. --- .../src/avatars/AvatarMixerSlave.cpp | 60 ++------------ libraries/avatars/src/AvatarData.cpp | 82 +++++++++++-------- 2 files changed, 52 insertions(+), 90 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index c6d8c7f495..2aa8de6cf4 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -261,7 +261,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // max number of avatarBytes per frame int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND); - // keep track of the number of other avatars held back in this frame int numAvatarsHeldBack = 0; @@ -457,23 +456,22 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) bool includeThisAvatar = true; QVector& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID()); - lastSentJointsForOther.resize(otherAvatar->getJointCount()); - - bool distanceAdjust = true; - glm::vec3 viewerPosition = myPosition; + const bool distanceAdjust = true; + const bool dropFaceTracking = false; AvatarDataPacket::HasFlags includeFlags = 0; // the result of the toByteArray - bool dropFaceTracking = false; do { auto startSerialize = chrono::high_resolution_clock::now(); QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, - includeFlags, dropFaceTracking, distanceAdjust, viewerPosition, + includeFlags, dropFaceTracking, distanceAdjust, myPosition, &lastSentJointsForOther, avatarSpaceAvailable); auto endSerialize = chrono::high_resolution_clock::now(); _stats.toByteArrayElapsedTime += (quint64)chrono::duration_cast(endSerialize - startSerialize).count(); + avatarPacket->write(bytes); avatarSpaceAvailable -= bytes.size(); + numAvatarDataBytes += bytes.size(); if (includeFlags != 0) { // Weren't able to fit everything. nodeList->sendPacket(std::move(avatarPacket), *destinationNode); @@ -494,54 +492,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) otherNodeData->getLastReceivedSequenceNumber()); nodeData->setLastOtherAvatarEncodeTime(otherNode->getUUID(), usecTimestampNow()); } -#if 0 - if (bytes.size() > maxAvatarDataBytes) { - qCWarning(avatars) << "otherAvatar.toByteArray() for" << otherNode->getUUID() - << "resulted in very large buffer of" << bytes.size() << "bytes - dropping facial data"; - - dropFaceTracking = true; // first try dropping the facial data - bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, - hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther, maxAvatarDataBytes); - - if (bytes.size() > maxAvatarDataBytes) { - qCWarning(avatars) << "otherAvatar.toByteArray() for" << otherNode->getUUID() - << "without facial data resulted in very large buffer of" << bytes.size() - << "bytes - reducing to MinimumData"; - bytes = otherAvatar->toByteArray(AvatarData::MinimumData, lastEncodeForOther, lastSentJointsForOther, - hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther, maxAvatarDataBytes); - - if (bytes.size() > maxAvatarDataBytes, maxAvatarDataBytes) { - qCWarning(avatars) << "otherAvatar.toByteArray() for" << otherNode->getUUID() - << "MinimumData resulted in very large buffer of" << bytes.size() - << "bytes - refusing to send avatar"; - includeThisAvatar = false; - } - } - } - - if (includeThisAvatar) { - // start a new segment in the PacketList for this avatar - avatarPacketList->startSegment(); - numAvatarDataBytes += avatarPacketList->write(otherNode->getUUID().toRfc4122()); - numAvatarDataBytes += avatarPacketList->write(bytes); - avatarPacketList->endSegment(); - - if (detail != AvatarData::NoData) { - _stats.numOthersIncluded++; - - // increment the number of avatars sent to this reciever - nodeData->incrementNumAvatarsSentLastFrame(); - - // set the last sent sequence number for this sender on the receiver - nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(), - otherNodeData->getLastReceivedSequenceNumber()); - nodeData->setLastOtherAvatarEncodeTime(otherNode->getUUID(), usecTimestampNow()); - } - } else { - // TODO? this avatar is not included now, and will probably not be included next frame. - // It would be nice if we could tweak its future sort priority to put it at the back of the list. - } -#endif auto endAvatarDataPacking = chrono::high_resolution_clock::now(); _stats.avatarDataPackingElapsedTime += diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 6e5dc3fc6a..2b46eb9cdd 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -65,7 +65,7 @@ size_t AvatarDataPacket::maxFaceTrackerInfoSize(size_t numBlendshapeCoefficients } size_t AvatarDataPacket::maxJointDataSize(size_t numJoints, bool hasGrabJoints) { - const size_t validityBitsSize = (size_t) calcBitVectorSize(numJoints); + const size_t validityBitsSize = calcBitVectorSize((int)numJoints); size_t totalSize = sizeof(uint8_t); // numJoints @@ -74,9 +74,9 @@ size_t AvatarDataPacket::maxJointDataSize(size_t numJoints, bool hasGrabJoints) totalSize += validityBitsSize; // Translations mask totalSize += numJoints * sizeof(SixByteTrans); // Translations - size_t NUM_FAUX_JOINT = 2; - size_t num_grab_joints = (hasGrabJoints ? 2 : 0); - totalSize += (NUM_FAUX_JOINT + num_grab_joints) * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); // faux joints + static const size_t NUM_FAUX_JOINT = 2; + totalSize += NUM_FAUX_JOINT * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); // faux joints + totalSize += hasGrabJoints ? sizeof(FarGrabJoints) : 0; return totalSize; } @@ -222,12 +222,14 @@ float AvatarData::getDistanceBasedMinTranslationDistance(glm::vec3 viewerPositio // we want to track outbound data in this case... QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) { - AvatarDataPacket::HasFlags hasFlagsOut; + AvatarDataPacket::HasFlags hasFlagsOut = 0; auto lastSentTime = _lastToByteArray; _lastToByteArray = usecTimestampNow(); - return AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(), + auto avatarByteArray = AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(), hasFlagsOut, dropFaceTracking, false, glm::vec3(0), nullptr, 0, &_outboundDataRate); + // Strip UUID + return avatarByteArray.right(avatarByteArray.size() - NUM_BYTES_RFC4122_UUID); } QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, @@ -245,7 +247,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // special case, if we were asked for no data, then just include the flags all set to nothing if (dataDetail == NoData) { AvatarDataPacket::HasFlags packetStateFlags = 0; - QByteArray avatarDataByteArray(reinterpret_cast(&packetStateFlags), sizeof(packetStateFlags)); + + QByteArray avatarDataByteArray(getSessionUUID().toRfc4122().data(), NUM_BYTES_RFC4122_UUID + sizeof(packetStateFlags)); + avatarDataByteArray.append((char*) &packetStateFlags, sizeof packetStateFlags); return avatarDataByteArray; } @@ -287,7 +291,6 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent bool hasSensorToWorldMatrix = false; bool hasJointData = false; bool hasJointDefaultPoseFlags = false; - bool hasGrabJoints = false; bool hasAdditionalFlags = false; // local position, and parent info only apply to avatars that are parented. The local position @@ -315,26 +318,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent hasFaceTrackerInfo = !dropFaceTracking && (hasFaceTracker() || getHasScriptedBlendshapes()) && (sendAll || faceTrackerInfoChangedSince(lastSentTime)); - hasJointData = sendAll || !sendMinimum; + hasJointData = !sendMinimum; hasJointDefaultPoseFlags = hasJointData; - if (hasJointData) { - bool leftValid; - leftFarGrabMatrix = _farGrabLeftMatrixCache.get(leftValid); - if (!leftValid) { - leftFarGrabMatrix = glm::mat4(); - } - bool rightValid; - rightFarGrabMatrix = _farGrabRightMatrixCache.get(rightValid); - if (!rightValid) { - rightFarGrabMatrix = glm::mat4(); - } - bool mouseValid; - mouseFarGrabMatrix = _farGrabMouseMatrixCache.get(mouseValid); - if (!mouseValid) { - mouseFarGrabMatrix = glm::mat4(); - } - hasGrabJoints = (leftValid || rightValid || mouseValid); - } } packetStateFlags = @@ -351,17 +336,45 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent | (hasFaceTrackerInfo ? AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO : 0) | (hasJointData ? AvatarDataPacket::PACKET_HAS_JOINT_DATA : 0) | (hasJointDefaultPoseFlags ? AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS : 0) - | (hasGrabJoints ? AvatarDataPacket::PACKET_HAS_GRAB_JOINTS : 0); + | (hasJointData ? AvatarDataPacket::PACKET_HAS_GRAB_JOINTS : 0); itemFlags = packetStateFlags; } else { packetStateFlags = itemFlags; + if (packetStateFlags & AvatarDataPacket::PACKET_HAS_JOINT_DATA) { + // Force all joints upon continuation, as deltas aren't valid. + sendAll = true; + } + if (packetStateFlags & AvatarDataPacket::PACKET_HAS_GRAB_JOINTS) { + packetStateFlags |= AvatarDataPacket::PACKET_HAS_JOINT_DATA; + } + } + + if (packetStateFlags & AvatarDataPacket::PACKET_HAS_GRAB_JOINTS) { + bool leftValid; + leftFarGrabMatrix = _farGrabLeftMatrixCache.get(leftValid); + if (!leftValid) { + leftFarGrabMatrix = glm::mat4(); + } + bool rightValid; + rightFarGrabMatrix = _farGrabRightMatrixCache.get(rightValid); + if (!rightValid) { + rightFarGrabMatrix = glm::mat4(); + } + bool mouseValid; + mouseFarGrabMatrix = _farGrabMouseMatrixCache.get(mouseValid); + if (!mouseValid) { + mouseFarGrabMatrix = glm::mat4(); + } + if (!(leftValid || rightValid || mouseValid)) { + packetStateFlags &= ~AvatarDataPacket::PACKET_HAS_GRAB_JOINTS; + } } const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE + NUM_BYTES_RFC4122_UUID + AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getBlendshapeCoefficients().size()) + - (packetStateFlags & AvatarDataPacket::PACKET_HAS_JOINT_DATA ? AvatarDataPacket::maxJointDataSize(_jointData.size(), true) : 0) + - (packetStateFlags & AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS ? AvatarDataPacket::maxJointDefaultPoseFlagsSize(_jointData.size()) : 0); + AvatarDataPacket::maxJointDataSize(_jointData.size(), true) + + AvatarDataPacket::maxJointDefaultPoseFlagsSize(_jointData.size()); if (maxDataSize == 0) { maxDataSize = (int)byteArraySize; @@ -381,16 +394,13 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent unsigned char * packetFlagsLocation = destinationBuffer; destinationBuffer += sizeof(packetStateFlags); - if (itemFlags == 0) { - } - #define AVATAR_MEMCPY(src) \ memcpy(destinationBuffer, &(src), sizeof(src)); \ destinationBuffer += sizeof(src); // If we want an item and there's sufficient space: #define IF_AVATAR_SPACE(flag, space) \ - if ((itemFlags & AvatarDataPacket::flag) && (int)(packetEnd - destinationBuffer) >= (space) \ + if ((packetStateFlags & AvatarDataPacket::flag) && (int)(packetEnd - destinationBuffer) >= (space) \ && (includedFlags |= AvatarDataPacket::flag)) IF_AVATAR_SPACE(PACKET_HAS_AVATAR_GLOBAL_POSITION, sizeof _globalPosition) { @@ -583,7 +593,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent const int jointBitVectorSize = calcBitVectorSize(numJoints); // Check against full size or minimum size: count + two bit-vectors + two controllers - const int approxJointSpace = sendAll ? AvatarDataPacket::maxJointDataSize(numJoints, true) : 1 + 2 * jointBitVectorSize + 2 * (6 + 2); + const int approxJointSpace = sendAll ? (int)AvatarDataPacket::maxJointDataSize(numJoints, true) : + 1 + 2 * jointBitVectorSize + 2 * (sizeof(AvatarDataPacket::SixByteQuat) + sizeof(AvatarDataPacket::SixByteTrans)); + IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, approxJointSpace) { auto startSection = destinationBuffer; From c88a713ef86e4dacf4d46f5a845fef0acc769cad Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 19 Sep 2018 12:05:06 -0700 Subject: [PATCH 014/362] Use Local ID for last time & sequence # Also try to fix size_t warnings on gcc --- assignment-client/src/avatars/AvatarMixer.cpp | 6 +++--- .../src/avatars/AvatarMixerClientData.cpp | 14 +++++++------- .../src/avatars/AvatarMixerClientData.h | 18 +++++++++--------- .../src/avatars/AvatarMixerSlave.cpp | 17 ++++++++--------- libraries/avatars/src/AvatarData.cpp | 8 ++++---- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 561afee296..ebd86b749b 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -541,7 +541,7 @@ void AvatarMixer::handleRequestsDomainListDataPacket(QSharedPointersetLastBroadcastTime(node->getUUID(), 0); + nodeData->setLastBroadcastTime(node->getLocalID(), 0); nodeData->resetSentTraitData(node->getLocalID()); } ); @@ -637,7 +637,7 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer // Reset the lastBroadcastTime for the ignored avatar to 0 // so the AvatarMixer knows it'll have to send identity data about the ignored avatar // to the ignorer if the ignorer unignores. - nodeData->setLastBroadcastTime(ignoredUUID, 0); + nodeData->setLastBroadcastTime(ignoredNode->getLocalID(), 0); nodeData->resetSentTraitData(ignoredNode->getLocalID()); } @@ -647,7 +647,7 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer // to the ignored if the ignorer unignores. AvatarMixerClientData* ignoredNodeData = reinterpret_cast(ignoredNode->getLinkedData()); if (ignoredNodeData) { - ignoredNodeData->setLastBroadcastTime(senderNode->getUUID(), 0); + ignoredNodeData->setLastBroadcastTime(senderNode->getLocalID(), 0); ignoredNodeData->resetSentTraitData(senderNode->getLocalID()); } } diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 6c01e6e02b..2226a4c7a6 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -200,7 +200,7 @@ void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedDa } } -uint64_t AvatarMixerClientData::getLastBroadcastTime(const QUuid& nodeUUID) const { +uint64_t AvatarMixerClientData::getLastBroadcastTime(NLPacket::LocalID nodeUUID) const { // return the matching PacketSequenceNumber, or the default if we don't have it auto nodeMatch = _lastBroadcastTimes.find(nodeUUID); if (nodeMatch != _lastBroadcastTimes.end()) { @@ -209,9 +209,9 @@ uint64_t AvatarMixerClientData::getLastBroadcastTime(const QUuid& nodeUUID) cons return 0; } -uint16_t AvatarMixerClientData::getLastBroadcastSequenceNumber(const QUuid& nodeUUID) const { +uint16_t AvatarMixerClientData::getLastBroadcastSequenceNumber(NLPacket::LocalID nodeID) const { // return the matching PacketSequenceNumber, or the default if we don't have it - auto nodeMatch = _lastBroadcastSequenceNumbers.find(nodeUUID); + auto nodeMatch = _lastBroadcastSequenceNumbers.find(nodeID); if (nodeMatch != _lastBroadcastSequenceNumbers.end()) { return nodeMatch->second; } @@ -232,7 +232,7 @@ void AvatarMixerClientData::ignoreOther(const Node* self, const Node* other) { } else { killPacket->writePrimitive(KillAvatarReason::YourAvatarEnteredTheirBubble); } - setLastBroadcastTime(other->getUUID(), 0); + setLastBroadcastTime(other->getLocalID(), 0); resetSentTraitData(other->getLocalID()); @@ -311,9 +311,9 @@ AvatarMixerClientData::TraitsCheckTimestamp AvatarMixerClientData::getLastOtherA } } -void AvatarMixerClientData::cleanupKilledNode(const QUuid& nodeUUID, Node::LocalID nodeLocalID) { - removeLastBroadcastSequenceNumber(nodeUUID); - removeLastBroadcastTime(nodeUUID); +void AvatarMixerClientData::cleanupKilledNode(const QUuid&, Node::LocalID nodeLocalID) { + removeLastBroadcastSequenceNumber(nodeLocalID); + removeLastBroadcastTime(nodeLocalID); _lastSentTraitsTimestamps.erase(nodeLocalID); _sentTraitVersions.erase(nodeLocalID); } diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index d38a90ef1f..ccb0f4001d 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -49,14 +49,14 @@ public: const AvatarData* getConstAvatarData() const { return _avatar.get(); } AvatarSharedPointer getAvatarSharedPointer() const { return _avatar; } - uint16_t getLastBroadcastSequenceNumber(const QUuid& nodeUUID) const; - void setLastBroadcastSequenceNumber(const QUuid& nodeUUID, uint16_t sequenceNumber) - { _lastBroadcastSequenceNumbers[nodeUUID] = sequenceNumber; } - Q_INVOKABLE void removeLastBroadcastSequenceNumber(const QUuid& nodeUUID) { _lastBroadcastSequenceNumbers.erase(nodeUUID); } + uint16_t getLastBroadcastSequenceNumber(NLPacket::LocalID nodeID) const; + void setLastBroadcastSequenceNumber(NLPacket::LocalID nodeID, uint16_t sequenceNumber) + { _lastBroadcastSequenceNumbers[nodeID] = sequenceNumber; } + Q_INVOKABLE void removeLastBroadcastSequenceNumber(NLPacket::LocalID nodeID) { _lastBroadcastSequenceNumbers.erase(nodeID); } - uint64_t getLastBroadcastTime(const QUuid& nodeUUID) const; - void setLastBroadcastTime(const QUuid& nodeUUID, uint64_t broadcastTime) { _lastBroadcastTimes[nodeUUID] = broadcastTime; } - Q_INVOKABLE void removeLastBroadcastTime(const QUuid& nodeUUID) { _lastBroadcastTimes.erase(nodeUUID); } + uint64_t getLastBroadcastTime(NLPacket::LocalID nodeUUID) const; + void setLastBroadcastTime(NLPacket::LocalID nodeUUID, uint64_t broadcastTime) { _lastBroadcastTimes[nodeUUID] = broadcastTime; } + Q_INVOKABLE void removeLastBroadcastTime(NLPacket::LocalID nodeUUID) { _lastBroadcastTimes.erase(nodeUUID); } Q_INVOKABLE void cleanupKilledNode(const QUuid& nodeUUID, Node::LocalID nodeLocalID); @@ -147,8 +147,8 @@ private: AvatarSharedPointer _avatar { new AvatarData() }; uint16_t _lastReceivedSequenceNumber { 0 }; - std::unordered_map _lastBroadcastSequenceNumbers; - std::unordered_map _lastBroadcastTimes; + std::unordered_map _lastBroadcastSequenceNumbers; + std::unordered_map _lastBroadcastTimes; // this is a map of the last time we encoded an "other" avatar for // sending to "this" node diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 2aa8de6cf4..ef10cb730a 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -359,7 +359,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) } if (!shouldIgnore) { - AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getUUID()); + AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getLocalID()); AvatarDataSequenceNumber lastSeqFromSender = avatarClientNodeData->getLastReceivedSequenceNumber(); // FIXME - This code does appear to be working. But it seems brittle. @@ -435,11 +435,11 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A. if (otherAvatar->hasProcessedFirstIdentity() - && nodeData->getLastBroadcastTime(otherNode->getUUID()) <= otherNodeData->getIdentityChangeTimestamp()) { + && nodeData->getLastBroadcastTime(otherNode->getLocalID()) <= otherNodeData->getIdentityChangeTimestamp()) { identityBytesSent += sendIdentityPacket(otherNodeData, node); // remember the last time we sent identity details about this other node to the receiver - nodeData->setLastBroadcastTime(otherNode->getUUID(), usecTimestampNow()); + nodeData->setLastBroadcastTime(otherNode->getLocalID(), usecTimestampNow()); } // Typically all out-of-view avatars but such avatars' priorities will rise with time: @@ -453,7 +453,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) nodeData->incrementAvatarInView(); } - bool includeThisAvatar = true; QVector& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID()); const bool distanceAdjust = true; @@ -484,11 +483,11 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) if (detail != AvatarData::NoData) { _stats.numOthersIncluded++; - // increment the number of avatars sent to this reciever + // increment the number of avatars sent to this receiver nodeData->incrementNumAvatarsSentLastFrame(); // set the last sent sequence number for this sender on the receiver - nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(), + nodeData->setLastBroadcastSequenceNumber(otherNode->getLocalID(), otherNodeData->getLastReceivedSequenceNumber()); nodeData->setLastOtherAvatarEncodeTime(otherNode->getUUID(), usecTimestampNow()); } @@ -582,11 +581,11 @@ void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePoin quint64 end = usecTimestampNow(); _stats.toByteArrayElapsedTime += (end - start); - auto lastBroadcastTime = nodeData->getLastBroadcastTime(agentNode->getUUID()); + auto lastBroadcastTime = nodeData->getLastBroadcastTime(agentNode->getLocalID()); if (lastBroadcastTime <= agentNodeData->getIdentityChangeTimestamp() || (start - lastBroadcastTime) >= REBROADCAST_IDENTITY_TO_DOWNSTREAM_EVERY_US) { sendReplicatedIdentityPacket(*agentNode, agentNodeData, *node); - nodeData->setLastBroadcastTime(agentNode->getUUID(), start); + nodeData->setLastBroadcastTime(agentNode->getLocalID(), start); } // figure out how large our avatar byte array can be to fit in the packet list @@ -620,7 +619,7 @@ void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePoin nodeData->incrementNumAvatarsSentLastFrame(); // set the last sent sequence number for this sender on the receiver - nodeData->setLastBroadcastSequenceNumber(agentNode->getUUID(), + nodeData->setLastBroadcastSequenceNumber(agentNode->getLocalID(), agentNodeData->getLastReceivedSequenceNumber()); // increment the number of avatars sent to this reciever diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 2b46eb9cdd..5c50805e3b 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -400,7 +400,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // If we want an item and there's sufficient space: #define IF_AVATAR_SPACE(flag, space) \ - if ((packetStateFlags & AvatarDataPacket::flag) && (int)(packetEnd - destinationBuffer) >= (space) \ + if ((packetStateFlags & AvatarDataPacket::flag) && (size_t)(packetEnd - destinationBuffer) >= (size_t)(space) \ && (includedFlags |= AvatarDataPacket::flag)) IF_AVATAR_SPACE(PACKET_HAS_AVATAR_GLOBAL_POSITION, sizeof _globalPosition) { @@ -563,7 +563,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent const auto& blendshapeCoefficients = _headData->getBlendshapeCoefficients(); // If it is connected, pack up the data - IF_AVATAR_SPACE(PACKET_HAS_FACE_TRACKER_INFO, sizeof(AvatarDataPacket::FaceTrackerInfo) + blendshapeCoefficients.size() * sizeof(float)) { + IF_AVATAR_SPACE(PACKET_HAS_FACE_TRACKER_INFO, sizeof(AvatarDataPacket::FaceTrackerInfo) + (size_t)blendshapeCoefficients.size() * sizeof(float)) { auto startSection = destinationBuffer; auto faceTrackerInfo = reinterpret_cast(destinationBuffer); // note: we don't use the blink and average loudness, we just use the numBlendShapes and @@ -593,7 +593,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent const int jointBitVectorSize = calcBitVectorSize(numJoints); // Check against full size or minimum size: count + two bit-vectors + two controllers - const int approxJointSpace = sendAll ? (int)AvatarDataPacket::maxJointDataSize(numJoints, true) : + const size_t approxJointSpace = sendAll ? AvatarDataPacket::maxJointDataSize(numJoints, true) : 1 + 2 * jointBitVectorSize + 2 * (sizeof(AvatarDataPacket::SixByteQuat) + sizeof(AvatarDataPacket::SixByteTrans)); IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, approxJointSpace) { @@ -753,7 +753,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent data->mouseFarGrabRotation[3] = mouseFarGrabRotation.z; destinationBuffer += sizeof(data->mouseFarGrabRotation); - int numGrabJointBytes = destinationBuffer - startSection; + numGrabJointBytes = destinationBuffer - startSection; } #ifdef WANT_DEBUG From 04c47943ba61cf73dd34b2f4a266bc384d2ed7ef Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 19 Sep 2018 13:45:08 -0700 Subject: [PATCH 015/362] Handle small packet space correctly --- assignment-client/src/avatars/AvatarMixerSlave.cpp | 2 +- libraries/avatars/src/AvatarData.cpp | 2 ++ libraries/avatars/src/AvatarData.h | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index ef10cb730a..ee7dfd85f3 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -471,7 +471,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) avatarPacket->write(bytes); avatarSpaceAvailable -= bytes.size(); numAvatarDataBytes += bytes.size(); - if (includeFlags != 0) { + if (includeFlags != 0 || avatarSpaceAvailable < AvatarDataPacket::MIN_BULK_PACKET_SIZE) { // Weren't able to fit everything. nodeList->sendPacket(std::move(avatarPacket), *destinationNode); ++numPacketsSent; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 5c50805e3b..20456012a3 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -243,10 +243,12 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent bool sendPALMinimum = (dataDetail == PALMinimum); lazyInitHeadData(); + ASSERT(maxDataSize == 0 || maxDataSize >= AvatarDataPacket::MIN_BULK_PACKET_SIZE); // special case, if we were asked for no data, then just include the flags all set to nothing if (dataDetail == NoData) { AvatarDataPacket::HasFlags packetStateFlags = 0; + itemFlags = packetStateFlags; QByteArray avatarDataByteArray(getSessionUUID().toRfc4122().data(), NUM_BYTES_RFC4122_UUID + sizeof(packetStateFlags)); avatarDataByteArray.append((char*) &packetStateFlags, sizeof packetStateFlags); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 057b351670..df7129b7a5 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -296,6 +296,8 @@ namespace AvatarDataPacket { } PACKED_END; const size_t FAR_GRAB_JOINTS_SIZE = 84; static_assert(sizeof(FarGrabJoints) == FAR_GRAB_JOINTS_SIZE, "AvatarDataPacket::FarGrabJoints size doesn't match."); + + const size_t MIN_BULK_PACKET_SIZE = NUM_BYTES_RFC4122_UUID + HEADER_SIZE; } const float MAX_AUDIO_LOUDNESS = 1000.0f; // close enough for mouth animation From f1b7097edb69ce6f0801538470f020919a481d89 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 19 Sep 2018 15:32:56 -0700 Subject: [PATCH 016/362] Priority experiment --- assignment-client/src/avatars/AvatarMixerSlave.cpp | 2 +- libraries/avatars/src/AvatarData.cpp | 2 +- libraries/shared/src/PrioritySortUtil.h | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index ee7dfd85f3..15330ab5c3 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -471,7 +471,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) avatarPacket->write(bytes); avatarSpaceAvailable -= bytes.size(); numAvatarDataBytes += bytes.size(); - if (includeFlags != 0 || avatarSpaceAvailable < AvatarDataPacket::MIN_BULK_PACKET_SIZE) { + if (includeFlags != 0 || avatarSpaceAvailable < (int)AvatarDataPacket::MIN_BULK_PACKET_SIZE) { // Weren't able to fit everything. nodeList->sendPacket(std::move(avatarPacket), *destinationNode); ++numPacketsSent; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 20456012a3..4007856a73 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -243,7 +243,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent bool sendPALMinimum = (dataDetail == PALMinimum); lazyInitHeadData(); - ASSERT(maxDataSize == 0 || maxDataSize >= AvatarDataPacket::MIN_BULK_PACKET_SIZE); + ASSERT(maxDataSize == 0 || (size_t)maxDataSize >= AvatarDataPacket::MIN_BULK_PACKET_SIZE); // special case, if we were asked for no data, then just include the flags all set to nothing if (dataDetail == NoData) { diff --git a/libraries/shared/src/PrioritySortUtil.h b/libraries/shared/src/PrioritySortUtil.h index 3a40fead71..c3d0f352ce 100644 --- a/libraries/shared/src/PrioritySortUtil.h +++ b/libraries/shared/src/PrioritySortUtil.h @@ -23,7 +23,7 @@ const float OUT_OF_VIEW_THRESHOLD = 0.5f * OUT_OF_VIEW_PENALTY; namespace PrioritySortUtil { - constexpr float DEFAULT_ANGULAR_COEF { 1.0f }; + constexpr float DEFAULT_ANGULAR_COEF { 2.5f }; constexpr float DEFAULT_CENTER_COEF { 0.5f }; constexpr float DEFAULT_AGE_COEF { 0.25f }; @@ -103,6 +103,9 @@ namespace PrioritySortUtil { float radius = glm::max(thing.getRadius(), MIN_RADIUS); // Other item's angle from view centre: float cosineAngle = glm::dot(offset, view.getDirection()) / distance; + if (cosineAngle > 0.0f) { + cosineAngle = std::sqrt(cosineAngle); + } float age = float((_usecCurrentTime - thing.getTimestamp()) / USECS_PER_SECOND); // the "age" term accumulates at the sum of all weights From 7bba4d4badaea550a67b3ffe4fac9416203a5e0c Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 19 Sep 2018 18:38:20 -0700 Subject: [PATCH 017/362] Change priorities in the correct place --- libraries/avatars/src/AvatarData.cpp | 2 +- libraries/shared/src/PrioritySortUtil.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 4007856a73..73525f69a7 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2879,7 +2879,7 @@ void RayToAvatarIntersectionResultFromScriptValue(const QScriptValue& object, Ra value.extraInfo = object.property("extraInfo").toVariant().toMap(); } -float AvatarData::_avatarSortCoefficientSize { 8.0f }; +float AvatarData::_avatarSortCoefficientSize { 20.0f }; float AvatarData::_avatarSortCoefficientCenter { 4.0f }; float AvatarData::_avatarSortCoefficientAge { 1.0f }; diff --git a/libraries/shared/src/PrioritySortUtil.h b/libraries/shared/src/PrioritySortUtil.h index c3d0f352ce..8020bde09d 100644 --- a/libraries/shared/src/PrioritySortUtil.h +++ b/libraries/shared/src/PrioritySortUtil.h @@ -23,7 +23,7 @@ const float OUT_OF_VIEW_THRESHOLD = 0.5f * OUT_OF_VIEW_PENALTY; namespace PrioritySortUtil { - constexpr float DEFAULT_ANGULAR_COEF { 2.5f }; + constexpr float DEFAULT_ANGULAR_COEF { 1.0f }; constexpr float DEFAULT_CENTER_COEF { 0.5f }; constexpr float DEFAULT_AGE_COEF { 0.25f }; From 328ed8d9766b2c2e1a418f00df8d0ceaca49a72c Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 20 Sep 2018 09:31:01 -0700 Subject: [PATCH 018/362] Remove unused variable --- assignment-client/src/avatars/AvatarMixerSlave.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 15330ab5c3..c8cc4d0c8a 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -250,9 +250,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // reset the number of sent avatars nodeData->resetNumAvatarsSentLastFrame(); - // keep a counter of the number of considered avatars - int numOtherAvatars = 0; - // keep track of outbound data rate specifically for avatar data int numAvatarDataBytes = 0; int identityBytesSent = 0; @@ -427,8 +424,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) auto startAvatarDataPacking = chrono::high_resolution_clock::now(); - ++numOtherAvatars; - const AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); const AvatarData* otherAvatar = otherNodeData->getConstAvatarData(); From f2e69d5c818363e58566ceeca5a3aa75be209bfd Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 20 Sep 2018 17:37:39 -0700 Subject: [PATCH 019/362] Make default bubble-box AvatarData property for efficiency Also a use of getClientGlobalPosition(), etc --- .../src/avatars/AvatarMixerClientData.h | 2 +- .../src/avatars/AvatarMixerSlave.cpp | 6 ++--- libraries/avatars/src/AvatarData.cpp | 24 +++++++++++++++++-- libraries/avatars/src/AvatarData.h | 6 +++++ 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index ccb0f4001d..492d21a807 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -90,7 +90,7 @@ public: void loadJSONStats(QJsonObject& jsonObject) const; - glm::vec3 getPosition() const { return _avatar ? _avatar->getWorldPosition() : glm::vec3(0); } + glm::vec3 getPosition() const { return _avatar ? _avatar->getClientGlobalPosition() : glm::vec3(0); } bool isRadiusIgnoring(const QUuid& other) const; void addToRadiusIgnoringSet(const QUuid& other); void removeFromRadiusIgnoringSet(const QUuid& other); diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index c8cc4d0c8a..f83bb2d3cb 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -244,7 +244,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // reset the internal state for correct random number distribution distribution.reset(); - // Base number to sort on number previously sent. + // Estimate number to sort on number sent last frame. const int numToSendEst = std::max(nodeData->getNumAvatarsSentLastFrame() * 2, 20); // reset the number of sent avatars @@ -342,8 +342,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // Don't bother with these checks if the other avatar has their bubble enabled and we're gettingAnyIgnored if (destinationNode->isIgnoreRadiusEnabled() || (avatarNode->isIgnoreRadiusEnabled() && !getsAnyIgnored)) { // Perform the collision check between the two bounding boxes - const float OTHER_AVATAR_BUBBLE_EXPANSION_FACTOR = 2.4f; // magic number determined empirically - AABox otherNodeBox = computeBubbleBox(avatarClientNodeData->getAvatar(), OTHER_AVATAR_BUBBLE_EXPANSION_FACTOR); + AABox otherNodeBox = avatarClientNodeData->getAvatar().getDefaultBubbleBox(); if (nodeBox.touches(otherNodeBox)) { nodeData->ignoreOther(destinationNode, avatarNode); shouldIgnore = !getsAnyIgnored; @@ -396,7 +395,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) auto avatarPacket = NLPacket::create(PacketType::BulkAvatarData); const int avatarPacketCapacity = avatarPacket->getPayloadCapacity(); int avatarSpaceAvailable = avatarPacketCapacity; - //avatarSpaceAvailable = 100; int numPacketsSent = 0; const auto& sortedAvatarVector = sortedAvatars.getSortedVector(numToSendEst); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 73525f69a7..782483a6c1 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -384,8 +384,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent QByteArray avatarDataByteArray((int)byteArraySize, 0); unsigned char* destinationBuffer = reinterpret_cast(avatarDataByteArray.data()); - unsigned char* startPosition = destinationBuffer; - const unsigned char * packetEnd = destinationBuffer + maxDataSize; + const unsigned char* const startPosition = destinationBuffer; + const unsigned char* const packetEnd = destinationBuffer + maxDataSize; AvatarDataPacket::HasFlags includedFlags = 0; @@ -971,6 +971,8 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { _avatarBoundingBoxChanged = now; } + _defaultBubbleBox = computeBubbleBox(); + sourceBuffer += sizeof(AvatarDataPacket::AvatarBoundingBox); int numBytesRead = sourceBuffer - startSection; _avatarBoundingBoxRate.increment(numBytesRead); @@ -2916,3 +2918,21 @@ void AvatarEntityMapFromScriptValue(const QScriptValue& object, AvatarEntityMap& value[EntityID] = binaryEntityProperties; } } + +const float AvatarData::DEFAULT_BUBBLE_SCALE = 2.4f; // magic number determined empirically + +AABox AvatarData::computeBubbleBox(float bubbleScale) const { + AABox box = AABox(_globalBoundingBoxOffset - _globalBoundingBoxDimensions, _globalBoundingBoxDimensions); + glm::vec3 size = box.getScale(); + size *= bubbleScale; + const glm::vec3 MIN_BUBBLE_SCALE(0.3f, 1.3f, 0.3); + size= glm::max(size, MIN_BUBBLE_SCALE); + box.setScaleStayCentered(size); + return box; +} + +AABox AvatarData::getDefaultBubbleBox() const { + AABox bubbleBox(_defaultBubbleBox); + bubbleBox.translate(_globalPosition); + return bubbleBox; +} diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index df7129b7a5..3ea7631e0c 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1100,6 +1100,7 @@ public: glm::vec3 getClientGlobalPosition() const { return _globalPosition; } AABox getGlobalBoundingBox() const { return AABox(_globalPosition + _globalBoundingBoxOffset - _globalBoundingBoxDimensions, _globalBoundingBoxDimensions); } + AABox getDefaultBubbleBox() const; /**jsdoc * @function MyAvatar.getAvatarEntityData @@ -1192,6 +1193,9 @@ public: void setReplicaIndex(int replicaIndex) { _replicaIndex = replicaIndex; } int getReplicaIndex() { return _replicaIndex; } + static const float DEFAULT_BUBBLE_SCALE; /* = 2.4 */ + AABox computeBubbleBox(float bubbleScale = DEFAULT_BUBBLE_SCALE) const; + signals: /**jsdoc @@ -1425,6 +1429,8 @@ protected: glm::vec3 _globalBoundingBoxDimensions; glm::vec3 _globalBoundingBoxOffset; + AABox _defaultBubbleBox; + mutable ReadWriteLockable _avatarEntitiesLock; AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar AvatarEntityIDs _avatarEntityForRecording; // create new entities id for avatar recording From 82b81c907670a0eb26053648bc551045219c6841 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Mon, 24 Sep 2018 11:48:48 -0700 Subject: [PATCH 020/362] Limit no. of joints sent per avatar to prevent lock-up With this the avatar mixer will sent the full bit-vector but only the first 111 actual joints. --- libraries/avatars/src/AvatarData.cpp | 10 +++++++--- libraries/avatars/src/AvatarData.h | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 782483a6c1..31687a7f27 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -592,10 +592,12 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent jointData = _jointData; } const int numJoints = jointData.size(); + assert(numJoints <= 255); const int jointBitVectorSize = calcBitVectorSize(numJoints); + const int cappedNumJoints = std::min(numJoints, AvatarDataPacket::MAX_NUM_JOINTS); // Check against full size or minimum size: count + two bit-vectors + two controllers - const size_t approxJointSpace = sendAll ? AvatarDataPacket::maxJointDataSize(numJoints, true) : + const size_t approxJointSpace = sendAll ? AvatarDataPacket::maxJointDataSize(cappedNumJoints, false) : 1 + 2 * jointBitVectorSize + 2 * (sizeof(AvatarDataPacket::SixByteQuat) + sizeof(AvatarDataPacket::SixByteTrans)); IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, approxJointSpace) { @@ -605,6 +607,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent *destinationBuffer++ = (uint8_t)numJoints; unsigned char* validityPosition = destinationBuffer; + memset(validityPosition, 0, jointBitVectorSize); unsigned char validity = 0; int validityBit = 0; @@ -622,7 +625,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent float minRotationDOT = (distanceAdjust && cullSmallChanges) ? getDistanceBasedMinRotationDOT(viewerPosition) : AVATAR_MIN_ROTATION_DOT; - for (int i = 0; i < jointData.size(); i++) { + for (int i = 0; i < cappedNumJoints; i++) { const JointData& data = jointData[i]; const JointData& last = lastSentJointData[i]; @@ -666,12 +669,13 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent unsigned char* beforeTranslations = destinationBuffer; #endif + memset(destinationBuffer, 0, jointBitVectorSize); destinationBuffer += jointBitVectorSize; // Move pointer past the validity bytes float minTranslation = (distanceAdjust && cullSmallChanges) ? getDistanceBasedMinTranslationDistance(viewerPosition) : AVATAR_MIN_TRANSLATION; float maxTranslationDimension = 0.0; - for (int i = 0; i < jointData.size(); i++) { + for (int i = 0; i < cappedNumJoints; i++) { const JointData& data = jointData[i]; const JointData& last = lastSentJointData[i]; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 3ea7631e0c..5b5d49feea 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -297,7 +297,8 @@ namespace AvatarDataPacket { const size_t FAR_GRAB_JOINTS_SIZE = 84; static_assert(sizeof(FarGrabJoints) == FAR_GRAB_JOINTS_SIZE, "AvatarDataPacket::FarGrabJoints size doesn't match."); - const size_t MIN_BULK_PACKET_SIZE = NUM_BYTES_RFC4122_UUID + HEADER_SIZE; + static const size_t MIN_BULK_PACKET_SIZE = NUM_BYTES_RFC4122_UUID + HEADER_SIZE; + static const int MAX_NUM_JOINTS = 111; } const float MAX_AUDIO_LOUDNESS = 1000.0f; // close enough for mouth animation From 07bdaeede794271a98b56275e9e687b930d0022a Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Mon, 24 Sep 2018 18:23:49 -0700 Subject: [PATCH 021/362] Split avatar joint-data across multiple packets if necessary --- .../src/avatars/AvatarMixerSlave.cpp | 16 +-- libraries/avatars/src/AvatarData.cpp | 136 ++++++++---------- libraries/avatars/src/AvatarData.h | 11 +- 3 files changed, 80 insertions(+), 83 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index f83bb2d3cb..3810456f71 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -450,12 +450,12 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) const bool distanceAdjust = true; const bool dropFaceTracking = false; - AvatarDataPacket::HasFlags includeFlags = 0; // the result of the toByteArray + AvatarDataPacket::SendStatus sendStatus; do { auto startSerialize = chrono::high_resolution_clock::now(); QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, - includeFlags, dropFaceTracking, distanceAdjust, myPosition, + sendStatus, dropFaceTracking, distanceAdjust, myPosition, &lastSentJointsForOther, avatarSpaceAvailable); auto endSerialize = chrono::high_resolution_clock::now(); _stats.toByteArrayElapsedTime += @@ -464,14 +464,14 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) avatarPacket->write(bytes); avatarSpaceAvailable -= bytes.size(); numAvatarDataBytes += bytes.size(); - if (includeFlags != 0 || avatarSpaceAvailable < (int)AvatarDataPacket::MIN_BULK_PACKET_SIZE) { + if (sendStatus.itemFlags != 0 || avatarSpaceAvailable < (int)AvatarDataPacket::MIN_BULK_PACKET_SIZE) { // Weren't able to fit everything. nodeList->sendPacket(std::move(avatarPacket), *destinationNode); ++numPacketsSent; avatarPacket = NLPacket::create(PacketType::BulkAvatarData); avatarSpaceAvailable = avatarPacketCapacity; } - } while (includeFlags != 0); + } while (!sendStatus); if (detail != AvatarData::NoData) { _stats.numOthersIncluded++; @@ -565,12 +565,12 @@ void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePoin // so we always send a full update for this avatar quint64 start = usecTimestampNow(); - AvatarDataPacket::HasFlags flagsOut; + AvatarDataPacket::SendStatus sendStatus; QVector emptyLastJointSendData { otherAvatar->getJointCount() }; QByteArray avatarByteArray = otherAvatar->toByteArray(AvatarData::SendAllData, 0, emptyLastJointSendData, - flagsOut, false, false, glm::vec3(0), nullptr, 0); + sendStatus, false, false, glm::vec3(0), nullptr, 0); quint64 end = usecTimestampNow(); _stats.toByteArrayElapsedTime += (end - start); @@ -596,14 +596,14 @@ void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePoin << "-" << avatarByteArray.size() << "bytes"; avatarByteArray = otherAvatar->toByteArray(AvatarData::SendAllData, 0, emptyLastJointSendData, - flagsOut, true, false, glm::vec3(0), nullptr, 0); + sendStatus, true, false, glm::vec3(0), nullptr, 0); if (avatarByteArray.size() > maxAvatarByteArraySize) { qCWarning(avatars) << "Replicated avatar data without facial data still too large for" << otherAvatar->getSessionUUID() << "-" << avatarByteArray.size() << "bytes"; avatarByteArray = otherAvatar->toByteArray(AvatarData::MinimumData, 0, emptyLastJointSendData, - flagsOut, true, false, glm::vec3(0), nullptr, 0); + sendStatus, true, false, glm::vec3(0), nullptr, 0); } } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 31687a7f27..f393ca9ba8 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -225,16 +225,16 @@ QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail, bool dro AvatarDataPacket::HasFlags hasFlagsOut = 0; auto lastSentTime = _lastToByteArray; _lastToByteArray = usecTimestampNow(); + AvatarDataPacket::SendStatus sendStatus; auto avatarByteArray = AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(), - hasFlagsOut, dropFaceTracking, false, glm::vec3(0), nullptr, - 0, &_outboundDataRate); + sendStatus, dropFaceTracking, false, glm::vec3(0), nullptr, 0, &_outboundDataRate); // Strip UUID return avatarByteArray.right(avatarByteArray.size() - NUM_BYTES_RFC4122_UUID); } QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, - AvatarDataPacket::HasFlags& itemFlags, bool dropFaceTracking, bool distanceAdjust, + AvatarDataPacket::SendStatus& sendStatus, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, QVector* sentJointDataOut, int maxDataSize, AvatarDataRate* outboundDataRateOut) const { bool cullSmallChanges = (dataDetail == CullSmallData); @@ -248,7 +248,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // special case, if we were asked for no data, then just include the flags all set to nothing if (dataDetail == NoData) { AvatarDataPacket::HasFlags packetStateFlags = 0; - itemFlags = packetStateFlags; + sendStatus.itemFlags = packetStateFlags; QByteArray avatarDataByteArray(getSessionUUID().toRfc4122().data(), NUM_BYTES_RFC4122_UUID + sizeof(packetStateFlags)); avatarDataByteArray.append((char*) &packetStateFlags, sizeof packetStateFlags); @@ -274,7 +274,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // 3 translations * 6 bytes = 6.48kbps // - auto parentID = getParentID(); + QUuid parentID; glm::mat4 leftFarGrabMatrix; glm::mat4 rightFarGrabMatrix; @@ -283,7 +283,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // Leading flags, to indicate how much data is actually included in the packet... AvatarDataPacket::HasFlags packetStateFlags = 0; - if (itemFlags == 0) { + if (sendStatus.itemFlags == 0) { bool hasAvatarGlobalPosition = true; // always include global position bool hasAvatarOrientation = false; bool hasAvatarBoundingBox = false; @@ -340,13 +340,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent | (hasJointDefaultPoseFlags ? AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS : 0) | (hasJointData ? AvatarDataPacket::PACKET_HAS_GRAB_JOINTS : 0); - itemFlags = packetStateFlags; + sendStatus.itemFlags = packetStateFlags; + sendStatus.rotationsSent = 0; + sendStatus.translationsSent = 0; } else { - packetStateFlags = itemFlags; - if (packetStateFlags & AvatarDataPacket::PACKET_HAS_JOINT_DATA) { - // Force all joints upon continuation, as deltas aren't valid. - sendAll = true; - } + packetStateFlags = sendStatus.itemFlags; if (packetStateFlags & AvatarDataPacket::PACKET_HAS_GRAB_JOINTS) { packetStateFlags |= AvatarDataPacket::PACKET_HAS_JOINT_DATA; } @@ -372,6 +370,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent packetStateFlags &= ~AvatarDataPacket::PACKET_HAS_GRAB_JOINTS; } } + if (packetStateFlags & (AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS | AvatarDataPacket::PACKET_HAS_PARENT_INFO)) { + parentID = getParentID(); + } const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE + NUM_BYTES_RFC4122_UUID + AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getBlendshapeCoefficients().size()) + @@ -388,6 +389,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent const unsigned char* const packetEnd = destinationBuffer + maxDataSize; AvatarDataPacket::HasFlags includedFlags = 0; + AvatarDataPacket::HasFlags extraReturnedFlags = 0; // Packets always have UUID. memcpy(destinationBuffer, getSessionUUID().toRfc4122(), NUM_BYTES_RFC4122_UUID); @@ -594,13 +596,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent const int numJoints = jointData.size(); assert(numJoints <= 255); const int jointBitVectorSize = calcBitVectorSize(numJoints); - const int cappedNumJoints = std::min(numJoints, AvatarDataPacket::MAX_NUM_JOINTS); - // Check against full size or minimum size: count + two bit-vectors + two controllers - const size_t approxJointSpace = sendAll ? AvatarDataPacket::maxJointDataSize(cappedNumJoints, false) : - 1 + 2 * jointBitVectorSize + 2 * (sizeof(AvatarDataPacket::SixByteQuat) + sizeof(AvatarDataPacket::SixByteTrans)); - - IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, approxJointSpace) { + IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, 1 + 2 * jointBitVectorSize + AvatarDataPacket::FAUX_JOINT_SIZE) { + // Allow for faux joints + translation bit-vector: + static const ptrdiff_t MIN_SIZE_FOR_JOINT = sizeof(AvatarDataPacket::SixByteQuat) + + jointBitVectorSize + AvatarDataPacket::FAUX_JOINT_SIZE; auto startSection = destinationBuffer; // joint rotation data @@ -608,8 +608,6 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent unsigned char* validityPosition = destinationBuffer; memset(validityPosition, 0, jointBitVectorSize); - unsigned char validity = 0; - int validityBit = 0; #ifdef WANT_DEBUG int rotationSentCount = 0; @@ -625,44 +623,41 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent float minRotationDOT = (distanceAdjust && cullSmallChanges) ? getDistanceBasedMinRotationDOT(viewerPosition) : AVATAR_MIN_ROTATION_DOT; - for (int i = 0; i < cappedNumJoints; i++) { + int i = sendStatus.rotationsSent; + for (; i < numJoints; ++i) { const JointData& data = jointData[i]; const JointData& last = lastSentJointData[i]; - if (!data.rotationIsDefaultPose) { - // The dot product for larger rotations is a lower number, - // so if the dot() is less than the value, then the rotation is a larger angle of rotation - if (sendAll || last.rotationIsDefaultPose || (!cullSmallChanges && last.rotation != data.rotation) - || (cullSmallChanges && glm::dot(last.rotation, data.rotation) < minRotationDOT) ) { - validity |= (1 << validityBit); + if ((packetEnd - destinationBuffer) >= MIN_SIZE_FOR_JOINT) { + if (!data.rotationIsDefaultPose) { + // The dot product for larger rotations is a lower number, + // so if the dot() is less than the value, then the rotation is a larger angle of rotation + if (sendAll || last.rotationIsDefaultPose || (!cullSmallChanges && last.rotation != data.rotation) + || (cullSmallChanges && glm::dot(last.rotation, data.rotation) < minRotationDOT)) { + validityPosition[i / BITS_IN_BYTE] |= 1 << (i % BITS_IN_BYTE); #ifdef WANT_DEBUG - rotationSentCount++; + rotationSentCount++; #endif - destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation); + destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation); - if (sentJointDataOut) { - (*sentJointDataOut)[i].rotation = data.rotation; + if (sentJointDataOut) { + (*sentJointDataOut)[i].rotation = data.rotation; + } } } + } else { + break; } if (sentJointDataOut) { (*sentJointDataOut)[i].rotationIsDefaultPose = data.rotationIsDefaultPose; } - if (++validityBit == BITS_IN_BYTE) { - *validityPosition++ = validity; - validityBit = validity = 0; - } - } - if (validityBit != 0) { - *validityPosition++ = validity; } + sendStatus.rotationsSent = i; // joint translation data validityPosition = destinationBuffer; - validity = 0; - validityBit = 0; #ifdef WANT_DEBUG int translationSentCount = 0; @@ -675,44 +670,41 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent float minTranslation = (distanceAdjust && cullSmallChanges) ? getDistanceBasedMinTranslationDistance(viewerPosition) : AVATAR_MIN_TRANSLATION; float maxTranslationDimension = 0.0; - for (int i = 0; i < cappedNumJoints; i++) { + i = sendStatus.translationsSent; + for (; i < numJoints; ++i) { const JointData& data = jointData[i]; const JointData& last = lastSentJointData[i]; - if (!data.translationIsDefaultPose) { - if (sendAll || last.translationIsDefaultPose || (!cullSmallChanges && last.translation != data.translation) - || (cullSmallChanges && glm::distance(data.translation, lastSentJointData[i].translation) > minTranslation)) { - - validity |= (1 << validityBit); + if (packetEnd - destinationBuffer > MIN_SIZE_FOR_JOINT) { + if (!data.translationIsDefaultPose) { + if (sendAll || last.translationIsDefaultPose || (!cullSmallChanges && last.translation != data.translation) + || (cullSmallChanges && glm::distance(data.translation, lastSentJointData[i].translation) > minTranslation)) { + validityPosition[i / BITS_IN_BYTE] |= 1 << (i % BITS_IN_BYTE); #ifdef WANT_DEBUG - translationSentCount++; + translationSentCount++; #endif - maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension); - maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension); - maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension); + maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension); + maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension); + maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension); - destinationBuffer += - packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); + destinationBuffer += + packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); - if (sentJointDataOut) { - (*sentJointDataOut)[i].translation = data.translation; + if (sentJointDataOut) { + (*sentJointDataOut)[i].translation = data.translation; + } } } + } else { + break; } if (sentJointDataOut) { (*sentJointDataOut)[i].translationIsDefaultPose = data.translationIsDefaultPose; } - if (++validityBit == BITS_IN_BYTE) { - *validityPosition++ = validity; - validityBit = validity = 0; - } - } - - if (validityBit != 0) { - *validityPosition++ = validity; } + sendStatus.translationsSent = i; // faux joints Transform controllerLeftHandTransform = Transform(getControllerLeftHandMatrix()); @@ -775,16 +767,14 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } #endif - if (destinationBuffer >= packetEnd) { - // Joint data too large - revert - destinationBuffer = startSection; - includedFlags &= ~(AvatarDataPacket::PACKET_HAS_GRAB_JOINTS | AvatarDataPacket::PACKET_HAS_JOINT_DATA); - } else { - int numBytes = destinationBuffer - startSection; - if (outboundDataRateOut) { - outboundDataRateOut->jointDataRate.increment(numBytes); - outboundDataRateOut->farGrabJointRate.increment(numGrabJointBytes); - } + if (sendStatus.rotationsSent != numJoints || sendStatus.translationsSent != numJoints) { + extraReturnedFlags |= AvatarDataPacket::PACKET_HAS_JOINT_DATA; + } + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->jointDataRate.increment(numBytes); + outboundDataRateOut->farGrabJointRate.increment(numGrabJointBytes); } } @@ -812,7 +802,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent memcpy(packetFlagsLocation, &includedFlags, sizeof(includedFlags)); // Return dropped items. - itemFlags = packetStateFlags & ~includedFlags; + sendStatus.itemFlags = (packetStateFlags & ~includedFlags) | extraReturnedFlags; int avatarDataSize = destinationBuffer - startPosition; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 5b5d49feea..3ed90c059a 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -298,7 +298,14 @@ namespace AvatarDataPacket { static_assert(sizeof(FarGrabJoints) == FAR_GRAB_JOINTS_SIZE, "AvatarDataPacket::FarGrabJoints size doesn't match."); static const size_t MIN_BULK_PACKET_SIZE = NUM_BYTES_RFC4122_UUID + HEADER_SIZE; - static const int MAX_NUM_JOINTS = 111; + static const size_t FAUX_JOINT_SIZE = 2 * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); + + struct SendStatus { + HasFlags itemFlags { 0 }; + int rotationsSent { 0 }; + int translationsSent { 0 }; + operator bool() { return itemFlags == 0; } + }; } const float MAX_AUDIO_LOUDNESS = 1000.0f; // close enough for mouth animation @@ -452,7 +459,7 @@ public: virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false); virtual QByteArray toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, - AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, + AvatarDataPacket::SendStatus& sendStatus, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, QVector* sentJointDataOut, int maxDataSize = 0, AvatarDataRate* outboundDataRateOut = nullptr) const; virtual void doneEncoding(bool cullSmallChanges); From ad6bbc7ff6954461c35da6ddbdef7b71bf7c5c55 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 25 Sep 2018 11:22:44 -0700 Subject: [PATCH 022/362] latest squatty changes --- interface/src/avatar/MyAvatar.cpp | 25 +++++++++++++++++-------- interface/src/avatar/MyAvatar.h | 1 + plugins/oculus/src/OculusHelpers.cpp | 6 ++++++ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index bf684ccd9a..c52e029d94 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -463,6 +463,7 @@ void MyAvatar::update(float deltaTime) { if ((acosHead > 0.98f) && !getIsInSittingState() && (sensorHeadPoseDebug.getTranslation().y < -0.5f)) { qCDebug(interfaceapp) << "we are going to sitting state because it looks like we should" << sensorHeadPoseDebug.getTranslation().y; } + // put the average hand azimuth into sensor space. // then mix it with head facing direction to determine rotation recenter if (getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid()) { @@ -3601,9 +3602,9 @@ glm::vec3 MyAvatar::computeCounterBalance() { } else if (counterBalancedCg.y < sitSquatThreshold) { // do a height reset setResetMode(true); - _follow.activate(FollowHelper::Vertical); + //_follow.activate(FollowHelper::Vertical); // disable cg behaviour in this case. - setIsInSittingState(true); + //setIsInSittingState(true); } return counterBalancedCg; } @@ -4059,12 +4060,6 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons bool stepDetected = false; float myScale = myAvatar.getAvatarScale(); - - // debug head hips angle - //glm::vec3 hipsPos = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); - //glm::vec3 headHipsBody = currentHeadPose.getTranslation() - hipsPos; - //qCDebug(interfaceapp) << "head in sensor space " << withinBaseOfSupport(currentHeadPose); - if (myAvatar.getIsInWalkingState()) { stepDetected = true; } else if (myAvatar.getIsInSittingState()) { @@ -4137,6 +4132,20 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat if (myAvatar.getHMDLeanRecenterEnabled() && qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { + + // debug head hips angle + glm::vec3 headDefaultPos = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Head")); + if (myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPos.y - 0.05f)) { + _squatCount++; + if ((_squatCount > 300) && !isActive(Vertical) && !isActive(Horizontal)) { + activate(Horizontal); + activate(Vertical); + _squatCount = 0; + } + } else { + _squatCount = 0; + } + if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 6f3858c5bf..78dc6307e8 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1727,6 +1727,7 @@ private: std::atomic _forceActivateVertical { false }; std::atomic _forceActivateHorizontal { false }; std::atomic _toggleHipsFollowing { true }; + int _squatCount{ 0 }; }; FollowHelper _follow; diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 511984c657..e543d3ca00 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -79,6 +79,12 @@ private: if (!OVR_SUCCESS(ovr_Initialize(&initParams))) { qCWarning(oculusLog) << "Failed to initialze Oculus SDK" << ovr::getError(); return; + } else { + qCWarning(oculusLog) << "successful init of oculus!!!!!!!!"; + ovrTrackingOrigin fred; + fred = ovr_GetTrackingOriginType(session); + qCWarning(oculusLog) << (int)fred; + } ovrGraphicsLuid luid; From f95ab1b040aca9562fe222b08321a4b5e05cc56a Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 25 Sep 2018 12:07:59 -0700 Subject: [PATCH 023/362] Clean-up & other small changes --- .../src/avatars/AvatarMixerSlave.cpp | 4 +- libraries/avatars/src/AvatarData.cpp | 51 ++++++++++--------- libraries/avatars/src/AvatarData.h | 2 +- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 3810456f71..6a1241756c 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -244,7 +244,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // reset the internal state for correct random number distribution distribution.reset(); - // Estimate number to sort on number sent last frame. + // Estimate number to sort on number sent last frame (with min. of 20). const int numToSendEst = std::max(nodeData->getNumAvatarsSentLastFrame() * 2, 20); // reset the number of sent avatars @@ -464,7 +464,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) avatarPacket->write(bytes); avatarSpaceAvailable -= bytes.size(); numAvatarDataBytes += bytes.size(); - if (sendStatus.itemFlags != 0 || avatarSpaceAvailable < (int)AvatarDataPacket::MIN_BULK_PACKET_SIZE) { + if (!sendStatus || avatarSpaceAvailable < (int)AvatarDataPacket::MIN_BULK_PACKET_SIZE) { // Weren't able to fit everything. nodeList->sendPacket(std::move(avatarPacket), *destinationNode); ++numPacketsSent; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index f393ca9ba8..4295eb66a7 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -245,13 +245,17 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent lazyInitHeadData(); ASSERT(maxDataSize == 0 || (size_t)maxDataSize >= AvatarDataPacket::MIN_BULK_PACKET_SIZE); + // Leading flags, to indicate how much data is actually included in the packet... + AvatarDataPacket::HasFlags wantedFlags = 0; + AvatarDataPacket::HasFlags includedFlags = 0; + AvatarDataPacket::HasFlags extraReturnedFlags = 0; // For partial joint data. + // special case, if we were asked for no data, then just include the flags all set to nothing if (dataDetail == NoData) { - AvatarDataPacket::HasFlags packetStateFlags = 0; - sendStatus.itemFlags = packetStateFlags; + sendStatus.itemFlags = wantedFlags; - QByteArray avatarDataByteArray(getSessionUUID().toRfc4122().data(), NUM_BYTES_RFC4122_UUID + sizeof(packetStateFlags)); - avatarDataByteArray.append((char*) &packetStateFlags, sizeof packetStateFlags); + QByteArray avatarDataByteArray(getSessionUUID().toRfc4122().data(), NUM_BYTES_RFC4122_UUID + sizeof wantedFlags); + avatarDataByteArray.append((char*) &wantedFlags, sizeof wantedFlags); return avatarDataByteArray; } @@ -280,10 +284,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent glm::mat4 rightFarGrabMatrix; glm::mat4 mouseFarGrabMatrix; - // Leading flags, to indicate how much data is actually included in the packet... - AvatarDataPacket::HasFlags packetStateFlags = 0; - if (sendStatus.itemFlags == 0) { + // New avatar ... bool hasAvatarGlobalPosition = true; // always include global position bool hasAvatarOrientation = false; bool hasAvatarBoundingBox = false; @@ -324,7 +326,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent hasJointDefaultPoseFlags = hasJointData; } - packetStateFlags = + wantedFlags = (hasAvatarGlobalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_GLOBAL_POSITION : 0) | (hasAvatarBoundingBox ? AvatarDataPacket::PACKET_HAS_AVATAR_BOUNDING_BOX : 0) | (hasAvatarOrientation ? AvatarDataPacket::PACKET_HAS_AVATAR_ORIENTATION : 0) @@ -340,17 +342,18 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent | (hasJointDefaultPoseFlags ? AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS : 0) | (hasJointData ? AvatarDataPacket::PACKET_HAS_GRAB_JOINTS : 0); - sendStatus.itemFlags = packetStateFlags; + sendStatus.itemFlags = wantedFlags; sendStatus.rotationsSent = 0; sendStatus.translationsSent = 0; - } else { - packetStateFlags = sendStatus.itemFlags; - if (packetStateFlags & AvatarDataPacket::PACKET_HAS_GRAB_JOINTS) { - packetStateFlags |= AvatarDataPacket::PACKET_HAS_JOINT_DATA; + } else { // Continuing avatar ... + wantedFlags = sendStatus.itemFlags; + if (wantedFlags & AvatarDataPacket::PACKET_HAS_GRAB_JOINTS) { + // Must send joints for grab joints - + wantedFlags |= AvatarDataPacket::PACKET_HAS_JOINT_DATA; } } - if (packetStateFlags & AvatarDataPacket::PACKET_HAS_GRAB_JOINTS) { + if (wantedFlags & AvatarDataPacket::PACKET_HAS_GRAB_JOINTS) { bool leftValid; leftFarGrabMatrix = _farGrabLeftMatrixCache.get(leftValid); if (!leftValid) { @@ -367,10 +370,10 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent mouseFarGrabMatrix = glm::mat4(); } if (!(leftValid || rightValid || mouseValid)) { - packetStateFlags &= ~AvatarDataPacket::PACKET_HAS_GRAB_JOINTS; + wantedFlags &= ~AvatarDataPacket::PACKET_HAS_GRAB_JOINTS; } } - if (packetStateFlags & (AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS | AvatarDataPacket::PACKET_HAS_PARENT_INFO)) { + if (wantedFlags & (AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS | AvatarDataPacket::PACKET_HAS_PARENT_INFO)) { parentID = getParentID(); } @@ -388,24 +391,22 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent const unsigned char* const startPosition = destinationBuffer; const unsigned char* const packetEnd = destinationBuffer + maxDataSize; - AvatarDataPacket::HasFlags includedFlags = 0; - AvatarDataPacket::HasFlags extraReturnedFlags = 0; - // Packets always have UUID. memcpy(destinationBuffer, getSessionUUID().toRfc4122(), NUM_BYTES_RFC4122_UUID); destinationBuffer += NUM_BYTES_RFC4122_UUID; unsigned char * packetFlagsLocation = destinationBuffer; - destinationBuffer += sizeof(packetStateFlags); + destinationBuffer += sizeof(wantedFlags); #define AVATAR_MEMCPY(src) \ memcpy(destinationBuffer, &(src), sizeof(src)); \ destinationBuffer += sizeof(src); // If we want an item and there's sufficient space: -#define IF_AVATAR_SPACE(flag, space) \ - if ((packetStateFlags & AvatarDataPacket::flag) && (size_t)(packetEnd - destinationBuffer) >= (size_t)(space) \ - && (includedFlags |= AvatarDataPacket::flag)) +#define IF_AVATAR_SPACE(flag, space) \ + if ((wantedFlags & AvatarDataPacket::flag) \ + && (size_t)(packetEnd - destinationBuffer) >= (size_t)(space) \ + && (includedFlags |= AvatarDataPacket::flag)) IF_AVATAR_SPACE(PACKET_HAS_AVATAR_GLOBAL_POSITION, sizeof _globalPosition) { auto startSection = destinationBuffer; @@ -589,7 +590,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } QVector jointData; - if (packetStateFlags & (AvatarDataPacket::PACKET_HAS_JOINT_DATA | AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS)) { + if (wantedFlags & (AvatarDataPacket::PACKET_HAS_JOINT_DATA | AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS)) { QReadLocker readLock(&_jointDataLock); jointData = _jointData; } @@ -802,7 +803,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent memcpy(packetFlagsLocation, &includedFlags, sizeof(includedFlags)); // Return dropped items. - sendStatus.itemFlags = (packetStateFlags & ~includedFlags) | extraReturnedFlags; + sendStatus.itemFlags = (wantedFlags & ~includedFlags) | extraReturnedFlags; int avatarDataSize = destinationBuffer - startPosition; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 3ed90c059a..40334f9b86 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -302,7 +302,7 @@ namespace AvatarDataPacket { struct SendStatus { HasFlags itemFlags { 0 }; - int rotationsSent { 0 }; + int rotationsSent { 0 }; // ie: index of next unsent joint int translationsSent { 0 }; operator bool() { return itemFlags == 0; } }; From 13890f1d16bf2c33945c9ce2aef0c668b276ae09 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 26 Sep 2018 09:47:26 -0700 Subject: [PATCH 024/362] Use local ID in place of UUID in a couple more cases --- .../src/avatars/AvatarMixerClientData.cpp | 10 +++++----- assignment-client/src/avatars/AvatarMixerClientData.h | 10 +++++----- assignment-client/src/avatars/AvatarMixerSlave.cpp | 6 +++--- libraries/avatars/src/AvatarData.cpp | 1 - 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 2226a4c7a6..801ef61abf 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -26,20 +26,20 @@ AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID _avatar->setID(nodeID); } -uint64_t AvatarMixerClientData::getLastOtherAvatarEncodeTime(QUuid otherAvatar) const { - std::unordered_map::const_iterator itr = _lastOtherAvatarEncodeTime.find(otherAvatar); +uint64_t AvatarMixerClientData::getLastOtherAvatarEncodeTime(NLPacket::LocalID otherAvatar) const { + const auto itr = _lastOtherAvatarEncodeTime.find(otherAvatar); if (itr != _lastOtherAvatarEncodeTime.end()) { return itr->second; } return 0; } -void AvatarMixerClientData::setLastOtherAvatarEncodeTime(const QUuid& otherAvatar, uint64_t time) { - std::unordered_map::iterator itr = _lastOtherAvatarEncodeTime.find(otherAvatar); +void AvatarMixerClientData::setLastOtherAvatarEncodeTime(NLPacket::LocalID otherAvatar, uint64_t time) { + auto itr = _lastOtherAvatarEncodeTime.find(otherAvatar); if (itr != _lastOtherAvatarEncodeTime.end()) { itr->second = time; } else { - _lastOtherAvatarEncodeTime.emplace(std::pair(otherAvatar, time)); + _lastOtherAvatarEncodeTime.emplace(std::pair(otherAvatar, time)); } } diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 492d21a807..634c202611 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -111,10 +111,10 @@ public: const ConicalViewFrustums& getViewFrustums() const { return _currentViewFrustums; } - uint64_t getLastOtherAvatarEncodeTime(QUuid otherAvatar) const; - void setLastOtherAvatarEncodeTime(const QUuid& otherAvatar, uint64_t time); + uint64_t getLastOtherAvatarEncodeTime(NLPacket::LocalID otherAvatar) const; + void setLastOtherAvatarEncodeTime(NLPacket::LocalID otherAvatar, uint64_t time); - QVector& getLastOtherAvatarSentJoints(QUuid otherAvatar) { return _lastOtherAvatarSentJoints[otherAvatar]; } + QVector& getLastOtherAvatarSentJoints(NLPacket::LocalID otherAvatar) { return _lastOtherAvatarSentJoints[otherAvatar]; } void queuePacket(QSharedPointer message, SharedNodePointer node); int processPackets(const SlaveSharedData& slaveSharedData); // returns number of packets processed @@ -152,8 +152,8 @@ private: // this is a map of the last time we encoded an "other" avatar for // sending to "this" node - std::unordered_map _lastOtherAvatarEncodeTime; - std::unordered_map> _lastOtherAvatarSentJoints; + std::unordered_map _lastOtherAvatarEncodeTime; + std::unordered_map> _lastOtherAvatarSentJoints; uint64_t _identityChangeTimestamp; bool _avatarSessionDisplayNameMustChange{ true }; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 6a1241756c..5e1e1019e4 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -382,7 +382,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) if (!shouldIgnore) { // sort this one for later const AvatarData* avatarNodeData = avatarClientNodeData->getConstAvatarData(); - auto lastEncodeTime = nodeData->getLastOtherAvatarEncodeTime(avatarNodeData->getSessionUUID()); + auto lastEncodeTime = nodeData->getLastOtherAvatarEncodeTime(avatarNode->getLocalID()); sortedAvatars.push(SortableAvatar(avatarNodeData, avatarNode, lastEncodeTime)); } @@ -446,7 +446,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) nodeData->incrementAvatarInView(); } - QVector& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID()); + QVector& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getLocalID()); const bool distanceAdjust = true; const bool dropFaceTracking = false; @@ -482,7 +482,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // set the last sent sequence number for this sender on the receiver nodeData->setLastBroadcastSequenceNumber(otherNode->getLocalID(), otherNodeData->getLastReceivedSequenceNumber()); - nodeData->setLastOtherAvatarEncodeTime(otherNode->getUUID(), usecTimestampNow()); + nodeData->setLastOtherAvatarEncodeTime(otherNode->getLocalID(), usecTimestampNow()); } auto endAvatarDataPacking = chrono::high_resolution_clock::now(); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 2282745905..91e7939d9c 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -222,7 +222,6 @@ float AvatarData::getDistanceBasedMinTranslationDistance(glm::vec3 viewerPositio // we want to track outbound data in this case... QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) { - AvatarDataPacket::HasFlags hasFlagsOut = 0; auto lastSentTime = _lastToByteArray; _lastToByteArray = usecTimestampNow(); AvatarDataPacket::SendStatus sendStatus; From 1e79329e834bd8f4e50a4e71f67a40694f6f9c84 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 26 Sep 2018 14:23:52 -0700 Subject: [PATCH 025/362] Access joint data via C pointer instead of operator[] QVector::detach() was showing up in the linux profiles - it's related to the copy-on-write capability. Const QVectors don't use it though. Also a bug fix for the minimum joint space needed. --- libraries/avatars/src/AvatarData.cpp | 39 +++++++++++++++------------- libraries/avatars/src/AvatarData.h | 2 +- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 91e7939d9c..7fb0f786ba 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -402,9 +402,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += sizeof(src); // If we want an item and there's sufficient space: -#define IF_AVATAR_SPACE(flag, space) \ - if ((wantedFlags & AvatarDataPacket::flag) \ - && (size_t)(packetEnd - destinationBuffer) >= (size_t)(space) \ +#define IF_AVATAR_SPACE(flag, space) \ + if ((wantedFlags & AvatarDataPacket::flag) \ + && (packetEnd - destinationBuffer) >= (ptrdiff_t)(space) \ && (includedFlags |= AvatarDataPacket::flag)) IF_AVATAR_SPACE(PACKET_HAS_AVATAR_GLOBAL_POSITION, sizeof _globalPosition) { @@ -597,10 +597,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent assert(numJoints <= 255); const int jointBitVectorSize = calcBitVectorSize(numJoints); - IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, 1 + 2 * jointBitVectorSize + AvatarDataPacket::FAUX_JOINT_SIZE) { + // Start joints if room for at least the faux joints. + IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, 1 + 2 * jointBitVectorSize + AvatarDataPacket::FAUX_JOINTS_SIZE) { // Allow for faux joints + translation bit-vector: - static const ptrdiff_t MIN_SIZE_FOR_JOINT = sizeof(AvatarDataPacket::SixByteQuat) - + jointBitVectorSize + AvatarDataPacket::FAUX_JOINT_SIZE; + const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) + + jointBitVectorSize + AvatarDataPacket::FAUX_JOINTS_SIZE; auto startSection = destinationBuffer; // joint rotation data @@ -620,15 +621,17 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent if (sentJointDataOut) { sentJointDataOut->resize(numJoints); // Make sure the destination is resized before using it } + const JointData *const joints = jointData.data(); + JointData *const sentJoints = sentJointDataOut ? sentJointDataOut->data() : nullptr; float minRotationDOT = (distanceAdjust && cullSmallChanges) ? getDistanceBasedMinRotationDOT(viewerPosition) : AVATAR_MIN_ROTATION_DOT; int i = sendStatus.rotationsSent; for (; i < numJoints; ++i) { - const JointData& data = jointData[i]; + const JointData& data = joints[i]; const JointData& last = lastSentJointData[i]; - if ((packetEnd - destinationBuffer) >= MIN_SIZE_FOR_JOINT) { + if (packetEnd - destinationBuffer >= minSizeForJoint) { if (!data.rotationIsDefaultPose) { // The dot product for larger rotations is a lower number, // so if the dot() is less than the value, then the rotation is a larger angle of rotation @@ -640,8 +643,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent #endif destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation); - if (sentJointDataOut) { - (*sentJointDataOut)[i].rotation = data.rotation; + if (sentJoints) { + sentJoints[i].rotation = data.rotation; } } } @@ -649,8 +652,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent break; } - if (sentJointDataOut) { - (*sentJointDataOut)[i].rotationIsDefaultPose = data.rotationIsDefaultPose; + if (sentJoints) { + sentJoints[i].rotationIsDefaultPose = data.rotationIsDefaultPose; } } @@ -672,10 +675,10 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent float maxTranslationDimension = 0.0; i = sendStatus.translationsSent; for (; i < numJoints; ++i) { - const JointData& data = jointData[i]; + const JointData& data = joints[i]; const JointData& last = lastSentJointData[i]; - if (packetEnd - destinationBuffer > MIN_SIZE_FOR_JOINT) { + if (packetEnd - destinationBuffer >= minSizeForJoint) { if (!data.translationIsDefaultPose) { if (sendAll || last.translationIsDefaultPose || (!cullSmallChanges && last.translation != data.translation) || (cullSmallChanges && glm::distance(data.translation, lastSentJointData[i].translation) > minTranslation)) { @@ -690,8 +693,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); - if (sentJointDataOut) { - (*sentJointDataOut)[i].translation = data.translation; + if (sentJoints) { + sentJoints[i].translation = data.translation; } } } @@ -699,8 +702,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent break; } - if (sentJointDataOut) { - (*sentJointDataOut)[i].translationIsDefaultPose = data.translationIsDefaultPose; + if (sentJoints) { + sentJoints[i].translationIsDefaultPose = data.translationIsDefaultPose; } } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index a4dfaa594e..3ca8bd4775 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -298,7 +298,7 @@ namespace AvatarDataPacket { static_assert(sizeof(FarGrabJoints) == FAR_GRAB_JOINTS_SIZE, "AvatarDataPacket::FarGrabJoints size doesn't match."); static const size_t MIN_BULK_PACKET_SIZE = NUM_BYTES_RFC4122_UUID + HEADER_SIZE; - static const size_t FAUX_JOINT_SIZE = 2 * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); + static const size_t FAUX_JOINTS_SIZE = 2 * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); struct SendStatus { HasFlags itemFlags { 0 }; From fa9abf0fff45ba8205a8647bdb28f18bc068e676 Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 26 Sep 2018 18:08:00 -0700 Subject: [PATCH 026/362] added the floor at 0.0 in sensor space for oculus. to do: vive --- interface/src/avatar/MyAvatar.cpp | 35 ++++++++++++++++++++-------- interface/src/avatar/MyAvatar.h | 5 ++-- plugins/oculus/src/OculusHelpers.cpp | 13 ++++++----- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 6394631484..2152c12b64 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -477,10 +477,20 @@ void MyAvatar::update(float deltaTime) { auto sensorHeadPoseDebug = getControllerPoseInSensorFrame(controller::Action::HEAD); glm::vec3 upHead = transformVectorFast(sensorHeadPoseDebug.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); + qCDebug(interfaceapp) << "sensor space head pos " << sensorHeadPoseDebug.getTranslation().y; if ((acosHead > 0.98f) && !getIsInSittingState() && (sensorHeadPoseDebug.getTranslation().y < -0.5f)) { - qCDebug(interfaceapp) << "we are going to sitting state because it looks like we should" << sensorHeadPoseDebug.getTranslation().y; + //qCDebug(interfaceapp) << "we are going to sitting state because it looks like we should" << sensorHeadPoseDebug.getTranslation().y; } - + if (!_lastFrameHMDMode && qApp->isHMDMode()) { + // we have entered hmd mode, so make the best guess about sitting or standing + if (sensorHeadPoseDebug.getTranslation().y < 1.3f) { + // then we are sitting. + // setIsInSittingState(true); + } else { + // setIsInSittingState(false); + } + } + // put the average hand azimuth into sensor space. // then mix it with head facing direction to determine rotation recenter if (getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid()) { @@ -3575,9 +3585,9 @@ glm::vec3 MyAvatar::computeCounterBalance() { } else if (counterBalancedCg.y < sitSquatThreshold) { // do a height reset setResetMode(true); - //_follow.activate(FollowHelper::Vertical); + // _follow.activate(FollowHelper::Vertical); // disable cg behaviour in this case. - //setIsInSittingState(true); + // setIsInSittingState(true); } return counterBalancedCg; } @@ -4090,7 +4100,7 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl return true; } else if (offset.y > CYLINDER_TOP) { // if we recenter upwards then no longer in sitting state - myAvatar.setIsInSittingState(false); + // myAvatar.setIsInSittingState(false); return true; } else { return false; @@ -4110,15 +4120,20 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat glm::vec3 headDefaultPos = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Head")); if (myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPos.y - 0.05f)) { _squatCount++; - if ((_squatCount > 300) && !isActive(Vertical) && !isActive(Horizontal)) { - activate(Horizontal); - activate(Vertical); - _squatCount = 0; + if ((_squatCount > 600) && !isActive(Vertical) && !isActive(Horizontal)) { + if (myAvatar.getIsInSittingState()) { + // activate(Horizontal); + activate(Vertical); + _squatCount = 0; + } else { + activate(Horizontal); + _squatCount = 0; + } } } else { _squatCount = 0; } - + if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 1dabe38116..4ccb1a8d75 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1728,7 +1728,7 @@ private: std::atomic _forceActivateVertical { false }; std::atomic _forceActivateHorizontal { false }; std::atomic _toggleHipsFollowing { true }; - int _squatCount{ 0 }; + int _squatCount { 0 }; }; FollowHelper _follow; @@ -1760,6 +1760,7 @@ private: glm::quat _customListenOrientation; AtRestDetector _hmdAtRestDetector; + bool _lastFrameHMDMode { false } ; bool _lastIsMoving { false }; // all poses are in sensor-frame @@ -1804,7 +1805,7 @@ private: ThreadSafeValueCache _sprintSpeed { AVATAR_SPRINT_SPEED_SCALAR }; float _walkSpeedScalar { AVATAR_WALK_SPEED_SCALAR }; bool _isInWalkingState { false }; - bool _isInSittingState { false }; + bool _isInSittingState { true }; // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index e543d3ca00..38d93d088d 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -79,18 +79,19 @@ private: if (!OVR_SUCCESS(ovr_Initialize(&initParams))) { qCWarning(oculusLog) << "Failed to initialze Oculus SDK" << ovr::getError(); return; - } else { - qCWarning(oculusLog) << "successful init of oculus!!!!!!!!"; - ovrTrackingOrigin fred; - fred = ovr_GetTrackingOriginType(session); - qCWarning(oculusLog) << (int)fred; - } ovrGraphicsLuid luid; if (!OVR_SUCCESS(ovr_Create(&session, &luid))) { qCWarning(oculusLog) << "Failed to acquire Oculus session" << ovr::getError(); return; + } else { + qCWarning(oculusLog) << "successful init of oculus!!!!!!!!"; + ovrTrackingOrigin fred; + //fred = ovr_GetTrackingOriginType(session); + ovrResult retTrackingType = ovr_SetTrackingOriginType(session, ovrTrackingOrigin::ovrTrackingOrigin_FloorLevel); + fred = ovr_GetTrackingOriginType(session); + qCWarning(oculusLog) << OVR_SUCCESS(retTrackingType) << (int)fred; } } From 52355e53f1e69ff4032813c13b229c04c6cfdb0d Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 27 Sep 2018 16:44:55 -0700 Subject: [PATCH 027/362] adding the menu item to the avatar app for the sit state. --- interface/resources/qml/hifi/AvatarApp.qml | 1 + .../resources/qml/hifi/avatarapp/Settings.qml | 57 +- interface/src/avatar/MyAvatar.cpp | 14 +- interface/src/avatar/MyAvatar.h | 9 + .../src/avatars-renderer/Avatar.cpp | 1 + scripts/developer/objectOrientedStep.js | 688 ++++++++++++++++++ scripts/system/avatarapp.js | 15 +- 7 files changed, 778 insertions(+), 7 deletions(-) create mode 100644 scripts/developer/objectOrientedStep.js diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index aea5931627..b06a2ca67c 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -252,6 +252,7 @@ Rectangle { var avatarSettings = { dominantHand : settings.dominantHandIsLeft ? 'left' : 'right', collisionsEnabled : settings.avatarCollisionsOn, + sittingEnabled : settings.avatarSittingOn, animGraphOverrideUrl : settings.avatarAnimationOverrideJSON, collisionSoundUrl : settings.avatarCollisionSoundUrl }; diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index 71bfbb084d..af76ba04d6 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -20,6 +20,7 @@ Rectangle { property real scaleValue: scaleSlider.value / 10 property alias dominantHandIsLeft: leftHandRadioButton.checked property alias avatarCollisionsOn: collisionsEnabledRadiobutton.checked + property alias avatarSittingOn: sitRadiobutton.checked property alias avatarAnimationOverrideJSON: avatarAnimationUrlInputText.text property alias avatarAnimationJSON: avatarAnimationUrlInputText.placeholderText property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text @@ -45,6 +46,12 @@ Rectangle { collisionsDisabledRadioButton.checked = true; } + if (settings.sittingEnabled) { + sitRadiobutton.checked = true; + } else { + standRadioButton.checked = true; + } + avatarAnimationJSON = settings.animGraphUrl; avatarAnimationOverrideJSON = settings.animGraphOverrideUrl; avatarCollisionSoundUrl = settings.collisionSoundUrl; @@ -289,8 +296,56 @@ Rectangle { text: "OFF" boxSize: 20 } - } + + // TextStyle9 + + RalewaySemiBold { + size: 17; + Layout.row: 2 + Layout.column: 0 + + text: "Sitting State" + } + + ButtonGroup { + id: sitStand + } + + HifiControlsUit.RadioButton { + id: sitRadiobutton + + Layout.row: 2 + Layout.column: 1 + Layout.leftMargin: -40 + + ButtonGroup.group: sitStand + checked: true + + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + text: "Sit" + boxSize: 20 + } + + HifiControlsUit.RadioButton { + id: standRadioButton + + Layout.row: 2 + Layout.column: 2 + Layout.rightMargin: 20 + + ButtonGroup.group: sitStand + + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + text: "Stand" + boxSize: 20 + } + } + ColumnLayout { id: avatarAnimationLayout anchors.top: handAndCollisions.bottom diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 2152c12b64..631a4b0670 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -477,7 +477,7 @@ void MyAvatar::update(float deltaTime) { auto sensorHeadPoseDebug = getControllerPoseInSensorFrame(controller::Action::HEAD); glm::vec3 upHead = transformVectorFast(sensorHeadPoseDebug.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); - qCDebug(interfaceapp) << "sensor space head pos " << sensorHeadPoseDebug.getTranslation().y; + // qCDebug(interfaceapp) << "sensor space head pos " << sensorHeadPoseDebug.getTranslation().y; if ((acosHead > 0.98f) && !getIsInSittingState() && (sensorHeadPoseDebug.getTranslation().y < -0.5f)) { //qCDebug(interfaceapp) << "we are going to sitting state because it looks like we should" << sensorHeadPoseDebug.getTranslation().y; } @@ -3854,6 +3854,7 @@ void MyAvatar::setIsInWalkingState(bool isWalking) { void MyAvatar::setIsInSittingState(bool isSitting) { _isInSittingState = isSitting; + emit sittingEnabledChanged(isSitting); } void MyAvatar::setWalkSpeed(float value) { @@ -4098,9 +4099,9 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl if (offset.y < SITTING_BOTTOM) { // we recenter when sitting. return true; - } else if (offset.y > CYLINDER_TOP) { + } else if (offset.y > 2.0*CYLINDER_TOP) { // if we recenter upwards then no longer in sitting state - // myAvatar.setIsInSittingState(false); + myAvatar.setIsInSittingState(false); return true; } else { return false; @@ -4126,7 +4127,12 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat activate(Vertical); _squatCount = 0; } else { - activate(Horizontal); + if (myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPos.y - 0.20f)) { + myAvatar.setIsInSittingState(true); + activate(Vertical); + } else { + activate(Horizontal); + } _squatCount = 0; } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 4ccb1a8d75..d9744e93fe 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -239,6 +239,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(float walkSpeed READ getWalkSpeed WRITE setWalkSpeed); Q_PROPERTY(float walkBackwardSpeed READ getWalkBackwardSpeed WRITE setWalkBackwardSpeed); Q_PROPERTY(float sprintSpeed READ getSprintSpeed WRITE setSprintSpeed); + Q_PROPERTY(bool isInSittingState READ getIsInSittingState WRITE setIsInSittingState); const QString DOMINANT_LEFT_HAND = "left"; const QString DOMINANT_RIGHT_HAND = "right"; @@ -1506,6 +1507,14 @@ signals: */ void disableHandTouchForIDChanged(const QUuid& entityID, bool disable); + /**jsdoc + * Triggered when the sit state is enabled or disabled + * @function MyAvatar.sittingEnabledChanged + * @param {boolean} enabled + * @returns {Signal} + */ + void sittingEnabledChanged(bool enabled); + private slots: void leaveDomain(); void updateCollisionCapsuleCache(); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 914a3b7c6e..51c51b6a20 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1618,6 +1618,7 @@ void Avatar::getCapsule(glm::vec3& start, glm::vec3& end, float& radius) { glm::vec3 Avatar::getWorldFeetPosition() { ShapeInfo shapeInfo; + computeShapeInfo(shapeInfo); glm::vec3 halfExtents = shapeInfo.getHalfExtents(); // x = radius, y = halfHeight glm::vec3 localFeet(0.0f, shapeInfo.getOffset().y - halfExtents.y - halfExtents.x, 0.0f); diff --git a/scripts/developer/objectOrientedStep.js b/scripts/developer/objectOrientedStep.js new file mode 100644 index 0000000000..a5c27e36b9 --- /dev/null +++ b/scripts/developer/objectOrientedStep.js @@ -0,0 +1,688 @@ +/* jslint bitwise: true */ + +/* global Script, Vec3, MyAvatar, Tablet, Messages, Quat, +DebugDraw, Mat4, Entities, Xform, Controller, Camera, console, document*/ + +Script.registerValue("STEPAPP", true); +var CENTIMETERSPERMETER = 100.0; +var LEFT = 0; +var RIGHT = 1; +var INCREASING = 1.0; +var DECREASING = -1.0; +var DEFAULT_AVATAR_HEIGHT = 1.64; +var TABLET_BUTTON_NAME = "STEP"; +var CHANGE_OF_BASIS_ROTATION = { x: 0, y: 1, z: 0, w: 0 }; +// in meters (mostly) +var DEFAULT_ANTERIOR = 0.04; +var DEFAULT_POSTERIOR = 0.06; +var DEFAULT_LATERAL = 0.10; +var DEFAULT_HEIGHT_DIFFERENCE = 0.02; +var DEFAULT_ANGULAR_VELOCITY = 0.3; +var DEFAULT_HAND_VELOCITY = 0.4; +var DEFAULT_ANGULAR_HAND_VELOCITY = 3.3; +var DEFAULT_HEAD_VELOCITY = 0.14; +var DEFAULT_LEVEL_PITCH = 7; +var DEFAULT_LEVEL_ROLL = 7; +var DEFAULT_DIFF = 0.0; +var DEFAULT_DIFF_EULERS = { x: 0.0, y: 0.0, z: 0.0 }; +var DEFAULT_HIPS_POSITION; +var DEFAULT_HEAD_POSITION; +var DEFAULT_TORSO_LENGTH; +var SPINE_STRETCH_LIMIT = 0.02; + +var VELOCITY_EPSILON = 0.02; +var AVERAGING_RATE = 0.03; +var HEIGHT_AVERAGING_RATE = 0.01; +var STEP_TIME_SECS = 0.2; +var MODE_SAMPLE_LENGTH = 100; +var RESET_MODE = false; +var HEAD_TURN_THRESHOLD = 25.0; +var NO_SHARED_DIRECTION = -0.98; +var LOADING_DELAY = 500; +var FAILSAFE_TIMEOUT = 2.5; + +var debugDrawBase = true; +var activated = false; +var documentLoaded = false; +var failsafeFlag = false; +var failsafeSignalTimer = -1.0; +var stepTimer = -1.0; + + +var modeArray = new Array(MODE_SAMPLE_LENGTH); +var modeHeight = -10.0; + +var handPosition; +var handOrientation; +var hands = []; +var hipToHandAverage = []; +var handDotHead = []; +var headAverageOrientation = MyAvatar.orientation; +var headPoseAverageOrientation = { x: 0, y: 0, z: 0, w: 1 }; +var averageHeight = 1.0; +var headEulers = { x: 0.0, y: 0.0, z: 0.0 }; +var headAverageEulers = { x: 0.0, y: 0.0, z: 0.0 }; +var headAveragePosition = { x: 0, y: 0.4, z: 0 }; +var frontLeft = { x: -DEFAULT_LATERAL, y: 0, z: -DEFAULT_ANTERIOR }; +var frontRight = { x: DEFAULT_LATERAL, y: 0, z: -DEFAULT_ANTERIOR }; +var backLeft = { x: -DEFAULT_LATERAL, y: 0, z: DEFAULT_POSTERIOR }; +var backRight = { x: DEFAULT_LATERAL, y: 0, z: DEFAULT_POSTERIOR }; + + +// define state readings constructor +function StateReading(headPose, rhandPose, lhandPose, backLength, diffFromMode, diffFromAverageHeight, diffFromAveragePosition, + diffFromAverageEulers) { + this.headPose = headPose; + this.rhandPose = rhandPose; + this.lhandPose = lhandPose; + this.backLength = backLength; + this.diffFromMode = diffFromMode; + this.diffFromAverageHeight = diffFromAverageHeight; + this.diffFromAveragePosition = diffFromAveragePosition; + this.diffFromAverageEulers = diffFromAverageEulers; +} + +// define current state readings object for holding tracker readings and current differences from averages +var currentStateReadings = new StateReading(Controller.getPoseValue(Controller.Standard.Head), + Controller.getPoseValue(Controller.Standard.RightHand), Controller.getPoseValue(Controller.Standard.LeftHand), + DEFAULT_TORSO_LENGTH, DEFAULT_DIFF, DEFAULT_DIFF, DEFAULT_DIFF, DEFAULT_DIFF_EULERS); + +// declare the checkbox constructor +function AppCheckbox(type,id,eventType,isChecked) { + this.type = type; + this.id = id; + this.eventType = eventType; + this.data = {value: isChecked}; +} + +// define the checkboxes in the html file +var usingAverageHeight = new AppCheckbox("checkboxtick", "runningAverageHeightCheck", "onRunningAverageHeightCheckBox", + false); +var usingModeHeight = new AppCheckbox("checkboxtick","modeCheck","onModeCheckBox",true); +var usingBaseOfSupport = new AppCheckbox("checkboxtick","baseOfSupportCheck","onBaseOfSupportCheckBox",true); +var usingAverageHeadPosition = new AppCheckbox("checkboxtick", "headAveragePositionCheck", "onHeadAveragePositionCheckBox", + false); + +var checkBoxArray = new Array(usingAverageHeight,usingModeHeight,usingBaseOfSupport,usingAverageHeadPosition); + +// declare the html slider constructor +function AppProperty(name, type, eventType, signalType, setFunction, initValue, convertToThreshold, convertToSlider, signalOn) { + this.name = name; + this.type = type; + this.eventType = eventType; + this.signalType = signalType; + this.setValue = setFunction; + this.value = initValue; + this.get = function () { + return this.value; + }; + this.convertToThreshold = convertToThreshold; + this.convertToSlider = convertToSlider; + this.signalOn = signalOn; +} + +// define the sliders +var frontBaseProperty = new AppProperty("#anteriorBase-slider", "slider", "onAnteriorBaseSlider", "frontSignal", + setAnteriorDistance, DEFAULT_ANTERIOR, function (num) { + return convertToMeters(num); + }, function (num) { + return convertToCentimeters(num); + },true); +var backBaseProperty = new AppProperty("#posteriorBase-slider", "slider", "onPosteriorBaseSlider", "backSignal", + setPosteriorDistance, DEFAULT_POSTERIOR, function (num) { + return convertToMeters(num); + }, function (num) { + return convertToCentimeters(num); + }, true); +var lateralBaseProperty = new AppProperty("#lateralBase-slider", "slider", "onLateralBaseSlider", "lateralSignal", + setLateralDistance, DEFAULT_LATERAL, function (num) { + return convertToMeters(num); + }, function (num) { + return convertToCentimeters(num); + }, true); +var headAngularVelocityProperty = new AppProperty("#angularVelocityHead-slider", "slider", "onAngularVelocitySlider", + "angularHeadSignal", setAngularThreshold, DEFAULT_ANGULAR_VELOCITY, function (num) { + var base = 4; + var shift = 2; + return convertExponential(base, num, DECREASING, shift); + }, function (num) { + var base = 4; + var shift = 2; + return convertLog(base, num, DECREASING, shift); + }, true); +var heightDifferenceProperty = new AppProperty("#heightDifference-slider", "slider", "onHeightDifferenceSlider", "heightSignal", + setHeightThreshold, DEFAULT_HEIGHT_DIFFERENCE, function (num) { + return convertToMeters(-num); + }, function (num) { + return convertToCentimeters(-num); + }, true); +var handsVelocityProperty = new AppProperty("#handsVelocity-slider", "slider", "onHandsVelocitySlider", "handVelocitySignal", + setHandVelocityThreshold, DEFAULT_HAND_VELOCITY, function (num) { + return num; + }, function (num) { + return num; + }, true); +var handsAngularVelocityProperty = new AppProperty("#handsAngularVelocity-slider", "slider", "onHandsAngularVelocitySlider", + "handAngularSignal", setHandAngularVelocityThreshold, DEFAULT_ANGULAR_HAND_VELOCITY, function (num) { + var base = 7; + var shift = 2; + return convertExponential(base, num, DECREASING, shift); + }, function (num) { + var base = 7; + var shift = 2; + return convertLog(base, num, DECREASING, shift); + }, true); +var headVelocityProperty = new AppProperty("#headVelocity-slider", "slider", "onHeadVelocitySlider", "headVelocitySignal", + setHeadVelocityThreshold, DEFAULT_HEAD_VELOCITY, function (num) { + var base = 2; + var shift = 0; + return convertExponential(base, num, INCREASING, shift); + }, function (num) { + var base = 2; + var shift = 0; + return convertLog(base, num, INCREASING, shift); + }, true); +var headPitchProperty = new AppProperty("#headPitch-slider", "slider", "onHeadPitchSlider", "headPitchSignal", + setHeadPitchThreshold, DEFAULT_LEVEL_PITCH, function (num) { + var base = 2.5; + var shift = 5; + return convertExponential(base, num, DECREASING, shift); + }, function (num) { + var base = 2.5; + var shift = 5; + return convertLog(base, num, DECREASING, shift); + }, true); +var headRollProperty = new AppProperty("#headRoll-slider", "slider", "onHeadRollSlider", "headRollSignal", setHeadRollThreshold, + DEFAULT_LEVEL_ROLL, function (num) { + var base = 2.5; + var shift = 5; + return convertExponential(base, num, DECREASING, shift); + }, function (num) { + var base = 2.5; + var shift = 5; + return convertLog(base, num, DECREASING, shift); + }, true); + +var propArray = new Array(frontBaseProperty, backBaseProperty, lateralBaseProperty, headAngularVelocityProperty, + heightDifferenceProperty, handsVelocityProperty, handsAngularVelocityProperty, headVelocityProperty, headPitchProperty, + headRollProperty); + +// var HTML_URL = Script.resolvePath("http://hifi-content.s3.amazonaws.com/angus/stepApp/stepApp.html"); +var HTML_URL = Script.resolvePath("http://hifi-content.s3.amazonaws.com/angus/stepApp/stepAppExtra.html"); +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + +function manageClick() { + if (activated) { + tablet.gotoHomeScreen(); + } else { + tablet.gotoWebScreen(HTML_URL); + } +} + +var tabletButton = tablet.addButton({ + text: TABLET_BUTTON_NAME, + icon: Script.resolvePath("http://hifi-content.s3.amazonaws.com/angus/stepApp/foot.svg"), + activeIcon: Script.resolvePath("http://hifi-content.s3.amazonaws.com/angus/stepApp/foot.svg") +}); + +function drawBase() { + // transform corners into world space, for rendering. + var worldPointLf = Vec3.sum(MyAvatar.position,Vec3.multiplyQbyV(MyAvatar.orientation, frontLeft)); + var worldPointRf = Vec3.sum(MyAvatar.position,Vec3.multiplyQbyV(MyAvatar.orientation, frontRight)); + var worldPointLb = Vec3.sum(MyAvatar.position,Vec3.multiplyQbyV(MyAvatar.orientation, backLeft)); + var worldPointRb = Vec3.sum(MyAvatar.position,Vec3.multiplyQbyV(MyAvatar.orientation, backRight)); + + var GREEN = { r: 0, g: 1, b: 0, a: 1 }; + // draw border + DebugDraw.drawRay(worldPointLf, worldPointRf, GREEN); + DebugDraw.drawRay(worldPointRf, worldPointRb, GREEN); + DebugDraw.drawRay(worldPointRb, worldPointLb, GREEN); + DebugDraw.drawRay(worldPointLb, worldPointLf, GREEN); +} + +function onKeyPress(event) { + if (event.text === "'") { + // when the sensors are reset, then reset the mode. + RESET_MODE = false; + } +} + +function onWebEventReceived(msg) { + var message = JSON.parse(msg); + print(" we have a message from html dialog " + message.type); + propArray.forEach(function (prop) { + if (prop.eventType === message.type) { + prop.setValue(prop.convertToThreshold(message.data.value)); + print("message from " + prop.name); + // break; + } + }); + checkBoxArray.forEach(function(cbox) { + if (cbox.eventType === message.type) { + cbox.data.value = message.data.value; + // break; + } + }); + if (message.type === "onCreateStepApp") { + print("document loaded"); + documentLoaded = true; + Script.setTimeout(initAppForm, LOADING_DELAY); + } +} + +function initAppForm() { + print("step app is loaded: " + documentLoaded); + if (documentLoaded === true) { + propArray.forEach(function (prop) { + tablet.emitScriptEvent(JSON.stringify({ + "type": "trigger", + "id": prop.signalType, + "data": { "value": "green" } + })); + tablet.emitScriptEvent(JSON.stringify({ + "type": "slider", + "id": prop.name, + "data": { "value": prop.convertToSlider(prop.value) } + })); + }); + checkBoxArray.forEach(function (cbox) { + tablet.emitScriptEvent(JSON.stringify({ + "type": "checkboxtick", + "id": cbox.id, + "data": { "value": cbox.data.value } + })); + }); + } +} + +function updateSignalColors() { + + // force the updates by running the threshold comparisons + withinBaseOfSupport(currentStateReadings.headPose.translation); + withinThresholdOfStandingHeightMode(currentStateReadings.diffFromMode); + headAngularVelocityBelowThreshold(currentStateReadings.headPose.angularVelocity); + handDirectionMatchesHeadDirection(currentStateReadings.lhandPose, currentStateReadings.rhandPose); + handAngularVelocityBelowThreshold(currentStateReadings.lhandPose, currentStateReadings.rhandPose); + headVelocityGreaterThanThreshold(Vec3.length(currentStateReadings.headPose.velocity)); + headMovedAwayFromAveragePosition(currentStateReadings.diffFromAveragePosition); + headLowerThanHeightAverage(currentStateReadings.diffFromAverageHeight); + isHeadLevel(currentStateReadings.diffFromAverageEulers); + + propArray.forEach(function (prop) { + if (prop.signalOn) { + tablet.emitScriptEvent(JSON.stringify({ "type": "trigger", "id": prop.signalType, "data": { "value": "green" } })); + } else { + tablet.emitScriptEvent(JSON.stringify({ "type": "trigger", "id": prop.signalType, "data": { "value": "red" } })); + } + }); +} + +function onScreenChanged(type, url) { + print("Screen changed"); + if (type === "Web" && url === HTML_URL) { + if (!activated) { + // hook up to event bridge + tablet.webEventReceived.connect(onWebEventReceived); + print("after connect web event"); + MyAvatar.hmdLeanRecenterEnabled = false; + + } + activated = true; + } else { + if (activated) { + // disconnect from event bridge + tablet.webEventReceived.disconnect(onWebEventReceived); + documentLoaded = false; + } + activated = false; + } +} + +function getLog(x, y) { + return Math.log(y) / Math.log(x); +} + +function noConversion(num) { + return num; +} + +function convertLog(base, num, direction, shift) { + return direction * getLog(base, (num + 1.0)) + shift; +} + +function convertExponential(base, num, direction, shift) { + return Math.pow(base, (direction*num + shift)) - 1.0; +} + +function convertToCentimeters(num) { + return num * CENTIMETERSPERMETER; +} + +function convertToMeters(num) { + print("convert to meters " + num); + return num / CENTIMETERSPERMETER; +} + +function isInsideLine(a, b, c) { + return (((b.x - a.x)*(c.z - a.z) - (b.z - a.z)*(c.x - a.x)) > 0); +} + +function setAngularThreshold(num) { + headAngularVelocityProperty.value = num; + print("angular threshold " + headAngularVelocityProperty.get()); +} + +function setHeadRollThreshold(num) { + headRollProperty.value = num; + print("head roll threshold " + headRollProperty.get()); +} + +function setHeadPitchThreshold(num) { + headPitchProperty.value = num; + print("head pitch threshold " + headPitchProperty.get()); +} + +function setHeightThreshold(num) { + heightDifferenceProperty.value = num; + print("height threshold " + heightDifferenceProperty.get()); +} + +function setLateralDistance(num) { + lateralBaseProperty.value = num; + frontLeft.x = -lateralBaseProperty.get(); + frontRight.x = lateralBaseProperty.get(); + backLeft.x = -lateralBaseProperty.get(); + backRight.x = lateralBaseProperty.get(); + print("lateral distance " + lateralBaseProperty.get()); +} + +function setAnteriorDistance(num) { + frontBaseProperty.value = num; + frontLeft.z = -frontBaseProperty.get(); + frontRight.z = -frontBaseProperty.get(); + print("anterior distance " + frontBaseProperty.get()); +} + +function setPosteriorDistance(num) { + backBaseProperty.value = num; + backLeft.z = backBaseProperty.get(); + backRight.z = backBaseProperty.get(); + print("posterior distance " + backBaseProperty.get()); +} + +function setHandAngularVelocityThreshold(num) { + handsAngularVelocityProperty.value = num; + print("hand angular velocity threshold " + handsAngularVelocityProperty.get()); +} + +function setHandVelocityThreshold(num) { + handsVelocityProperty.value = num; + print("hand velocity threshold " + handsVelocityProperty.get()); +} + +function setHeadVelocityThreshold(num) { + headVelocityProperty.value = num; + print("headvelocity threshold " + headVelocityProperty.get()); +} + +function withinBaseOfSupport(pos) { + var userScale = 1.0; + frontBaseProperty.signalOn = !(isInsideLine(Vec3.multiply(userScale, frontLeft), Vec3.multiply(userScale, frontRight), pos)); + backBaseProperty.signalOn = !(isInsideLine(Vec3.multiply(userScale, backRight), Vec3.multiply(userScale, backLeft), pos)); + lateralBaseProperty.signalOn = !(isInsideLine(Vec3.multiply(userScale, frontRight), Vec3.multiply(userScale, backRight), pos) + && isInsideLine(Vec3.multiply(userScale, backLeft), Vec3.multiply(userScale, frontLeft), pos)); + return (!frontBaseProperty.signalOn && !backBaseProperty.signalOn && !lateralBaseProperty.signalOn); +} + +function withinThresholdOfStandingHeightMode(heightDiff) { + if (usingModeHeight.data.value) { + heightDifferenceProperty.signalOn = heightDiff < heightDifferenceProperty.get(); + return heightDifferenceProperty.signalOn; + } else { + return true; + } +} + +function headAngularVelocityBelowThreshold(headAngularVelocity) { + var angVel = Vec3.length({ x: headAngularVelocity.x, y: 0, z: headAngularVelocity.z }); + headAngularVelocityProperty.signalOn = angVel < headAngularVelocityProperty.get(); + return headAngularVelocityProperty.signalOn; +} + +function handDirectionMatchesHeadDirection(lhPose, rhPose) { + handsVelocityProperty.signalOn = ((handsVelocityProperty.get() < NO_SHARED_DIRECTION) || + ((!lhPose.valid || ((handDotHead[LEFT] > handsVelocityProperty.get()) && + (Vec3.length(lhPose.velocity) > VELOCITY_EPSILON))) && + (!rhPose.valid || ((handDotHead[RIGHT] > handsVelocityProperty.get()) && + (Vec3.length(rhPose.velocity) > VELOCITY_EPSILON))))); + return handsVelocityProperty.signalOn; +} + +function handAngularVelocityBelowThreshold(lhPose, rhPose) { + var xzRHandAngularVelocity = Vec3.length({ x: rhPose.angularVelocity.x, y: 0.0, z: rhPose.angularVelocity.z }); + var xzLHandAngularVelocity = Vec3.length({ x: lhPose.angularVelocity.x, y: 0.0, z: lhPose.angularVelocity.z }); + handsAngularVelocityProperty.signalOn = ((!rhPose.valid ||(xzRHandAngularVelocity < handsAngularVelocityProperty.get())) + && (!lhPose.valid || (xzLHandAngularVelocity < handsAngularVelocityProperty.get()))); + return handsAngularVelocityProperty.signalOn; +} + +function headVelocityGreaterThanThreshold(headVel) { + headVelocityProperty.signalOn = (headVel > headVelocityProperty.get()) || (headVelocityProperty.get() < VELOCITY_EPSILON); + return headVelocityProperty.signalOn; +} + +function headMovedAwayFromAveragePosition(headDelta) { + return !withinBaseOfSupport(headDelta) || !usingAverageHeadPosition.data.value; +} + +function headLowerThanHeightAverage(heightDiff) { + if (usingAverageHeight.data.value) { + print("head lower than height average"); + heightDifferenceProperty.signalOn = heightDiff < heightDifferenceProperty.get(); + return heightDifferenceProperty.signalOn; + } else { + return true; + } +} + +function isHeadLevel(diffEulers) { + headRollProperty.signalOn = Math.abs(diffEulers.z) < headRollProperty.get(); + headPitchProperty.signalOn = Math.abs(diffEulers.x) < headPitchProperty.get(); + return (headRollProperty.signalOn && headPitchProperty.signalOn); +} + +function findAverage(arr) { + var sum = arr.reduce(function (acc, val) { + return acc + val; + },0); + return sum / arr.length; +} + +function addToModeArray(arr,num) { + for (var i = 0 ;i < (arr.length - 1); i++) { + arr[i] = arr[i+1]; + } + arr[arr.length - 1] = (Math.floor(num*CENTIMETERSPERMETER))/CENTIMETERSPERMETER; +} + +function findMode(ary, currentMode, backLength, defaultBack, currentHeight) { + var numMapping = {}; + var greatestFreq = 0; + var mode; + ary.forEach(function (number) { + numMapping[number] = (numMapping[number] || 0) + 1; + if ((greatestFreq < numMapping[number]) || ((numMapping[number] === MODE_SAMPLE_LENGTH) && (number > currentMode) )) { + greatestFreq = numMapping[number]; + mode = number; + } + }); + if (mode > currentMode) { + return Number(mode); + } else { + if (!RESET_MODE && HMD.active) { + print("resetting the mode............................................. "); + print("resetting the mode............................................. "); + RESET_MODE = true; + var correction = 0.02; + return currentHeight - correction; + } else { + return currentMode; + } + } +} + +function update(dt) { + if (debugDrawBase) { + drawBase(); + } + // Update current state information + currentStateReadings.headPose = Controller.getPoseValue(Controller.Standard.Head); + currentStateReadings.rhandPose = Controller.getPoseValue(Controller.Standard.RightHand); + currentStateReadings.lhandPose = Controller.getPoseValue(Controller.Standard.LeftHand); + + // back length + var headMinusHipLean = Vec3.subtract(currentStateReadings.headPose.translation, DEFAULT_HIPS_POSITION); + currentStateReadings.backLength = Vec3.length(headMinusHipLean); + // print("back length and default " + currentStateReadings.backLength + " " + DEFAULT_TORSO_LENGTH); + + // mode height + addToModeArray(modeArray, currentStateReadings.headPose.translation.y); + modeHeight = findMode(modeArray, modeHeight, currentStateReadings.backLength, DEFAULT_TORSO_LENGTH, + currentStateReadings.headPose.translation.y); + currentStateReadings.diffFromMode = modeHeight - currentStateReadings.headPose.translation.y; + + // hand direction + var leftHandLateralPoseVelocity = currentStateReadings.lhandPose.velocity; + leftHandLateralPoseVelocity.y = 0.0; + var rightHandLateralPoseVelocity = currentStateReadings.rhandPose.velocity; + rightHandLateralPoseVelocity.y = 0.0; + var headLateralPoseVelocity = currentStateReadings.headPose.velocity; + headLateralPoseVelocity.y = 0.0; + handDotHead[LEFT] = Vec3.dot(Vec3.normalize(leftHandLateralPoseVelocity), Vec3.normalize(headLateralPoseVelocity)); + handDotHead[RIGHT] = Vec3.dot(Vec3.normalize(rightHandLateralPoseVelocity), Vec3.normalize(headLateralPoseVelocity)); + + // average head position + headAveragePosition = Vec3.mix(headAveragePosition, currentStateReadings.headPose.translation, AVERAGING_RATE); + currentStateReadings.diffFromAveragePosition = Vec3.subtract(currentStateReadings.headPose.translation, + headAveragePosition); + + // average height + averageHeight = currentStateReadings.headPose.translation.y * HEIGHT_AVERAGING_RATE + + averageHeight * (1.0 - HEIGHT_AVERAGING_RATE); + currentStateReadings.diffFromAverageHeight = Math.abs(currentStateReadings.headPose.translation.y - averageHeight); + + // eulers diff + headEulers = Quat.safeEulerAngles(currentStateReadings.headPose.rotation); + headAverageOrientation = Quat.slerp(headAverageOrientation, currentStateReadings.headPose.rotation, AVERAGING_RATE); + headAverageEulers = Quat.safeEulerAngles(headAverageOrientation); + currentStateReadings.diffFromAverageEulers = Vec3.subtract(headAverageEulers, headEulers); + + // headpose rig space is for determining when to recenter rotation. + var headPoseRigSpace = Quat.multiply(CHANGE_OF_BASIS_ROTATION, currentStateReadings.headPose.rotation); + headPoseAverageOrientation = Quat.slerp(headPoseAverageOrientation, headPoseRigSpace, AVERAGING_RATE); + var headPoseAverageEulers = Quat.safeEulerAngles(headPoseAverageOrientation); + + // make the signal colors reflect the current thresholds that have been crossed + updateSignalColors(); + + SPINE_STRETCH_LIMIT = (0.04) * DEFAULT_TORSO_LENGTH * MyAvatar.scale; + + //print("the spine stretch limit is " + SPINE_STRETCH_LIMIT + " head avatar space is " + currentStateReadings.headPose.translation.y); + //print("the current back length is " + currentStateReadings.backLength + " " + DEFAULT_TORSO_LENGTH); + // Conditions for taking a step. + // 1. off the base of support. front, lateral, back edges. + // 2. head is not lower than the height mode value by more than the maxHeightChange tolerance + // 3. the angular velocity of the head is not greater than the threshold value + // ie this reflects the speed the head is rotating away from having up = (0,1,0) in Avatar frame.. + // 4. the hands velocity vector has the same direction as the head, within the given tolerance + // the tolerance is an acos value, -1 means the hands going in any direction will not block translating + // up to 1 where the hands velocity direction must exactly match that of the head. -1 threshold disables this condition. + // 5. the angular velocity xz magnitude for each hand is below the threshold value + // ie here this reflects the speed that each hand is rotating away from having up = (0,1,0) in Avatar frame. + // 6. head velocity is below step threshold + // 7. head has moved further than the threshold from the running average position of the head. + // 8. head height is not lower than the running average head height with a difference of maxHeightChange. + // 9. head's rotation in avatar space is not pitching or rolling greater than the pitch or roll thresholds + if (!withinBaseOfSupport(currentStateReadings.headPose.translation) && + withinThresholdOfStandingHeightMode(currentStateReadings.diffFromMode) && + headAngularVelocityBelowThreshold(currentStateReadings.headPose.angularVelocity) && + handDirectionMatchesHeadDirection(currentStateReadings.lhandPose, currentStateReadings.rhandPose) && + handAngularVelocityBelowThreshold(currentStateReadings.lhandPose, currentStateReadings.rhandPose) && + headVelocityGreaterThanThreshold(Vec3.length(currentStateReadings.headPose.velocity)) && + headMovedAwayFromAveragePosition(currentStateReadings.diffFromAveragePosition) && + headLowerThanHeightAverage(currentStateReadings.diffFromAverageHeight) && + isHeadLevel(currentStateReadings.diffFromAverageEulers)) { + + if (stepTimer < 0.0) { //!MyAvatar.isRecenteringHorizontally() + print("trigger recenter========================================================"); + MyAvatar.triggerHorizontalRecenter(); + stepTimer = STEP_TIME_SECS; + } + } else if ((currentStateReadings.backLength > (DEFAULT_TORSO_LENGTH + SPINE_STRETCH_LIMIT)) && + (failsafeSignalTimer < 0.0) && HMD.active) { + // do the failsafe recenter. + // failsafeFlag stops repeated setting of failsafe button color. + // RESET_MODE false forces a reset of the height + RESET_MODE = false; + failsafeFlag = true; + failsafeSignalTimer = FAILSAFE_TIMEOUT; + MyAvatar.triggerHorizontalRecenter(); + tablet.emitScriptEvent(JSON.stringify({ "type": "failsafe", "id": "failsafeSignal", "data": { "value": "green" } })); + // in fail safe we debug print the values that were blocking us. + print("failsafe debug---------------------------------------------------------------"); + propArray.forEach(function (prop) { + print(prop.name); + if (!prop.signalOn) { + print(prop.signalType + " contributed to failsafe call"); + } + }); + print("end failsafe debug---------------------------------------------------------------"); + + } + + if ((failsafeSignalTimer < 0.0) && failsafeFlag) { + failsafeFlag = false; + tablet.emitScriptEvent(JSON.stringify({ "type": "failsafe", "id": "failsafeSignal", "data": { "value": "orange" } })); + } + + stepTimer -= dt; + failsafeSignalTimer -= dt; + + if (!HMD.active) { + RESET_MODE = false; + } + + if (Math.abs(headPoseAverageEulers.y) > HEAD_TURN_THRESHOLD) { + // Turn feet + // MyAvatar.triggerRotationRecenter(); + // headPoseAverageOrientation = { x: 0, y: 0, z: 0, w: 1 }; + } +} + +function shutdownTabletApp() { + // GlobalDebugger.stop(); + tablet.removeButton(tabletButton); + if (activated) { + tablet.webEventReceived.disconnect(onWebEventReceived); + tablet.gotoHomeScreen(); + } + tablet.screenChanged.disconnect(onScreenChanged); +} + +tabletButton.clicked.connect(manageClick); +tablet.screenChanged.connect(onScreenChanged); + +Script.setTimeout(function() { + DEFAULT_HIPS_POSITION = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(MyAvatar.getJointIndex("Hips")); + DEFAULT_HEAD_POSITION = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(MyAvatar.getJointIndex("Head")); + DEFAULT_TORSO_LENGTH = Vec3.length(Vec3.subtract(DEFAULT_HEAD_POSITION, DEFAULT_HIPS_POSITION)); + SPINE_STRETCH_LIMIT = (0.04) * DEFAULT_TORSO_LENGTH * MyAvatar.scale; +},(4*LOADING_DELAY)); + +Script.update.connect(update); +Controller.keyPressEvent.connect(onKeyPress); +Script.scriptEnding.connect(function () { + MyAvatar.hmdLeanRecenterEnabled = true; + Script.update.disconnect(update); + shutdownTabletApp(); +}); diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 10ccb66d96..ee4c6736a2 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -63,7 +63,8 @@ function getMyAvatar() { function getMyAvatarSettings() { return { dominantHand: MyAvatar.getDominantHand(), - collisionsEnabled : MyAvatar.getCollisionsEnabled(), + collisionsEnabled: MyAvatar.getCollisionsEnabled(), + sittingEnabled: MyAvatar.isInSittingState, collisionSoundUrl : MyAvatar.collisionSoundURL, animGraphUrl: MyAvatar.getAnimGraphUrl(), animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(), @@ -136,6 +137,13 @@ function onCollisionsEnabledChanged(enabled) { } } +function onSittingEnabledChanged(isSitting) { + if (currentAvatarSettings.sittingEnabled !== isSitting) { + currentAvatarSettings.sittingEnabled = isSitting; + sendToQml({ 'method': 'settingChanged', 'name': 'sittingEnabled', 'value': isSitting }) + } +} + function onNewCollisionSoundUrl(url) { if(currentAvatarSettings.collisionSoundUrl !== url) { currentAvatarSettings.collisionSoundUrl = url; @@ -314,9 +322,10 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See MyAvatar.setDominantHand(message.settings.dominantHand); MyAvatar.setCollisionsEnabled(message.settings.collisionsEnabled); + MyAvatar.isInSittingState = message.settings.sittingEnabled; MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl); - + print("save settings"); settings = getMyAvatarSettings(); break; default: @@ -507,6 +516,7 @@ function off() { MyAvatar.skeletonModelURLChanged.disconnect(onSkeletonModelURLChanged); MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); MyAvatar.collisionsEnabledChanged.disconnect(onCollisionsEnabledChanged); + MyAvatar.sittingEnabledChanged.disconnect(onSittingEnabledChanged); MyAvatar.newCollisionSoundURL.disconnect(onNewCollisionSoundUrl); MyAvatar.animGraphUrlChanged.disconnect(onAnimGraphUrlChanged); MyAvatar.targetScaleChanged.disconnect(onTargetScaleChanged); @@ -521,6 +531,7 @@ function on() { MyAvatar.skeletonModelURLChanged.connect(onSkeletonModelURLChanged); MyAvatar.dominantHandChanged.connect(onDominantHandChanged); MyAvatar.collisionsEnabledChanged.connect(onCollisionsEnabledChanged); + MyAvatar.sittingEnabledChanged.connect(onSittingEnabledChanged); MyAvatar.newCollisionSoundURL.connect(onNewCollisionSoundUrl); MyAvatar.animGraphUrlChanged.connect(onAnimGraphUrlChanged); MyAvatar.targetScaleChanged.connect(onTargetScaleChanged); From 42cb8a7ef0159f130c65d6a424bf8b54ae14323e Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 27 Sep 2018 16:59:54 -0700 Subject: [PATCH 028/362] avatar app changes --- interface/src/avatar/MyAvatar.cpp | 2 +- scripts/system/avatarapp.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 631a4b0670..d54616f245 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -477,7 +477,7 @@ void MyAvatar::update(float deltaTime) { auto sensorHeadPoseDebug = getControllerPoseInSensorFrame(controller::Action::HEAD); glm::vec3 upHead = transformVectorFast(sensorHeadPoseDebug.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); - // qCDebug(interfaceapp) << "sensor space head pos " << sensorHeadPoseDebug.getTranslation().y; + // qCDebug(interfaceapp) << "sensor space head pos " << sensorHeadPoseDebug.getTranslation().y; if ((acosHead > 0.98f) && !getIsInSittingState() && (sensorHeadPoseDebug.getTranslation().y < -0.5f)) { //qCDebug(interfaceapp) << "we are going to sitting state because it looks like we should" << sensorHeadPoseDebug.getTranslation().y; } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index ee4c6736a2..16761f29a6 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -140,6 +140,7 @@ function onCollisionsEnabledChanged(enabled) { function onSittingEnabledChanged(isSitting) { if (currentAvatarSettings.sittingEnabled !== isSitting) { currentAvatarSettings.sittingEnabled = isSitting; + print("emit sitting changed"); sendToQml({ 'method': 'settingChanged', 'name': 'sittingEnabled', 'value': isSitting }) } } From 7a0043c01002e359e291b931eaaa166de87eb846 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 28 Sep 2018 10:26:52 -0700 Subject: [PATCH 029/362] Send AvatarIdentity in NLPacketList; be more selective --- .../src/avatars/AvatarMixerSlave.cpp | 37 +++++++++++-------- .../src/avatars/AvatarMixerSlave.h | 2 +- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 5e1e1019e4..3834516b80 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -68,13 +68,13 @@ void AvatarMixerSlave::processIncomingPackets(const SharedNodePointer& node) { _stats.processIncomingPacketsElapsedTime += (end - start); } -int AvatarMixerSlave::sendIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode) { - if (destinationNode->getType() == NodeType::Agent && !destinationNode->isUpstream()) { +int AvatarMixerSlave::sendIdentityPacket(NLPacketList& packetList, const AvatarMixerClientData* nodeData, const Node& destinationNode) { + if (destinationNode.getType() == NodeType::Agent && !destinationNode.isUpstream()) { QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray(); individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious - auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true); - identityPackets->write(individualData); - DependencyManager::get()->sendPacketList(std::move(identityPackets), *destinationNode); + packetList.startSegment(); + packetList.write(individualData); + packetList.endSegment(); _stats.numIdentityPackets++; return individualData.size(); } else { @@ -396,6 +396,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) const int avatarPacketCapacity = avatarPacket->getPayloadCapacity(); int avatarSpaceAvailable = avatarPacketCapacity; int numPacketsSent = 0; + auto identityPacketList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true); const auto& sortedAvatarVector = sortedAvatars.getSortedVector(numToSendEst); for (const auto& sortedAvatar : sortedAvatarVector) { @@ -425,16 +426,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) const AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); const AvatarData* otherAvatar = otherNodeData->getConstAvatarData(); - // If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO - // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A. - if (otherAvatar->hasProcessedFirstIdentity() - && nodeData->getLastBroadcastTime(otherNode->getLocalID()) <= otherNodeData->getIdentityChangeTimestamp()) { - identityBytesSent += sendIdentityPacket(otherNodeData, node); - - // remember the last time we sent identity details about this other node to the receiver - nodeData->setLastBroadcastTime(otherNode->getLocalID(), usecTimestampNow()); - } - // Typically all out-of-view avatars but such avatars' priorities will rise with time: bool isLowerPriority = sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD; @@ -444,6 +435,16 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) } else if (!overBudget) { detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO ? AvatarData::SendAllData : AvatarData::CullSmallData; nodeData->incrementAvatarInView(); + + // If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO + // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A. + if (otherAvatar->hasProcessedFirstIdentity() + && nodeData->getLastBroadcastTime(otherNode->getLocalID()) <= otherNodeData->getIdentityChangeTimestamp()) { + identityBytesSent += sendIdentityPacket(*identityPacketList, otherNodeData, *destinationNode); + + // remember the last time we sent identity details about this other node to the receiver + nodeData->setLastBroadcastTime(otherNode->getLocalID(), usecTimestampNow()); + } } QVector& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getLocalID()); @@ -520,6 +521,12 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) nodeList->sendPacketList(std::move(traitsPacketList), *destinationNode); } + // Send any AvatarIdentity packets: + identityPacketList->closeCurrentPacket(); + if (identityBytesSent > 0) { + nodeList->sendPacketList(std::move(identityPacketList), *destinationNode); + } + // record the number of avatars held back this frame nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack); nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames); diff --git a/assignment-client/src/avatars/AvatarMixerSlave.h b/assignment-client/src/avatars/AvatarMixerSlave.h index bcb70f8743..2ef90af38e 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.h +++ b/assignment-client/src/avatars/AvatarMixerSlave.h @@ -101,7 +101,7 @@ public: void harvestStats(AvatarMixerSlaveStats& stats); private: - int sendIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode); + int sendIdentityPacket(NLPacketList& packet, const AvatarMixerClientData* nodeData, const Node& destinationNode); int sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode); qint64 addChangedTraitsToBulkPacket(AvatarMixerClientData* listeningNodeData, From 884ad66a146541fba08975af43e8cbfe1e46cd16 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 28 Sep 2018 17:18:24 -0700 Subject: [PATCH 030/362] added sensor space detection of height of the user in MyAvatar.cpp 1.2 * average sensor space height changes to standing. .833 sensorspace height changes to sitting. there is also a manual override in the avatar app settings now --- interface/src/avatar/MyAvatar.cpp | 38 +++++++++++++++++++++++++------ interface/src/avatar/MyAvatar.h | 4 +++- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index d54616f245..819778b1e7 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -475,6 +475,12 @@ void MyAvatar::update(float deltaTime) { // if the head is close to the floor in sensor space // and the up of the head is close to sensor up then try switching us to sitting state. auto sensorHeadPoseDebug = getControllerPoseInSensorFrame(controller::Action::HEAD); + if (sensorHeadPoseDebug.isValid()) { + _sumUserHeightSensorSpace += sensorHeadPoseDebug.getTranslation().y; + _averageUserHeightCount++; + } + qCDebug(interfaceapp) << sensorHeadPoseDebug.isValid() << " valid. sensor space average " << (_sumUserHeightSensorSpace / _averageUserHeightCount) << " head position sensor y value " << sensorHeadPoseDebug.getTranslation().y; + glm::vec3 upHead = transformVectorFast(sensorHeadPoseDebug.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); // qCDebug(interfaceapp) << "sensor space head pos " << sensorHeadPoseDebug.getTranslation().y; @@ -3854,6 +3860,9 @@ void MyAvatar::setIsInWalkingState(bool isWalking) { void MyAvatar::setIsInSittingState(bool isSitting) { _isInSittingState = isSitting; + controller::Pose sensorHeadPoseDebug = getControllerPoseInSensorFrame(controller::Action::HEAD); + _sumUserHeightSensorSpace = sensorHeadPoseDebug.getTranslation().y; + _averageUserHeightCount = 1; emit sittingEnabledChanged(isSitting); } @@ -4093,21 +4102,36 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl glm::vec3 headWorldSpace = myAvatar.getHead()->getPosition(); glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar.getSensorToWorldMatrix()); glm::vec3 headSensorSpace = transformVectorFast(myAvatar.getSensorToWorldMatrix(), headWorldSpace); - //qCDebug(interfaceapp) << "sensor space position " << extractTranslation(currentBodyMatrix) << " head position sensor " << sensorHeadPose.getTranslation(); + //get the mode. + //put it in sensor space. + // if we are 20% higher switch to standing. + // 16.6% lower then switch to sitting. + // add this !!!! And the head is upright. + float averageSensorSpaceHeight = myAvatar._sumUserHeightSensorSpace / myAvatar._averageUserHeightCount; if (myAvatar.getIsInSittingState()) { if (offset.y < SITTING_BOTTOM) { // we recenter when sitting. return true; - } else if (offset.y > 2.0*CYLINDER_TOP) { + } else if (sensorHeadPose.getTranslation().y > (1.2f * averageSensorSpaceHeight)) { // if we recenter upwards then no longer in sitting state myAvatar.setIsInSittingState(false); + //myAvatar._sumUserHeightSensorSpace = 1.2f * averageSensorSpaceHeight; + // myAvatar._averageUserHeightCount = 1; return true; } else { return false; } } else { - return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + // in the standing state + if (sensorHeadPose.getTranslation().y < (0.83f * averageSensorSpaceHeight)) { + myAvatar.setIsInSittingState(true); + // myAvatar._sumUserHeightSensorSpace = 0.83f * averageSensorSpaceHeight; + // myAvatar._averageUserHeightCount = 1; + return true; + } else { + return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + } } } @@ -4124,14 +4148,14 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat if ((_squatCount > 600) && !isActive(Vertical) && !isActive(Horizontal)) { if (myAvatar.getIsInSittingState()) { // activate(Horizontal); - activate(Vertical); + //activate(Vertical); _squatCount = 0; } else { if (myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPos.y - 0.20f)) { - myAvatar.setIsInSittingState(true); - activate(Vertical); + //myAvatar.setIsInSittingState(true); + //activate(Vertical); } else { - activate(Horizontal); + //activate(Horizontal); } _squatCount = 0; } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index d9744e93fe..628c358202 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1805,6 +1805,8 @@ private: // height of user in sensor space, when standing erect. ThreadSafeValueCache _userHeight { DEFAULT_AVATAR_HEIGHT }; + float _sumUserHeightSensorSpace { DEFAULT_AVATAR_HEIGHT }; + int _averageUserHeightCount { 1 }; void updateChildCauterization(SpatiallyNestablePointer object, bool cauterize); @@ -1814,7 +1816,7 @@ private: ThreadSafeValueCache _sprintSpeed { AVATAR_SPRINT_SPEED_SCALAR }; float _walkSpeedScalar { AVATAR_WALK_SPEED_SCALAR }; bool _isInWalkingState { false }; - bool _isInSittingState { true }; + bool _isInSittingState { false }; // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; From 18c1371321cea741e2c9b9ec6f49eef0759ca3b7 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 28 Sep 2018 17:53:06 -0700 Subject: [PATCH 031/362] Process multiple avatars in an AvatarIdentity message --- assignment-client/src/avatars/AvatarMixer.cpp | 2 +- libraries/avatars/src/AvatarData.cpp | 21 +++--- libraries/avatars/src/AvatarData.h | 2 +- libraries/avatars/src/AvatarHashMap.cpp | 70 ++++++++++--------- 4 files changed, 50 insertions(+), 45 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index ebd86b749b..168c95cb77 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -565,7 +565,7 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer mes // parse the identity packet and update the change timestamp if appropriate bool identityChanged = false; bool displayNameChanged = false; - avatar.processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged); + avatar.processAvatarIdentity(QDataStream(message->getMessage()), identityChanged, displayNameChanged); if (identityChanged) { QMutexLocker nodeDataLocker(&nodeData->getMutex()); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 7fb0f786ba..07751d03e3 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1767,11 +1767,9 @@ glm::quat AvatarData::getOrientationOutbound() const { return (getLocalOrientation()); } -void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& identityChanged, +void AvatarData::processAvatarIdentity(QDataStream& packetStream, bool& identityChanged, bool& displayNameChanged) { - QDataStream packetStream(identityData); - QUuid avatarSessionID; // peek the sequence number, this will tell us if we should be processing this identity packet at all @@ -1786,17 +1784,18 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide << (udt::SequenceNumber::Type) incomingSequenceNumber; } - if (incomingSequenceNumber > _identitySequenceNumber) { - Identity identity; + Identity identity; - packetStream - >> identity.attachmentData - >> identity.displayName - >> identity.sessionDisplayName - >> identity.isReplicated - >> identity.lookAtSnappingEnabled + packetStream + >> identity.attachmentData + >> identity.displayName + >> identity.sessionDisplayName + >> identity.isReplicated + >> identity.lookAtSnappingEnabled ; + if (incomingSequenceNumber > _identitySequenceNumber) { + // set the store identity sequence number to match the incoming identity _identitySequenceNumber = incomingSequenceNumber; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 3ca8bd4775..76b699a3d2 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -967,7 +967,7 @@ public: // identityChanged returns true if identity has changed, false otherwise. // identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange. - void processAvatarIdentity(const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged); + void processAvatarIdentity(QDataStream& packetStream, bool& identityChanged, bool& displayNameChanged); qint64 packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION); diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index d205a915f8..79ba5d06f9 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -70,7 +70,7 @@ void AvatarReplicas::processAvatarIdentity(const QUuid& parentID, const QByteArr if (_replicasMap.find(parentID) != _replicasMap.end()) { auto &replicas = _replicasMap[parentID]; for (auto avatar : replicas) { - avatar->processAvatarIdentity(identityData, identityChanged, displayNameChanged); + avatar->processAvatarIdentity(QDataStream(identityData), identityChanged, displayNameChanged); } } } @@ -266,41 +266,47 @@ AvatarSharedPointer AvatarHashMap::parseAvatarData(QSharedPointer message, SharedNodePointer sendingNode) { + QDataStream avatarIdentityStream(message->getMessage()); - // peek the avatar UUID from the incoming packet - QUuid identityUUID = QUuid::fromRfc4122(message->peek(NUM_BYTES_RFC4122_UUID)); + while (!avatarIdentityStream.atEnd()) { + // peek the avatar UUID from the incoming packet + avatarIdentityStream.startTransaction(); + QUuid identityUUID; + avatarIdentityStream >> identityUUID; + avatarIdentityStream.rollbackTransaction(); - if (identityUUID.isNull()) { - qCDebug(avatars) << "Refusing to process identity packet for null avatar ID"; - return; - } - - // make sure this isn't for an ignored avatar - auto nodeList = DependencyManager::get(); - static auto EMPTY = QUuid(); - - { - QReadLocker locker(&_hashLock); - _pendingAvatars.remove(identityUUID); - auto me = _avatarHash.find(EMPTY); - if ((me != _avatarHash.end()) && (identityUUID == me.value()->getSessionUUID())) { - // We add MyAvatar to _avatarHash with an empty UUID. Code relies on this. In order to correctly handle an - // identity packet for ourself (such as when we are assigned a sessionDisplayName by the mixer upon joining), - // we make things match here. - identityUUID = EMPTY; + if (identityUUID.isNull()) { + qCDebug(avatars) << "Refusing to process identity packet for null avatar ID"; + return; } - } - - if (!nodeList->isIgnoringNode(identityUUID) || nodeList->getRequestsDomainListData()) { - // mesh URL for a UUID, find avatar in our list - bool isNewAvatar; - auto avatar = newOrExistingAvatar(identityUUID, sendingNode, isNewAvatar); - bool identityChanged = false; - bool displayNameChanged = false; - // In this case, the "sendingNode" is the Avatar Mixer. - avatar->processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged); - _replicas.processAvatarIdentity(identityUUID, message->getMessage(), identityChanged, displayNameChanged); + // make sure this isn't for an ignored avatar + auto nodeList = DependencyManager::get(); + static auto EMPTY = QUuid(); + + { + QReadLocker locker(&_hashLock); + _pendingAvatars.remove(identityUUID); + auto me = _avatarHash.find(EMPTY); + if ((me != _avatarHash.end()) && (identityUUID == me.value()->getSessionUUID())) { + // We add MyAvatar to _avatarHash with an empty UUID. Code relies on this. In order to correctly handle an + // identity packet for ourself (such as when we are assigned a sessionDisplayName by the mixer upon joining), + // we make things match here. + identityUUID = EMPTY; + } + } + + if (!nodeList->isIgnoringNode(identityUUID) || nodeList->getRequestsDomainListData()) { + // mesh URL for a UUID, find avatar in our list + bool isNewAvatar; + auto avatar = newOrExistingAvatar(identityUUID, sendingNode, isNewAvatar); + bool identityChanged = false; + bool displayNameChanged = false; + // In this case, the "sendingNode" is the Avatar Mixer. + avatar->processAvatarIdentity(avatarIdentityStream, identityChanged, displayNameChanged); + _replicas.processAvatarIdentity(identityUUID, message->getMessage(), identityChanged, displayNameChanged); + + } } } From 4534b87ea07c934e927b70a6fd23c8d7f3d3dc84 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 28 Sep 2018 18:24:59 -0700 Subject: [PATCH 032/362] Fix error passing rvalue ref as lvalue --- libraries/avatars/src/AvatarHashMap.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 79ba5d06f9..e453f5d298 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -69,8 +69,9 @@ void AvatarReplicas::removeReplicas(const QUuid& parentID) { void AvatarReplicas::processAvatarIdentity(const QUuid& parentID, const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged) { if (_replicasMap.find(parentID) != _replicasMap.end()) { auto &replicas = _replicasMap[parentID]; + QDataStream identityDataStream(identityData); for (auto avatar : replicas) { - avatar->processAvatarIdentity(QDataStream(identityData), identityChanged, displayNameChanged); + avatar->processAvatarIdentity(identityDataStream, identityChanged, displayNameChanged); } } } From 3a2a8fe4f8d4dfa7eb965fa4dff59d2914b207d3 Mon Sep 17 00:00:00 2001 From: amantley Date: Sat, 29 Sep 2018 15:49:11 -0700 Subject: [PATCH 033/362] adjusted openvr to use 0.0 for the floor instead of -1.6 --- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index fae2144caf..1638a17631 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -463,7 +463,7 @@ bool OpenVrDisplayPlugin::internalActivate() { auto chaperone = vr::VRChaperone(); if (chaperone) { float const UI_RADIUS = 1.0f; - float const UI_HEIGHT = 1.6f; + float const UI_HEIGHT = 0.0f; float const UI_Z_OFFSET = 0.5; float xSize, zSize; From 756d1a6fc4fd8fab92e060cce70bc201165a20f2 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Sat, 29 Sep 2018 21:15:10 -0700 Subject: [PATCH 034/362] Fix another bind-to-temporary --- assignment-client/src/avatars/AvatarMixer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 168c95cb77..48f3edb94f 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -565,7 +565,8 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer mes // parse the identity packet and update the change timestamp if appropriate bool identityChanged = false; bool displayNameChanged = false; - avatar.processAvatarIdentity(QDataStream(message->getMessage()), identityChanged, displayNameChanged); + QDataStream avatarIdentityStream(message->getMessage()); + avatar.processAvatarIdentity(avatarIdentityStream, identityChanged, displayNameChanged); if (identityChanged) { QMutexLocker nodeDataLocker(&nodeData->getMutex()); From 9e400459270ad90fd896d75025ca7cf5c60a109d Mon Sep 17 00:00:00 2001 From: amantley Date: Sun, 30 Sep 2018 17:13:34 -0700 Subject: [PATCH 035/362] added criteria to stop false positive for sit down --- interface/src/avatar/MyAvatar.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 02ea49cda0..f281d8c87d 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -4079,13 +4079,21 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl // if we are 20% higher switch to standing. // 16.6% lower then switch to sitting. // add this !!!! And the head is upright. + glm::vec3 upHead = transformVectorFast(sensorHeadPose.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); + float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::vec3 avatarHips = myAvatar.getAbsoluteJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); + glm::vec3 worldHips = transformVectorFast(myAvatar.getTransform().getMatrix(),avatarHips); + glm::vec3 sensorHips = transformVectorFast(myAvatar.getSensorToWorldMatrix(), worldHips); float averageSensorSpaceHeight = myAvatar._sumUserHeightSensorSpace / myAvatar._averageUserHeightCount; + // we could add a counting here to make sure that a lean forward doesn't accidentally put you in sitting mode. + // but maybe so what. + // the real test is... can I pick something up in standing mode? if (myAvatar.getIsInSittingState()) { if (offset.y < SITTING_BOTTOM) { // we recenter when sitting. return true; - } else if (sensorHeadPose.getTranslation().y > (1.2f * averageSensorSpaceHeight)) { + } else if (sensorHeadPose.getTranslation().y > (1.2f * averageSensorSpaceHeight)) { // if we recenter upwards then no longer in sitting state myAvatar.setIsInSittingState(false); //myAvatar._sumUserHeightSensorSpace = 1.2f * averageSensorSpaceHeight; @@ -4096,7 +4104,7 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl } } else { // in the standing state - if (sensorHeadPose.getTranslation().y < (0.83f * averageSensorSpaceHeight)) { + if ((sensorHeadPose.getTranslation().y < (0.83f * averageSensorSpaceHeight)) && (acosHead > 0.98f) && !(sensorHips.y > (0.4f * averageSensorSpaceHeight)) { myAvatar.setIsInSittingState(true); // myAvatar._sumUserHeightSensorSpace = 0.83f * averageSensorSpaceHeight; // myAvatar._averageUserHeightCount = 1; From fe7c14e5cf3523beb1be9cb1d5b0feb23188deb5 Mon Sep 17 00:00:00 2001 From: amantley Date: Sun, 30 Sep 2018 17:15:07 -0700 Subject: [PATCH 036/362] removed extraneous script --- scripts/developer/objectOrientedStep.js | 688 ------------------------ 1 file changed, 688 deletions(-) delete mode 100644 scripts/developer/objectOrientedStep.js diff --git a/scripts/developer/objectOrientedStep.js b/scripts/developer/objectOrientedStep.js deleted file mode 100644 index a5c27e36b9..0000000000 --- a/scripts/developer/objectOrientedStep.js +++ /dev/null @@ -1,688 +0,0 @@ -/* jslint bitwise: true */ - -/* global Script, Vec3, MyAvatar, Tablet, Messages, Quat, -DebugDraw, Mat4, Entities, Xform, Controller, Camera, console, document*/ - -Script.registerValue("STEPAPP", true); -var CENTIMETERSPERMETER = 100.0; -var LEFT = 0; -var RIGHT = 1; -var INCREASING = 1.0; -var DECREASING = -1.0; -var DEFAULT_AVATAR_HEIGHT = 1.64; -var TABLET_BUTTON_NAME = "STEP"; -var CHANGE_OF_BASIS_ROTATION = { x: 0, y: 1, z: 0, w: 0 }; -// in meters (mostly) -var DEFAULT_ANTERIOR = 0.04; -var DEFAULT_POSTERIOR = 0.06; -var DEFAULT_LATERAL = 0.10; -var DEFAULT_HEIGHT_DIFFERENCE = 0.02; -var DEFAULT_ANGULAR_VELOCITY = 0.3; -var DEFAULT_HAND_VELOCITY = 0.4; -var DEFAULT_ANGULAR_HAND_VELOCITY = 3.3; -var DEFAULT_HEAD_VELOCITY = 0.14; -var DEFAULT_LEVEL_PITCH = 7; -var DEFAULT_LEVEL_ROLL = 7; -var DEFAULT_DIFF = 0.0; -var DEFAULT_DIFF_EULERS = { x: 0.0, y: 0.0, z: 0.0 }; -var DEFAULT_HIPS_POSITION; -var DEFAULT_HEAD_POSITION; -var DEFAULT_TORSO_LENGTH; -var SPINE_STRETCH_LIMIT = 0.02; - -var VELOCITY_EPSILON = 0.02; -var AVERAGING_RATE = 0.03; -var HEIGHT_AVERAGING_RATE = 0.01; -var STEP_TIME_SECS = 0.2; -var MODE_SAMPLE_LENGTH = 100; -var RESET_MODE = false; -var HEAD_TURN_THRESHOLD = 25.0; -var NO_SHARED_DIRECTION = -0.98; -var LOADING_DELAY = 500; -var FAILSAFE_TIMEOUT = 2.5; - -var debugDrawBase = true; -var activated = false; -var documentLoaded = false; -var failsafeFlag = false; -var failsafeSignalTimer = -1.0; -var stepTimer = -1.0; - - -var modeArray = new Array(MODE_SAMPLE_LENGTH); -var modeHeight = -10.0; - -var handPosition; -var handOrientation; -var hands = []; -var hipToHandAverage = []; -var handDotHead = []; -var headAverageOrientation = MyAvatar.orientation; -var headPoseAverageOrientation = { x: 0, y: 0, z: 0, w: 1 }; -var averageHeight = 1.0; -var headEulers = { x: 0.0, y: 0.0, z: 0.0 }; -var headAverageEulers = { x: 0.0, y: 0.0, z: 0.0 }; -var headAveragePosition = { x: 0, y: 0.4, z: 0 }; -var frontLeft = { x: -DEFAULT_LATERAL, y: 0, z: -DEFAULT_ANTERIOR }; -var frontRight = { x: DEFAULT_LATERAL, y: 0, z: -DEFAULT_ANTERIOR }; -var backLeft = { x: -DEFAULT_LATERAL, y: 0, z: DEFAULT_POSTERIOR }; -var backRight = { x: DEFAULT_LATERAL, y: 0, z: DEFAULT_POSTERIOR }; - - -// define state readings constructor -function StateReading(headPose, rhandPose, lhandPose, backLength, diffFromMode, diffFromAverageHeight, diffFromAveragePosition, - diffFromAverageEulers) { - this.headPose = headPose; - this.rhandPose = rhandPose; - this.lhandPose = lhandPose; - this.backLength = backLength; - this.diffFromMode = diffFromMode; - this.diffFromAverageHeight = diffFromAverageHeight; - this.diffFromAveragePosition = diffFromAveragePosition; - this.diffFromAverageEulers = diffFromAverageEulers; -} - -// define current state readings object for holding tracker readings and current differences from averages -var currentStateReadings = new StateReading(Controller.getPoseValue(Controller.Standard.Head), - Controller.getPoseValue(Controller.Standard.RightHand), Controller.getPoseValue(Controller.Standard.LeftHand), - DEFAULT_TORSO_LENGTH, DEFAULT_DIFF, DEFAULT_DIFF, DEFAULT_DIFF, DEFAULT_DIFF_EULERS); - -// declare the checkbox constructor -function AppCheckbox(type,id,eventType,isChecked) { - this.type = type; - this.id = id; - this.eventType = eventType; - this.data = {value: isChecked}; -} - -// define the checkboxes in the html file -var usingAverageHeight = new AppCheckbox("checkboxtick", "runningAverageHeightCheck", "onRunningAverageHeightCheckBox", - false); -var usingModeHeight = new AppCheckbox("checkboxtick","modeCheck","onModeCheckBox",true); -var usingBaseOfSupport = new AppCheckbox("checkboxtick","baseOfSupportCheck","onBaseOfSupportCheckBox",true); -var usingAverageHeadPosition = new AppCheckbox("checkboxtick", "headAveragePositionCheck", "onHeadAveragePositionCheckBox", - false); - -var checkBoxArray = new Array(usingAverageHeight,usingModeHeight,usingBaseOfSupport,usingAverageHeadPosition); - -// declare the html slider constructor -function AppProperty(name, type, eventType, signalType, setFunction, initValue, convertToThreshold, convertToSlider, signalOn) { - this.name = name; - this.type = type; - this.eventType = eventType; - this.signalType = signalType; - this.setValue = setFunction; - this.value = initValue; - this.get = function () { - return this.value; - }; - this.convertToThreshold = convertToThreshold; - this.convertToSlider = convertToSlider; - this.signalOn = signalOn; -} - -// define the sliders -var frontBaseProperty = new AppProperty("#anteriorBase-slider", "slider", "onAnteriorBaseSlider", "frontSignal", - setAnteriorDistance, DEFAULT_ANTERIOR, function (num) { - return convertToMeters(num); - }, function (num) { - return convertToCentimeters(num); - },true); -var backBaseProperty = new AppProperty("#posteriorBase-slider", "slider", "onPosteriorBaseSlider", "backSignal", - setPosteriorDistance, DEFAULT_POSTERIOR, function (num) { - return convertToMeters(num); - }, function (num) { - return convertToCentimeters(num); - }, true); -var lateralBaseProperty = new AppProperty("#lateralBase-slider", "slider", "onLateralBaseSlider", "lateralSignal", - setLateralDistance, DEFAULT_LATERAL, function (num) { - return convertToMeters(num); - }, function (num) { - return convertToCentimeters(num); - }, true); -var headAngularVelocityProperty = new AppProperty("#angularVelocityHead-slider", "slider", "onAngularVelocitySlider", - "angularHeadSignal", setAngularThreshold, DEFAULT_ANGULAR_VELOCITY, function (num) { - var base = 4; - var shift = 2; - return convertExponential(base, num, DECREASING, shift); - }, function (num) { - var base = 4; - var shift = 2; - return convertLog(base, num, DECREASING, shift); - }, true); -var heightDifferenceProperty = new AppProperty("#heightDifference-slider", "slider", "onHeightDifferenceSlider", "heightSignal", - setHeightThreshold, DEFAULT_HEIGHT_DIFFERENCE, function (num) { - return convertToMeters(-num); - }, function (num) { - return convertToCentimeters(-num); - }, true); -var handsVelocityProperty = new AppProperty("#handsVelocity-slider", "slider", "onHandsVelocitySlider", "handVelocitySignal", - setHandVelocityThreshold, DEFAULT_HAND_VELOCITY, function (num) { - return num; - }, function (num) { - return num; - }, true); -var handsAngularVelocityProperty = new AppProperty("#handsAngularVelocity-slider", "slider", "onHandsAngularVelocitySlider", - "handAngularSignal", setHandAngularVelocityThreshold, DEFAULT_ANGULAR_HAND_VELOCITY, function (num) { - var base = 7; - var shift = 2; - return convertExponential(base, num, DECREASING, shift); - }, function (num) { - var base = 7; - var shift = 2; - return convertLog(base, num, DECREASING, shift); - }, true); -var headVelocityProperty = new AppProperty("#headVelocity-slider", "slider", "onHeadVelocitySlider", "headVelocitySignal", - setHeadVelocityThreshold, DEFAULT_HEAD_VELOCITY, function (num) { - var base = 2; - var shift = 0; - return convertExponential(base, num, INCREASING, shift); - }, function (num) { - var base = 2; - var shift = 0; - return convertLog(base, num, INCREASING, shift); - }, true); -var headPitchProperty = new AppProperty("#headPitch-slider", "slider", "onHeadPitchSlider", "headPitchSignal", - setHeadPitchThreshold, DEFAULT_LEVEL_PITCH, function (num) { - var base = 2.5; - var shift = 5; - return convertExponential(base, num, DECREASING, shift); - }, function (num) { - var base = 2.5; - var shift = 5; - return convertLog(base, num, DECREASING, shift); - }, true); -var headRollProperty = new AppProperty("#headRoll-slider", "slider", "onHeadRollSlider", "headRollSignal", setHeadRollThreshold, - DEFAULT_LEVEL_ROLL, function (num) { - var base = 2.5; - var shift = 5; - return convertExponential(base, num, DECREASING, shift); - }, function (num) { - var base = 2.5; - var shift = 5; - return convertLog(base, num, DECREASING, shift); - }, true); - -var propArray = new Array(frontBaseProperty, backBaseProperty, lateralBaseProperty, headAngularVelocityProperty, - heightDifferenceProperty, handsVelocityProperty, handsAngularVelocityProperty, headVelocityProperty, headPitchProperty, - headRollProperty); - -// var HTML_URL = Script.resolvePath("http://hifi-content.s3.amazonaws.com/angus/stepApp/stepApp.html"); -var HTML_URL = Script.resolvePath("http://hifi-content.s3.amazonaws.com/angus/stepApp/stepAppExtra.html"); -var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - -function manageClick() { - if (activated) { - tablet.gotoHomeScreen(); - } else { - tablet.gotoWebScreen(HTML_URL); - } -} - -var tabletButton = tablet.addButton({ - text: TABLET_BUTTON_NAME, - icon: Script.resolvePath("http://hifi-content.s3.amazonaws.com/angus/stepApp/foot.svg"), - activeIcon: Script.resolvePath("http://hifi-content.s3.amazonaws.com/angus/stepApp/foot.svg") -}); - -function drawBase() { - // transform corners into world space, for rendering. - var worldPointLf = Vec3.sum(MyAvatar.position,Vec3.multiplyQbyV(MyAvatar.orientation, frontLeft)); - var worldPointRf = Vec3.sum(MyAvatar.position,Vec3.multiplyQbyV(MyAvatar.orientation, frontRight)); - var worldPointLb = Vec3.sum(MyAvatar.position,Vec3.multiplyQbyV(MyAvatar.orientation, backLeft)); - var worldPointRb = Vec3.sum(MyAvatar.position,Vec3.multiplyQbyV(MyAvatar.orientation, backRight)); - - var GREEN = { r: 0, g: 1, b: 0, a: 1 }; - // draw border - DebugDraw.drawRay(worldPointLf, worldPointRf, GREEN); - DebugDraw.drawRay(worldPointRf, worldPointRb, GREEN); - DebugDraw.drawRay(worldPointRb, worldPointLb, GREEN); - DebugDraw.drawRay(worldPointLb, worldPointLf, GREEN); -} - -function onKeyPress(event) { - if (event.text === "'") { - // when the sensors are reset, then reset the mode. - RESET_MODE = false; - } -} - -function onWebEventReceived(msg) { - var message = JSON.parse(msg); - print(" we have a message from html dialog " + message.type); - propArray.forEach(function (prop) { - if (prop.eventType === message.type) { - prop.setValue(prop.convertToThreshold(message.data.value)); - print("message from " + prop.name); - // break; - } - }); - checkBoxArray.forEach(function(cbox) { - if (cbox.eventType === message.type) { - cbox.data.value = message.data.value; - // break; - } - }); - if (message.type === "onCreateStepApp") { - print("document loaded"); - documentLoaded = true; - Script.setTimeout(initAppForm, LOADING_DELAY); - } -} - -function initAppForm() { - print("step app is loaded: " + documentLoaded); - if (documentLoaded === true) { - propArray.forEach(function (prop) { - tablet.emitScriptEvent(JSON.stringify({ - "type": "trigger", - "id": prop.signalType, - "data": { "value": "green" } - })); - tablet.emitScriptEvent(JSON.stringify({ - "type": "slider", - "id": prop.name, - "data": { "value": prop.convertToSlider(prop.value) } - })); - }); - checkBoxArray.forEach(function (cbox) { - tablet.emitScriptEvent(JSON.stringify({ - "type": "checkboxtick", - "id": cbox.id, - "data": { "value": cbox.data.value } - })); - }); - } -} - -function updateSignalColors() { - - // force the updates by running the threshold comparisons - withinBaseOfSupport(currentStateReadings.headPose.translation); - withinThresholdOfStandingHeightMode(currentStateReadings.diffFromMode); - headAngularVelocityBelowThreshold(currentStateReadings.headPose.angularVelocity); - handDirectionMatchesHeadDirection(currentStateReadings.lhandPose, currentStateReadings.rhandPose); - handAngularVelocityBelowThreshold(currentStateReadings.lhandPose, currentStateReadings.rhandPose); - headVelocityGreaterThanThreshold(Vec3.length(currentStateReadings.headPose.velocity)); - headMovedAwayFromAveragePosition(currentStateReadings.diffFromAveragePosition); - headLowerThanHeightAverage(currentStateReadings.diffFromAverageHeight); - isHeadLevel(currentStateReadings.diffFromAverageEulers); - - propArray.forEach(function (prop) { - if (prop.signalOn) { - tablet.emitScriptEvent(JSON.stringify({ "type": "trigger", "id": prop.signalType, "data": { "value": "green" } })); - } else { - tablet.emitScriptEvent(JSON.stringify({ "type": "trigger", "id": prop.signalType, "data": { "value": "red" } })); - } - }); -} - -function onScreenChanged(type, url) { - print("Screen changed"); - if (type === "Web" && url === HTML_URL) { - if (!activated) { - // hook up to event bridge - tablet.webEventReceived.connect(onWebEventReceived); - print("after connect web event"); - MyAvatar.hmdLeanRecenterEnabled = false; - - } - activated = true; - } else { - if (activated) { - // disconnect from event bridge - tablet.webEventReceived.disconnect(onWebEventReceived); - documentLoaded = false; - } - activated = false; - } -} - -function getLog(x, y) { - return Math.log(y) / Math.log(x); -} - -function noConversion(num) { - return num; -} - -function convertLog(base, num, direction, shift) { - return direction * getLog(base, (num + 1.0)) + shift; -} - -function convertExponential(base, num, direction, shift) { - return Math.pow(base, (direction*num + shift)) - 1.0; -} - -function convertToCentimeters(num) { - return num * CENTIMETERSPERMETER; -} - -function convertToMeters(num) { - print("convert to meters " + num); - return num / CENTIMETERSPERMETER; -} - -function isInsideLine(a, b, c) { - return (((b.x - a.x)*(c.z - a.z) - (b.z - a.z)*(c.x - a.x)) > 0); -} - -function setAngularThreshold(num) { - headAngularVelocityProperty.value = num; - print("angular threshold " + headAngularVelocityProperty.get()); -} - -function setHeadRollThreshold(num) { - headRollProperty.value = num; - print("head roll threshold " + headRollProperty.get()); -} - -function setHeadPitchThreshold(num) { - headPitchProperty.value = num; - print("head pitch threshold " + headPitchProperty.get()); -} - -function setHeightThreshold(num) { - heightDifferenceProperty.value = num; - print("height threshold " + heightDifferenceProperty.get()); -} - -function setLateralDistance(num) { - lateralBaseProperty.value = num; - frontLeft.x = -lateralBaseProperty.get(); - frontRight.x = lateralBaseProperty.get(); - backLeft.x = -lateralBaseProperty.get(); - backRight.x = lateralBaseProperty.get(); - print("lateral distance " + lateralBaseProperty.get()); -} - -function setAnteriorDistance(num) { - frontBaseProperty.value = num; - frontLeft.z = -frontBaseProperty.get(); - frontRight.z = -frontBaseProperty.get(); - print("anterior distance " + frontBaseProperty.get()); -} - -function setPosteriorDistance(num) { - backBaseProperty.value = num; - backLeft.z = backBaseProperty.get(); - backRight.z = backBaseProperty.get(); - print("posterior distance " + backBaseProperty.get()); -} - -function setHandAngularVelocityThreshold(num) { - handsAngularVelocityProperty.value = num; - print("hand angular velocity threshold " + handsAngularVelocityProperty.get()); -} - -function setHandVelocityThreshold(num) { - handsVelocityProperty.value = num; - print("hand velocity threshold " + handsVelocityProperty.get()); -} - -function setHeadVelocityThreshold(num) { - headVelocityProperty.value = num; - print("headvelocity threshold " + headVelocityProperty.get()); -} - -function withinBaseOfSupport(pos) { - var userScale = 1.0; - frontBaseProperty.signalOn = !(isInsideLine(Vec3.multiply(userScale, frontLeft), Vec3.multiply(userScale, frontRight), pos)); - backBaseProperty.signalOn = !(isInsideLine(Vec3.multiply(userScale, backRight), Vec3.multiply(userScale, backLeft), pos)); - lateralBaseProperty.signalOn = !(isInsideLine(Vec3.multiply(userScale, frontRight), Vec3.multiply(userScale, backRight), pos) - && isInsideLine(Vec3.multiply(userScale, backLeft), Vec3.multiply(userScale, frontLeft), pos)); - return (!frontBaseProperty.signalOn && !backBaseProperty.signalOn && !lateralBaseProperty.signalOn); -} - -function withinThresholdOfStandingHeightMode(heightDiff) { - if (usingModeHeight.data.value) { - heightDifferenceProperty.signalOn = heightDiff < heightDifferenceProperty.get(); - return heightDifferenceProperty.signalOn; - } else { - return true; - } -} - -function headAngularVelocityBelowThreshold(headAngularVelocity) { - var angVel = Vec3.length({ x: headAngularVelocity.x, y: 0, z: headAngularVelocity.z }); - headAngularVelocityProperty.signalOn = angVel < headAngularVelocityProperty.get(); - return headAngularVelocityProperty.signalOn; -} - -function handDirectionMatchesHeadDirection(lhPose, rhPose) { - handsVelocityProperty.signalOn = ((handsVelocityProperty.get() < NO_SHARED_DIRECTION) || - ((!lhPose.valid || ((handDotHead[LEFT] > handsVelocityProperty.get()) && - (Vec3.length(lhPose.velocity) > VELOCITY_EPSILON))) && - (!rhPose.valid || ((handDotHead[RIGHT] > handsVelocityProperty.get()) && - (Vec3.length(rhPose.velocity) > VELOCITY_EPSILON))))); - return handsVelocityProperty.signalOn; -} - -function handAngularVelocityBelowThreshold(lhPose, rhPose) { - var xzRHandAngularVelocity = Vec3.length({ x: rhPose.angularVelocity.x, y: 0.0, z: rhPose.angularVelocity.z }); - var xzLHandAngularVelocity = Vec3.length({ x: lhPose.angularVelocity.x, y: 0.0, z: lhPose.angularVelocity.z }); - handsAngularVelocityProperty.signalOn = ((!rhPose.valid ||(xzRHandAngularVelocity < handsAngularVelocityProperty.get())) - && (!lhPose.valid || (xzLHandAngularVelocity < handsAngularVelocityProperty.get()))); - return handsAngularVelocityProperty.signalOn; -} - -function headVelocityGreaterThanThreshold(headVel) { - headVelocityProperty.signalOn = (headVel > headVelocityProperty.get()) || (headVelocityProperty.get() < VELOCITY_EPSILON); - return headVelocityProperty.signalOn; -} - -function headMovedAwayFromAveragePosition(headDelta) { - return !withinBaseOfSupport(headDelta) || !usingAverageHeadPosition.data.value; -} - -function headLowerThanHeightAverage(heightDiff) { - if (usingAverageHeight.data.value) { - print("head lower than height average"); - heightDifferenceProperty.signalOn = heightDiff < heightDifferenceProperty.get(); - return heightDifferenceProperty.signalOn; - } else { - return true; - } -} - -function isHeadLevel(diffEulers) { - headRollProperty.signalOn = Math.abs(diffEulers.z) < headRollProperty.get(); - headPitchProperty.signalOn = Math.abs(diffEulers.x) < headPitchProperty.get(); - return (headRollProperty.signalOn && headPitchProperty.signalOn); -} - -function findAverage(arr) { - var sum = arr.reduce(function (acc, val) { - return acc + val; - },0); - return sum / arr.length; -} - -function addToModeArray(arr,num) { - for (var i = 0 ;i < (arr.length - 1); i++) { - arr[i] = arr[i+1]; - } - arr[arr.length - 1] = (Math.floor(num*CENTIMETERSPERMETER))/CENTIMETERSPERMETER; -} - -function findMode(ary, currentMode, backLength, defaultBack, currentHeight) { - var numMapping = {}; - var greatestFreq = 0; - var mode; - ary.forEach(function (number) { - numMapping[number] = (numMapping[number] || 0) + 1; - if ((greatestFreq < numMapping[number]) || ((numMapping[number] === MODE_SAMPLE_LENGTH) && (number > currentMode) )) { - greatestFreq = numMapping[number]; - mode = number; - } - }); - if (mode > currentMode) { - return Number(mode); - } else { - if (!RESET_MODE && HMD.active) { - print("resetting the mode............................................. "); - print("resetting the mode............................................. "); - RESET_MODE = true; - var correction = 0.02; - return currentHeight - correction; - } else { - return currentMode; - } - } -} - -function update(dt) { - if (debugDrawBase) { - drawBase(); - } - // Update current state information - currentStateReadings.headPose = Controller.getPoseValue(Controller.Standard.Head); - currentStateReadings.rhandPose = Controller.getPoseValue(Controller.Standard.RightHand); - currentStateReadings.lhandPose = Controller.getPoseValue(Controller.Standard.LeftHand); - - // back length - var headMinusHipLean = Vec3.subtract(currentStateReadings.headPose.translation, DEFAULT_HIPS_POSITION); - currentStateReadings.backLength = Vec3.length(headMinusHipLean); - // print("back length and default " + currentStateReadings.backLength + " " + DEFAULT_TORSO_LENGTH); - - // mode height - addToModeArray(modeArray, currentStateReadings.headPose.translation.y); - modeHeight = findMode(modeArray, modeHeight, currentStateReadings.backLength, DEFAULT_TORSO_LENGTH, - currentStateReadings.headPose.translation.y); - currentStateReadings.diffFromMode = modeHeight - currentStateReadings.headPose.translation.y; - - // hand direction - var leftHandLateralPoseVelocity = currentStateReadings.lhandPose.velocity; - leftHandLateralPoseVelocity.y = 0.0; - var rightHandLateralPoseVelocity = currentStateReadings.rhandPose.velocity; - rightHandLateralPoseVelocity.y = 0.0; - var headLateralPoseVelocity = currentStateReadings.headPose.velocity; - headLateralPoseVelocity.y = 0.0; - handDotHead[LEFT] = Vec3.dot(Vec3.normalize(leftHandLateralPoseVelocity), Vec3.normalize(headLateralPoseVelocity)); - handDotHead[RIGHT] = Vec3.dot(Vec3.normalize(rightHandLateralPoseVelocity), Vec3.normalize(headLateralPoseVelocity)); - - // average head position - headAveragePosition = Vec3.mix(headAveragePosition, currentStateReadings.headPose.translation, AVERAGING_RATE); - currentStateReadings.diffFromAveragePosition = Vec3.subtract(currentStateReadings.headPose.translation, - headAveragePosition); - - // average height - averageHeight = currentStateReadings.headPose.translation.y * HEIGHT_AVERAGING_RATE + - averageHeight * (1.0 - HEIGHT_AVERAGING_RATE); - currentStateReadings.diffFromAverageHeight = Math.abs(currentStateReadings.headPose.translation.y - averageHeight); - - // eulers diff - headEulers = Quat.safeEulerAngles(currentStateReadings.headPose.rotation); - headAverageOrientation = Quat.slerp(headAverageOrientation, currentStateReadings.headPose.rotation, AVERAGING_RATE); - headAverageEulers = Quat.safeEulerAngles(headAverageOrientation); - currentStateReadings.diffFromAverageEulers = Vec3.subtract(headAverageEulers, headEulers); - - // headpose rig space is for determining when to recenter rotation. - var headPoseRigSpace = Quat.multiply(CHANGE_OF_BASIS_ROTATION, currentStateReadings.headPose.rotation); - headPoseAverageOrientation = Quat.slerp(headPoseAverageOrientation, headPoseRigSpace, AVERAGING_RATE); - var headPoseAverageEulers = Quat.safeEulerAngles(headPoseAverageOrientation); - - // make the signal colors reflect the current thresholds that have been crossed - updateSignalColors(); - - SPINE_STRETCH_LIMIT = (0.04) * DEFAULT_TORSO_LENGTH * MyAvatar.scale; - - //print("the spine stretch limit is " + SPINE_STRETCH_LIMIT + " head avatar space is " + currentStateReadings.headPose.translation.y); - //print("the current back length is " + currentStateReadings.backLength + " " + DEFAULT_TORSO_LENGTH); - // Conditions for taking a step. - // 1. off the base of support. front, lateral, back edges. - // 2. head is not lower than the height mode value by more than the maxHeightChange tolerance - // 3. the angular velocity of the head is not greater than the threshold value - // ie this reflects the speed the head is rotating away from having up = (0,1,0) in Avatar frame.. - // 4. the hands velocity vector has the same direction as the head, within the given tolerance - // the tolerance is an acos value, -1 means the hands going in any direction will not block translating - // up to 1 where the hands velocity direction must exactly match that of the head. -1 threshold disables this condition. - // 5. the angular velocity xz magnitude for each hand is below the threshold value - // ie here this reflects the speed that each hand is rotating away from having up = (0,1,0) in Avatar frame. - // 6. head velocity is below step threshold - // 7. head has moved further than the threshold from the running average position of the head. - // 8. head height is not lower than the running average head height with a difference of maxHeightChange. - // 9. head's rotation in avatar space is not pitching or rolling greater than the pitch or roll thresholds - if (!withinBaseOfSupport(currentStateReadings.headPose.translation) && - withinThresholdOfStandingHeightMode(currentStateReadings.diffFromMode) && - headAngularVelocityBelowThreshold(currentStateReadings.headPose.angularVelocity) && - handDirectionMatchesHeadDirection(currentStateReadings.lhandPose, currentStateReadings.rhandPose) && - handAngularVelocityBelowThreshold(currentStateReadings.lhandPose, currentStateReadings.rhandPose) && - headVelocityGreaterThanThreshold(Vec3.length(currentStateReadings.headPose.velocity)) && - headMovedAwayFromAveragePosition(currentStateReadings.diffFromAveragePosition) && - headLowerThanHeightAverage(currentStateReadings.diffFromAverageHeight) && - isHeadLevel(currentStateReadings.diffFromAverageEulers)) { - - if (stepTimer < 0.0) { //!MyAvatar.isRecenteringHorizontally() - print("trigger recenter========================================================"); - MyAvatar.triggerHorizontalRecenter(); - stepTimer = STEP_TIME_SECS; - } - } else if ((currentStateReadings.backLength > (DEFAULT_TORSO_LENGTH + SPINE_STRETCH_LIMIT)) && - (failsafeSignalTimer < 0.0) && HMD.active) { - // do the failsafe recenter. - // failsafeFlag stops repeated setting of failsafe button color. - // RESET_MODE false forces a reset of the height - RESET_MODE = false; - failsafeFlag = true; - failsafeSignalTimer = FAILSAFE_TIMEOUT; - MyAvatar.triggerHorizontalRecenter(); - tablet.emitScriptEvent(JSON.stringify({ "type": "failsafe", "id": "failsafeSignal", "data": { "value": "green" } })); - // in fail safe we debug print the values that were blocking us. - print("failsafe debug---------------------------------------------------------------"); - propArray.forEach(function (prop) { - print(prop.name); - if (!prop.signalOn) { - print(prop.signalType + " contributed to failsafe call"); - } - }); - print("end failsafe debug---------------------------------------------------------------"); - - } - - if ((failsafeSignalTimer < 0.0) && failsafeFlag) { - failsafeFlag = false; - tablet.emitScriptEvent(JSON.stringify({ "type": "failsafe", "id": "failsafeSignal", "data": { "value": "orange" } })); - } - - stepTimer -= dt; - failsafeSignalTimer -= dt; - - if (!HMD.active) { - RESET_MODE = false; - } - - if (Math.abs(headPoseAverageEulers.y) > HEAD_TURN_THRESHOLD) { - // Turn feet - // MyAvatar.triggerRotationRecenter(); - // headPoseAverageOrientation = { x: 0, y: 0, z: 0, w: 1 }; - } -} - -function shutdownTabletApp() { - // GlobalDebugger.stop(); - tablet.removeButton(tabletButton); - if (activated) { - tablet.webEventReceived.disconnect(onWebEventReceived); - tablet.gotoHomeScreen(); - } - tablet.screenChanged.disconnect(onScreenChanged); -} - -tabletButton.clicked.connect(manageClick); -tablet.screenChanged.connect(onScreenChanged); - -Script.setTimeout(function() { - DEFAULT_HIPS_POSITION = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(MyAvatar.getJointIndex("Hips")); - DEFAULT_HEAD_POSITION = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(MyAvatar.getJointIndex("Head")); - DEFAULT_TORSO_LENGTH = Vec3.length(Vec3.subtract(DEFAULT_HEAD_POSITION, DEFAULT_HIPS_POSITION)); - SPINE_STRETCH_LIMIT = (0.04) * DEFAULT_TORSO_LENGTH * MyAvatar.scale; -},(4*LOADING_DELAY)); - -Script.update.connect(update); -Controller.keyPressEvent.connect(onKeyPress); -Script.scriptEnding.connect(function () { - MyAvatar.hmdLeanRecenterEnabled = true; - Script.update.disconnect(update); - shutdownTabletApp(); -}); From 1a0e2c6ea181bbae814554967ed8b60729eb56b8 Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 1 Oct 2018 08:30:51 -0700 Subject: [PATCH 037/362] clean up for pr --- interface/resources/qml/hifi/avatarapp/Settings.qml | 3 +-- interface/src/avatar/MyAvatar.cpp | 12 ------------ 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index af76ba04d6..c4289ca650 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -296,7 +296,6 @@ Rectangle { text: "OFF" boxSize: 20 } - // TextStyle9 @@ -345,7 +344,7 @@ Rectangle { boxSize: 20 } } - + ColumnLayout { id: avatarAnimationLayout anchors.top: handAndCollisions.bottom diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index f281d8c87d..1bd8d4b2f0 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -485,18 +485,6 @@ void MyAvatar::update(float deltaTime) { glm::vec3 upHead = transformVectorFast(sensorHeadPoseDebug.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); // qCDebug(interfaceapp) << "sensor space head pos " << sensorHeadPoseDebug.getTranslation().y; - if ((acosHead > 0.98f) && !getIsInSittingState() && (sensorHeadPoseDebug.getTranslation().y < -0.5f)) { - //qCDebug(interfaceapp) << "we are going to sitting state because it looks like we should" << sensorHeadPoseDebug.getTranslation().y; - } - if (!_lastFrameHMDMode && qApp->isHMDMode()) { - // we have entered hmd mode, so make the best guess about sitting or standing - if (sensorHeadPoseDebug.getTranslation().y < 1.3f) { - // then we are sitting. - // setIsInSittingState(true); - } else { - // setIsInSittingState(false); - } - } // put the average hand azimuth into sensor space. // then mix it with head facing direction to determine rotation recenter From 90feeffa9db5076f3ca7e2bf8012d60ad11b2c1e Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 1 Oct 2018 14:21:00 -0700 Subject: [PATCH 038/362] cleaning up. putting squat fix in vertical recenter --- interface/src/avatar/MyAvatar.cpp | 70 +++++++++++++--------------- interface/src/avatar/MyAvatar.h | 6 +-- plugins/oculus/src/OculusHelpers.cpp | 10 ++-- scripts/system/avatarapp.js | 6 +-- 4 files changed, 43 insertions(+), 49 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 1bd8d4b2f0..278d2703ab 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -484,6 +484,19 @@ void MyAvatar::update(float deltaTime) { glm::vec3 upHead = transformVectorFast(sensorHeadPoseDebug.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); + + glm::vec3 headDefaultPositionAvatarSpace = getAbsoluteDefaultJointTranslationInObjectFrame(getJointIndex("Head")); + glm::quat spine2OrientationAvatarSpace = getAbsoluteJointRotationInObjectFrame(getJointIndex("Spine2")); + glm::vec3 headCurrentPositionAvatarSpace = getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation(); + glm::vec3 upSpine2 = spine2OrientationAvatarSpace * glm::vec3(0.0f, 1.0f, 0.0f); + float angleSpine2 = glm::dot(upSpine2, glm::vec3(0.0f, 1.0f, 0.0f)); + if (headCurrentPositionAvatarSpace.y < (headDefaultPositionAvatarSpace.y - 0.05) && (angleSpine2 > 0.98f)) { + _squatCount++; + } else { + _squatCount = 0; + } + + // qCDebug(interfaceapp) << "sensor space head pos " << sensorHeadPoseDebug.getTranslation().y; // put the average hand azimuth into sensor space. @@ -3795,7 +3808,7 @@ bool MyAvatar::getIsInWalkingState() const { } bool MyAvatar::getIsInSittingState() const { - return _isInSittingState; + return _isInSittingState.get(); } float MyAvatar::getWalkSpeed() const { @@ -3819,7 +3832,7 @@ void MyAvatar::setIsInWalkingState(bool isWalking) { } void MyAvatar::setIsInSittingState(bool isSitting) { - _isInSittingState = isSitting; + _isInSittingState.set(isSitting); controller::Pose sensorHeadPoseDebug = getControllerPoseInSensorFrame(controller::Action::HEAD); _sumUserHeightSensorSpace = sensorHeadPoseDebug.getTranslation().y; _averageUserHeightCount = 1; @@ -4064,8 +4077,8 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl glm::vec3 headSensorSpace = transformVectorFast(myAvatar.getSensorToWorldMatrix(), headWorldSpace); //get the mode. //put it in sensor space. - // if we are 20% higher switch to standing. - // 16.6% lower then switch to sitting. + // if we are 20% higher switch to standing. + // 16.6% lower then switch to sitting. // add this !!!! And the head is upright. glm::vec3 upHead = transformVectorFast(sensorHeadPose.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); @@ -4074,8 +4087,9 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl glm::vec3 sensorHips = transformVectorFast(myAvatar.getSensorToWorldMatrix(), worldHips); float averageSensorSpaceHeight = myAvatar._sumUserHeightSensorSpace / myAvatar._averageUserHeightCount; // we could add a counting here to make sure that a lean forward doesn't accidentally put you in sitting mode. - // but maybe so what. + // but maybe so what. // the real test is... can I pick something up in standing mode? + if (myAvatar.getIsInSittingState()) { if (offset.y < SITTING_BOTTOM) { @@ -4084,22 +4098,27 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl } else if (sensorHeadPose.getTranslation().y > (1.2f * averageSensorSpaceHeight)) { // if we recenter upwards then no longer in sitting state myAvatar.setIsInSittingState(false); - //myAvatar._sumUserHeightSensorSpace = 1.2f * averageSensorSpaceHeight; - // myAvatar._averageUserHeightCount = 1; return true; } else { return false; } } else { - // in the standing state - if ((sensorHeadPose.getTranslation().y < (0.83f * averageSensorSpaceHeight)) && (acosHead > 0.98f) && !(sensorHips.y > (0.4f * averageSensorSpaceHeight)) { - myAvatar.setIsInSittingState(true); - // myAvatar._sumUserHeightSensorSpace = 0.83f * averageSensorSpaceHeight; - // myAvatar._averageUserHeightCount = 1; - return true; + // in the standing state + if ((sensorHeadPose.getTranslation().y < (0.83f * averageSensorSpaceHeight)) && (acosHead > 0.98f) && !(sensorHips.y > (0.4f * averageSensorSpaceHeight))) { + myAvatar._sitStandStateCount++; + if (myAvatar._sitStandStateCount > 300) { + myAvatar.setIsInSittingState(true); + myAvatar._sitStandStateCount = 0; + myAvatar._squatCount = 0; + return true; + } } else { - return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + if (myAvatar._squatCount > 600) { + return true; + myAvatar._squatCount = 0; + } } + return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); } } @@ -4109,29 +4128,6 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat if (myAvatar.getHMDLeanRecenterEnabled() && qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { - // debug head hips angle - glm::vec3 headDefaultPos = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Head")); - if (myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPos.y - 0.05f)) { - _squatCount++; - if ((_squatCount > 600) && !isActive(Vertical) && !isActive(Horizontal)) { - if (myAvatar.getIsInSittingState()) { - // activate(Horizontal); - //activate(Vertical); - _squatCount = 0; - } else { - if (myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPos.y - 0.20f)) { - //myAvatar.setIsInSittingState(true); - //activate(Vertical); - } else { - //activate(Horizontal); - } - _squatCount = 0; - } - } - } else { - _squatCount = 0; - } - if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 62ba8d68b1..c7f6fa89ae 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1737,7 +1737,6 @@ private: std::atomic _forceActivateVertical { false }; std::atomic _forceActivateHorizontal { false }; std::atomic _toggleHipsFollowing { true }; - int _squatCount { 0 }; }; FollowHelper _follow; @@ -1770,7 +1769,6 @@ private: glm::quat _customListenOrientation; AtRestDetector _hmdAtRestDetector; - bool _lastFrameHMDMode { false } ; bool _lastIsMoving { false }; // all poses are in sensor-frame @@ -1817,7 +1815,9 @@ private: ThreadSafeValueCache _sprintSpeed { AVATAR_SPRINT_SPEED_SCALAR }; float _walkSpeedScalar { AVATAR_WALK_SPEED_SCALAR }; bool _isInWalkingState { false }; - bool _isInSittingState { false }; + ThreadSafeValueCache _isInSittingState { false }; + int _sitStandStateCount { 0 }; + int _squatCount { 0 }; // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 38d93d088d..402b05f39c 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -86,12 +86,10 @@ private: qCWarning(oculusLog) << "Failed to acquire Oculus session" << ovr::getError(); return; } else { - qCWarning(oculusLog) << "successful init of oculus!!!!!!!!"; - ovrTrackingOrigin fred; - //fred = ovr_GetTrackingOriginType(session); - ovrResult retTrackingType = ovr_SetTrackingOriginType(session, ovrTrackingOrigin::ovrTrackingOrigin_FloorLevel); - fred = ovr_GetTrackingOriginType(session); - qCWarning(oculusLog) << OVR_SUCCESS(retTrackingType) << (int)fred; + ovrResult setFloorLevelOrigin = ovr_SetTrackingOriginType(session, ovrTrackingOrigin::ovrTrackingOrigin_FloorLevel); + if (!OVR_SUCCESS(setFloorLevelOrigin)) { + qCWarning(oculusLog) << "Failed to set the Oculus tracking origin to floor level" << ovr::getError(); + } } } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 16761f29a6..faf624392a 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -63,8 +63,8 @@ function getMyAvatar() { function getMyAvatarSettings() { return { dominantHand: MyAvatar.getDominantHand(), - collisionsEnabled: MyAvatar.getCollisionsEnabled(), - sittingEnabled: MyAvatar.isInSittingState, + collisionsEnabled : MyAvatar.getCollisionsEnabled(), + sittingEnabled : MyAvatar.isInSittingState, collisionSoundUrl : MyAvatar.collisionSoundURL, animGraphUrl: MyAvatar.getAnimGraphUrl(), animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(), @@ -326,7 +326,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See MyAvatar.isInSittingState = message.settings.sittingEnabled; MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl); - print("save settings"); + settings = getMyAvatarSettings(); break; default: From f0676d796c74a8464954bf1a8b7aae3e40df00a0 Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 1 Oct 2018 17:57:00 -0700 Subject: [PATCH 039/362] added constants and tipping point for running average. this should be replaced by the mode --- interface/src/avatar/MyAvatar.cpp | 86 +++++++++++++------------------ interface/src/avatar/MyAvatar.h | 2 + 2 files changed, 38 insertions(+), 50 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 278d2703ab..4f28ae8e90 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -471,34 +471,6 @@ void MyAvatar::update(float deltaTime) { float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); - // qCDebug(interfaceapp) << "sitting state is " << getIsInSittingState(); - // debug setting for sitting state if you start in the chair. - // if the head is close to the floor in sensor space - // and the up of the head is close to sensor up then try switching us to sitting state. - auto sensorHeadPoseDebug = getControllerPoseInSensorFrame(controller::Action::HEAD); - if (sensorHeadPoseDebug.isValid()) { - _sumUserHeightSensorSpace += sensorHeadPoseDebug.getTranslation().y; - _averageUserHeightCount++; - } - qCDebug(interfaceapp) << sensorHeadPoseDebug.isValid() << " valid. sensor space average " << (_sumUserHeightSensorSpace / _averageUserHeightCount) << " head position sensor y value " << sensorHeadPoseDebug.getTranslation().y; - - glm::vec3 upHead = transformVectorFast(sensorHeadPoseDebug.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); - float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); - - glm::vec3 headDefaultPositionAvatarSpace = getAbsoluteDefaultJointTranslationInObjectFrame(getJointIndex("Head")); - glm::quat spine2OrientationAvatarSpace = getAbsoluteJointRotationInObjectFrame(getJointIndex("Spine2")); - glm::vec3 headCurrentPositionAvatarSpace = getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation(); - glm::vec3 upSpine2 = spine2OrientationAvatarSpace * glm::vec3(0.0f, 1.0f, 0.0f); - float angleSpine2 = glm::dot(upSpine2, glm::vec3(0.0f, 1.0f, 0.0f)); - if (headCurrentPositionAvatarSpace.y < (headDefaultPositionAvatarSpace.y - 0.05) && (angleSpine2 > 0.98f)) { - _squatCount++; - } else { - _squatCount = 0; - } - - - // qCDebug(interfaceapp) << "sensor space head pos " << sensorHeadPoseDebug.getTranslation().y; - // put the average hand azimuth into sensor space. // then mix it with head facing direction to determine rotation recenter if (getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid()) { @@ -527,6 +499,29 @@ void MyAvatar::update(float deltaTime) { setCurrentStandingHeight(computeStandingHeightMode(getControllerPoseInAvatarFrame(controller::Action::HEAD))); setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); + // if the head tracker reading is valid then add it to the running average. + auto sensorHeadPoseDebug = getControllerPoseInSensorFrame(controller::Action::HEAD); + if (sensorHeadPoseDebug.isValid()) { + _sumUserHeightSensorSpace += sensorHeadPoseDebug.getTranslation().y; + _averageUserHeightCount++; + } + + // if the spine is straight and the head is below the default position by 5 cm then increment squatty count. + const float SQUAT_THRESHOLD = 0.05f; + const float COSINE_TEN_DEGREES = 0.98f; + glm::vec3 headDefaultPositionAvatarSpace = getAbsoluteDefaultJointTranslationInObjectFrame(getJointIndex("Head")); + glm::quat spine2OrientationAvatarSpace = getAbsoluteJointRotationInObjectFrame(getJointIndex("Spine2")); + glm::vec3 upSpine2 = spine2OrientationAvatarSpace * glm::vec3(0.0f, 1.0f, 0.0f); + if (glm::length(upSpine2) > 0.0f) { + upSpine2 = glm::normalize(upSpine2); + } + float angleSpine2 = glm::dot(upSpine2, glm::vec3(0.0f, 1.0f, 0.0f)); + if (newHeightReading < (headDefaultPositionAvatarSpace.y - SQUAT_THRESHOLD) && (angleSpine2 > COSINE_TEN_DEGREES)) { + _squatCount++; + } else { + _squatCount = 0; + } + if (_drawAverageFacingEnabled) { auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); glm::vec3 worldHeadPos = transformPoint(getSensorToWorldMatrix(), sensorHeadPose.getTranslation()); @@ -3561,12 +3556,6 @@ glm::vec3 MyAvatar::computeCounterBalance() { if (counterBalancedCg.y > (tposeHips.y + 0.05f)) { // if the height is higher than default hips, clamp to default hips counterBalancedCg.y = tposeHips.y + 0.05f; - } else if (counterBalancedCg.y < sitSquatThreshold) { - // do a height reset - setResetMode(true); - // _follow.activate(FollowHelper::Vertical); - // disable cg behaviour in this case. - // setIsInSittingState(true); } return counterBalancedCg; } @@ -4068,34 +4057,29 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl const float CYLINDER_TOP = 0.1f; const float CYLINDER_BOTTOM = -1.5f; const float SITTING_BOTTOM = -0.02f; + const float STANDING_HEIGHT_MULTIPLE = 1.2f; + const float SITTING_HEIGHT_MULTIPLE = 0.833f; + const float COSINE_TEN_DEGREES = 0.98f; + const int SITTING_COUNT_THRESHOLD = 300; + const int SQUATTY_COUNT_THRESHOLD = 600; glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); auto sensorHeadPose = myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD); - glm::vec3 headWorldSpace = myAvatar.getHead()->getPosition(); - glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar.getSensorToWorldMatrix()); - glm::vec3 headSensorSpace = transformVectorFast(myAvatar.getSensorToWorldMatrix(), headWorldSpace); - //get the mode. - //put it in sensor space. - // if we are 20% higher switch to standing. - // 16.6% lower then switch to sitting. - // add this !!!! And the head is upright. glm::vec3 upHead = transformVectorFast(sensorHeadPose.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::vec3 avatarHips = myAvatar.getAbsoluteJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); glm::vec3 worldHips = transformVectorFast(myAvatar.getTransform().getMatrix(),avatarHips); glm::vec3 sensorHips = transformVectorFast(myAvatar.getSensorToWorldMatrix(), worldHips); + float averageSensorSpaceHeight = myAvatar._sumUserHeightSensorSpace / myAvatar._averageUserHeightCount; - // we could add a counting here to make sure that a lean forward doesn't accidentally put you in sitting mode. - // but maybe so what. - // the real test is... can I pick something up in standing mode? - if (myAvatar.getIsInSittingState()) { if (offset.y < SITTING_BOTTOM) { // we recenter when sitting. return true; - } else if (sensorHeadPose.getTranslation().y > (1.2f * averageSensorSpaceHeight)) { + } else if (sensorHeadPose.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * averageSensorSpaceHeight)) { // if we recenter upwards then no longer in sitting state myAvatar.setIsInSittingState(false); return true; @@ -4104,16 +4088,18 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl } } else { // in the standing state - if ((sensorHeadPose.getTranslation().y < (0.83f * averageSensorSpaceHeight)) && (acosHead > 0.98f) && !(sensorHips.y > (0.4f * averageSensorSpaceHeight))) { + if ((sensorHeadPose.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * myAvatar._tippingPoint)) && (acosHead > COSINE_TEN_DEGREES)) { //&& !(sensorHips.y > (0.4f * averageSensorSpaceHeight) myAvatar._sitStandStateCount++; - if (myAvatar._sitStandStateCount > 300) { + if (myAvatar._sitStandStateCount > SITTING_COUNT_THRESHOLD) { myAvatar.setIsInSittingState(true); + myAvatar._tippingPoint = averageSensorSpaceHeight; myAvatar._sitStandStateCount = 0; myAvatar._squatCount = 0; return true; } } else { - if (myAvatar._squatCount > 600) { + myAvatar._tippingPoint = averageSensorSpaceHeight; + if (myAvatar._squatCount > SQUATTY_COUNT_THRESHOLD) { return true; myAvatar._squatCount = 0; } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index c7f6fa89ae..6fe5aeda47 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -140,6 +140,7 @@ class MyAvatar : public Avatar { * @property {number} walkSpeed * @property {number} walkBackwardSpeed * @property {number} sprintSpeed + * @property {number} isInSittingState * * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the * registration point of the 3D model. @@ -1818,6 +1819,7 @@ private: ThreadSafeValueCache _isInSittingState { false }; int _sitStandStateCount { 0 }; int _squatCount { 0 }; + float _tippingPoint { DEFAULT_AVATAR_HEIGHT }; // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; From 8a0fbc3fe9049e3322fd88e42024d698ae344d18 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 2 Oct 2018 17:47:56 -0700 Subject: [PATCH 040/362] changed the height to use the mode . to do fix sensor space bug. --- interface/src/avatar/MyAvatar.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 4f28ae8e90..4892f98da1 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -4074,12 +4074,18 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl glm::vec3 sensorHips = transformVectorFast(myAvatar.getSensorToWorldMatrix(), worldHips); float averageSensorSpaceHeight = myAvatar._sumUserHeightSensorSpace / myAvatar._averageUserHeightCount; + glm::vec3 modeWorldSpace = myAvatar.getTransform().getRotation() * glm::vec3(0.0f,myAvatar.getCurrentStandingHeight(),0.0f); + glm::vec3 modeSensorSpace = extractRotation(myAvatar.getSensorToWorldMatrix()) * modeWorldSpace; + glm::vec3 bodyInSensorSpace = transformPoint(glm::inverse(myAvatar.getSensorToWorldMatrix()), myAvatar.getTransform().getTranslation()); + modeSensorSpace.y = modeSensorSpace.y + bodyInSensorSpace.y; + qCDebug(interfaceapp) << "mode world sensor " << myAvatar.getCurrentStandingHeight() << " " << modeWorldSpace << " " << modeSensorSpace << " " << bodyInSensorSpace; if (myAvatar.getIsInSittingState()) { if (offset.y < SITTING_BOTTOM) { // we recenter when sitting. return true; - } else if (sensorHeadPose.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * averageSensorSpaceHeight)) { + // } else if (sensorHeadPose.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * averageSensorSpaceHeight)) { + } else if (sensorHeadPose.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * modeSensorSpace.y)) { // if we recenter upwards then no longer in sitting state myAvatar.setIsInSittingState(false); return true; @@ -4088,10 +4094,12 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl } } else { // in the standing state - if ((sensorHeadPose.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * myAvatar._tippingPoint)) && (acosHead > COSINE_TEN_DEGREES)) { //&& !(sensorHips.y > (0.4f * averageSensorSpaceHeight) + // if ((sensorHeadPose.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * myAvatar._tippingPoint)) && (acosHead > COSINE_TEN_DEGREES)) { //&& !(sensorHips.y > (0.4f * averageSensorSpaceHeight) + if ((sensorHeadPose.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * modeSensorSpace.y)) && (acosHead > COSINE_TEN_DEGREES)) { myAvatar._sitStandStateCount++; if (myAvatar._sitStandStateCount > SITTING_COUNT_THRESHOLD) { myAvatar.setIsInSittingState(true); + myAvatar.setResetMode(true); myAvatar._tippingPoint = averageSensorSpaceHeight; myAvatar._sitStandStateCount = 0; myAvatar._squatCount = 0; From 809ca0e51266ee8b25edb2ceeb5ce4e8b5491cd0 Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 3 Oct 2018 18:05:30 -0700 Subject: [PATCH 041/362] vive hmd input is implemented. not using mode --- interface/src/avatar/MyAvatar.cpp | 45 ++++++++++++++++++------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 4892f98da1..9db07347c3 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3821,10 +3821,14 @@ void MyAvatar::setIsInWalkingState(bool isWalking) { } void MyAvatar::setIsInSittingState(bool isSitting) { - _isInSittingState.set(isSitting); + _sitStandStateCount = 0; + _squatCount = 0; controller::Pose sensorHeadPoseDebug = getControllerPoseInSensorFrame(controller::Action::HEAD); _sumUserHeightSensorSpace = sensorHeadPoseDebug.getTranslation().y; + _tippingPoint = sensorHeadPoseDebug.getTranslation().y; _averageUserHeightCount = 1; + setResetMode(true); + _isInSittingState.set(isSitting); emit sittingEnabledChanged(isSitting); } @@ -4070,45 +4074,48 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); glm::vec3 avatarHips = myAvatar.getAbsoluteJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); - glm::vec3 worldHips = transformVectorFast(myAvatar.getTransform().getMatrix(),avatarHips); - glm::vec3 sensorHips = transformVectorFast(myAvatar.getSensorToWorldMatrix(), worldHips); + glm::vec3 worldHips = transformPoint(myAvatar.getTransform().getMatrix(),avatarHips); + glm::vec3 sensorHips = transformPoint(glm::inverse(myAvatar.getSensorToWorldMatrix()), worldHips); float averageSensorSpaceHeight = myAvatar._sumUserHeightSensorSpace / myAvatar._averageUserHeightCount; - glm::vec3 modeWorldSpace = myAvatar.getTransform().getRotation() * glm::vec3(0.0f,myAvatar.getCurrentStandingHeight(),0.0f); - glm::vec3 modeSensorSpace = extractRotation(myAvatar.getSensorToWorldMatrix()) * modeWorldSpace; - glm::vec3 bodyInSensorSpace = transformPoint(glm::inverse(myAvatar.getSensorToWorldMatrix()), myAvatar.getTransform().getTranslation()); - modeSensorSpace.y = modeSensorSpace.y + bodyInSensorSpace.y; - qCDebug(interfaceapp) << "mode world sensor " << myAvatar.getCurrentStandingHeight() << " " << modeWorldSpace << " " << modeSensorSpace << " " << bodyInSensorSpace; + glm::vec3 modeWorldSpace = transformPoint(myAvatar.getTransform().getMatrix(), glm::vec3(0.0f,myAvatar.getCurrentStandingHeight(),0.0f)); + glm::vec3 modeSensorSpace = transformPoint(glm::inverse(myAvatar.getSensorToWorldMatrix()), modeWorldSpace); + //qCDebug(interfaceapp) << "mode reading avatar " << myAvatar.getCurrentStandingHeight() << "mode w " << modeWorldSpace << "mode s " << modeSensorSpace << "sensor pose " < (STANDING_HEIGHT_MULTIPLE * averageSensorSpaceHeight)) { - } else if (sensorHeadPose.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * modeSensorSpace.y)) { + } else if (sensorHeadPose.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * averageSensorSpaceHeight)) { + // } else if (sensorHeadPose.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * modeSensorSpace.y)) { // if we recenter upwards then no longer in sitting state - myAvatar.setIsInSittingState(false); - return true; + //myAvatar._sitStandStateCount++; + //if (myAvatar._sitStandStateCount > SITTING_COUNT_THRESHOLD) { + myAvatar.setIsInSittingState(false); + return true; + //} } else { + myAvatar._sitStandStateCount = 0; + myAvatar._tippingPoint = averageSensorSpaceHeight; return false; } } else { // in the standing state // if ((sensorHeadPose.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * myAvatar._tippingPoint)) && (acosHead > COSINE_TEN_DEGREES)) { //&& !(sensorHips.y > (0.4f * averageSensorSpaceHeight) - if ((sensorHeadPose.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * modeSensorSpace.y)) && (acosHead > COSINE_TEN_DEGREES)) { + if ((sensorHeadPose.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * myAvatar._tippingPoint))) { myAvatar._sitStandStateCount++; if (myAvatar._sitStandStateCount > SITTING_COUNT_THRESHOLD) { myAvatar.setIsInSittingState(true); - myAvatar.setResetMode(true); - myAvatar._tippingPoint = averageSensorSpaceHeight; - myAvatar._sitStandStateCount = 0; - myAvatar._squatCount = 0; return true; } } else { myAvatar._tippingPoint = averageSensorSpaceHeight; + myAvatar._sitStandStateCount = 0; if (myAvatar._squatCount > SQUATTY_COUNT_THRESHOLD) { - return true; + // return true; myAvatar._squatCount = 0; } } @@ -4145,7 +4152,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } } if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { - activate(Vertical); + //activate(Vertical); } } else { if (!isActive(Rotation) && getForceActivateRotation()) { From acce675efc4280740508d8e3562e6d39ea85bf40 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 4 Oct 2018 14:53:51 -0700 Subject: [PATCH 042/362] added snap for recentering with sit stand state change. using average height with tipping point for thresholds --- interface/src/avatar/MyAvatar.cpp | 41 ++++++++++++++++++++----------- interface/src/avatar/MyAvatar.h | 1 + 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 9db07347c3..55536748c7 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -4081,26 +4081,26 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl glm::vec3 modeWorldSpace = transformPoint(myAvatar.getTransform().getMatrix(), glm::vec3(0.0f,myAvatar.getCurrentStandingHeight(),0.0f)); glm::vec3 modeSensorSpace = transformPoint(glm::inverse(myAvatar.getSensorToWorldMatrix()), modeWorldSpace); - //qCDebug(interfaceapp) << "mode reading avatar " << myAvatar.getCurrentStandingHeight() << "mode w " << modeWorldSpace << "mode s " << modeSensorSpace << "sensor pose " < (STANDING_HEIGHT_MULTIPLE * averageSensorSpaceHeight)) { + //returnValue = true; + } else if (sensorHeadPose.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * myAvatar._tippingPoint)) { // } else if (sensorHeadPose.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * modeSensorSpace.y)) { // if we recenter upwards then no longer in sitting state - //myAvatar._sitStandStateCount++; - //if (myAvatar._sitStandStateCount > SITTING_COUNT_THRESHOLD) { + myAvatar._sitStandStateCount++; + if (myAvatar._sitStandStateCount > SITTING_COUNT_THRESHOLD) { myAvatar.setIsInSittingState(false); - return true; - //} + myAvatar._sitStandStateChange = true; + returnValue = true; + } } else { myAvatar._sitStandStateCount = 0; myAvatar._tippingPoint = averageSensorSpaceHeight; - return false; } } else { // in the standing state @@ -4109,7 +4109,8 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl myAvatar._sitStandStateCount++; if (myAvatar._sitStandStateCount > SITTING_COUNT_THRESHOLD) { myAvatar.setIsInSittingState(true); - return true; + myAvatar._sitStandStateChange = true; + returnValue = true; } } else { myAvatar._tippingPoint = averageSensorSpaceHeight; @@ -4119,8 +4120,9 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl myAvatar._squatCount = 0; } } - return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + // returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); } + return returnValue; } void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, @@ -4152,7 +4154,9 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } } if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { - //activate(Vertical); + activate(Vertical); + _timeRemaining[(int)Vertical] = 0.1f; + qCDebug(interfaceapp) << "recenter vertically!!!!!! " << hasDriveInput; } } else { if (!isActive(Rotation) && getForceActivateRotation()) { @@ -4209,6 +4213,9 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, co // apply follow displacement to the body matrix. glm::vec3 worldLinearDisplacement = myAvatar.getCharacterController()->getFollowLinearDisplacement(); + if (worldLinearDisplacement.y > 0.0001f) { + qCDebug(interfaceapp) << "linear displacement " << worldLinearDisplacement << " time remaining " << getMaxTimeRemaining(); + } glm::quat worldAngularDisplacement = myAvatar.getCharacterController()->getFollowAngularDisplacement(); glm::mat4 sensorToWorldMatrix = myAvatar.getSensorToWorldMatrix(); @@ -4219,6 +4226,12 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, co glm::mat4 newBodyMat = createMatFromQuatAndPos(sensorAngularDisplacement * glmExtractRotation(currentBodyMatrix), sensorLinearDisplacement + extractTranslation(currentBodyMatrix)); + if (isActive(Vertical)) { + deactivate(Vertical); + return myAvatar.deriveBodyFromHMDSensor(); + } else { + return newBodyMat; + } return newBodyMat; } else { return currentBodyMatrix; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 6fe5aeda47..7854f5cb41 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1807,6 +1807,7 @@ private: ThreadSafeValueCache _userHeight { DEFAULT_AVATAR_HEIGHT }; float _sumUserHeightSensorSpace { DEFAULT_AVATAR_HEIGHT }; int _averageUserHeightCount { 1 }; + bool _sitStandStateChange { false }; void updateChildCauterization(SpatiallyNestablePointer object, bool cauterize); From 82f8c6743678527e6c14ae8b13b66038f2bc6143 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 4 Oct 2018 15:37:01 -0700 Subject: [PATCH 043/362] put the mode in sensor space for cg --- interface/src/avatar/MyAvatar.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 55536748c7..db37b5003b 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -493,7 +493,7 @@ void MyAvatar::update(float deltaTime) { _smoothOrientationTimer += deltaTime; } - float newHeightReading = getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y; + float newHeightReading = getControllerPoseInSensorFrame(controller::Action::HEAD).getTranslation().y; int newHeightReadingInCentimeters = glm::floor(newHeightReading * CENTIMETERS_PER_METER); _recentModeReadings.insert(newHeightReadingInCentimeters); setCurrentStandingHeight(computeStandingHeightMode(getControllerPoseInAvatarFrame(controller::Action::HEAD))); @@ -4015,6 +4015,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD); controller::Pose currentLeftHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND); controller::Pose currentRightHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND); + controller::Pose currentHeadSensorPose = myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD); bool stepDetected = false; float myScale = myAvatar.getAvatarScale(); @@ -4028,7 +4029,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons } else { if (!withinBaseOfSupport(currentHeadPose) && headAngularVelocityBelowThreshold(currentHeadPose) && - isWithinThresholdHeightMode(currentHeadPose, myAvatar.getCurrentStandingHeight(), myScale) && + isWithinThresholdHeightMode(currentHeadSensorPose, myAvatar.getCurrentStandingHeight(), myScale) && handDirectionMatchesHeadDirection(currentLeftHandPose, currentRightHandPose, currentHeadPose) && handAngularVelocityBelowThreshold(currentLeftHandPose, currentRightHandPose) && headVelocityGreaterThanThreshold(currentHeadPose) && @@ -4083,7 +4084,7 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl // qCDebug(interfaceapp) << "mode reading avatar " << myAvatar.getCurrentStandingHeight() << "mode w " << modeWorldSpace << "mode s " << modeSensorSpace << "sensor pose " < Date: Thu, 4 Oct 2018 16:40:44 -0700 Subject: [PATCH 044/362] Restore missing abs() to quaternion dot-product See https://github.com/highfidelity/hifi/pull/14138 --- libraries/avatars/src/AvatarData.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 07751d03e3..57d36e1f0f 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -636,7 +636,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // The dot product for larger rotations is a lower number, // so if the dot() is less than the value, then the rotation is a larger angle of rotation if (sendAll || last.rotationIsDefaultPose || (!cullSmallChanges && last.rotation != data.rotation) - || (cullSmallChanges && glm::dot(last.rotation, data.rotation) < minRotationDOT)) { + || (cullSmallChanges && fabsf(glm::dot(last.rotation, data.rotation)) < minRotationDOT)) { validityPosition[i / BITS_IN_BYTE] |= 1 << (i % BITS_IN_BYTE); #ifdef WANT_DEBUG rotationSentCount++; From 635300c6d5a7f8b686bf100ef39577af56650c12 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 4 Oct 2018 18:05:05 -0700 Subject: [PATCH 045/362] changed pre physics to use cg for desired body when in cg mode --- interface/src/avatar/MyAvatar.cpp | 58 +++++++++++++------------------ interface/src/avatar/MyAvatar.h | 4 +-- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index db37b5003b..09896e66c3 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -493,17 +493,14 @@ void MyAvatar::update(float deltaTime) { _smoothOrientationTimer += deltaTime; } - float newHeightReading = getControllerPoseInSensorFrame(controller::Action::HEAD).getTranslation().y; - int newHeightReadingInCentimeters = glm::floor(newHeightReading * CENTIMETERS_PER_METER); - _recentModeReadings.insert(newHeightReadingInCentimeters); - setCurrentStandingHeight(computeStandingHeightMode(getControllerPoseInAvatarFrame(controller::Action::HEAD))); - setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); - - // if the head tracker reading is valid then add it to the running average. - auto sensorHeadPoseDebug = getControllerPoseInSensorFrame(controller::Action::HEAD); - if (sensorHeadPoseDebug.isValid()) { - _sumUserHeightSensorSpace += sensorHeadPoseDebug.getTranslation().y; + controller::Pose newHeightReading = getControllerPoseInSensorFrame(controller::Action::HEAD); + if (newHeightReading.isValid()) { + int newHeightReadingInCentimeters = glm::floor(newHeightReading.getTranslation().y * CENTIMETERS_PER_METER); + _sumUserHeightSensorSpace += newHeightReading.getTranslation().y; _averageUserHeightCount++; + _recentModeReadings.insert(newHeightReadingInCentimeters); + setCurrentStandingHeight(computeStandingHeightMode(newHeightReading)); + setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); } // if the spine is straight and the head is below the default position by 5 cm then increment squatty count. @@ -516,7 +513,7 @@ void MyAvatar::update(float deltaTime) { upSpine2 = glm::normalize(upSpine2); } float angleSpine2 = glm::dot(upSpine2, glm::vec3(0.0f, 1.0f, 0.0f)); - if (newHeightReading < (headDefaultPositionAvatarSpace.y - SQUAT_THRESHOLD) && (angleSpine2 > COSINE_TEN_DEGREES)) { + if (getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPositionAvatarSpace.y - SQUAT_THRESHOLD) && (angleSpine2 > COSINE_TEN_DEGREES)) { _squatCount++; } else { _squatCount = 0; @@ -2031,6 +2028,11 @@ void MyAvatar::prepareForPhysicsSimulation() { _characterController.setPositionAndOrientation(getWorldPosition(), getWorldOrientation()); auto headPose = getControllerPoseInAvatarFrame(controller::Action::HEAD); if (headPose.isValid()) { + if (getCenterOfGravityModelEnabled() && !getIsInSittingState()) { + _follow.prePhysicsUpdate(*this, deriveBodyUsingCgModel(), _bodySensorMatrix, hasDriveInput()); + } else { + _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput()); + } _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput()); } else { _follow.deactivate(); @@ -3823,9 +3825,11 @@ void MyAvatar::setIsInWalkingState(bool isWalking) { void MyAvatar::setIsInSittingState(bool isSitting) { _sitStandStateCount = 0; _squatCount = 0; - controller::Pose sensorHeadPoseDebug = getControllerPoseInSensorFrame(controller::Action::HEAD); - _sumUserHeightSensorSpace = sensorHeadPoseDebug.getTranslation().y; - _tippingPoint = sensorHeadPoseDebug.getTranslation().y; + controller::Pose sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); + if (sensorHeadPose.isValid()) { + _sumUserHeightSensorSpace = sensorHeadPose.getTranslation().y; + _tippingPoint = sensorHeadPose.getTranslation().y; + } _averageUserHeightCount = 1; setResetMode(true); _isInSittingState.set(isSitting); @@ -4077,21 +4081,14 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl glm::vec3 avatarHips = myAvatar.getAbsoluteJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); glm::vec3 worldHips = transformPoint(myAvatar.getTransform().getMatrix(),avatarHips); glm::vec3 sensorHips = transformPoint(glm::inverse(myAvatar.getSensorToWorldMatrix()), worldHips); - + //qCDebug(interfaceapp) << " current mode " << myAvatar.getCurrentStandingHeight() << " " << sensorHeadPose.getTranslation().y << " state " << myAvatar.getIsInSittingState(); float averageSensorSpaceHeight = myAvatar._sumUserHeightSensorSpace / myAvatar._averageUserHeightCount; - glm::vec3 modeWorldSpace = transformPoint(myAvatar.getTransform().getMatrix(), glm::vec3(0.0f,myAvatar.getCurrentStandingHeight(),0.0f)); - glm::vec3 modeSensorSpace = transformPoint(glm::inverse(myAvatar.getSensorToWorldMatrix()), modeWorldSpace); - - // qCDebug(interfaceapp) << "mode reading avatar " << myAvatar.getCurrentStandingHeight() << "mode w " << modeWorldSpace << "mode s " << modeSensorSpace << "sensor pose " < (STANDING_HEIGHT_MULTIPLE * myAvatar._tippingPoint)) { - // } else if (sensorHeadPose.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * modeSensorSpace.y)) { // if we recenter upwards then no longer in sitting state myAvatar._sitStandStateCount++; if (myAvatar._sitStandStateCount > SITTING_COUNT_THRESHOLD) { @@ -4105,7 +4102,7 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl } } else { // in the standing state - // if ((sensorHeadPose.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * myAvatar._tippingPoint)) && (acosHead > COSINE_TEN_DEGREES)) { //&& !(sensorHips.y > (0.4f * averageSensorSpaceHeight) + // && (acosHead > COSINE_TEN_DEGREES)) { //&& !(sensorHips.y > (0.4f * averageSensorSpaceHeight) if ((sensorHeadPose.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * myAvatar._tippingPoint))) { myAvatar._sitStandStateCount++; if (myAvatar._sitStandStateCount > SITTING_COUNT_THRESHOLD) { @@ -4114,14 +4111,14 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl returnValue = true; } } else { - myAvatar._tippingPoint = averageSensorSpaceHeight; + myAvatar._tippingPoint = myAvatar.getCurrentStandingHeight(); myAvatar._sitStandStateCount = 0; if (myAvatar._squatCount > SQUATTY_COUNT_THRESHOLD) { // return true; myAvatar._squatCount = 0; } } - // returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); } return returnValue; } @@ -4156,7 +4153,6 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Vertical); - _timeRemaining[(int)Vertical] = 0.1f; qCDebug(interfaceapp) << "recenter vertically!!!!!! " << hasDriveInput; } } else { @@ -4214,9 +4210,6 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, co // apply follow displacement to the body matrix. glm::vec3 worldLinearDisplacement = myAvatar.getCharacterController()->getFollowLinearDisplacement(); - if (worldLinearDisplacement.y > 0.0001f) { - qCDebug(interfaceapp) << "linear displacement " << worldLinearDisplacement << " time remaining " << getMaxTimeRemaining(); - } glm::quat worldAngularDisplacement = myAvatar.getCharacterController()->getFollowAngularDisplacement(); glm::mat4 sensorToWorldMatrix = myAvatar.getSensorToWorldMatrix(); @@ -4228,10 +4221,9 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, co glm::mat4 newBodyMat = createMatFromQuatAndPos(sensorAngularDisplacement * glmExtractRotation(currentBodyMatrix), sensorLinearDisplacement + extractTranslation(currentBodyMatrix)); if (isActive(Vertical)) { + // myAvatar._sitStandStateChange = false; deactivate(Vertical); - return myAvatar.deriveBodyFromHMDSensor(); - } else { - return newBodyMat; + newBodyMat = myAvatar.deriveBodyFromHMDSensor(); } return newBodyMat; } else { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 7854f5cb41..0c6b1b01a5 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1805,7 +1805,7 @@ private: // height of user in sensor space, when standing erect. ThreadSafeValueCache _userHeight { DEFAULT_AVATAR_HEIGHT }; - float _sumUserHeightSensorSpace { DEFAULT_AVATAR_HEIGHT }; + float _sumUserHeightSensorSpace { 0.0f }; int _averageUserHeightCount { 1 }; bool _sitStandStateChange { false }; @@ -1820,7 +1820,7 @@ private: ThreadSafeValueCache _isInSittingState { false }; int _sitStandStateCount { 0 }; int _squatCount { 0 }; - float _tippingPoint { DEFAULT_AVATAR_HEIGHT }; + float _tippingPoint { 0.0f }; // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; From cbe638bfdb18d1420dacc470d46671ee4bb8c79e Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 4 Oct 2018 18:16:39 -0700 Subject: [PATCH 046/362] added flag for vertical recentering that is state change, snap, versus the usual .5 second recentering --- interface/src/avatar/MyAvatar.cpp | 6 +++--- interface/src/avatar/MyAvatar.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 09896e66c3..8756f1bbd8 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -4203,7 +4203,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat myAvatar.getCharacterController()->setFollowParameters(followWorldPose, getMaxTimeRemaining()); } -glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix) { +glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix) { if (isActive()) { float dt = myAvatar.getCharacterController()->getFollowTime(); decrementTimeRemaining(dt); @@ -4220,8 +4220,8 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, co glm::mat4 newBodyMat = createMatFromQuatAndPos(sensorAngularDisplacement * glmExtractRotation(currentBodyMatrix), sensorLinearDisplacement + extractTranslation(currentBodyMatrix)); - if (isActive(Vertical)) { - // myAvatar._sitStandStateChange = false; + if (myAvatar._sitStandStateChange) { + myAvatar._sitStandStateChange = false; deactivate(Vertical); newBodyMat = myAvatar.deriveBodyFromHMDSensor(); } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 0c6b1b01a5..072ea04ee7 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1725,7 +1725,7 @@ private: bool shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateHorizontalCG(MyAvatar& myAvatar) const; void prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& bodySensorMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput); - glm::mat4 postPhysicsUpdate(const MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix); + glm::mat4 postPhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix); bool getForceActivateRotation() const; void setForceActivateRotation(bool val); bool getForceActivateVertical() const; From ad46b7196634e301f9fdd5d5f8d0e11342959446 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 5 Oct 2018 13:04:14 -0700 Subject: [PATCH 047/362] before moving the step state counting to myavatar::update() --- interface/src/avatar/MyAvatar.cpp | 28 ++++++++++++++++++++++------ interface/src/avatar/MyAvatar.h | 1 + 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 8756f1bbd8..c5defe3d72 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -4103,6 +4103,7 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl } else { // in the standing state // && (acosHead > COSINE_TEN_DEGREES)) { //&& !(sensorHips.y > (0.4f * averageSensorSpaceHeight) + if ((sensorHeadPose.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * myAvatar._tippingPoint))) { myAvatar._sitStandStateCount++; if (myAvatar._sitStandStateCount > SITTING_COUNT_THRESHOLD) { @@ -4111,14 +4112,15 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl returnValue = true; } } else { + returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); myAvatar._tippingPoint = myAvatar.getCurrentStandingHeight(); myAvatar._sitStandStateCount = 0; if (myAvatar._squatCount > SQUATTY_COUNT_THRESHOLD) { - // return true; + // returnValue = true; myAvatar._squatCount = 0; } } - returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + } return returnValue; } @@ -4151,10 +4153,20 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } } } - if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { - activate(Vertical); - qCDebug(interfaceapp) << "recenter vertically!!!!!! " << hasDriveInput; + + qCDebug(interfaceapp) << "velocity of headset " << glm::length(myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD).getVelocity()); + + if (_velocityCount > 60) { + if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { + activate(Vertical); + qCDebug(interfaceapp) << "recenter vertically!!!!!! " << hasDriveInput; + } + } else { + if ((glm::length(myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD).getVelocity()) > 0.1f)) { + _velocityCount++; + } } + } else { if (!isActive(Rotation) && getForceActivateRotation()) { activate(Rotation); @@ -4223,7 +4235,11 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(MyAvatar& myAvatar, const gl if (myAvatar._sitStandStateChange) { myAvatar._sitStandStateChange = false; deactivate(Vertical); - newBodyMat = myAvatar.deriveBodyFromHMDSensor(); + + qCDebug(interfaceapp) << "before snap " << extractTranslation(newBodyMat); + //newBodyMat = myAvatar.deriveBodyFromHMDSensor(); + setTranslation(newBodyMat, extractTranslation(myAvatar.deriveBodyFromHMDSensor())); + qCDebug(interfaceapp) << "after snap " << extractTranslation(newBodyMat); } return newBodyMat; } else { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 072ea04ee7..24a58745a3 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1738,6 +1738,7 @@ private: std::atomic _forceActivateVertical { false }; std::atomic _forceActivateHorizontal { false }; std::atomic _toggleHipsFollowing { true }; + int _velocityCount { 0 }; }; FollowHelper _follow; From 96872f841202f4a60fc8b3b2a77f9632d0197ce8 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 5 Oct 2018 14:55:32 -0700 Subject: [PATCH 048/362] moved all the state update for sit and stand to myavatar::update() also made it so that sit horizontal reset is handled in non-cg recentering. to do: revisit what should be sent for desired body in prephysics update call --- interface/src/avatar/MyAvatar.cpp | 143 ++++++++++++++++++------------ 1 file changed, 87 insertions(+), 56 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c5defe3d72..58a0ef4046 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -467,6 +467,11 @@ void MyAvatar::update(float deltaTime) { // update moving average of HMD facing in xz plane. const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); const float PERCENTAGE_WEIGHT_HEAD_VS_SHOULDERS_AZIMUTH = 0.0f; // 100 percent shoulders + const float STANDING_HEIGHT_MULTIPLE = 1.2f; + const float SITTING_HEIGHT_MULTIPLE = 0.833f; + const float COSINE_TEN_DEGREES = 0.98f; + const int SITTING_COUNT_THRESHOLD = 300; + const int SQUATTY_COUNT_THRESHOLD = 600; float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); @@ -505,7 +510,6 @@ void MyAvatar::update(float deltaTime) { // if the spine is straight and the head is below the default position by 5 cm then increment squatty count. const float SQUAT_THRESHOLD = 0.05f; - const float COSINE_TEN_DEGREES = 0.98f; glm::vec3 headDefaultPositionAvatarSpace = getAbsoluteDefaultJointTranslationInObjectFrame(getJointIndex("Head")); glm::quat spine2OrientationAvatarSpace = getAbsoluteJointRotationInObjectFrame(getJointIndex("Spine2")); glm::vec3 upSpine2 = spine2OrientationAvatarSpace * glm::vec3(0.0f, 1.0f, 0.0f); @@ -519,6 +523,65 @@ void MyAvatar::update(float deltaTime) { _squatCount = 0; } + float averageSensorSpaceHeight = _sumUserHeightSensorSpace / _averageUserHeightCount; + //auto sensorHeadPose = myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD); + //glm::vec3 upHead = transformVectorFast(sensorHeadPose.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); + //float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); + + glm::vec3 avatarHips = getAbsoluteJointTranslationInObjectFrame(getJointIndex("Hips")); + glm::vec3 worldHips = transformPoint(getTransform().getMatrix(), avatarHips); + glm::vec3 sensorHips = transformPoint(glm::inverse(getSensorToWorldMatrix()), worldHips); + + // put update sit stand state counts here + if (getIsInSittingState()) { + if (newHeightReading.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { + // if we recenter upwards then no longer in sitting state + _sitStandStateCount++; + if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sitStandStateCount = 0; + _squatCount = 0; + if (newHeightReading.isValid()) { + _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; + _tippingPoint = newHeightReading.getTranslation().y; + } + _averageUserHeightCount = 1; + setResetMode(true); + setIsInSittingState(false); + setCenterOfGravityModelEnabled(true); + _sitStandStateChange = true; + } + } else { + _sitStandStateCount = 0; + // tipping point is average height when sitting. + _tippingPoint = averageSensorSpaceHeight; + } + } else { + // in the standing state + if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint))) {// && (angleSpine2 > COSINE_TEN_DEGREES) && !(sensorHips.y > (0.4f * averageSensorSpaceHeight))) { + _sitStandStateCount++; + if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sitStandStateCount = 0; + _squatCount = 0; + if (newHeightReading.isValid()) { + _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; + _tippingPoint = newHeightReading.getTranslation().y; + } + _averageUserHeightCount = 1; + setResetMode(true); + setIsInSittingState(true); + setCenterOfGravityModelEnabled(false); + _sitStandStateChange = true; + } + } else { + // returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + // use the mode height for the tipping point when we are standing. + _tippingPoint = getCurrentStandingHeight(); + _sitStandStateCount = 0; + } + + } + + if (_drawAverageFacingEnabled) { auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); glm::vec3 worldHeadPos = transformPoint(getSensorToWorldMatrix(), sensorHeadPose.getTranslation()); @@ -2028,10 +2091,10 @@ void MyAvatar::prepareForPhysicsSimulation() { _characterController.setPositionAndOrientation(getWorldPosition(), getWorldOrientation()); auto headPose = getControllerPoseInAvatarFrame(controller::Action::HEAD); if (headPose.isValid()) { - if (getCenterOfGravityModelEnabled() && !getIsInSittingState()) { - _follow.prePhysicsUpdate(*this, deriveBodyUsingCgModel(), _bodySensorMatrix, hasDriveInput()); + if (getCenterOfGravityModelEnabled()) { + //_follow.prePhysicsUpdate(*this, deriveBodyUsingCgModel(), _bodySensorMatrix, hasDriveInput()); } else { - _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput()); + //_follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput()); } _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput()); } else { @@ -3995,6 +4058,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, // x axis of currentBodyMatrix in world space. glm::vec3 right = glm::normalize(glm::vec3(currentBodyMatrix[0][0], currentBodyMatrix[1][0], currentBodyMatrix[2][0])); glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); + controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD); float forwardLeanAmount = glm::dot(forward, offset); float lateralLeanAmount = glm::dot(right, offset); @@ -4003,14 +4067,19 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, const float MAX_FORWARD_LEAN = 0.15f; const float MAX_BACKWARD_LEAN = 0.1f; - - if (forwardLeanAmount > 0 && forwardLeanAmount > MAX_FORWARD_LEAN) { - return true; + bool stepDetected = false; + if (myAvatar.getIsInSittingState()) { + if (!withinBaseOfSupport(currentHeadPose)) { + stepDetected = true; + } + } else if (forwardLeanAmount > 0 && forwardLeanAmount > MAX_FORWARD_LEAN) { + stepDetected = true; } else if (forwardLeanAmount < 0 && forwardLeanAmount < -MAX_BACKWARD_LEAN) { - return true; + stepDetected = true; + } else { + stepDetected = fabs(lateralLeanAmount) > MAX_LATERAL_LEAN; } - - return fabs(lateralLeanAmount) > MAX_LATERAL_LEAN; + return stepDetected; } bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) const { @@ -4026,10 +4095,6 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons if (myAvatar.getIsInWalkingState()) { stepDetected = true; - } else if (myAvatar.getIsInSittingState()) { - if (!withinBaseOfSupport(currentHeadPose)) { - stepDetected = true; - } } else { if (!withinBaseOfSupport(currentHeadPose) && headAngularVelocityBelowThreshold(currentHeadPose) && @@ -4066,61 +4131,27 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl const float CYLINDER_TOP = 0.1f; const float CYLINDER_BOTTOM = -1.5f; const float SITTING_BOTTOM = -0.02f; - const float STANDING_HEIGHT_MULTIPLE = 1.2f; - const float SITTING_HEIGHT_MULTIPLE = 0.833f; - const float COSINE_TEN_DEGREES = 0.98f; - const int SITTING_COUNT_THRESHOLD = 300; const int SQUATTY_COUNT_THRESHOLD = 600; glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); - - auto sensorHeadPose = myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD); - glm::vec3 upHead = transformVectorFast(sensorHeadPose.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); - float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); - - glm::vec3 avatarHips = myAvatar.getAbsoluteJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); - glm::vec3 worldHips = transformPoint(myAvatar.getTransform().getMatrix(),avatarHips); - glm::vec3 sensorHips = transformPoint(glm::inverse(myAvatar.getSensorToWorldMatrix()), worldHips); //qCDebug(interfaceapp) << " current mode " << myAvatar.getCurrentStandingHeight() << " " << sensorHeadPose.getTranslation().y << " state " << myAvatar.getIsInSittingState(); - float averageSensorSpaceHeight = myAvatar._sumUserHeightSensorSpace / myAvatar._averageUserHeightCount; + bool returnValue = false; + if (myAvatar._sitStandStateChange) { + returnValue = true; + } if (myAvatar.getIsInSittingState()) { if (offset.y < SITTING_BOTTOM) { // we recenter when sitting. returnValue = true; - } else if (sensorHeadPose.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * myAvatar._tippingPoint)) { - // if we recenter upwards then no longer in sitting state - myAvatar._sitStandStateCount++; - if (myAvatar._sitStandStateCount > SITTING_COUNT_THRESHOLD) { - myAvatar.setIsInSittingState(false); - myAvatar._sitStandStateChange = true; - returnValue = true; - } - } else { - myAvatar._sitStandStateCount = 0; - myAvatar._tippingPoint = averageSensorSpaceHeight; } } else { // in the standing state - // && (acosHead > COSINE_TEN_DEGREES)) { //&& !(sensorHips.y > (0.4f * averageSensorSpaceHeight) - - if ((sensorHeadPose.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * myAvatar._tippingPoint))) { - myAvatar._sitStandStateCount++; - if (myAvatar._sitStandStateCount > SITTING_COUNT_THRESHOLD) { - myAvatar.setIsInSittingState(true); - myAvatar._sitStandStateChange = true; - returnValue = true; - } - } else { - returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); - myAvatar._tippingPoint = myAvatar.getCurrentStandingHeight(); - myAvatar._sitStandStateCount = 0; - if (myAvatar._squatCount > SQUATTY_COUNT_THRESHOLD) { - // returnValue = true; - myAvatar._squatCount = 0; - } + returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + if (myAvatar._squatCount > SQUATTY_COUNT_THRESHOLD) { + myAvatar._squatCount = 0; + returnValue = true; } - } return returnValue; } From 3517905435d5b0fa2c276f9c313de8de5460712e Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 5 Oct 2018 16:22:30 -0700 Subject: [PATCH 049/362] added getter and setter for sitstandstatechange bool --- interface/src/avatar/MyAvatar.cpp | 50 ++++++++++--------------------- interface/src/avatar/MyAvatar.h | 18 ++++++----- 2 files changed, 27 insertions(+), 41 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 58a0ef4046..995126f3ca 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -524,14 +524,11 @@ void MyAvatar::update(float deltaTime) { } float averageSensorSpaceHeight = _sumUserHeightSensorSpace / _averageUserHeightCount; - //auto sensorHeadPose = myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD); - //glm::vec3 upHead = transformVectorFast(sensorHeadPose.getMatrix(), glm::vec3(0.0f, 1.0f, 0.0f)); - //float acosHead = glm::dot(upHead, glm::vec3(0.0f, 1.0f, 0.0f)); glm::vec3 avatarHips = getAbsoluteJointTranslationInObjectFrame(getJointIndex("Hips")); glm::vec3 worldHips = transformPoint(getTransform().getMatrix(), avatarHips); glm::vec3 sensorHips = transformPoint(glm::inverse(getSensorToWorldMatrix()), worldHips); - + // put update sit stand state counts here if (getIsInSittingState()) { if (newHeightReading.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { @@ -548,11 +545,11 @@ void MyAvatar::update(float deltaTime) { setResetMode(true); setIsInSittingState(false); setCenterOfGravityModelEnabled(true); - _sitStandStateChange = true; + setSitStandStateChange(true); } } else { _sitStandStateCount = 0; - // tipping point is average height when sitting. + // tipping point is average height when sitting. _tippingPoint = averageSensorSpaceHeight; } } else { @@ -570,7 +567,7 @@ void MyAvatar::update(float deltaTime) { setResetMode(true); setIsInSittingState(true); setCenterOfGravityModelEnabled(false); - _sitStandStateChange = true; + setSitStandStateChange(true); } } else { // returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); @@ -2091,11 +2088,6 @@ void MyAvatar::prepareForPhysicsSimulation() { _characterController.setPositionAndOrientation(getWorldPosition(), getWorldOrientation()); auto headPose = getControllerPoseInAvatarFrame(controller::Action::HEAD); if (headPose.isValid()) { - if (getCenterOfGravityModelEnabled()) { - //_follow.prePhysicsUpdate(*this, deriveBodyUsingCgModel(), _bodySensorMatrix, hasDriveInput()); - } else { - //_follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput()); - } _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput()); } else { _follow.deactivate(); @@ -3886,15 +3878,6 @@ void MyAvatar::setIsInWalkingState(bool isWalking) { } void MyAvatar::setIsInSittingState(bool isSitting) { - _sitStandStateCount = 0; - _squatCount = 0; - controller::Pose sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); - if (sensorHeadPose.isValid()) { - _sumUserHeightSensorSpace = sensorHeadPose.getTranslation().y; - _tippingPoint = sensorHeadPose.getTranslation().y; - } - _averageUserHeightCount = 1; - setResetMode(true); _isInSittingState.set(isSitting); emit sittingEnabledChanged(isSitting); } @@ -3915,6 +3898,14 @@ float MyAvatar::getSprintSpeed() const { return _sprintSpeed.get(); } +void MyAvatar::setSitStandStateChange(bool stateChanged) { + _sitStandStateChange = stateChanged; +} + +float MyAvatar::getSitStandStateChange() const { + return _sitStandStateChange; +} + QVector MyAvatar::getScriptUrls() { QVector scripts = _skeletonModel->isLoaded() ? _skeletonModel->getFBXGeometry().scripts : QVector(); return scripts; @@ -4134,10 +4125,8 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl const int SQUATTY_COUNT_THRESHOLD = 600; glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); - //qCDebug(interfaceapp) << " current mode " << myAvatar.getCurrentStandingHeight() << " " << sensorHeadPose.getTranslation().y << " state " << myAvatar.getIsInSittingState(); - bool returnValue = false; - if (myAvatar._sitStandStateChange) { + if (myAvatar.getSitStandStateChange()) { returnValue = true; } if (myAvatar.getIsInSittingState()) { @@ -4184,13 +4173,10 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } } } - - qCDebug(interfaceapp) << "velocity of headset " << glm::length(myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD).getVelocity()); - + if (_velocityCount > 60) { if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Vertical); - qCDebug(interfaceapp) << "recenter vertically!!!!!! " << hasDriveInput; } } else { if ((glm::length(myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD).getVelocity()) > 0.1f)) { @@ -4263,14 +4249,10 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(MyAvatar& myAvatar, const gl glm::mat4 newBodyMat = createMatFromQuatAndPos(sensorAngularDisplacement * glmExtractRotation(currentBodyMatrix), sensorLinearDisplacement + extractTranslation(currentBodyMatrix)); - if (myAvatar._sitStandStateChange) { - myAvatar._sitStandStateChange = false; + if (myAvatar.getSitStandStateChange()) { + myAvatar.setSitStandStateChange(false); deactivate(Vertical); - - qCDebug(interfaceapp) << "before snap " << extractTranslation(newBodyMat); - //newBodyMat = myAvatar.deriveBodyFromHMDSensor(); setTranslation(newBodyMat, extractTranslation(myAvatar.deriveBodyFromHMDSensor())); - qCDebug(interfaceapp) << "after snap " << extractTranslation(newBodyMat); } return newBodyMat; } else { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 24a58745a3..54aa015aff 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1105,6 +1105,8 @@ public: float getWalkBackwardSpeed() const; void setSprintSpeed(float value); float getSprintSpeed() const; + void setSitStandStateChange(bool stateChanged); + float getSitStandStateChange() const; QVector getScriptUrls(); @@ -1804,14 +1806,16 @@ private: std::mutex _pinnedJointsMutex; std::vector _pinnedJoints; - // height of user in sensor space, when standing erect. - ThreadSafeValueCache _userHeight { DEFAULT_AVATAR_HEIGHT }; - float _sumUserHeightSensorSpace { 0.0f }; - int _averageUserHeightCount { 1 }; - bool _sitStandStateChange { false }; - void updateChildCauterization(SpatiallyNestablePointer object, bool cauterize); + const float DEFAULT_FLOOR_HEIGHT = 0.0f; + + // height of user in sensor space, when standing erect. + ThreadSafeValueCache _userHeight{ DEFAULT_AVATAR_HEIGHT }; + float _sumUserHeightSensorSpace{ DEFAULT_AVATAR_HEIGHT }; + int _averageUserHeightCount{ 1 }; + bool _sitStandStateChange{ false }; + // max unscaled forward movement speed ThreadSafeValueCache _walkSpeed { DEFAULT_AVATAR_MAX_WALKING_SPEED }; ThreadSafeValueCache _walkBackwardSpeed { DEFAULT_AVATAR_MAX_WALKING_BACKWARD_SPEED }; @@ -1821,7 +1825,7 @@ private: ThreadSafeValueCache _isInSittingState { false }; int _sitStandStateCount { 0 }; int _squatCount { 0 }; - float _tippingPoint { 0.0f }; + float _tippingPoint { DEFAULT_FLOOR_HEIGHT }; // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; From ea8229fca49862635cf90e396815d795567fa46d Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 8 Oct 2018 13:10:41 -0700 Subject: [PATCH 050/362] adding fix for length check --- .../system/controllers/controllerModules/farActionGrabEntity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 2e73526728..2fe98ae673 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -317,7 +317,7 @@ Script.include("/~/system/libraries/Xform.js"); }; this.restoreIgnoredEntities = function() { - for (var i = 0; i < this.ignoredEntities; i++) { + for (var i = 0; i < this.ignoredEntities.length; i++) { var data = { action: 'remove', id: this.ignoredEntities[i] From 6f9593dabd4dc06b48faeb04f4d5d2869b21660a Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 8 Oct 2018 16:59:39 -0700 Subject: [PATCH 051/362] adding fix for far action grab --- .../controllerModules/farActionGrabEntity.js | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 2fe98ae673..4e9ce44d64 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -336,15 +336,6 @@ Script.include("/~/system/libraries/Xform.js"); if ((intersection.type === Picks.INTERSECTED_ENTITY && entityType === "Web") || intersection.type === Picks.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) { return true; - } else if (intersection.type === Picks.INTERSECTED_ENTITY && !Window.isPhysicsEnabled()) { - // add to ignored items. - var data = { - action: 'add', - id: intersection.objectID - }; - Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)); - this.ignoredEntities.push(intersection.objectID); - } return false; }; @@ -405,7 +396,6 @@ Script.include("/~/system/libraries/Xform.js"); this.isReady = function (controllerData) { if (HMD.active) { if (this.notPointingAtEntity(controllerData)) { - this.restoreIgnoredEntities(); return makeRunningValues(false, [], []); } @@ -417,17 +407,28 @@ Script.include("/~/system/libraries/Xform.js"); return makeRunningValues(true, [], []); } else { this.destroyContextOverlay(); - this.restoreIgnoredEntities(); return makeRunningValues(false, [], []); } } - this.restoreIgnoredEntities(); 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) || this.targetIsNull()) { + (this.notPointingAtEntity(controllerData) && Window.isPhysicsEnabled()) || this.targetIsNull()) { this.endFarGrabAction(); Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity); From 5fbf10c2f390918f126ea287c52302df8038add3 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 8 Oct 2018 23:48:12 -0700 Subject: [PATCH 052/362] INtroducing the LODEnabled flag on the render item in order prevent LOD to hide important objects regardless of the LOD level. --- interface/src/ui/overlays/ModelOverlay.cpp | 22 ++++++++++++++++++++++ interface/src/ui/overlays/ModelOverlay.h | 3 +++ libraries/render-utils/src/Model.cpp | 11 +++++++++++ libraries/render-utils/src/Model.h | 3 +++ libraries/render/src/render/CullTask.cpp | 6 +++--- libraries/render/src/render/Item.h | 9 +++++++++ scripts/system/libraries/WebTablet.js | 3 ++- 7 files changed, 53 insertions(+), 4 deletions(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index eee8222051..2379685252 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -127,6 +127,11 @@ void ModelOverlay::update(float deltatime) { _model->setGroupCulled(_isGroupCulled, scene); metaDirty = true; } + if (_lodEnabledDirty) { + _lodEnabledDirty = false; + _model->setLODEnabled(_isLODEnabled, scene); + metaDirty = true; + } if (metaDirty) { transaction.updateItem(getRenderItemID(), [](Overlay& data) {}); } @@ -191,6 +196,14 @@ void ModelOverlay::setGroupCulled(bool groupCulled) { } } +void ModelOverlay::setLODEnabled(bool lodEnabled) { + if (lodEnabled != _isLODEnabled) { + _isLODEnabled = lodEnabled; + _lodEnabledDirty = true; + } +} + + void ModelOverlay::setProperties(const QVariantMap& properties) { auto origPosition = getWorldPosition(); auto origRotation = getWorldOrientation(); @@ -248,6 +261,12 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { setGroupCulled(groupCulledValue.toBool()); } + auto lodEnabledValue = properties["isLODEnabled"]; + if (lodEnabledValue.isValid() && lodEnabledValue.canConvert(QVariant::Bool)) { + setLODEnabled(lodEnabledValue.toBool()); + } + + // jointNames is read-only. // jointPositions is read-only. // jointOrientations is read-only. @@ -765,5 +784,8 @@ render::ItemKey ModelOverlay::getKey() { if (_isGroupCulled) { builder.withMetaCullGroup(); } + if (!_isLODEnabled) { + builder.withLODDisabled(); + } return builder.build(); } \ No newline at end of file diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index bd922e258a..dc127afe48 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -69,6 +69,7 @@ public: void setDrawInFront(bool drawInFront) override; void setDrawHUDLayer(bool drawHUDLayer) override; void setGroupCulled(bool groupCulled); + void setLODEnabled(bool lodEnabled); void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) override; void removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName) override; @@ -130,6 +131,8 @@ private: bool _drawInHUDDirty { false }; bool _isGroupCulled { false }; bool _groupCulledDirty { false }; + bool _isLODEnabled{ true }; + bool _lodEnabledDirty{ true }; void processMaterials(); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index a700c200f8..8b50f2e420 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -973,6 +973,17 @@ bool Model::isGroupCulled() const { return _renderItemKeyGlobalFlags.isSubMetaCulled(); } +void Model::setLODEnabled(bool isLODEnabled, const render::ScenePointer& scene) { + if (Model::isLODEnabled() != isLODEnabled) { + auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags); + _renderItemKeyGlobalFlags = (isLODEnabled ? keyBuilder.withLODEnabled() : keyBuilder.withLODDisabled()); + updateRenderItemsKey(scene); + } +} +bool Model::isLODEnabled() const { + return _renderItemKeyGlobalFlags.isLODEnabled(); +} + const render::ItemKey Model::getRenderItemKeyGlobalFlags() const { return _renderItemKeyGlobalFlags; } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index e42da4ecb1..954cc57e0b 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -119,6 +119,9 @@ public: bool isGroupCulled() const; void setGroupCulled(bool isGroupCulled, const render::ScenePointer& scene = nullptr); + bool isLODEnabled() const; + void setLODEnabled(bool isLODEnabled, const render::ScenePointer& scene = nullptr); + bool canCastShadow() const; void setCanCastShadow(bool canCastShadow, const render::ScenePointer& scene = nullptr); diff --git a/libraries/render/src/render/CullTask.cpp b/libraries/render/src/render/CullTask.cpp index b2930032a3..ad9f6b7076 100644 --- a/libraries/render/src/render/CullTask.cpp +++ b/libraries/render/src/render/CullTask.cpp @@ -205,7 +205,7 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext, if (!srcFilter.selectsNothing()) { auto filter = render::ItemFilter::Builder(srcFilter).withoutSubMetaCulled().build(); - // Now get the bound, and + // Now get the bound, and // filter individually against the _filter // visibility cull if partially selected ( octree cell contianing it was partial) // distance cull if was a subcell item ( octree cell is way bigger than the item bound itself, so now need to test per item) @@ -294,7 +294,7 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext, auto& item = scene->getItem(id); if (filter.test(item.getKey())) { ItemBound itemBound(id, item.getBound()); - if (test.solidAngleTest(itemBound.bound)) { + if (item.getKey().isLODDisabled() || test.solidAngleTest(itemBound.bound)) { outItems.emplace_back(itemBound); if (item.getKey().isMetaCullGroup()) { item.fetchMetaSubItemBounds(outItems, (*scene)); @@ -329,7 +329,7 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext, if (filter.test(item.getKey())) { ItemBound itemBound(id, item.getBound()); if (test.frustumTest(itemBound.bound)) { - if (test.solidAngleTest(itemBound.bound)) { + if (item.getKey().isLODDisabled() || test.solidAngleTest(itemBound.bound)) { outItems.emplace_back(itemBound); if (item.getKey().isMetaCullGroup()) { item.fetchMetaSubItemBounds(outItems, (*scene)); diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h index 88e85c604a..a30067cb5c 100644 --- a/libraries/render/src/render/Item.h +++ b/libraries/render/src/render/Item.h @@ -101,6 +101,7 @@ public: SHADOW_CASTER, // Item cast shadows META_CULL_GROUP, // As a meta item, the culling of my sub items is based solely on my bounding box and my visibility in the view SUB_META_CULLED, // As a sub item of a meta render item set as cull group, need to be set to my culling to the meta render it + LOD_DISABLED, // Item LOD behavior is disabled, it won't be LODed because of lod behavior in the view FIRST_TAG_BIT, // 8 Tags available to organize the items and filter them against LAST_TAG_BIT = FIRST_TAG_BIT + NUM_TAGS, @@ -160,6 +161,8 @@ public: Builder& withoutMetaCullGroup() { _flags.reset(META_CULL_GROUP); return (*this); } Builder& withSubMetaCulled() { _flags.set(SUB_META_CULLED); return (*this); } Builder& withoutSubMetaCulled() { _flags.reset(SUB_META_CULLED); return (*this); } + Builder& withLODDisabled() { _flags.set(LOD_DISABLED); return (*this); } + Builder& withLODEnabled() { _flags.reset(LOD_DISABLED); return (*this); } Builder& withTag(Tag tag) { _flags.set(FIRST_TAG_BIT + tag); return (*this); } // Set ALL the tags in one call using the Tag bits @@ -203,6 +206,9 @@ public: bool isSubMetaCulled() const { return _flags[SUB_META_CULLED]; } void setSubMetaCulled(bool metaCulled) { (metaCulled ? _flags.set(SUB_META_CULLED) : _flags.reset(SUB_META_CULLED)); } + bool isLODEnabled() const { return !_flags[LOD_DISABLED]; } + bool isLODDisabled() const { return _flags[LOD_DISABLED]; } + bool isTag(Tag tag) const { return _flags[FIRST_TAG_BIT + tag]; } uint8_t getTagBits() const { return ((_flags.to_ulong() & KEY_TAG_BITS_MASK) >> FIRST_TAG_BIT); } @@ -274,6 +280,9 @@ public: Builder& withoutSubMetaCulled() { _value.reset(ItemKey::SUB_META_CULLED); _mask.set(ItemKey::SUB_META_CULLED); return (*this); } Builder& withSubMetaCulled() { _value.set(ItemKey::SUB_META_CULLED); _mask.set(ItemKey::SUB_META_CULLED); return (*this); } + Builder& withLODEnabled() { _value.reset(ItemKey::LOD_DISABLED); _mask.set(ItemKey::LOD_DISABLED); return (*this); } + Builder& withLODDisabled() { _value.set(ItemKey::LOD_DISABLED); _mask.set(ItemKey::LOD_DISABLED); return (*this); } + Builder& withoutTag(ItemKey::Tag tagIndex) { _value.reset(ItemKey::FIRST_TAG_BIT + tagIndex); _mask.set(ItemKey::FIRST_TAG_BIT + tagIndex); return (*this); } Builder& withTag(ItemKey::Tag tagIndex) { _value.set(ItemKey::FIRST_TAG_BIT + tagIndex); _mask.set(ItemKey::FIRST_TAG_BIT + tagIndex); return (*this); } // Set ALL the tags in one call using the Tag bits and the Tag bits touched diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index c201a251d0..0d13874e4c 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -135,7 +135,8 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) { dimensions: { x: tabletWidth, y: tabletHeight, z: tabletDepth }, parentID: MyAvatar.SELF_ID, visible: visible, - isGroupCulled: true + isGroupCulled: true, + isLODEnabled: false }; // compute position, rotation & parentJointIndex of the tablet From 27d9e8bd23a8cf00aa5b591e26f50bbfdfa73a39 Mon Sep 17 00:00:00 2001 From: David Back Date: Tue, 9 Oct 2018 10:11:30 -0700 Subject: [PATCH 053/362] initial pass refactored re-ordered data-driver entity properties --- scripts/system/html/css/edit-style.css | 495 +-- scripts/system/html/entityProperties.html | 870 ----- scripts/system/html/js/entityProperties.js | 3781 +++++++++++--------- 3 files changed, 2043 insertions(+), 3103 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 6c1931932a..b7aac02c7c 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -448,10 +448,6 @@ input[type=checkbox]:checked + label:hover { border: 1.5pt solid black; } -.shape-section, .light-section, .model-section, .web-section, .image-section, .hyperlink-section, .text-section, .zone-section, .material-section { - display: table; -} - #properties-list fieldset { position: relative; /* 0.1px on the top is to prevent margin collapsing between this and it's first child */ @@ -560,21 +556,6 @@ hr { padding-top: 2px; } -.text-group[collapsed="true"] ~ .text-group, -.zone-group[collapsed="true"] ~ .zone-group, -.image-group[collapsed="true"] ~ .image-group, -.web-group[collapsed="true"] ~ .web-group, -.hyperlink-group[collapsed="true"] ~ .hyperlink-group, -.spatial-group[collapsed="true"] ~ .spatial-group, -.physical-group[collapsed="true"] ~ .physical-group, -.behavior-group[collapsed="true"] ~ .behavior-group, -.model-group[collapsed="true"] ~ .model-group, -.material-group[collapsed="true"] ~ .material-group, -.light-group[collapsed="true"] ~ .light-group { - display: none !important; -} - - .property { display: table; width: 100%; @@ -633,10 +614,6 @@ hr { margin-top: 0; } -.checkbox-sub-props { - margin-top: 18px; -} - .property .number { float: left; } @@ -800,15 +777,6 @@ div.refresh input[type="button"] { display: none !important; } -#property-color-control1 { - display: table-cell; - float: none; -} - -#property-color-control1 + label { - border-left: 20px transparent solid; -} - .rgb label { float: left; margin-top: 10px; @@ -873,10 +841,10 @@ div.refresh input[type="button"] { font-family: FiraSans-SemiBold; font-size: 12px; } -.tuple .red + label, .tuple .x + label, .tuple .pitch + label { +.tuple .red + label, .tuple .x + label, .tuple .pitch + label, .tuple .width + label { color: #e2334d; } -.tuple .green + label, .tuple .y + label, .tuple .yaw + label { +.tuple .green + label, .tuple .y + label, .tuple .yaw + label, .tuple .height + label { color: #1ac567; } .tuple .blue + label, .tuple .z + label, .tuple .roll + label { @@ -950,14 +918,6 @@ tuple, .blue:focus, .tuple .z:focus, .tuple .roll:focus { outline: none; } -fieldset .checkbox-sub-props { - margin-top: 0; - } - -fieldset .checkbox-sub-props .property:first-child { - margin-top: 0; -} - .column { vertical-align: top; } @@ -1298,51 +1258,48 @@ th#entity-hasScript { color: #afafaf; } - -#properties-list #properties-header { - display: table-row; - height: 28px; - border-top: none; - box-shadow: none; -} - -#properties-header .property { - display: table-cell; - vertical-align: middle; -} -#properties-header .checkbox { - position: relative; - top: -1px; -} - -#properties-header #type-icon { +#base #property-type-icon { font-family: hifi-glyphs; font-size: 31px; color: #00b4ef; margin: -4px 12px -4px -2px; width: auto; display: none; - vertical-align: middle; } -#properties-header #property-type { +#base #property-type { padding: 5px 24px 5px 0; - border-right: 1px solid #808080; - height: 100%; + border-right: 1px solid #808080; width: auto; display: inline-block; - vertical-align: middle; } -#properties-header .checkbox:last-child { +#base #div-locked { + position: absolute; + top: 0px; + right: 140px; +} + +#base #div-visible { + position: absolute; + top: 20px; + right: 20px; +} + +#base .checkbox { + position: relative; + top: -1px; +} + +#base .checkbox:last-child { padding-left: 24px; } -#properties-header .checkbox label { +#base .checkbox label { background-position-y: 1px; } -#properties-header .checkbox label span { +#base .checkbox label span { font-family: HiFi-Glyphs; font-size: 20px; padding-right: 6px; @@ -1351,11 +1308,11 @@ th#entity-hasScript { top: -4px; } -#properties-header input[type=checkbox]:checked + label span { +#base input[type=checkbox]:checked + label span { color: #ffffff; } -#properties-header + hr { +#base + hr { margin-top: 12px; } @@ -1371,30 +1328,21 @@ th#entity-hasScript { background-color: #00b4ef; } -input#property-parent-id { - width: 340px; -} - -input#dimension-rescale-button { +input#property-scale-button-rescale { min-width: 50px; margin-left: 6px; } -input#reset-to-natural-dimensions { +input#property-scale-button-reset { margin-right: 0; } -#animation-fps { - margin-top: 48px; -} - -#userdata-clear, -#materialdata-clear { +#property-userData-button-clear, +#property-materialData-button-clear { margin-bottom: 10px; } - -#static-userdata, -#static-materialData { +#property-userData-static, +#property-materialData-static { display: none; z-index: 99; position: absolute; @@ -1405,13 +1353,19 @@ input#reset-to-natural-dimensions { background-color: #2e2e2e; } -#userdata-saved, -#materialData-saved { +#property-userData-saved, +#property-materialData-saved { margin-top:5px; font-size:16px; display:none; } +#property-serverScripts-status { + position: relative; + top: -3px; + right: -20px; +} + #properties-list #collision-info > fieldset:first-of-type { border-top: none !important; box-shadow: none; @@ -1421,371 +1375,4 @@ input#reset-to-natural-dimensions { #properties-list { display: flex; flex-direction: column; -} - -/* ----- Order of Menu items for Primitive ----- */ -/* Entity Menu classes are specified by selected entity - within entityProperties.js -*/ -#properties-list.ShapeMenu #general, -#properties-list.BoxMenu #general, -#properties-list.SphereMenu #general { - order: 1; -} - -#properties-list.ShapeMenu #collision-info, -#properties-list.BoxMenu #collision-info, -#properties-list.SphereMenu #collision-info { - order: 2; -} - -#properties-list.ShapeMenu #physical, -#properties-list.BoxMenu #physical, -#properties-list.SphereMenu #physical { - order: 3; -} - -#properties-list.ShapeMenu #spatial, -#properties-list.BoxMenu #spatial, -#properties-list.SphereMenu #spatial { - order: 4; -} - -#properties-list.ShapeMenu #behavior, -#properties-list.BoxMenu #behavior, -#properties-list.SphereMenu #behavior { - order: 5; -} - -#properties-list.ShapeMenu #hyperlink, -#properties-list.BoxMenu #hyperlink, -#properties-list.SphereMenu #hyperlink { - order: 6; -} - -#properties-list.ShapeMenu #material, -#properties-list.BoxMenu #material, -#properties-list.SphereMenu #material, -#properties-list.ShapeMenu #light, -#properties-list.BoxMenu #light, -#properties-list.SphereMenu #light, -#properties-list.ShapeMenu #model, -#properties-list.BoxMenu #model, -#properties-list.SphereMenu #model, -#properties-list.ShapeMenu #zone, -#properties-list.BoxMenu #zone, -#properties-list.SphereMenu #zone, -#properties-list.ShapeMenu #text, -#properties-list.BoxMenu #text, -#properties-list.SphereMenu #text, -#properties-list.ShapeMenu #image, -#properties-list.BoxMenu #image, -#properties-list.SphereMenu #image, -#properties-list.ShapeMenu #web, -#properties-list.BoxMenu #web, -#properties-list.SphereMenu #web { - display: none; -} - -/* ----- ParticleEffectMenu ----- */ -#properties-list.ParticleEffectMenu #general { - order: 1; -} -#properties-list.ParticleEffectMenu #collision-info { - order: 2; -} -#properties-list.ParticleEffectMenu #physical { - order: 3; -} -#properties-list.ParticleEffectMenu #spatial { - order: 4; -} -#properties-list.ParticleEffectMenu #behavior { - order: 5; -} - -/* items to hide */ -#properties-list.ParticleEffectMenu #material, -#properties-list.ParticleEffectMenu #base-color-section, -#properties-list.ParticleEffectMenu #hyperlink, -#properties-list.ParticleEffectMenu #light, -#properties-list.ParticleEffectMenu #model, -#properties-list.ParticleEffectMenu #shape-list, -#properties-list.ParticleEffectMenu #text, -#properties-list.ParticleEffectMenu #web, -#properties-list.ParticleEffectMenu #image, -#properties-list.ParticleEffectMenu #zone { - display: none; -} - -/* ----- Order of Menu items for Light ----- */ -#properties-list.LightMenu #general { - order: 1; -} -#properties-list.LightMenu #light { - order: 2; -} -#properties-list.LightMenu #physical { - order: 3; -} -#properties-list.LightMenu #spatial { - order: 4; -} -#properties-list.LightMenu #behavior { - order: 5; -} -#properties-list.LightMenu #collision-info { - order: 6; -} -#properties-list.LightMenu #hyperlink { - order: 7; -} -/* sections to hide */ -#properties-list.LightMenu #material, -#properties-list.LightMenu #model, -#properties-list.LightMenu #zone, -#properties-list.LightMenu #text, -#properties-list.LightMenu #image, -#properties-list.LightMenu #web { - display: none; -} -/* items to hide */ -#properties-list.LightMenu #shape-list, -#properties-list.LightMenu #base-color-section { - display: none -} - - -/* ----- Order of Menu items for Model ----- */ -#properties-list.ModelMenu #general { - order: 1; -} -#properties-list.ModelMenu #model { - order: 2; -} -#properties-list.ModelMenu #collision-info { - order: 3; -} -#properties-list.ModelMenu #physical { - order: 4; -} -#properties-list.ModelMenu #spatial { - order: 5; -} -#properties-list.ModelMenu #behavior { - order: 6; -} -#properties-list.ModelMenu #hyperlink { - order: 7; -} -/* sections to hide */ -#properties-list.ModelMenu #material, -#properties-list.ModelMenu #light, -#properties-list.ModelMenu #zone, -#properties-list.ModelMenu #text, -#properties-list.ModelMenu #image, -#properties-list.ModelMenu #web { - display: none; -} -/* items to hide */ -#properties-list.ModelMenu #shape-list, -#properties-list.ModelMenu #base-color-section { - display: none -} - - -/* ----- Order of Menu items for Zone ----- */ -#properties-list.ZoneMenu #general { - order: 1; -} -#properties-list.ZoneMenu #zone { - order: 2; -} -#properties-list.ZoneMenu #physical { - order: 3; -} -#properties-list.ZoneMenu #spatial { - order: 4; -} -#properties-list.ZoneMenu #behavior { - order: 5; -} -#properties-list.ZoneMenu #collision-info { - order: 6; -} -#properties-list.ZoneMenu #hyperlink { - order: 7; -} -/* sections to hide */ -#properties-list.ZoneMenu #material, -#properties-list.ZoneMenu #light, -#properties-list.ZoneMenu #model, -#properties-list.ZoneMenu #text, -#properties-list.ZoneMenu #image, -#properties-list.ZoneMenu #web { - display: none; -} -/* items to hide */ -#properties-list.ZoneMenu #shape-list, -#properties-list.ZoneMenu #base-color-section { - display: none -} - - -/* ----- Order of Menu items for Image ----- */ -#properties-list.ImageMenu #general { - order: 1; -} -#properties-list.ImageMenu #image { - order: 2; -} -#properties-list.ImageMenu #collision-info { - order: 3; -} -#properties-list.ImageMenu #physical { - order: 4; -} -#properties-list.ImageMenu #spatial { - order: 5; -} -#properties-list.ImageMenu #behavior { - order: 6; -} -#properties-list.ImageMenu #hyperlink { - order: 7; -} -/* sections to hide */ -#properties-list.ImageMenu #material, -#properties-list.ImageMenu #light, -#properties-list.ImageMenu #model, -#properties-list.ImageMenu #zone, -#properties-list.ImageMenu #web, -#properties-list.ImageMenu #text { - display: none; -} -/* items to hide */ -#properties-list.ImageMenu #shape-list, -#properties-list.ImageMenu #base-color-section { - display: none; -} - - -/* ----- Order of Menu items for Web ----- */ -#properties-list.WebMenu #general { - order: 1; -} -#properties-list.WebMenu #web { - order: 2; -} -#properties-list.WebMenu #collision-info { - order: 3; -} -#properties-list.WebMenu #physical { - order: 4; -} -#properties-list.WebMenu #spatial { - order: 5; -} -#properties-list.WebMenu #behavior { - order: 6; -} -#properties-list.WebMenu #hyperlink { - order: 7; -} -/* sections to hide */ -#properties-list.WebMenu #material, -#properties-list.WebMenu #light, -#properties-list.WebMenu #model, -#properties-list.WebMenu #zone, -#properties-list.WebMenu #image, -#properties-list.WebMenu #text { - display: none; -} -/* items to hide */ -#properties-list.WebMenu #shape-list, -#properties-list.WebMenu #base-color-section { - display: none; -} - - - -/* ----- Order of Menu items for Text ----- */ -#properties-list.TextMenu #general { - order: 1; -} -#properties-list.TextMenu #text { - order: 2; -} -#properties-list.TextMenu #collision-info { - order: 3; -} -#properties-list.TextMenu #physical { - order: 4; -} -#properties-list.TextMenu #spatial { - order: 5; -} -#properties-list.TextMenu #behavior { - order: 6; -} -#properties-list.TextMenu #hyperlink { - order: 7; -} -/* sections to hide */ -#properties-list.TextMenu #material, -#properties-list.TextMenu #light, -#properties-list.TextMenu #model, -#properties-list.TextMenu #zone, -#properties-list.TextMenu #image, -#properties-list.TextMenu #web { - display: none; -} -/* items to hide */ -#properties-list.TextMenu #shape-list, -#properties-list.TextMenu #base-color-section { - display: none -} - -/* ----- Order of Menu items for Material ----- */ -#properties-list.MaterialMenu #general { - order: 1; -} -#properties-list.MaterialMenu #material { - order: 2; -} -#properties-list.MaterialMenu #spatial { - order: 3; -} -#properties-list.MaterialMenu #hyperlink { - order: 4; -} -#properties-list.MaterialMenu #behavior { - order: 5; -} - -/* sections to hide */ -#properties-list.MaterialMenu #physical, -#properties-list.MaterialMenu #collision-info, -#properties-list.MaterialMenu #model, -#properties-list.MaterialMenu #light, -#properties-list.MaterialMenu #zone, -#properties-list.MaterialMenu #text, -#properties-list.MaterialMenu #web, -#properties-list.MaterialMenu #image { - display: none; -} -/* items to hide */ -#properties-list.MaterialMenu #shape-list, -#properties-list.MaterialMenu #base-color-section { - display: none -} - - -/* Currently always hidden */ -#properties-list #polyvox { - display: none; -} - -.skybox-section { - display: none; } \ No newline at end of file diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 744150253d..93f80e19b3 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -26,876 +26,6 @@
- -
-
- -
-
- - -
-
- - -
-
- -
- -
- - -
-
-
- -
-
-
-
-
-
-
- -
- CollisionM -
-
- - -
-
- - -
-
-
-
-
- - Collides With - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - Grabbing - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
- - -
- - Physical M - -
-
- Linear velocity m/s -
-
-
-
-
-
-
- - -
-
-
-
- Angular velocity deg/s -
-
-
-
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
- Gravity m/s2 -
-
-
-
-
-
-
- Acceleration m/s2 -
-
-
-
-
-
-
-
- - -
- - SpatialM - -
- Position m -
-
-
-
-
-
-
- Rotation deg -
-
-
-
-
-
-
- Dimensions m -
-
-
-
-
-
-
- Registration (pivot offset as ratio of dimension) -
-
-
-
-
-
-
- Scale % -
- - - -
-
-
-
- - -
-
- - -
-
-
-
- -
-
-
- - -
-
-
-
- -
- - BehaviorM - -
- -
-
- - - - Saved! -
-
-
- -
-
- - -
-
- - -
- -
-
- - -
-
- - -
-
-
-
- - - - -
-
-
-
- - - -
-
- - -
-
- -
-
-
- - -
-
- - - - -
- - LightM - -
-
- Light color -
-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
-
-
-
-
-
-
-
-
- -
- - ModelM - -
-
- - -
- -
- - -
-
-
-
- - -
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
-
-
- - -
-
- - -
-
-
- -
- - ZoneM - -
-
- - -
-
- - -
-
-
-
- - -
-
- -
-
- Inherit - Off - On -
-
-
- -
-
-
-
-
-
-
- - -
-
-
-
-
-
-
-
-
- - -
-
- - Skybox - -
- Inherit - Off - On -
-
-
- Skybox color -
-
- - -
-
- - -
-
- - -
-
-
-
-
- - -
-
- -
-
-
- -
-
- Inherit - Off - On -
-
- - -
-
- - -
-
-
- - Haze - -
- Inherit - Off - On -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
-
-
-
- Haze Color -
-
-
-
-
-
-
-
-
-
-
-
-
- - - - -
-
-
-
-
- - -
-
-
- Glare Color -
-
-
-
-
-
-
-
-
-
-
-
-
- - - -
-
-
-
-
- -
-
- - Bloom - -
- Inherit - Off - On -
-
-
-
-
- - - -
-
-
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
-
- - TextM - -
- - -
-
- - -
-
- - -
-
-
- -
-
-
-
-
-
-
-
- Background color -
-
-
-
-
-
-
- -
- - ImageM - -
- - -
-
- -
- - WebM - -
- - -
-
- - -
-
- -
- - Voxel volume size m - -
-
-
-
-
- -
- - -
-
- - -
-
- - -
-
- -
- - MaterialM - -
-
- - -
- -
- -

-
- - - - Saved! -
-
-
- -
-
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
-
-
-
- -
- -
-
-
-
- -
- - -
-
- -
-
-
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index de9027586e..03dad80be6 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1,4 +1,5 @@ // entityProperties.js +// entityProperties.js // // Created by Ryan Huffman on 13 Nov 2014 // Copyright 2014 High Fidelity, Inc. @@ -8,11 +9,8 @@ /* global alert, augmentSpinButtons, clearTimeout, console, document, Element, EventBridge, HifiEntityUI, JSONEditor, openEventBridge, setTimeout, window, _ $ */ - -var PI = 3.14159265358979; -var DEGREES_TO_RADIANS = PI / 180.0; -var RADIANS_TO_DEGREES = 180.0 / PI; -var ICON_FOR_TYPE = { + +const ICON_FOR_TYPE = { Box: "V", Sphere: "n", Shape: "n", @@ -27,17 +25,901 @@ var ICON_FOR_TYPE = { Multiple: "", PolyLine: "", Material: "" +}; + +const PI = 3.14159265358979; +const DEGREES_TO_RADIANS = PI / 180.0; +const RADIANS_TO_DEGREES = 180.0 / PI; + +const NO_SELECTION = "No selection"; + +const GROUPS = [ + { + id: "base", + properties: [ + { + label: NO_SELECTION, + type: "icon", + icons: ICON_FOR_TYPE, + propertyName: "type", + }, + { + label: "Name", + type: "string", + propertyName: "name", + }, + { + label: "ID", + type: "string", + propertyName: "id", + readOnly: true, + }, + { + label: "Parent", + type: "string", + propertyName: "parentID", + }, + { + label: "Locked", + glyph: "", + type: "bool", + propertyName: "locked", + }, + { + label: "Visible", + glyph: "", + type: "bool", + propertyName: "visible", + }, + ] + }, + { + id: "shape", + addToGroup: "base", + properties: [ + { + label: "Shape", + type: "dropdown", + options: { Cube: "Box", Sphere: "Sphere", Tetrahedron: "Tetrahedron", Octahedron: "Octahedron", Icosahedron: "Icosahedron", Dodecahedron: "Dodecahedron", Hexagon: "Hexagon", Triangle: "Triangle", Octagon: "Octagon", Cylinder: "Cylinder", Cone: "Cone", Circle: "Circle", Quad: "Quad" }, + propertyName: "shape", + }, + { + label: "Color", + type: "color", + propertyName: "color", + }, + /* + { + label: "Material", + type: "string", + propertyName: "", + }, + */ + ] + }, + { + id: "text", + addToGroup: "base", + properties: [ + { + label: "Text", + type: "string", + propertyName: "text", + }, + { + label: "Text Color", + type: "color", + propertyName: "textColor", + }, + { + label: "Background Color", + type: "color", + propertyName: "backgroundColor", + }, + /* + { + label: "Transparent Background", + type: "bool", + propertyName: "" + }, + */ + { + label: "Line Height", + type: "number", + min: 0, + step: 0.005, + fixedDecimals: 4, + unit: "m", + propertyName: "lineHeight" + }, + { + label: "Face Camera", + type: "bool", + propertyName: "faceCamera" + }, + ] + }, + { + id: "zone", + addToGroup: "base", + properties: [ + { + label: "Flying Allowed", + type: "bool", + propertyName: "flyingAllowed", + }, + { + label: "Ghosting Allowed", + type: "bool", + propertyName: "ghostingAllowed", + }, + { + label: "Filter", + type: "string", + propertyName: "filterURL", + }, + { + label: "Key Light", + type: "dropdown", + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + propertyName: "keyLightMode", + + }, + { + label: "Key Light Color", + type: "color", + propertyName: "keyLight.color", + showPropertyRule: { "keyLightMode": "enabled" }, + }, + { + label: "Light Intensity", + type: "number", + min: 0, + max: 10, + step: 0.1, + fixedDecimals: 2, + propertyName: "keyLight.intensity", + showPropertyRule: { "keyLightMode": "enabled" }, + }, + { + label: "Light Altitude", + type: "number", + fixedDecimals: 2, + unit: "deg", + propertyName: "keyLight.direction.y", + showPropertyRule: { "keyLightMode": "enabled" }, + }, + { + label: "Light Azimuth", + type: "number", + fixedDecimals: 2, + unit: "deg", + propertyName: "keyLight.direction.x", + showPropertyRule: { "keyLightMode": "enabled" }, + }, + { + label: "Cast Shadows", + type: "bool", + propertyName: "keyLight.castShadows", + showPropertyRule: { "keyLightMode": "enabled" }, + }, + { + label: "Skybox", + type: "dropdown", + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + propertyName: "skyboxMode", + }, + { + label: "Skybox Color", + type: "color", + propertyName: "skybox.color", + showPropertyRule: { "skyboxMode": "enabled" }, + }, + { + label: "Skybox URL", + type: "string", + propertyName: "skybox.url", + showPropertyRule: { "skyboxMode": "enabled" }, + }, + { + type: "buttons", + buttons: [ { id: "copy", label: "Copy URL To Ambient", className: "black", onClick: copySkyboxURLToAmbientURL } ], + propertyName: "copyURLToAmbient", + showPropertyRule: { "skyboxMode": "enabled" }, + }, + { + label: "Ambient Light", + type: "dropdown", + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + propertyName: "ambientLightMode", + }, + { + label: "Ambient Intensity", + type: "number", + min: 0, + max: 10, + step: 0.1, + fixedDecimals: 2, + propertyName: "ambientLight.ambientIntensity", + showPropertyRule: { "ambientLightMode": "enabled" }, + }, + { + label: "Ambient URL", + type: "string", + propertyName: "ambientLight.ambientURL", + showPropertyRule: { "ambientLightMode": "enabled" }, + }, + { + label: "Haze", + type: "dropdown", + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + propertyName: "hazeMode", + }, + { + label: "Range", + type: "number", + min: 5, + max: 10000, + step: 5, + fixedDecimals: 0, + unit: "m", + propertyName: "haze.hazeRange", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Use Altitude", + type: "bool", + propertyName: "haze.hazeAltitudeEffect", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Base", + type: "number", + min: -1000, + max: 1000, + step: 10, + fixedDecimals: 0, + unit: "m", + propertyName: "haze.hazeBaseRef", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Ceiling", + type: "number", + min: -1000, + max: 5000, + step: 10, + fixedDecimals: 0, + unit: "m", + propertyName: "haze.hazeCeiling", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Haze Color", + type: "color", + propertyName: "haze.hazeColor", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Background Blend", + type: "number", + min: 0.0, + max: 1.0, + step: 0.01, + fixedDecimals: 2, + propertyName: "haze.hazeBackgroundBlend", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Enable Glare", + type: "bool", + propertyName: "haze.hazeEnableGlare", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Glare Color", + type: "color", + propertyName: "haze.hazeGlareColor", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Glare Angle", + type: "number", + min: 0, + max: 180, + step: 1, + fixedDecimals: 0, + propertyName: "haze.hazeGlareAngle", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Bloom", + type: "dropdown", + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + propertyName: "bloomMode", + }, + { + label: "Bloom Intensity", + type: "number", + min: 0, + max: 1, + step: 0.01, + fixedDecimals: 2, + propertyName: "bloom.bloomIntensity", + showPropertyRule: { "bloomMode": "enabled" }, + }, + { + label: "Bloom Threshold", + type: "number", + min: 0, + min: 1, + step: 0.01, + fixedDecimals: 2, + propertyName: "bloom.bloomThreshold", + showPropertyRule: { "bloomMode": "enabled" }, + }, + { + label: "Bloom Size", + type: "number", + min: 0, + min: 2, + step: 0.01, + fixedDecimals: 2, + propertyName: "bloom.bloomSize", + showPropertyRule: { "bloomMode": "enabled" }, + }, + ] + }, + { + id: "model", + addToGroup: "base", + properties: [ + { + label: "Model", + type: "string", + propertyName: "modelURL", + }, + { + label: "Collision Shape", + type: "dropdown", + options: { "none": "No Collision", "box": "Box", "sphere": "Sphere", "compound": "Compound" , "simple-hull": "Basic - Whole model", "simple-compound": "Good - Sub-meshes" , "static-mesh": "Exact - All polygons (non-dynamic only)" }, + propertyName: "shapeType", + }, + { + label: "Compound Shape", + type: "string", + propertyName: "compoundShapeURL", + }, + { + label: "Animation", + type: "string", + propertyName: "animation.url", + }, + { + label: "Play Automatically", + type: "bool", + propertyName: "animation.running", + }, + { + label: "Allow Transition", + type: "bool", + propertyName: "animation.allowTranslation", + }, + { + label: "Loop", + type: "bool", + propertyName: "animation.loop", + }, + { + label: "Hold", + type: "bool", + propertyName: "animation.hold", + }, + { + label: "Animation Frame", + type: "number", + propertyName: "animation.currentFrame", + }, + { + label: "First Frame", + type: "number", + propertyName: "animation.firstFrame", + }, + { + label: "Last Frame", + type: "number", + propertyName: "animation.lastFrame", + }, + { + label: "Animation FPS", + type: "number", + propertyName: "animation.fps", + }, + { + label: "Texture", + type: "textarea", + propertyName: "textures", + }, + { + label: "Original Texture", + type: "textarea", + propertyName: "originalTextures", + readOnly: true, + }, + ] + }, + { + id: "image", + addToGroup: "base", + properties: [ + { + label: "Image", + type: "string", + propertyName: "image", + }, + ] + }, + { + id: "web", + addToGroup: "base", + properties: [ + { + label: "Source", + type: "string", + propertyName: "sourceUrl", + }, + { + label: "Source Resolution", + type: "number", + propertyName: "dpi", + }, + ] + }, + { + id: "light", + addToGroup: "base", + properties: [ + { + label: "Light Color", + type: "color", + propertyName: "lightColor", // this actually shares "color" property with shape Color + // but separating naming here to separate property fields + }, + { + label: "Intensity", + type: "number", + min: 0, + step: 0.1, + fixedDecimals: 1, + propertyName: "intensity", + }, + { + label: "Fall-Off Radius", + type: "number", + min: 0, + step: 0.1, + fixedDecimals: 1, + unit: "m", + propertyName: "falloffRadius", + }, + { + label: "Spotlight", + type: "bool", + propertyName: "isSpotlight", + }, + { + label: "Spotlight Exponent", + type: "number", + step: 0.01, + fixedDecimals: 2, + propertyName: "exponent", + }, + { + label: "Spotlight Cut-Off", + type: "number", + step: 0.01, + fixedDecimals: 2, + propertyName: "cutoff", + }, + ] + }, + { + id: "material", + addToGroup: "base", + properties: [ + { + label: "Material URL", + type: "string", + propertyName: "materialURL", + }, + { + label: "Material Data", + type: "textarea", + buttons: [ { id: "clear", label: "Clear Material Data", className: "red", onClick: clearMaterialData }, + { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONMaterialEditor }, + { id: "save", label: "Save Material Data", className: "black", onClick: saveMaterialData } ], + propertyName: "materialData", + }, + { + label: "Submesh to Replace", + type: "number", + min: 0, + step: 1, + propertyName: "submeshToReplace", + }, + { + label: "Material Name to Replace", + type: "string", + propertyName: "materialNameToReplace", + }, + { + label: "Select Submesh", + type: "bool", + propertyName: "selectSubmesh", + }, + { + label: "Priority", + type: "number", + min: 0, + propertyName: "priority", + }, + { + label: "Material Position", + type: "vec2", + min: 0, + min: 1, + step: 0.1, + vec2Type: "xy", + subLabels: [ "x", "y" ], + propertyName: "materialMappingPos", + }, + { + label: "Material Scale", + type: "vec2", + min: 0, + step: 0.1, + vec2Type: "wh", + subLabels: [ "width", "height" ], + propertyName: "materialMappingScale", + }, + { + label: "Material Rotation", + type: "number", + step: 0.1, + fixedDecimals: 2, + unit: "deg", + propertyName: "materialMappingRot", + }, + ] + }, + { + id: "spatial", + label: "SPATIAL", + properties: [ + { + label: "Position", + type: "vec3", + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + unit: "m", + propertyName: "position", + }, + { + label: "Rotation", + type: "vec3", + step: 0.1, + vec3Type: "pyr", + subLabels: [ "pitch", "yaw", "roll" ], + unit: "deg", + propertyName: "rotation", + }, + { + label: "Dimension", + type: "vec3", + step: 0.1, + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + unit: "m", + propertyName: "dimensions", + }, + { + label: "Scale", + type: "number", + defaultValue: 100, + unit: "%", + buttons: [ { id: "rescale", label: "Rescale", className: "blue", onClick: rescaleDimensions }, + { id: "reset", label: "Reset Dimensions", className: "red", onClick: resetToNaturalDimensions } ], + propertyName: "scale", + }, + { + label: "Pivot", + type: "vec3", + step: 0.1, + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + unit: "(ratio of dimension)", + propertyName: "registrationPoint", + }, + { + label: "Align", + type: "buttons", + buttons: [ { id: "selection", label: "Selection to Grid", className: "black", onClick: moveSelectionToGrid }, + { id: "all", label: "All to Grid", className: "black", onClick: moveAllToGrid } ], + propertyName: "alignToGrid", + }, + ] + }, + { + id: "collision", + label: "COLLISION", + twoColumn: true, + properties: [ + { + label: "Collides", + type: "bool", + propertyName: "collisionless", + inverse: true, + column: -1, // before two columns div + }, + { + label: "Dynamic", + type: "bool", + propertyName: "dynamic", + column: -1, // before two columns div + }, + { + label: "Collides With", + type: "sub-header", + propertyName: "collidesWithHeader", + showPropertyRule: { "collisionless": "false" }, + column: 1, + }, + { + label: "", + type: "sub-header", + propertyName: "collidesWithHeaderHelper", + showPropertyRule: { "collisionless": "false" }, + column: 2, + }, + { + label: "Static Entities", + type: "bool", + propertyName: "static", + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + column: 1, + }, + { + label: "Dynamic Entities", + type: "bool", + propertyName: "dynamic", + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + column: 2, + }, + { + label: "Kinematic Entities", + type: "bool", + propertyName: "kinematic", + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + column: 1, + }, + { + label: "My Avatar", + type: "bool", + propertyName: "myAvatar", + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + column: 2, + }, + { + label: "Other Avatars", + type: "bool", + propertyName: "otherAvatar", + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + column: 1, + }, + { + label: "Collision sound URL", + type: "string", + propertyName: "collisionSoundURL", + showPropertyRule: { "collisionless": "false" }, + }, + ] + }, + { + id: "behavior", + label: "BEHAVIOR", + twoColumn: true, + properties: [ + { + label: "Grabbable", + type: "bool", + propertyName: "grabbable", + column: 1, + }, + { + label: "Triggerable", + type: "bool", + propertyName: "triggerable", + column: 2, + }, + { + label: "Cloneable", + type: "bool", + propertyName: "cloneable", + column: 1, + }, + { + label: "Ignore inverse kinematics", + type: "bool", + propertyName: "ignoreIK", + column: 2, + }, + { + label: "Clone Lifetime", + type: "number", + unit: "s", + propertyName: "cloneLifetime", + showPropertyRule: { "cloneable": "true" }, + column: 1, + }, + { + label: "Clone Limit", + type: "number", + propertyName: "cloneLimit", + showPropertyRule: { "cloneable": "true" }, + column: 1, + }, + { + label: "Clone Dynamic", + type: "bool", + propertyName: "cloneDynamic", + showPropertyRule: { "cloneable": "true" }, + column: 1, + }, + { + label: "Clone Avatar Entity", + type: "bool", + propertyName: "cloneAvatarEntity", + showPropertyRule: { "cloneable": "true" }, + column: 1, + }, + { + label: "Can cast shadow", + type: "bool", + propertyName: "castShadow", + }, + { + label: "Script", + type: "string", + buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadScripts } ], + propertyName: "script", + }, + { + label: "Server Script", + type: "string", + buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadServerScripts } ], + propertyName: "serverScripts", + }, + { + label: "Lifetime", + type: "number", + unit: "s", + propertyName: "lifetime", + }, + { + label: "User Data", + type: "textarea", + buttons: [ { id: "clear", label: "Clear User Data", className: "red", onClick: clearUserData }, + { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONEditor }, + { id: "save", label: "Save User Data", className: "black", onClick: saveUserData } ], + propertyName: "userData", + }, + ] + }, + { + id: "physics", + label: "PHYSICS", + properties: [ + { + label: "Linear Velocity", + type: "vec3", + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + unit: "m/s", + propertyName: "velocity", + }, + { + label: "Linear Damping", + type: "number", + fixedDecimals: 2, + propertyName: "damping", + }, + { + label: "Angular Velocity", + type: "vec3", + multiplier: DEGREES_TO_RADIANS, + vec3Type: "pyr", + subLabels: [ "pitch", "yaw", "roll" ], + unit: "deg/s", + propertyName: "angularVelocity", + }, + { + label: "Angular Damping", + type: "number", + fixedDecimals: 4, + propertyName: "angularDamping", + }, + { + label: "Bounciness", + type: "number", + fixedDecimals: 4, + propertyName: "restitution", + }, + { + label: "Friction", + type: "number", + fixedDecimals: 4, + propertyName: "friction", + }, + { + label: "Density", + type: "number", + fixedDecimals: 4, + propertyName: "density", + }, + { + label: "Gravity", + type: "vec3", + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + unit: "m/s2", + propertyName: "gravity", + }, + { + label: "Acceleration", + type: "vec3", + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + unit: "m/s2", + propertyName: "acceleration", + }, + ] + }, +]; + +const GROUPS_PER_TYPE = { + None: [ 'base', 'spatial', 'collision', 'behavior', 'physics' ], + Shape: [ 'base', 'shape', 'spatial', 'collision', 'behavior', 'physics' ], + Text: [ 'base', 'text', 'spatial', 'collision', 'behavior', 'physics' ], + Zone: [ 'base', 'zone', 'spatial', 'collision', 'behavior', 'physics' ], + Model: [ 'base', 'model', 'spatial', 'collision', 'behavior', 'physics' ], + Image: [ 'base', 'image', 'spatial', 'collision', 'behavior', 'physics' ], + Web: [ 'base', 'web', 'spatial', 'collision', 'behavior', 'physics' ], + Light: [ 'base', 'light', 'spatial', 'collision', 'behavior', 'physics' ], + Material: [ 'base', 'material', 'spatial', 'behavior' ], + ParticleEffect: [ 'base', 'spatial', 'behavior', 'physics' ], + Multiple: [ 'base', 'spatial', 'collision', 'behavior', 'physics' ], }; -var EDITOR_TIMEOUT_DURATION = 1500; -var KEY_P = 80; // Key code for letter p used for Parenting hotkey. +const EDITOR_TIMEOUT_DURATION = 1500; +const KEY_P = 80; // Key code for letter p used for Parenting hotkey. + +const MATERIAL_PREFIX_STRING = "mat::"; + +const PENDING_SCRIPT_STATUS = "[ Fetching status ]"; + +var elGroups = {}; +var elPropertyElements = {}; +var showPropertyRules = {}; +var subProperties = []; +var icons = {}; var colorPickers = {}; var lastEntityID = null; -var MATERIAL_PREFIX_STRING = "mat::"; - -var PENDING_SCRIPT_STATUS = "[ Fetching status ]"; - function debugPrint(message) { EventBridge.emitWebEvent( JSON.stringify({ @@ -64,7 +946,7 @@ function disableChildren(el, selector) { function enableProperties() { enableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); enableChildren(document, ".colpick"); - var elLocked = document.getElementById("property-locked"); + var elLocked = elPropertyElements["locked"]; if (elLocked.checked === false) { removeStaticUserData(); @@ -72,20 +954,19 @@ function enableProperties() { } } - function disableProperties() { disableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); disableChildren(document, ".colpick"); for (var pickKey in colorPickers) { colorPickers[pickKey].colpickHide(); } - var elLocked = document.getElementById("property-locked"); + var elLocked = elPropertyElements["locked"]; if (elLocked.checked === true) { - if ($('#userdata-editor').css('display') === "block") { + if ($('#property-userData-editor').css('display') === "block") { showStaticUserData(); } - if ($('#materialdata-editor').css('display') === "block") { + if ($('#property-materialData-editor').css('display') === "block") { showStaticMaterialData(); } } @@ -99,7 +980,21 @@ function showElements(els, show) { function updateProperty(propertyName, propertyValue) { var properties = {}; - properties[propertyName] = propertyValue; + let splitPropertyName = propertyName.split('.'); + if (splitPropertyName.length > 1) { + let propertyGroupName = splitPropertyName[0]; + let subPropertyName = splitPropertyName[1]; + properties[propertyGroupName] = {}; + if (splitPropertyName.length === 3) { + let subSubPropertyName = splitPropertyName[2]; + properties[propertyGroupName][subPropertyName] = {}; + properties[propertyGroupName][subPropertyName][subSubPropertyName] = propertyValue; + } else { + properties[propertyGroupName][subPropertyName] = propertyValue; + } + } else { + properties[propertyName] = propertyValue; + } updateProperties(properties); } @@ -111,18 +1006,9 @@ function updateProperties(properties) { })); } -function createEmitCheckedPropertyUpdateFunction(propertyName) { +function createEmitCheckedPropertyUpdateFunction(propertyName, inverse) { return function() { - updateProperty(propertyName, this.checked); - }; -} - -function createEmitGroupCheckedPropertyUpdateFunction(group, propertyName) { - return function() { - var properties = {}; - properties[group] = {}; - properties[group][propertyName] = this.checked; - updateProperties(properties); + updateProperty(propertyName, inverse ? !this.checked : this.checked); }; } @@ -134,15 +1020,6 @@ function createEmitNumberPropertyUpdateFunction(propertyName, decimals) { }; } -function createEmitGroupNumberPropertyUpdateFunction(group, propertyName) { - return function() { - var properties = {}; - properties[group] = {}; - properties[group][propertyName] = this.value; - updateProperties(properties); - }; -} - function createImageURLUpdateFunction(propertyName) { return function () { var newTextures = JSON.stringify({ "tex.picture": this.value }); @@ -156,33 +1033,6 @@ function createEmitTextPropertyUpdateFunction(propertyName) { }; } -function createZoneComponentModeChangedFunction(zoneComponent, zoneComponentModeInherit, - zoneComponentModeDisabled, zoneComponentModeEnabled) { - - return function() { - var zoneComponentMode; - - if (zoneComponentModeInherit.checked) { - zoneComponentMode = 'inherit'; - } else if (zoneComponentModeDisabled.checked) { - zoneComponentMode = 'disabled'; - } else if (zoneComponentModeEnabled.checked) { - zoneComponentMode = 'enabled'; - } - - updateProperty(zoneComponent, zoneComponentMode); - }; -} - -function createEmitGroupTextPropertyUpdateFunction(group, propertyName) { - return function() { - var properties = {}; - properties[group] = {}; - properties[group][propertyName] = this.value; - updateProperties(properties); - }; -} - function createEmitVec2PropertyUpdateFunction(property, elX, elY) { return function () { var properties = {}; @@ -194,23 +1044,21 @@ function createEmitVec2PropertyUpdateFunction(property, elX, elY) { }; } -function createEmitVec3PropertyUpdateFunction(property, elX, elY, elZ) { - return function() { +function createEmitVec2PropertyUpdateFunctionWithMultiplier(property, elX, elY, multiplier) { + return function () { var properties = {}; properties[property] = { - x: elX.value, - y: elY.value, - z: elZ.value + x: elX.value * multiplier, + y: elY.value * multiplier }; updateProperties(properties); }; } -function createEmitGroupVec3PropertyUpdateFunction(group, property, elX, elY, elZ) { +function createEmitVec3PropertyUpdateFunction(property, elX, elY, elZ) { return function() { var properties = {}; - properties[group] = {}; - properties[group][property] = { + properties[property] = { x: elX.value, y: elY.value, z: elZ ? elZ.value : 0 @@ -256,20 +1104,6 @@ function emitColorPropertyUpdate(property, red, green, blue, group) { updateProperties(properties); } - -function createEmitGroupColorPropertyUpdateFunction(group, property, elRed, elGreen, elBlue) { - return function() { - var properties = {}; - properties[group] = {}; - properties[group][property] = { - red: elRed.value, - green: elGreen.value, - blue: elBlue.value - }; - updateProperties(properties); - }; -} - function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString) { if (subPropertyElement.checked) { if (propertyValue.indexOf(subPropertyString)) { @@ -282,6 +1116,30 @@ function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElemen updateProperty(propertyName, propertyValue); } +function clearUserData() { + let elUserData = elPropertyElements["userData"]; + deleteJSONEditor(); + elUserData.value = ""; + showUserDataTextArea(); + showNewJSONEditorButton(); + hideSaveUserDataButton(); + updateProperty('userData', elUserData.value); +} + +function newJSONEditor() { + deleteJSONEditor(); + createJSONEditor(); + var data = {}; + setEditorJSON(data); + hideUserDataTextArea(); + hideNewJSONEditorButton(); + showSaveUserDataButton(); +} + +function saveUserData() { + saveJSONUserData(true); +} + function setUserDataFromEditor(noUpdate) { var json = null; try { @@ -315,7 +1173,7 @@ function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults, r var parsedData = {}; var keysToBeRemoved = removeKeys ? removeKeys : []; try { - if ($('#userdata-editor').css('height') !== "0px") { + if ($('#property-userData-editor').css('height') !== "0px") { // if there is an expanded, we want to use its json. parsedData = getEditorJSON(); } else { @@ -371,6 +1229,135 @@ function userDataChanger(groupName, keyName, values, userDataElement, defaultVal multiDataUpdater(groupName, val, userDataElement, def, removeKeys); } +var editor = null; + +function createJSONEditor() { + var container = document.getElementById("property-userData-editor"); + var options = { + search: false, + mode: 'tree', + modes: ['code', 'tree'], + name: 'userData', + onModeChange: function() { + $('.jsoneditor-poweredBy').remove(); + }, + onError: function(e) { + alert('JSON editor:' + e); + }, + onChange: function() { + var currentJSONString = editor.getText(); + + if (currentJSONString === '{"":""}') { + return; + } + $('#property-userData-button-save').attr('disabled', false); + + + } + }; + editor = new JSONEditor(container, options); +} + +function showSaveUserDataButton() { + $('#property-userData-button-save').show(); +} + +function hideSaveUserDataButton() { + $('#property-userData-button-save').hide(); +} + +function disableSaveUserDataButton() { + $('#property-userData-button-save').attr('disabled', true); +} + +function showNewJSONEditorButton() { + $('#property-userData-button-edit').show(); +} + +function hideNewJSONEditorButton() { + $('#property-userData-button-edit').hide(); +} + +function showUserDataTextArea() { + $('#property-userData').show(); +} + +function hideUserDataTextArea() { + $('#property-userData').hide(); +} + +function hideUserDataSaved() { + $('#property-userData-saved').hide(); +} + +function showStaticUserData() { + if (editor !== null) { + $('#property-userData-static').show(); + $('#property-userData-static').css('height', $('#property-userData-editor').height()); + $('#property-userData-static').text(editor.getText()); + } +} + +function removeStaticUserData() { + $('#property-userData-static').hide(); +} + +function setEditorJSON(json) { + editor.set(json); + if (editor.hasOwnProperty('expandAll')) { + editor.expandAll(); + } +} + +function getEditorJSON() { + return editor.get(); +} + +function deleteJSONEditor() { + if (editor !== null) { + editor.destroy(); + editor = null; + } +} + +var savedJSONTimer = null; + +function saveJSONUserData(noUpdate) { + setUserDataFromEditor(noUpdate); + $('#property-userData-saved').show(); + $('#property-userData-button-save').attr('disabled', true); + if (savedJSONTimer !== null) { + clearTimeout(savedJSONTimer); + } + savedJSONTimer = setTimeout(function() { + hideUserDataSaved(); + }, EDITOR_TIMEOUT_DURATION); +} + +function clearMaterialData() { + let elMaterialData = elPropertyElements["materialData"]; + deleteJSONMaterialEditor(); + elMaterialData.value = ""; + showMaterialDataTextArea(); + showNewJSONMaterialEditorButton(); + hideSaveMaterialDataButton(); + updateProperty('materialData', elMaterialData.value); +} + +function newJSONMaterialEditor() { + deleteJSONMaterialEditor(); + createJSONMaterialEditor(); + var data = {}; + setMaterialEditorJSON(data); + hideMaterialDataTextArea(); + hideNewJSONMaterialEditorButton(); + showSaveMaterialDataButton(); +} + +function saveMaterialData() { + saveJSONMaterialData(true); +} + function setMaterialDataFromEditor(noUpdate) { var json = null; try { @@ -399,16 +1386,10 @@ function setMaterialDataFromEditor(noUpdate) { } } -function setTextareaScrolling(element) { - var isScrolling = element.scrollHeight > element.offsetHeight; - element.setAttribute("scrolling", isScrolling ? "true" : "false"); -} - - var materialEditor = null; function createJSONMaterialEditor() { - var container = document.getElementById("materialdata-editor"); + var container = document.getElementById("property-materialData-editor"); var options = { search: false, mode: 'tree', @@ -426,7 +1407,7 @@ function createJSONMaterialEditor() { if (currentJSONString === '{"":""}') { return; } - $('#materialdata-save').attr('disabled', false); + $('#property-materialData-button-save').attr('disabled', false); } @@ -434,40 +1415,48 @@ function createJSONMaterialEditor() { materialEditor = new JSONEditor(container, options); } -function hideNewJSONMaterialEditorButton() { - $('#materialdata-new-editor').hide(); -} - function showSaveMaterialDataButton() { - $('#materialdata-save').show(); + $('#property-materialData-button-save').show(); } function hideSaveMaterialDataButton() { - $('#materialdata-save').hide(); + $('#property-materialData-button-save').hide(); +} + +function disableSaveMaterialDataButton() { + $('#property-materialData-button-save').attr('disabled', true); } function showNewJSONMaterialEditorButton() { - $('#materialdata-new-editor').show(); + $('#property-materialData-button-edit').show(); +} + +function hideNewJSONMaterialEditorButton() { + $('#property-materialData-button-edit').hide(); } function showMaterialDataTextArea() { - $('#property-material-data').show(); + $('#property-materialData').show(); } function hideMaterialDataTextArea() { - $('#property-material-data').hide(); + $('#property-materialData').hide(); +} + +function hideMaterialDataSaved() { + $('#property-materialData-saved').hide(); } function showStaticMaterialData() { if (materialEditor !== null) { - $('#static-materialdata').show(); - $('#static-materialdata').css('height', $('#materialdata-editor').height()); - $('#static-materialdata').text(materialEditor.getText()); + $('#property-materialData-static').show(); + $('#property-materialData-static').css('height', $('#property-materialData-editor').height()); + $('#property-materialData-static').text(materialEditor.getText()); } } function removeStaticMaterialData() { - $('#static-materialdata').hide(); + $('#property-materialData-static').hide(); } function setMaterialEditorJSON(json) { @@ -492,112 +1481,13 @@ var savedMaterialJSONTimer = null; function saveJSONMaterialData(noUpdate) { setMaterialDataFromEditor(noUpdate); - $('#materialdata-saved').show(); - $('#materialdata-save').attr('disabled', true); + $('#property-materialData-saved').show(); + $('#property-materialData-button-save').attr('disabled', true); if (savedMaterialJSONTimer !== null) { clearTimeout(savedMaterialJSONTimer); } savedMaterialJSONTimer = setTimeout(function() { - $('#materialdata-saved').hide(); - - }, EDITOR_TIMEOUT_DURATION); -} - -var editor = null; - -function createJSONEditor() { - var container = document.getElementById("userdata-editor"); - var options = { - search: false, - mode: 'tree', - modes: ['code', 'tree'], - name: 'userData', - onModeChange: function() { - $('.jsoneditor-poweredBy').remove(); - }, - onError: function(e) { - alert('JSON editor:' + e); - }, - onChange: function() { - var currentJSONString = editor.getText(); - - if (currentJSONString === '{"":""}') { - return; - } - $('#userdata-save').attr('disabled', false); - - - } - }; - editor = new JSONEditor(container, options); -} - -function hideNewJSONEditorButton() { - $('#userdata-new-editor').hide(); -} - -function showSaveUserDataButton() { - $('#userdata-save').show(); -} - -function hideSaveUserDataButton() { - $('#userdata-save').hide(); -} - -function showNewJSONEditorButton() { - $('#userdata-new-editor').show(); -} - -function showUserDataTextArea() { - $('#property-user-data').show(); -} - -function hideUserDataTextArea() { - $('#property-user-data').hide(); -} - -function showStaticUserData() { - if (editor !== null) { - $('#static-userdata').show(); - $('#static-userdata').css('height', $('#userdata-editor').height()); - $('#static-userdata').text(editor.getText()); - } -} - -function removeStaticUserData() { - $('#static-userdata').hide(); -} - -function setEditorJSON(json) { - editor.set(json); - if (editor.hasOwnProperty('expandAll')) { - editor.expandAll(); - } -} - -function getEditorJSON() { - return editor.get(); -} - -function deleteJSONEditor() { - if (editor !== null) { - editor.destroy(); - editor = null; - } -} - -var savedJSONTimer = null; - -function saveJSONUserData(noUpdate) { - setUserDataFromEditor(noUpdate); - $('#userdata-saved').show(); - $('#userdata-save').attr('disabled', true); - if (savedJSONTimer !== null) { - clearTimeout(savedJSONTimer); - } - savedJSONTimer = setTimeout(function() { - $('#userdata-saved').hide(); - + hideMaterialDataSaved(); }, EDITOR_TIMEOUT_DURATION); } @@ -610,14 +1500,14 @@ function bindAllNonJSONEditorElements() { // TODO FIXME: (JSHint) Functions declared within loops referencing // an outer scoped variable may lead to confusing semantics. field.on('focus', function(e) { - if (e.target.id === "userdata-new-editor" || e.target.id === "userdata-clear" || e.target.id === "materialdata-new-editor" || e.target.id === "materialdata-clear") { + if (e.target.id === "property-userData-button-edit" || e.target.id === "property-userData-button-clear" || e.target.id === "property-materialData-button-edit" || e.target.id === "property-materialData-button-clear") { return; } else { - if ($('#userdata-editor').css('height') !== "0px") { - saveJSONUserData(true); + if ($('#property-userData-editor').css('height') !== "0px") { + saveUserData(); } - if ($('#materialdata-editor').css('height') !== "0px") { - saveJSONMaterialData(true); + if ($('#property-materialData-editor').css('height') !== "0px") { + saveMaterialData(); } } }); @@ -634,264 +1524,575 @@ function unbindAllInputs() { } } +function setTextareaScrolling(element) { + var isScrolling = element.scrollHeight > element.offsetHeight; + element.setAttribute("scrolling", isScrolling ? "true" : "false"); +} + function showParentMaterialNameBox(number, elNumber, elString) { if (number) { - $('#property-parent-material-id-number-container').show(); - $('#property-parent-material-id-string-container').hide(); + $('#property-submeshToReplace').parent().show(); + $('#property-materialNameToReplace').parent().hide(); elString.value = ""; } else { - $('#property-parent-material-id-string-container').show(); - $('#property-parent-material-id-number-container').hide(); + $('#property-materialNameToReplace').parent().show(); + $('#property-submeshToReplace').parent().hide(); elNumber.value = 0; } } +function rescaleDimensions() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "rescaleDimensions", + percentage: parseFloat(document.getElementById("property-scale").value) + })); +} + +function moveSelectionToGrid() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveSelectionToGrid" + })); +} + +function moveAllToGrid() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveAllToGrid" + })); +} + +function resetToNaturalDimensions() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "resetToNaturalDimensions" + })); +} + +function reloadScripts() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "reloadClientScripts" + })); +} + +function reloadServerScripts() { + // invalidate the current status (so that same-same updates can still be observed visually) + document.getElementById("property-serverScripts-status").innerText = PENDING_SCRIPT_STATUS; + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "reloadServerScripts" + })); +} + +function copySkyboxURLToAmbientURL() { + let skyboxURL = elPropertyElements["skybox.url"].value; + elPropertyElements["ambientLight.ambientURL"].value = skyboxURL; + updateProperty("ambientLight.ambientURL", skyboxURL); +} + +function createTupleNumberInput(elTuple, propertyID, subLabel, min, max, step) { + let elementPropertyID = propertyID + "-" + subLabel.toLowerCase(); + let elDiv = document.createElement('div'); + let elLabel = document.createElement('label'); + elLabel.innerText = subLabel[0].toUpperCase() + subLabel.slice(1) + ":"; + elLabel.setAttribute("for", elementPropertyID); + let elInput = document.createElement('input'); + elInput.setAttribute("id", elementPropertyID); + elInput.setAttribute("type", "number"); + elInput.setAttribute("class", subLabel); + if (min !== undefined) { + elInput.setAttribute("min", min); + } + if (max !== undefined) { + elInput.setAttribute("max", max); + } + if (step !== undefined) { + elInput.setAttribute("step", step); + } + elDiv.appendChild(elInput); + elDiv.appendChild(elLabel); + elTuple.appendChild(elDiv); + return elInput; +} + +function addUnit(unit, elLabel) { + if (unit !== undefined) { + let elSpan = document.createElement('span'); + elSpan.setAttribute("class", "unit"); + elSpan.innerHTML = unit; + elLabel.appendChild(elSpan); + } +} + +function addButtons(elProperty, propertyID, buttons, newRow) { + let elDiv = document.createElement('div'); + elDiv.setAttribute("class", "row"); + + buttons.forEach(function(button) { + let elButton = document.createElement('input'); + elButton.setAttribute("type", "button"); + elButton.setAttribute("class", button.className); + elButton.setAttribute("id", propertyID + "-button-" + button.id); + elButton.setAttribute("value", button.label); + elButton.addEventListener("click", button.onClick); + if (newRow) { + elDiv.appendChild(elButton); + } else { + elProperty.appendChild(elButton); + } + }); + + if (newRow) { + elProperty.appendChild(document.createElement('br')); + elProperty.appendChild(elDiv); + } +} + +function showGroupsForType(type) { + if (type === "Box" || type === "Sphere") { + type = "Shape"; + } + + let typeGroups = GROUPS_PER_TYPE[type]; + + for (let groupKey in elGroups) { + let elGroup = elGroups[groupKey]; + if (typeGroups && typeGroups.indexOf(groupKey) > -1) { + elGroup.style.display = "block"; + } else { + elGroup.style.display = "none"; + } + } +} + +function resetProperties() { + for (let propertyName in elPropertyElements) { + let elProperty = elPropertyElements[propertyName]; + if (elProperty instanceof Array) { + if (elProperty.length === 2 || elProperty.length === 3) { + // vec2/vec3 are array of 2/3 input numbers + elProperty[0].value = ""; + elProperty[1].value = ""; + if (elProperty[2] !== undefined) { + elProperty[2].value = ""; + } + } else if (elProperty.length === 4) { + // color is array of color picker and 3 input numbers + elProperty[0].style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; + elProperty[1].value = ""; + elProperty[2].value = ""; + elProperty[3].value = ""; + } + } else if (elProperty.getAttribute("type") === "number") { + if (elProperty.getAttribute("defaultValue")) { + elProperty.value = elProperty.getAttribute("defaultValue"); + } else { + elProperty.value = ""; + } + } else if (elProperty.getAttribute("type") === "checkbox") { + elProperty.checked = false; + } else { + elProperty.value = ""; + } + } + + for (let showPropertyRule in showPropertyRules) { + let propertyShowRules = showPropertyRules[showPropertyRule]; + for (let propertyToHide in propertyShowRules) { + let elPropertyToHide = elPropertyElements[propertyToHide]; + if (elPropertyToHide) { + let parentNode = elPropertyToHide.parentNode; + if (parentNode === undefined && elPropertyToHide instanceof Array) { + parentNode = elPropertyToHide[0].parentNode; + } + parentNode.style.display = "none"; + } + } + } +} + function loaded() { - openEventBridge(function() { + openEventBridge(function() { + let elPropertiesList = document.getElementById("properties-list"); + + GROUPS.forEach(function(group) { + let elGroup; + if (group.addToGroup !== undefined) { + let fieldset = document.getElementById(group.addToGroup); + elGroup = document.createElement('div'); + fieldset.appendChild(elGroup); + } else { + elGroup = document.createElement('fieldset'); + elGroup.setAttribute("class", "major"); + elGroup.setAttribute("id", group.id); + elPropertiesList.appendChild(elGroup); + } - var elPropertiesList = document.getElementById("properties-list"); - var elID = document.getElementById("property-id"); - var elType = document.getElementById("property-type"); - var elTypeIcon = document.getElementById("type-icon"); - var elName = document.getElementById("property-name"); - var elLocked = document.getElementById("property-locked"); - var elVisible = document.getElementById("property-visible"); - var elPositionX = document.getElementById("property-pos-x"); - var elPositionY = document.getElementById("property-pos-y"); - var elPositionZ = document.getElementById("property-pos-z"); - var elMoveSelectionToGrid = document.getElementById("move-selection-to-grid"); - var elMoveAllToGrid = document.getElementById("move-all-to-grid"); + if (group.label !== undefined) { + let elLegend = document.createElement('legend'); + elLegend.innerText = group.label; + elLegend.setAttribute("class", "section-header"); + let elSpan = document.createElement('span'); + elSpan.setAttribute("class", ".collapse-icon"); + elSpan.innerText = "M"; + elLegend.appendChild(elSpan); + elGroup.appendChild(elLegend); + } + + group.properties.forEach(function(property) { + let propertyType = property.type; + let propertyName = property.propertyName; + let propertyID = "property-" + propertyName; + propertyID = propertyID.replace(".", "-"); + + let elProperty; + if (propertyType === "sub-header") { + elProperty = document.createElement('legend'); + elProperty.innerText = property.label; + elProperty.setAttribute("class", "sub-section-header"); + } else { + elProperty = document.createElement('div'); + elProperty.setAttribute("id", "div-" + propertyName); + } + + if (group.twoColumn && property.column !== undefined && property.column !== -1) { + let columnName = group.id + "column" + property.column; + let elColumn = document.getElementById(columnName); + if (!elColumn) { + let columnDivName = group.id + "columnDiv"; + let elColumnDiv = document.getElementById(columnDivName); + if (!elColumnDiv) { + elColumnDiv = document.createElement('div'); + elColumnDiv.setAttribute("class", "two-column"); + elColumnDiv.setAttribute("id", group.id + "columnDiv"); + elGroup.appendChild(elColumnDiv); + } + elColumn = document.createElement('fieldset'); + elColumn.setAttribute("class", "column"); + elColumn.setAttribute("id", columnName); + elColumnDiv.appendChild(elColumn); + } + elColumn.appendChild(elProperty); + } else { + elGroup.appendChild(elProperty); + } + + let elLabel = document.createElement('label'); + elLabel.innerText = property.label; + elLabel.setAttribute("for", propertyID); + + switch (propertyType) { + case 'vec3': { + elProperty.setAttribute("class", "property " + property.vec3Type + " fstuple"); + + let elTuple = document.createElement('div'); + elTuple.setAttribute("class", "tuple"); + + addUnit(property.unit, elLabel); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elTuple); + + let elInputX = createTupleNumberInput(elTuple, propertyID, property.subLabels[0], property.min, property.max, property.step); + let elInputY = createTupleNumberInput(elTuple, propertyID, property.subLabels[1], property.min, property.max, property.step); + let elInputZ = createTupleNumberInput(elTuple, propertyID, property.subLabels[2], property.min, property.max, property.step); + + let inputChangeFunction; + let multiplier = 1; + if (property.multiplier !== undefined) { + multiplier = property.multiplier; + inputChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elInputX, elInputY, elInputZ); + } else { + inputChangeFunction = createEmitVec3PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY, elInputZ, property.multiplier); + } + elInputX.setAttribute("multiplier", multiplier); + elInputY.setAttribute("multiplier", multiplier); + elInputZ.setAttribute("multiplier", multiplier); + elInputX.addEventListener('change', inputChangeFunction); + elInputY.addEventListener('change', inputChangeFunction); + elInputZ.addEventListener('change', inputChangeFunction); + + elPropertyElements[propertyName] = [elInputX, elInputY, elInputZ]; + break; + } + case 'vec2': { + elProperty.setAttribute("class", "property " + property.vec2Type + " fstuple"); + + let elTuple = document.createElement('div'); + elTuple.setAttribute("class", "tuple"); + + addUnit(property.unit, elLabel); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elTuple); + + let elInputX = createTupleNumberInput(elTuple, propertyID, property.subLabels[0], property.min, property.max, property.step); + let elInputY = createTupleNumberInput(elTuple, propertyID, property.subLabels[1], property.min, property.max, property.step); + + let inputChangeFunction; + let multiplier = 1; + if (property.multiplier !== undefined) { + multiplier = property.multiplier; + inputChangeFunction = createEmitVec2PropertyUpdateFunction(propertyName, elInputX, elInputY); + } else { + inputChangeFunction = createEmitVec2PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY, property.multiplier); + } + elInputX.setAttribute("multiplier", multiplier); + elInputY.setAttribute("multiplier", multiplier); + elInputX.addEventListener('change', inputChangeFunction); + elInputY.addEventListener('change', inputChangeFunction); + + elPropertyElements[propertyName] = [elInputX, elInputY]; + break; + } + case 'color': { + elProperty.setAttribute("class", "property rgb fstuple"); + + let elColorPicker = document.createElement('div'); + elColorPicker.setAttribute("class", "color-picker"); + elColorPicker.setAttribute("id", propertyID); + + let elTuple = document.createElement('div'); + elTuple.setAttribute("class", "tuple"); + + elProperty.appendChild(elColorPicker); + elProperty.appendChild(elLabel); + elProperty.appendChild(elTuple); + + let elInputR = createTupleNumberInput(elTuple, propertyID, "red", 0, 255, 1); + let elInputG = createTupleNumberInput(elTuple, propertyID, "green", 0, 255, 1); + let elInputB = createTupleNumberInput(elTuple, propertyID, "blue", 0, 255, 1); + + let inputChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elInputR, elInputG, elInputB); + elInputR.addEventListener('change', inputChangeFunction); + elInputG.addEventListener('change', inputChangeFunction); + elInputB.addEventListener('change', inputChangeFunction); + + let colorPickerID = "#" + propertyID; + colorPickers[colorPickerID] = $(colorPickerID).colpick({ + colorScheme: 'dark', + layout: 'hex', + color: '000000', + submit: false, // We don't want to have a submission button + onShow: function(colpick) { + $(colorPickerID).attr('active', 'true'); + // The original color preview within the picker needs to be updated on show because + // prior to the picker being shown we don't have access to the selections' starting color. + colorPickers[colorPickerID].colpickSetColor({ + "r": elInputR.value, + "g": elInputG.value, + "b": elInputB.value + }); + }, + onHide: function(colpick) { + $(colorPickerID).attr('active', 'false'); + }, + onChange: function(hsb, hex, rgb, el) { + $(el).css('background-color', '#' + hex); + emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); + } + }); + + elPropertyElements[propertyName] = [elColorPicker, elInputR, elInputG, elInputB]; + break; + } + case 'string': { + elProperty.setAttribute("class", "property text"); + + let elInput = document.createElement('input'); + elInput.setAttribute("id", propertyID); + elInput.setAttribute("type", "text"); + if (property.readOnly) { + elInput.readOnly = true; + } + + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elInput); + + if (property.buttons !== undefined) { + addButtons(elProperty, propertyID, property.buttons, false); + } + + elPropertyElements[propertyName] = elInput; + break; + } + case 'bool': { + elProperty.setAttribute("class", "property checkbox"); + + if (property.glyph !== undefined) { + elLabel.innerText = " " + property.label; + let elSpan = document.createElement('span'); + elSpan.innerHTML = property.glyph; + elLabel.insertBefore(elSpan, elLabel.firstChild); + } + + let elInput = document.createElement('input'); + elInput.setAttribute("id", propertyID); + elInput.setAttribute("type", "checkbox"); + + let inverse = property.inverse; + elInput.setAttribute("inverse", inverse ? "true" : "false"); + + elProperty.appendChild(elInput); + elProperty.appendChild(elLabel); + + let subPropertyOf = property.subPropertyOf; + if (subPropertyOf !== undefined) { + elInput.setAttribute("subPropertyOf", subPropertyOf); + elPropertyElements[propertyName] = elInput; + subProperties.push(propertyName); + elInput.addEventListener('change', function() { + updateCheckedSubProperty(subPropertyOf, properties[subPropertyOf], elInput, propertyName); + }); + } else { + elPropertyElements[propertyName] = elInput; + elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(propertyName, inverse)); + } + break; + } + case 'dropdown': { + elProperty.setAttribute("class", "property dropdown"); + + let elInput = document.createElement('select'); + elInput.setAttribute("id", propertyID); + + for (let optionKey in property.options) { + let option = document.createElement('option'); + option.value = optionKey; + option.text = property.options[optionKey]; + elInput.add(option); + } + + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elInput); + elPropertyElements[propertyName] = elInput; + break; + } + case 'number': { + elProperty.setAttribute("class", "property number"); + + addUnit(property.unit, elLabel); + + let elInput = document.createElement('input'); + elInput.setAttribute("id", propertyID); + elInput.setAttribute("type", "number"); + if (property.min !== undefined) { + elInput.setAttribute("min", property.min); + } + if (property.max !== undefined) { + elInput.setAttribute("max", property.max); + } + if (property.step !== undefined) { + elInput.setAttribute("step", property.step); + } + + let fixedDecimals = property.fixedDecimals !== undefined ? property.fixedDecimals : 0; + elInput.setAttribute("fixedDecimals", fixedDecimals); + + let defaultValue = property.defaultValue; + if (defaultValue !== undefined) { + elInput.setAttribute("defaultValue", defaultValue); + elInput.value = defaultValue; + } + + elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(propertyName, fixedDecimals)); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elInput); + + if (property.buttons !== undefined) { + addButtons(elProperty, propertyID, property.buttons, true); + } + + elPropertyElements[propertyName] = elInput; + break; + } + case 'textarea': { + elProperty.setAttribute("class", "property textarea"); + + elProperty.appendChild(elLabel); + + if (property.buttons !== undefined) { + addButtons(elProperty, propertyID, property.buttons, true); + } + + let elInput = document.createElement('textarea'); + elInput.setAttribute("id", propertyID); + if (property.readOnly) { + elInput.readOnly = true; + } + + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + + elProperty.appendChild(elInput); + elPropertyElements[propertyName] = elInput; + break; + } + case 'icon': { + elProperty.setAttribute("class", "property value"); + + elLabel.setAttribute("id", propertyID); + elLabel.innerHTML = " " + property.label; + + let elSpan = document.createElement('span'); + elSpan.setAttribute("id", propertyID + "-icon"); + icons[propertyName] = property.icons; - var elDimensionsX = document.getElementById("property-dim-x"); - var elDimensionsY = document.getElementById("property-dim-y"); - var elDimensionsZ = document.getElementById("property-dim-z"); - var elResetToNaturalDimensions = document.getElementById("reset-to-natural-dimensions"); - var elRescaleDimensionsPct = document.getElementById("dimension-rescale-pct"); - var elRescaleDimensionsButton = document.getElementById("dimension-rescale-button"); - - var elParentID = document.getElementById("property-parent-id"); - var elParentJointIndex = document.getElementById("property-parent-joint-index"); - - var elRegistrationX = document.getElementById("property-reg-x"); - var elRegistrationY = document.getElementById("property-reg-y"); - var elRegistrationZ = document.getElementById("property-reg-z"); - - var elRotationX = document.getElementById("property-rot-x"); - var elRotationY = document.getElementById("property-rot-y"); - var elRotationZ = document.getElementById("property-rot-z"); - - var elLinearVelocityX = document.getElementById("property-lvel-x"); - var elLinearVelocityY = document.getElementById("property-lvel-y"); - var elLinearVelocityZ = document.getElementById("property-lvel-z"); - var elLinearDamping = document.getElementById("property-ldamping"); - - var elAngularVelocityX = document.getElementById("property-avel-x"); - var elAngularVelocityY = document.getElementById("property-avel-y"); - var elAngularVelocityZ = document.getElementById("property-avel-z"); - var elAngularDamping = document.getElementById("property-adamping"); - - var elRestitution = document.getElementById("property-restitution"); - var elFriction = document.getElementById("property-friction"); - - var elGravityX = document.getElementById("property-grav-x"); - var elGravityY = document.getElementById("property-grav-y"); - var elGravityZ = document.getElementById("property-grav-z"); - - var elAccelerationX = document.getElementById("property-lacc-x"); - var elAccelerationY = document.getElementById("property-lacc-y"); - var elAccelerationZ = document.getElementById("property-lacc-z"); - - var elDensity = document.getElementById("property-density"); - var elCollisionless = document.getElementById("property-collisionless"); - var elDynamic = document.getElementById("property-dynamic"); - var elCollideStatic = document.getElementById("property-collide-static"); - var elCollideDynamic = document.getElementById("property-collide-dynamic"); - var elCollideKinematic = document.getElementById("property-collide-kinematic"); - var elCollideMyAvatar = document.getElementById("property-collide-myAvatar"); - var elCollideOtherAvatar = document.getElementById("property-collide-otherAvatar"); - var elCollisionSoundURL = document.getElementById("property-collision-sound-url"); - - var elGrabbable = document.getElementById("property-grabbable"); - - var elCloneable = document.getElementById("property-cloneable"); - var elCloneableDynamic = document.getElementById("property-cloneable-dynamic"); - var elCloneableAvatarEntity = document.getElementById("property-cloneable-avatarEntity"); - var elCloneableGroup = document.getElementById("group-cloneable-group"); - var elCloneableLifetime = document.getElementById("property-cloneable-lifetime"); - var elCloneableLimit = document.getElementById("property-cloneable-limit"); - - var elTriggerable = document.getElementById("property-triggerable"); - var elIgnoreIK = document.getElementById("property-ignore-ik"); - - var elLifetime = document.getElementById("property-lifetime"); - var elScriptURL = document.getElementById("property-script-url"); - var elScriptTimestamp = document.getElementById("property-script-timestamp"); - var elReloadScriptsButton = document.getElementById("reload-script-button"); - var elServerScripts = document.getElementById("property-server-scripts"); - var elReloadServerScriptsButton = document.getElementById("reload-server-scripts-button"); - var elServerScriptStatus = document.getElementById("server-script-status"); - var elServerScriptError = document.getElementById("server-script-error"); - var elUserData = document.getElementById("property-user-data"); - var elClearUserData = document.getElementById("userdata-clear"); - var elSaveUserData = document.getElementById("userdata-save"); - var elNewJSONEditor = document.getElementById('userdata-new-editor'); - var elColorControlVariant2 = document.getElementById("property-color-control2"); - var elColorRed = document.getElementById("property-color-red"); - var elColorGreen = document.getElementById("property-color-green"); - var elColorBlue = document.getElementById("property-color-blue"); - - var elShape = document.getElementById("property-shape"); - - var elCanCastShadow = document.getElementById("property-can-cast-shadow"); - - var elLightSpotLight = document.getElementById("property-light-spot-light"); - var elLightColor = document.getElementById("property-light-color"); - var elLightColorRed = document.getElementById("property-light-color-red"); - var elLightColorGreen = document.getElementById("property-light-color-green"); - var elLightColorBlue = document.getElementById("property-light-color-blue"); - - var elLightIntensity = document.getElementById("property-light-intensity"); - var elLightFalloffRadius = document.getElementById("property-light-falloff-radius"); - var elLightExponent = document.getElementById("property-light-exponent"); - var elLightCutoff = document.getElementById("property-light-cutoff"); - - var elModelURL = document.getElementById("property-model-url"); - var elShapeType = document.getElementById("property-shape-type"); - var elCompoundShapeURL = document.getElementById("property-compound-shape-url"); - var elModelAnimationURL = document.getElementById("property-model-animation-url"); - var elModelAnimationPlaying = document.getElementById("property-model-animation-playing"); - var elModelAnimationFPS = document.getElementById("property-model-animation-fps"); - var elModelAnimationFrame = document.getElementById("property-model-animation-frame"); - var elModelAnimationFirstFrame = document.getElementById("property-model-animation-first-frame"); - var elModelAnimationLastFrame = document.getElementById("property-model-animation-last-frame"); - var elModelAnimationLoop = document.getElementById("property-model-animation-loop"); - var elModelAnimationHold = document.getElementById("property-model-animation-hold"); - var elModelAnimationAllowTranslation = document.getElementById("property-model-animation-allow-translation"); - var elModelTextures = document.getElementById("property-model-textures"); - var elModelOriginalTextures = document.getElementById("property-model-original-textures"); - - var elMaterialURL = document.getElementById("property-material-url"); - //var elMaterialMappingMode = document.getElementById("property-material-mapping-mode"); - var elPriority = document.getElementById("property-priority"); - var elParentMaterialNameString = document.getElementById("property-parent-material-id-string"); - var elParentMaterialNameNumber = document.getElementById("property-parent-material-id-number"); - var elParentMaterialNameCheckbox = document.getElementById("property-parent-material-id-checkbox"); - var elMaterialMappingPosX = document.getElementById("property-material-mapping-pos-x"); - var elMaterialMappingPosY = document.getElementById("property-material-mapping-pos-y"); - var elMaterialMappingScaleX = document.getElementById("property-material-mapping-scale-x"); - var elMaterialMappingScaleY = document.getElementById("property-material-mapping-scale-y"); - var elMaterialMappingRot = document.getElementById("property-material-mapping-rot"); - var elMaterialData = document.getElementById("property-material-data"); - var elClearMaterialData = document.getElementById("materialdata-clear"); - var elSaveMaterialData = document.getElementById("materialdata-save"); - var elNewJSONMaterialEditor = document.getElementById('materialdata-new-editor'); - - var elImageURL = document.getElementById("property-image-url"); - - var elWebSourceURL = document.getElementById("property-web-source-url"); - var elWebDPI = document.getElementById("property-web-dpi"); - - var elDescription = document.getElementById("property-description"); - - var elHyperlinkHref = document.getElementById("property-hyperlink-href"); - - var elTextText = document.getElementById("property-text-text"); - var elTextLineHeight = document.getElementById("property-text-line-height"); - var elTextTextColor = document.getElementById("property-text-text-color"); - var elTextFaceCamera = document.getElementById("property-text-face-camera"); - var elTextTextColorRed = document.getElementById("property-text-text-color-red"); - var elTextTextColorGreen = document.getElementById("property-text-text-color-green"); - var elTextTextColorBlue = document.getElementById("property-text-text-color-blue"); - var elTextBackgroundColor = document.getElementById("property-text-background-color"); - var elTextBackgroundColorRed = document.getElementById("property-text-background-color-red"); - var elTextBackgroundColorGreen = document.getElementById("property-text-background-color-green"); - var elTextBackgroundColorBlue = document.getElementById("property-text-background-color-blue"); - - // Key light - var elZoneKeyLightModeInherit = document.getElementById("property-zone-key-light-mode-inherit"); - var elZoneKeyLightModeDisabled = document.getElementById("property-zone-key-light-mode-disabled"); - var elZoneKeyLightModeEnabled = document.getElementById("property-zone-key-light-mode-enabled"); - - var elZoneKeyLightColor = document.getElementById("property-zone-key-light-color"); - var elZoneKeyLightColorRed = document.getElementById("property-zone-key-light-color-red"); - var elZoneKeyLightColorGreen = document.getElementById("property-zone-key-light-color-green"); - var elZoneKeyLightColorBlue = document.getElementById("property-zone-key-light-color-blue"); - var elZoneKeyLightIntensity = document.getElementById("property-zone-key-intensity"); - var elZoneKeyLightDirectionX = document.getElementById("property-zone-key-light-direction-x"); - var elZoneKeyLightDirectionY = document.getElementById("property-zone-key-light-direction-y"); - - var elZoneKeyLightCastShadows = document.getElementById("property-zone-key-light-cast-shadows"); - - // Skybox - var elZoneSkyboxModeInherit = document.getElementById("property-zone-skybox-mode-inherit"); - var elZoneSkyboxModeDisabled = document.getElementById("property-zone-skybox-mode-disabled"); - var elZoneSkyboxModeEnabled = document.getElementById("property-zone-skybox-mode-enabled"); - - // Ambient light - var elCopySkyboxURLToAmbientURL = document.getElementById("copy-skybox-url-to-ambient-url"); - - var elZoneAmbientLightModeInherit = document.getElementById("property-zone-ambient-light-mode-inherit"); - var elZoneAmbientLightModeDisabled = document.getElementById("property-zone-ambient-light-mode-disabled"); - var elZoneAmbientLightModeEnabled = document.getElementById("property-zone-ambient-light-mode-enabled"); - - var elZoneAmbientLightIntensity = document.getElementById("property-zone-key-ambient-intensity"); - var elZoneAmbientLightURL = document.getElementById("property-zone-key-ambient-url"); - - // Haze - var elZoneHazeModeInherit = document.getElementById("property-zone-haze-mode-inherit"); - var elZoneHazeModeDisabled = document.getElementById("property-zone-haze-mode-disabled"); - var elZoneHazeModeEnabled = document.getElementById("property-zone-haze-mode-enabled"); - - var elZoneHazeRange = document.getElementById("property-zone-haze-range"); - var elZoneHazeColor = document.getElementById("property-zone-haze-color"); - var elZoneHazeColorRed = document.getElementById("property-zone-haze-color-red"); - var elZoneHazeColorGreen = document.getElementById("property-zone-haze-color-green"); - var elZoneHazeColorBlue = document.getElementById("property-zone-haze-color-blue"); - var elZoneHazeGlareColor = document.getElementById("property-zone-haze-glare-color"); - var elZoneHazeGlareColorRed = document.getElementById("property-zone-haze-glare-color-red"); - var elZoneHazeGlareColorGreen = document.getElementById("property-zone-haze-glare-color-green"); - var elZoneHazeGlareColorBlue = document.getElementById("property-zone-haze-glare-color-blue"); - var elZoneHazeEnableGlare = document.getElementById("property-zone-haze-enable-light-blend"); - var elZoneHazeGlareAngle = document.getElementById("property-zone-haze-blend-angle"); - - var elZoneHazeAltitudeEffect = document.getElementById("property-zone-haze-altitude-effect"); - var elZoneHazeBaseRef = document.getElementById("property-zone-haze-base"); - var elZoneHazeCeiling = document.getElementById("property-zone-haze-ceiling"); - - var elZoneHazeBackgroundBlend = document.getElementById("property-zone-haze-background-blend"); - - // Bloom - var elZoneBloomModeInherit = document.getElementById("property-zone-bloom-mode-inherit"); - var elZoneBloomModeDisabled = document.getElementById("property-zone-bloom-mode-disabled"); - var elZoneBloomModeEnabled = document.getElementById("property-zone-bloom-mode-enabled"); - - var elZoneBloomIntensity = document.getElementById("property-zone-bloom-intensity"); - var elZoneBloomThreshold = document.getElementById("property-zone-bloom-threshold"); - var elZoneBloomSize = document.getElementById("property-zone-bloom-size"); - - var elZoneSkyboxColor = document.getElementById("property-zone-skybox-color"); - var elZoneSkyboxColorRed = document.getElementById("property-zone-skybox-color-red"); - var elZoneSkyboxColorGreen = document.getElementById("property-zone-skybox-color-green"); - var elZoneSkyboxColorBlue = document.getElementById("property-zone-skybox-color-blue"); - var elZoneSkyboxURL = document.getElementById("property-zone-skybox-url"); - - var elZoneFlyingAllowed = document.getElementById("property-zone-flying-allowed"); - var elZoneGhostingAllowed = document.getElementById("property-zone-ghosting-allowed"); - var elZoneFilterURL = document.getElementById("property-zone-filter-url"); - - var elVoxelVolumeSizeX = document.getElementById("property-voxel-volume-size-x"); - var elVoxelVolumeSizeY = document.getElementById("property-voxel-volume-size-y"); - var elVoxelVolumeSizeZ = document.getElementById("property-voxel-volume-size-z"); - var elVoxelSurfaceStyle = document.getElementById("property-voxel-surface-style"); - var elXTextureURL = document.getElementById("property-x-texture-url"); - var elYTextureURL = document.getElementById("property-y-texture-url"); - var elZTextureURL = document.getElementById("property-z-texture-url"); - - if (window.EventBridge !== undefined) { + elProperty.appendChild(elSpan); + elProperty.appendChild(elLabel); + elPropertyElements[propertyName] = [ elSpan, elLabel ]; + break; + } + case 'buttons': { + elProperty.setAttribute("class", "property Text"); + + let hasLabel = property.label !== undefined; + if (hasLabel) { + elProperty.appendChild(elLabel); + } + + if (property.buttons !== undefined) { + addButtons(elProperty, propertyID, property.buttons, hasLabel); + } + + elPropertyElements[propertyName] = elProperty; + break; + } + case 'sub-header': { + if (propertyName !== undefined) { + elPropertyElements[propertyName] = elProperty; + } + break; + } + } + + let showPropertyRule = property.showPropertyRule; + if (showPropertyRule !== undefined) { + let dependentProperty = Object.keys(showPropertyRule)[0]; + let dependentPropertyValue = showPropertyRule[dependentProperty]; + if (!showPropertyRules[dependentProperty]) { + showPropertyRules[dependentProperty] = []; + } + showPropertyRules[dependentProperty][propertyName] = dependentPropertyValue; + } + }); + + elGroups[group.id] = elGroup; + }); + + if (window.EventBridge !== undefined) { var properties; EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); if (data.type === "server_script_status") { + let elServerScriptError = document.getElementById("property-serverScripts-error"); + let elServerScriptStatus = document.getElementById("property-serverScripts-status"); elServerScriptError.value = data.errorInfo; // If we just set elServerScriptError's diplay to block or none, we still end up with // it's parent contributing 21px bottom padding even when elServerScriptError is display:none. @@ -913,218 +2114,33 @@ function loaded() { elServerScriptStatus.innerText = "Not running"; } } else if (data.type === "update" && data.selections) { - if (data.selections.length === 0) { if (lastEntityID !== null) { if (editor !== null) { - saveJSONUserData(true); + saveUserData(); deleteJSONEditor(); } if (materialEditor !== null) { - saveJSONMaterialData(true); + saveMaterialData(); deleteJSONMaterialEditor(); } } - - elTypeIcon.style.display = "none"; - elType.innerHTML = "No selection"; - elPropertiesList.className = ''; - - elID.value = ""; - elName.value = ""; - elLocked.checked = false; - elVisible.checked = false; - - elParentID.value = ""; - elParentJointIndex.value = ""; - - elColorRed.value = ""; - elColorGreen.value = ""; - elColorBlue.value = ""; - elColorControlVariant2.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - - elPositionX.value = ""; - elPositionY.value = ""; - elPositionZ.value = ""; - - elRotationX.value = ""; - elRotationY.value = ""; - elRotationZ.value = ""; - - elDimensionsX.value = ""; - elDimensionsY.value = ""; - elDimensionsZ.value = ""; - - elRegistrationX.value = ""; - elRegistrationY.value = ""; - elRegistrationZ.value = ""; - - elLinearVelocityX.value = ""; - elLinearVelocityY.value = ""; - elLinearVelocityZ.value = ""; - elLinearDamping.value = ""; - - elAngularVelocityX.value = ""; - elAngularVelocityY.value = ""; - elAngularVelocityZ.value = ""; - elAngularDamping.value = ""; - - elGravityX.value = ""; - elGravityY.value = ""; - elGravityZ.value = ""; - - elAccelerationX.value = ""; - elAccelerationY.value = ""; - elAccelerationZ.value = ""; - - elRestitution.value = ""; - elFriction.value = ""; - elDensity.value = ""; - - elCollisionless.checked = false; - elDynamic.checked = false; - - elCollideStatic.checked = false; - elCollideKinematic.checked = false; - elCollideDynamic.checked = false; - elCollideMyAvatar.checked = false; - elCollideOtherAvatar.checked = false; - - elGrabbable.checked = false; - elTriggerable.checked = false; - elIgnoreIK.checked = false; - - elCloneable.checked = false; - elCloneableDynamic.checked = false; - elCloneableAvatarEntity.checked = false; - elCloneableGroup.style.display = "none"; - elCloneableLimit.value = ""; - elCloneableLifetime.value = ""; - - showElements(document.getElementsByClassName('can-cast-shadow-section'), true); - elCanCastShadow.checked = false; - - elCollisionSoundURL.value = ""; - elLifetime.value = ""; - elScriptURL.value = ""; - elServerScripts.value = ""; - elHyperlinkHref.value = ""; - elDescription.value = ""; - - deleteJSONEditor(); - elUserData.value = ""; + + elPropertyElements["type"][0].style.display = "none"; + elPropertyElements["type"][1].innerHTML = NO_SELECTION; + elPropertiesList.className = ''; + showGroupsForType("None"); + + resetProperties(); + + deleteJSONEditor(); + elPropertyElements["userData"].value = ""; showUserDataTextArea(); showSaveUserDataButton(); showNewJSONEditorButton(); - - // Shape Properties - elShape.value = "Cube"; - setDropdownText(elShape); - - // Light Properties - elLightSpotLight.checked = false; - elLightColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elLightColorRed.value = ""; - elLightColorGreen.value = ""; - elLightColorBlue.value = ""; - elLightIntensity.value = ""; - elLightFalloffRadius.value = ""; - elLightExponent.value = ""; - elLightCutoff.value = ""; - - // Model Properties - elModelURL.value = ""; - elCompoundShapeURL.value = ""; - elShapeType.value = "none"; - setDropdownText(elShapeType); - elModelAnimationURL.value = "" - elModelAnimationPlaying.checked = false; - elModelAnimationFPS.value = ""; - elModelAnimationFrame.value = ""; - elModelAnimationFirstFrame.value = ""; - elModelAnimationLastFrame.value = ""; - elModelAnimationLoop.checked = false; - elModelAnimationHold.checked = false; - elModelAnimationAllowTranslation.checked = false; - elModelTextures.value = ""; - elModelOriginalTextures.value = ""; - - // Zone Properties - elZoneFlyingAllowed.checked = false; - elZoneGhostingAllowed.checked = false; - elZoneFilterURL.value = ""; - elZoneKeyLightColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elZoneKeyLightColorRed.value = ""; - elZoneKeyLightColorGreen.value = ""; - elZoneKeyLightColorBlue.value = ""; - elZoneKeyLightIntensity.value = ""; - elZoneKeyLightDirectionX.value = ""; - elZoneKeyLightDirectionY.value = ""; - elZoneKeyLightCastShadows.checked = false; - elZoneAmbientLightIntensity.value = ""; - elZoneAmbientLightURL.value = ""; - elZoneHazeRange.value = ""; - elZoneHazeColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elZoneHazeColorRed.value = ""; - elZoneHazeColorGreen.value = ""; - elZoneHazeColorBlue.value = ""; - elZoneHazeBackgroundBlend.value = 0; - elZoneHazeGlareColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elZoneHazeGlareColorRed.value = ""; - elZoneHazeGlareColorGreen.value = ""; - elZoneHazeGlareColorBlue.value = ""; - elZoneHazeEnableGlare.checked = false; - elZoneHazeGlareAngle.value = ""; - elZoneHazeAltitudeEffect.checked = false; - elZoneHazeBaseRef.value = ""; - elZoneHazeCeiling.value = ""; - elZoneBloomIntensity.value = ""; - elZoneBloomThreshold.value = ""; - elZoneBloomSize.value = ""; - elZoneSkyboxColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elZoneSkyboxColorRed.value = ""; - elZoneSkyboxColorGreen.value = ""; - elZoneSkyboxColorBlue.value = ""; - elZoneSkyboxURL.value = ""; - showElements(document.getElementsByClassName('keylight-section'), true); - showElements(document.getElementsByClassName('skybox-section'), true); - showElements(document.getElementsByClassName('ambient-section'), true); - showElements(document.getElementsByClassName('haze-section'), true); - showElements(document.getElementsByClassName('bloom-section'), true); - - // Text Properties - elTextText.value = ""; - elTextLineHeight.value = ""; - elTextFaceCamera.checked = false; - elTextTextColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elTextTextColorRed.value = ""; - elTextTextColorGreen.value = ""; - elTextTextColorBlue.value = ""; - elTextBackgroundColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elTextBackgroundColorRed.value = ""; - elTextBackgroundColorGreen.value = ""; - elTextBackgroundColorBlue.value = ""; - - // Image Properties - elImageURL.value = ""; - - // Web Properties - elWebSourceURL.value = ""; - elWebDPI.value = ""; - - // Material Properties - elMaterialURL.value = ""; - elParentMaterialNameNumber.value = ""; - elParentMaterialNameCheckbox.checked = false; - elPriority.value = ""; - elMaterialMappingPosX.value = ""; - elMaterialMappingPosY.value = ""; - elMaterialMappingScaleX.value = ""; - elMaterialMappingScaleY.value = ""; - elMaterialMappingRot.value = ""; - - deleteJSONMaterialEditor(); - elMaterialData.value = ""; + + deleteJSONMaterialEditor(); + elPropertyElements["materialData"].value = ""; showMaterialDataTextArea(); showSaveMaterialDataButton(); showNewJSONMaterialEditorButton(); @@ -1133,15 +2149,16 @@ function loaded() { } else if (data.selections.length > 1) { deleteJSONEditor(); deleteJSONMaterialEditor(); - var selections = data.selections; + + let selections = data.selections; - var ids = []; - var types = {}; - var numTypes = 0; + let ids = []; + let types = {}; + let numTypes = 0; - for (var i = 0; i < selections.length; i++) { + for (let i = 0; i < selections.length; i++) { ids.push(selections[i].id); - var currentSelectedType = selections[i].properties.type; + let currentSelectedType = selections[i].properties.type; if (types[currentSelectedType] === undefined) { types[currentSelectedType] = 0; numTypes += 1; @@ -1149,400 +2166,188 @@ function loaded() { types[currentSelectedType]++; } - var type = "Multiple"; + let type = "Multiple"; if (numTypes === 1) { type = selections[0].properties.type; } - - elType.innerHTML = type + " (" + data.selections.length + ")"; - elTypeIcon.innerHTML = ICON_FOR_TYPE[type]; - elTypeIcon.style.display = "inline-block"; - elPropertiesList.className = ''; - - elID.value = ""; - - disableProperties(); + + elPropertyElements["type"][0].innerHTML = ICON_FOR_TYPE[type]; + elPropertyElements["type"][0].style.display = "inline-block"; + elPropertyElements["type"][1].innerHTML = type + " (" + data.selections.length + ")"; + elPropertiesList.className = ''; + showGroupsForType(type); + + resetProperties(); + disableProperties(); } else { - properties = data.selections[0].properties; - if (lastEntityID !== '"' + properties.id + '"' && lastEntityID !== null) { + + if (lastEntityID !== '"' + properties.id + '"' && lastEntityID !== null) { if (editor !== null) { - saveJSONUserData(true); + saveUserData(); } if (materialEditor !== null) { - saveJSONMaterialData(true); + saveMaterialData(); } } - var doSelectElement = lastEntityID === '"' + properties.id + '"'; + let doSelectElement = lastEntityID === '"' + properties.id + '"'; // the event bridge and json parsing handle our avatar id string differently. lastEntityID = '"' + properties.id + '"'; - elID.value = properties.id; // HTML workaround since image is not yet a separate entity type - var IMAGE_MODEL_NAME = 'default-image-model.fbx'; + let IMAGE_MODEL_NAME = 'default-image-model.fbx'; if (properties.type === "Model") { - var urlParts = properties.modelURL.split('/'); - var propsFilename = urlParts[urlParts.length - 1]; + let urlParts = properties.modelURL.split('/'); + let propsFilename = urlParts[urlParts.length - 1]; if (propsFilename === IMAGE_MODEL_NAME) { properties.type = "Image"; } } - + // Create class name for css ruleset filtering elPropertiesList.className = properties.type + 'Menu'; + showGroupsForType(properties.type); + + for (let propertyName in elPropertyElements) { + let elProperty = elPropertyElements[propertyName]; + + let propertyValue; + let splitPropertyName = propertyName.split('.'); + if (splitPropertyName.length > 1) { + let propertyGroupName = splitPropertyName[0]; + let subPropertyName = splitPropertyName[1]; + if (properties[propertyGroupName] === undefined || properties[propertyGroupName][subPropertyName] === undefined) { + continue; + } + if (splitPropertyName.length === 3) { + let subSubPropertyName = splitPropertyName[2]; + propertyValue = properties[propertyGroupName][subPropertyName][subSubPropertyName]; + } else { + propertyValue = properties[propertyGroupName][subPropertyName]; + } + } else { + propertyValue = properties[propertyName]; + } - elType.innerHTML = properties.type; - elTypeIcon.innerHTML = ICON_FOR_TYPE[properties.type]; - elTypeIcon.style.display = "inline-block"; - - elLocked.checked = properties.locked; - - elName.value = properties.name; - - elVisible.checked = properties.visible; - - elPositionX.value = properties.position.x.toFixed(4); - elPositionY.value = properties.position.y.toFixed(4); - elPositionZ.value = properties.position.z.toFixed(4); - - elDimensionsX.value = properties.dimensions.x.toFixed(4); - elDimensionsY.value = properties.dimensions.y.toFixed(4); - elDimensionsZ.value = properties.dimensions.z.toFixed(4); - - elParentID.value = properties.parentID; - elParentJointIndex.value = properties.parentJointIndex; - - elRegistrationX.value = properties.registrationPoint.x.toFixed(4); - elRegistrationY.value = properties.registrationPoint.y.toFixed(4); - elRegistrationZ.value = properties.registrationPoint.z.toFixed(4); - - elRotationX.value = properties.rotation.x.toFixed(4); - elRotationY.value = properties.rotation.y.toFixed(4); - elRotationZ.value = properties.rotation.z.toFixed(4); - - elLinearVelocityX.value = properties.velocity.x.toFixed(4); - elLinearVelocityY.value = properties.velocity.y.toFixed(4); - elLinearVelocityZ.value = properties.velocity.z.toFixed(4); - elLinearDamping.value = properties.damping.toFixed(2); - - elAngularVelocityX.value = (properties.angularVelocity.x * RADIANS_TO_DEGREES).toFixed(4); - elAngularVelocityY.value = (properties.angularVelocity.y * RADIANS_TO_DEGREES).toFixed(4); - elAngularVelocityZ.value = (properties.angularVelocity.z * RADIANS_TO_DEGREES).toFixed(4); - elAngularDamping.value = properties.angularDamping.toFixed(4); - - elRestitution.value = properties.restitution.toFixed(4); - elFriction.value = properties.friction.toFixed(4); - - elGravityX.value = properties.gravity.x.toFixed(4); - elGravityY.value = properties.gravity.y.toFixed(4); - elGravityZ.value = properties.gravity.z.toFixed(4); - - elAccelerationX.value = properties.acceleration.x.toFixed(4); - elAccelerationY.value = properties.acceleration.y.toFixed(4); - elAccelerationZ.value = properties.acceleration.z.toFixed(4); - - elDensity.value = properties.density.toFixed(4); - elCollisionless.checked = properties.collisionless; - elDynamic.checked = properties.dynamic; - - elCollideStatic.checked = properties.collidesWith.indexOf("static") > -1; - elCollideKinematic.checked = properties.collidesWith.indexOf("kinematic") > -1; - elCollideDynamic.checked = properties.collidesWith.indexOf("dynamic") > -1; - elCollideMyAvatar.checked = properties.collidesWith.indexOf("myAvatar") > -1; - elCollideOtherAvatar.checked = properties.collidesWith.indexOf("otherAvatar") > -1; - - elGrabbable.checked = properties.dynamic; - - elTriggerable.checked = false; - elIgnoreIK.checked = true; - - elCloneable.checked = properties.cloneable; - elCloneableDynamic.checked = properties.cloneDynamic; - elCloneableAvatarEntity.checked = properties.cloneAvatarEntity; - elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; - elCloneableLimit.value = properties.cloneLimit; - elCloneableLifetime.value = properties.cloneLifetime; - - var grabbablesSet = false; - var parsedUserData = {}; - try { - parsedUserData = JSON.parse(properties.userData); - - if ("grabbableKey" in parsedUserData) { - grabbablesSet = true; - var grabbableData = parsedUserData.grabbableKey; - if ("grabbable" in grabbableData) { - elGrabbable.checked = grabbableData.grabbable; - } else { - elGrabbable.checked = true; - } - if ("triggerable" in grabbableData) { - elTriggerable.checked = grabbableData.triggerable; - } else if ("wantsTrigger" in grabbableData) { - elTriggerable.checked = grabbableData.wantsTrigger; - } else { - elTriggerable.checked = false; - } - if ("ignoreIK" in grabbableData) { - elIgnoreIK.checked = grabbableData.ignoreIK; - } else { - elIgnoreIK.checked = true; - } - } - } catch (e) { - // TODO: What should go here? - } - if (!grabbablesSet) { - elGrabbable.checked = true; - elTriggerable.checked = false; - elIgnoreIK.checked = true; - elCloneable.checked = false; - } - - elCollisionSoundURL.value = properties.collisionSoundURL; - elLifetime.value = properties.lifetime; - elScriptURL.value = properties.script; - elScriptTimestamp.value = properties.scriptTimestamp; - elServerScripts.value = properties.serverScripts; - - var json = null; - try { - json = JSON.parse(properties.userData); - } catch (e) { - // normal text - deleteJSONEditor(); - elUserData.value = properties.userData; - showUserDataTextArea(); - showNewJSONEditorButton(); - hideSaveUserDataButton(); - } - if (json !== null) { - if (editor === null) { - createJSONEditor(); - } - - setEditorJSON(json); - showSaveUserDataButton(); - hideUserDataTextArea(); - hideNewJSONEditorButton(); - } - - var materialJson = null; - try { - materialJson = JSON.parse(properties.materialData); - } catch (e) { - // normal text - deleteJSONMaterialEditor(); - elMaterialData.value = properties.materialData; - showMaterialDataTextArea(); - showNewJSONMaterialEditorButton(); - hideSaveMaterialDataButton(); - } - if (materialJson !== null) { - if (materialEditor === null) { - createJSONMaterialEditor(); - } - - setMaterialEditorJSON(materialJson); - showSaveMaterialDataButton(); - hideMaterialDataTextArea(); - hideNewJSONMaterialEditorButton(); - } - - elHyperlinkHref.value = properties.href; - elDescription.value = properties.description; - - - if (properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { - elShape.value = properties.shape; - setDropdownText(elShape); - } - - if (properties.type === "Shape" || properties.type === "Box" || - properties.type === "Sphere" || properties.type === "ParticleEffect") { - elColorRed.value = properties.color.red; - elColorGreen.value = properties.color.green; - elColorBlue.value = properties.color.blue; - elColorControlVariant2.style.backgroundColor = "rgb(" + properties.color.red + "," + - properties.color.green + "," + properties.color.blue + ")"; - } - - if (properties.type === "Model" || - properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { - - elCanCastShadow.checked = properties.canCastShadow; - } - - if (properties.type === "Model") { - elModelURL.value = properties.modelURL; - elShapeType.value = properties.shapeType; - setDropdownText(elShapeType); - elCompoundShapeURL.value = properties.compoundShapeURL; - elModelAnimationURL.value = properties.animation.url; - elModelAnimationPlaying.checked = properties.animation.running; - elModelAnimationFPS.value = properties.animation.fps; - elModelAnimationFrame.value = properties.animation.currentFrame; - elModelAnimationFirstFrame.value = properties.animation.firstFrame; - elModelAnimationLastFrame.value = properties.animation.lastFrame; - elModelAnimationLoop.checked = properties.animation.loop; - elModelAnimationHold.checked = properties.animation.hold; - elModelAnimationAllowTranslation.checked = properties.animation.allowTranslation; - elModelTextures.value = properties.textures; - setTextareaScrolling(elModelTextures); - elModelOriginalTextures.value = properties.originalTextures; - setTextareaScrolling(elModelOriginalTextures); - } else if (properties.type === "Web") { - elWebSourceURL.value = properties.sourceUrl; - elWebDPI.value = properties.dpi; - } else if (properties.type === "Image") { + // workaround for shape Color & Light Color property fields sharing same property value "color" + if (propertyValue === undefined && propertyName === "lightColor") { + propertyValue = properties["color"] + } + + let isSubProperty = subProperties.indexOf(propertyName) > -1; + if (elProperty === undefined || (propertyValue === undefined && !isSubProperty)) { + continue; + } + + if (elProperty instanceof Array) { + if (elProperty[1].getAttribute("type") === "number") { // vectors + if (elProperty.length === 2 || elProperty.length === 3) { + // vec2/vec3 are array of 2/3 input numbers + elProperty[0].value = (propertyValue.x * 1 / elProperty[0].getAttribute("multiplier")).toFixed(4); + elProperty[1].value = (propertyValue.y * 1 / elProperty[1].getAttribute("multiplier")).toFixed(4); + if (elProperty[2] !== undefined) { + elProperty[2].value = (propertyValue.z * 1 / elProperty[1].getAttribute("multiplier")).toFixed(4); + } + } else if (elProperty.length === 4) { + // color is array of color picker and 3 input numbers + elProperty[0].style.backgroundColor = "rgb(" + propertyValue.red + "," + propertyValue.green + "," + propertyValue.blue + ")"; + elProperty[1].value = propertyValue.red; + elProperty[2].value = propertyValue.green; + elProperty[3].value = propertyValue.blue; + } + } else if (elProperty[0].nodeName === "SPAN") { // icons + // icon is array of elSpan (icon glyph) and elLabel + elProperty[0].innerHTML = icons[propertyName][propertyValue]; + elProperty[0].style.display = "inline-block"; + elProperty[1].innerHTML = propertyValue; //+ " (" + data.selections.length + ")"; + } + } else if (elProperty.getAttribute("subPropertyOf")) { + let propertyValue = properties[elProperty.getAttribute("subPropertyOf")]; + let subProperties = propertyValue.split(","); + elProperty.checked = subProperties.indexOf(propertyName) > -1; + } else if (elProperty.getAttribute("type") === "number") { + let fixedDecimals = elProperty.getAttribute("fixedDecimals"); + elProperty.value = propertyValue.toFixed(fixedDecimals); + } else if (elProperty.getAttribute("type") === "checkbox") { + let inverse = elProperty.getAttribute("inverse") === "true"; + elProperty.checked = inverse ? !propertyValue : propertyValue; + } else if (elProperty.nodeName === "TEXTAREA") { + elProperty.value = propertyValue; + setTextareaScrolling(elProperty); + } else if (elProperty.nodeName === "SELECT") { // dropdown + elProperty.value = propertyValue; + } else { + elProperty.value = propertyValue; + } + + let propertyShowRules = showPropertyRules[propertyName]; + if (propertyShowRules !== undefined) { + for (let propertyToShow in propertyShowRules) { + let showIfThisPropertyValue = propertyShowRules[propertyToShow]; + let show = String(propertyValue) === String(showIfThisPropertyValue); + let elPropertyToShow = elPropertyElements[propertyToShow]; + if (elPropertyToShow) { + let parentNode = elPropertyToShow.parentNode; + if (parentNode === undefined && elPropertyToShow instanceof Array) { + parentNode = elPropertyToShow[0].parentNode; + } + parentNode.style.display = show ? "block" : "none"; + } + } + } + } + + let elGrabbable = elPropertyElements["grabbable"]; + let elTriggerable = elPropertyElements["triggerable"]; + let elIgnoreIK = elPropertyElements["ignoreIK"]; + elGrabbable.checked = elPropertyElements["dynamic"].checked; + elTriggerable.checked = false; + elIgnoreIK.checked = true; + let grabbablesSet = false; + let parsedUserData = {}; + try { + parsedUserData = JSON.parse(properties.userData); + if ("grabbableKey" in parsedUserData) { + grabbablesSet = true; + let grabbableData = parsedUserData.grabbableKey; + if ("grabbable" in grabbableData) { + elGrabbable.checked = grabbableData.grabbable; + } else { + elGrabbable.checked = true; + } + if ("triggerable" in grabbableData) { + elTriggerable.checked = grabbableData.triggerable; + } else if ("wantsTrigger" in grabbableData) { + elTriggerable.checked = grabbableData.wantsTrigger; + } else { + elTriggerable.checked = false; + } + if ("ignoreIK" in grabbableData) { + elIgnoreIK.checked = grabbableData.ignoreIK; + } else { + elIgnoreIK.checked = true; + } + } + } catch (e) { + // TODO: What should go here? + } + if (!grabbablesSet) { + elGrabbable.checked = true; + elTriggerable.checked = false; + elIgnoreIK.checked = true; + } + + if (properties.type === "Image") { var imageLink = JSON.parse(properties.textures)["tex.picture"]; - elImageURL.value = imageLink; - } else if (properties.type === "Text") { - elTextText.value = properties.text; - elTextLineHeight.value = properties.lineHeight.toFixed(4); - elTextFaceCamera.checked = properties.faceCamera; - elTextTextColor.style.backgroundColor = "rgb(" + properties.textColor.red + "," + - properties.textColor.green + "," + - properties.textColor.blue + ")"; - elTextTextColorRed.value = properties.textColor.red; - elTextTextColorGreen.value = properties.textColor.green; - elTextTextColorBlue.value = properties.textColor.blue; - elTextBackgroundColor.style.backgroundColor = "rgb(" + properties.backgroundColor.red + "," + - properties.backgroundColor.green + "," + - properties.backgroundColor.blue + ")"; - elTextBackgroundColorRed.value = properties.backgroundColor.red; - elTextBackgroundColorGreen.value = properties.backgroundColor.green; - elTextBackgroundColorBlue.value = properties.backgroundColor.blue; - } else if (properties.type === "Light") { - elLightSpotLight.checked = properties.isSpotlight; - - elLightColor.style.backgroundColor = "rgb(" + properties.color.red + "," + - properties.color.green + "," + properties.color.blue + ")"; - elLightColorRed.value = properties.color.red; - elLightColorGreen.value = properties.color.green; - elLightColorBlue.value = properties.color.blue; - - elLightIntensity.value = properties.intensity.toFixed(1); - elLightFalloffRadius.value = properties.falloffRadius.toFixed(1); - elLightExponent.value = properties.exponent.toFixed(2); - elLightCutoff.value = properties.cutoff.toFixed(2); - } else if (properties.type === "Zone") { - // Key light - elZoneKeyLightModeInherit.checked = (properties.keyLightMode === 'inherit'); - elZoneKeyLightModeDisabled.checked = (properties.keyLightMode === 'disabled'); - elZoneKeyLightModeEnabled.checked = (properties.keyLightMode === 'enabled'); - - elZoneKeyLightColor.style.backgroundColor = "rgb(" + properties.keyLight.color.red + "," + - properties.keyLight.color.green + "," + properties.keyLight.color.blue + ")"; - elZoneKeyLightColorRed.value = properties.keyLight.color.red; - elZoneKeyLightColorGreen.value = properties.keyLight.color.green; - elZoneKeyLightColorBlue.value = properties.keyLight.color.blue; - elZoneKeyLightIntensity.value = properties.keyLight.intensity.toFixed(2); - elZoneKeyLightDirectionX.value = properties.keyLight.direction.x.toFixed(2); - elZoneKeyLightDirectionY.value = properties.keyLight.direction.y.toFixed(2); - - elZoneKeyLightCastShadows.checked = properties.keyLight.castShadows; - - // Skybox - elZoneSkyboxModeInherit.checked = (properties.skyboxMode === 'inherit'); - elZoneSkyboxModeDisabled.checked = (properties.skyboxMode === 'disabled'); - elZoneSkyboxModeEnabled.checked = (properties.skyboxMode === 'enabled'); - - // Ambient light - elZoneAmbientLightModeInherit.checked = (properties.ambientLightMode === 'inherit'); - elZoneAmbientLightModeDisabled.checked = (properties.ambientLightMode === 'disabled'); - elZoneAmbientLightModeEnabled.checked = (properties.ambientLightMode === 'enabled'); - - elZoneAmbientLightIntensity.value = properties.ambientLight.ambientIntensity.toFixed(2); - elZoneAmbientLightURL.value = properties.ambientLight.ambientURL; - - // Haze - elZoneHazeModeInherit.checked = (properties.hazeMode === 'inherit'); - elZoneHazeModeDisabled.checked = (properties.hazeMode === 'disabled'); - elZoneHazeModeEnabled.checked = (properties.hazeMode === 'enabled'); - - elZoneHazeRange.value = properties.haze.hazeRange.toFixed(0); - elZoneHazeColor.style.backgroundColor = "rgb(" + - properties.haze.hazeColor.red + "," + - properties.haze.hazeColor.green + "," + - properties.haze.hazeColor.blue + ")"; - - elZoneHazeColorRed.value = properties.haze.hazeColor.red; - elZoneHazeColorGreen.value = properties.haze.hazeColor.green; - elZoneHazeColorBlue.value = properties.haze.hazeColor.blue; - elZoneHazeBackgroundBlend.value = properties.haze.hazeBackgroundBlend.toFixed(2); - - elZoneHazeGlareColor.style.backgroundColor = "rgb(" + - properties.haze.hazeGlareColor.red + "," + - properties.haze.hazeGlareColor.green + "," + - properties.haze.hazeGlareColor.blue + ")"; - - elZoneHazeGlareColorRed.value = properties.haze.hazeGlareColor.red; - elZoneHazeGlareColorGreen.value = properties.haze.hazeGlareColor.green; - elZoneHazeGlareColorBlue.value = properties.haze.hazeGlareColor.blue; - - elZoneHazeEnableGlare.checked = properties.haze.hazeEnableGlare; - elZoneHazeGlareAngle.value = properties.haze.hazeGlareAngle.toFixed(0); - - elZoneHazeAltitudeEffect.checked = properties.haze.hazeAltitudeEffect; - elZoneHazeBaseRef.value = properties.haze.hazeBaseRef.toFixed(0); - elZoneHazeCeiling.value = properties.haze.hazeCeiling.toFixed(0); - - elZoneBloomModeInherit.checked = (properties.bloomMode === 'inherit'); - elZoneBloomModeDisabled.checked = (properties.bloomMode === 'disabled'); - elZoneBloomModeEnabled.checked = (properties.bloomMode === 'enabled'); - - elZoneBloomIntensity.value = properties.bloom.bloomIntensity.toFixed(2); - elZoneBloomThreshold.value = properties.bloom.bloomThreshold.toFixed(2); - elZoneBloomSize.value = properties.bloom.bloomSize.toFixed(2); - - elShapeType.value = properties.shapeType; - elCompoundShapeURL.value = properties.compoundShapeURL; - - elZoneSkyboxColor.style.backgroundColor = "rgb(" + properties.skybox.color.red + "," + - properties.skybox.color.green + "," + properties.skybox.color.blue + ")"; - elZoneSkyboxColorRed.value = properties.skybox.color.red; - elZoneSkyboxColorGreen.value = properties.skybox.color.green; - elZoneSkyboxColorBlue.value = properties.skybox.color.blue; - elZoneSkyboxURL.value = properties.skybox.url; - - elZoneFlyingAllowed.checked = properties.flyingAllowed; - elZoneGhostingAllowed.checked = properties.ghostingAllowed; - elZoneFilterURL.value = properties.filterURL; - - // Show/hide sections as required - showElements(document.getElementsByClassName('skybox-section'), - elZoneSkyboxModeEnabled.checked); - - showElements(document.getElementsByClassName('keylight-section'), - elZoneKeyLightModeEnabled.checked); - - showElements(document.getElementsByClassName('ambient-section'), - elZoneAmbientLightModeEnabled.checked); - - showElements(document.getElementsByClassName('haze-section'), - elZoneHazeModeEnabled.checked); - - showElements(document.getElementsByClassName('bloom-section'), - elZoneBloomModeEnabled.checked); - } else if (properties.type === "PolyVox") { - elVoxelVolumeSizeX.value = properties.voxelVolumeSize.x.toFixed(2); - elVoxelVolumeSizeY.value = properties.voxelVolumeSize.y.toFixed(2); - elVoxelVolumeSizeZ.value = properties.voxelVolumeSize.z.toFixed(2); - elVoxelSurfaceStyle.value = properties.voxelSurfaceStyle; - setDropdownText(elVoxelSurfaceStyle); - elXTextureURL.value = properties.xTextureURL; - elYTextureURL.value = properties.yTextureURL; - elZTextureURL.value = properties.zTextureURL; - } else if (properties.type === "Material") { - elMaterialURL.value = properties.materialURL; - //elMaterialMappingMode.value = properties.materialMappingMode; - //setDropdownText(elMaterialMappingMode); - elPriority.value = properties.priority; - if (properties.parentMaterialName.startsWith(MATERIAL_PREFIX_STRING)) { + elPropertyElements["image"].value = imageLink; + } else if (properties.type === "Material") { + let elParentMaterialNameString = elPropertyElements["materialNameToReplace"]; + let elParentMaterialNameNumber = elPropertyElements["submeshToReplace"]; + let elParentMaterialNameCheckbox = elPropertyElements["selectSubmesh"]; + if (properties.parentMaterialName.startsWith(MATERIAL_PREFIX_STRING)) { elParentMaterialNameString.value = properties.parentMaterialName.replace(MATERIAL_PREFIX_STRING, ""); showParentMaterialNameBox(false, elParentMaterialNameNumber, elParentMaterialNameString); elParentMaterialNameCheckbox.checked = false; @@ -1551,32 +2356,64 @@ function loaded() { showParentMaterialNameBox(true, elParentMaterialNameNumber, elParentMaterialNameString); elParentMaterialNameCheckbox.checked = true; } - elMaterialMappingPosX.value = properties.materialMappingPos.x.toFixed(4); - elMaterialMappingPosY.value = properties.materialMappingPos.y.toFixed(4); - elMaterialMappingScaleX.value = properties.materialMappingScale.x.toFixed(4); - elMaterialMappingScaleY.value = properties.materialMappingScale.y.toFixed(4); - elMaterialMappingRot.value = properties.materialMappingRot.toFixed(2); + } + + let json = null; + try { + json = JSON.parse(properties.userData); + } catch (e) { + // normal text + deleteJSONEditor(); + elPropertyElements["userData"].value = properties.userData; + showUserDataTextArea(); + showNewJSONEditorButton(); + hideSaveUserDataButton(); + hideUserDataSaved(); + } + if (json !== null) { + if (editor === null) { + createJSONEditor(); + } + setEditorJSON(json); + showSaveUserDataButton(); + hideUserDataTextArea(); + hideNewJSONEditorButton(); + hideUserDataSaved(); } - // Only these types can cast a shadow - if (properties.type === "Model" || - properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { - - showElements(document.getElementsByClassName('can-cast-shadow-section'), true); - } else { - showElements(document.getElementsByClassName('can-cast-shadow-section'), false); + let materialJson = null; + try { + materialJson = JSON.parse(properties.materialData); + } catch (e) { + // normal text + deleteJSONMaterialEditor(); + elPropertyElements["materialData"].value = properties.materialData; + showMaterialDataTextArea(); + showNewJSONMaterialEditorButton(); + hideSaveMaterialDataButton(); + hideMaterialDataSaved(); } - - if (properties.locked) { - disableProperties(); - elLocked.removeAttribute('disabled'); - } else { - enableProperties(); - elSaveUserData.disabled = true; - elSaveMaterialData.disabled = true; + if (materialJson !== null) { + if (materialEditor === null) { + createJSONMaterialEditor(); + } + setMaterialEditorJSON(materialJson); + showSaveMaterialDataButton(); + hideMaterialDataTextArea(); + hideNewJSONMaterialEditorButton(); + hideMaterialDataSaved(); } - - var activeElement = document.activeElement; + + if (properties.locked) { + disableProperties(); + elPropertyElements["locked"].removeAttribute('disabled'); + } else { + enableProperties(); + disableSaveUserDataButton(); + disableSaveMaterialDataButton() + } + + var activeElement = document.activeElement; if (doSelectElement && typeof activeElement.select !== "undefined") { activeElement.select(); } @@ -1584,270 +2421,79 @@ function loaded() { } }); } - - elLocked.addEventListener('change', createEmitCheckedPropertyUpdateFunction('locked')); - elName.addEventListener('change', createEmitTextPropertyUpdateFunction('name')); - elHyperlinkHref.addEventListener('change', createEmitTextPropertyUpdateFunction('href')); - elDescription.addEventListener('change', createEmitTextPropertyUpdateFunction('description')); - elVisible.addEventListener('change', createEmitCheckedPropertyUpdateFunction('visible')); - - var positionChangeFunction = createEmitVec3PropertyUpdateFunction( - 'position', elPositionX, elPositionY, elPositionZ); - elPositionX.addEventListener('change', positionChangeFunction); - elPositionY.addEventListener('change', positionChangeFunction); - elPositionZ.addEventListener('change', positionChangeFunction); - - var dimensionsChangeFunction = createEmitVec3PropertyUpdateFunction( - 'dimensions', elDimensionsX, elDimensionsY, elDimensionsZ); - elDimensionsX.addEventListener('change', dimensionsChangeFunction); - elDimensionsY.addEventListener('change', dimensionsChangeFunction); - elDimensionsZ.addEventListener('change', dimensionsChangeFunction); - - elParentID.addEventListener('change', createEmitTextPropertyUpdateFunction('parentID')); - elParentJointIndex.addEventListener('change', createEmitNumberPropertyUpdateFunction('parentJointIndex', 0)); - - var registrationChangeFunction = createEmitVec3PropertyUpdateFunction( - 'registrationPoint', elRegistrationX, elRegistrationY, elRegistrationZ); - elRegistrationX.addEventListener('change', registrationChangeFunction); - elRegistrationY.addEventListener('change', registrationChangeFunction); - elRegistrationZ.addEventListener('change', registrationChangeFunction); - - var rotationChangeFunction = createEmitVec3PropertyUpdateFunction( - 'rotation', elRotationX, elRotationY, elRotationZ); - elRotationX.addEventListener('change', rotationChangeFunction); - elRotationY.addEventListener('change', rotationChangeFunction); - elRotationZ.addEventListener('change', rotationChangeFunction); - - var velocityChangeFunction = createEmitVec3PropertyUpdateFunction( - 'velocity', elLinearVelocityX, elLinearVelocityY, elLinearVelocityZ); - elLinearVelocityX.addEventListener('change', velocityChangeFunction); - elLinearVelocityY.addEventListener('change', velocityChangeFunction); - elLinearVelocityZ.addEventListener('change', velocityChangeFunction); - elLinearDamping.addEventListener('change', createEmitNumberPropertyUpdateFunction('damping')); - - var angularVelocityChangeFunction = createEmitVec3PropertyUpdateFunctionWithMultiplier( - 'angularVelocity', elAngularVelocityX, elAngularVelocityY, elAngularVelocityZ, DEGREES_TO_RADIANS); - elAngularVelocityX.addEventListener('change', angularVelocityChangeFunction); - elAngularVelocityY.addEventListener('change', angularVelocityChangeFunction); - elAngularVelocityZ.addEventListener('change', angularVelocityChangeFunction); - elAngularDamping.addEventListener('change', createEmitNumberPropertyUpdateFunction('angularDamping')); - - elRestitution.addEventListener('change', createEmitNumberPropertyUpdateFunction('restitution')); - elFriction.addEventListener('change', createEmitNumberPropertyUpdateFunction('friction')); - - var gravityChangeFunction = createEmitVec3PropertyUpdateFunction( - 'gravity', elGravityX, elGravityY, elGravityZ); - elGravityX.addEventListener('change', gravityChangeFunction); - elGravityY.addEventListener('change', gravityChangeFunction); - elGravityZ.addEventListener('change', gravityChangeFunction); - - var accelerationChangeFunction = createEmitVec3PropertyUpdateFunction( - 'acceleration', elAccelerationX, elAccelerationY, elAccelerationZ); - elAccelerationX.addEventListener('change', accelerationChangeFunction); - elAccelerationY.addEventListener('change', accelerationChangeFunction); - elAccelerationZ.addEventListener('change', accelerationChangeFunction); - - elDensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('density')); - elCollisionless.addEventListener('change', createEmitCheckedPropertyUpdateFunction('collisionless')); - elDynamic.addEventListener('change', createEmitCheckedPropertyUpdateFunction('dynamic')); - - elCollideDynamic.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideDynamic, 'dynamic'); - }); - - elCollideKinematic.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideKinematic, 'kinematic'); - }); - - elCollideStatic.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideStatic, 'static'); - }); - elCollideMyAvatar.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideMyAvatar, 'myAvatar'); - }); - elCollideOtherAvatar.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideOtherAvatar, 'otherAvatar'); - }); - - elGrabbable.addEventListener('change', function() { - if (elCloneable.checked) { - elGrabbable.checked = false; - } + + // Server Script Status + let elServerScript = elPropertyElements["serverScripts"]; + let elDiv = document.createElement('div'); + elDiv.setAttribute("class", "property"); + let elLabel = document.createElement('label'); + elLabel.setAttribute("for", "property-serverScripts-status"); + elLabel.innerText = "Server Script Status"; + let elServerScriptStatus = document.createElement('span'); + elServerScriptStatus.setAttribute("id", "property-serverScripts-status"); + elDiv.appendChild(elLabel); + elDiv.appendChild(elServerScriptStatus); + elServerScript.parentNode.appendChild(elDiv); + + // Server Script Error + elDiv = document.createElement('div'); + elDiv.setAttribute("class", "property"); + let elServerScriptError = document.createElement('textarea'); + elServerScriptError.setAttribute("id", "property-serverScripts-error"); + elDiv.appendChild(elServerScriptError); + elServerScript.parentNode.appendChild(elDiv); + + let elScript = elPropertyElements["script"]; + elScript.parentNode.setAttribute("class", "property url refresh"); + elServerScript.parentNode.setAttribute("class", "property url refresh"); + + // User Data + let elUserData = elPropertyElements["userData"]; + elDiv = elUserData.parentNode; + let elStaticUserData = document.createElement('div'); + elStaticUserData.setAttribute("id", "property-userData-static"); + let elUserDataEditor = document.createElement('div'); + elUserDataEditor.setAttribute("id", "property-userData-editor"); + let elUserDataSaved = document.createElement('span'); + elUserDataSaved.setAttribute("id", "property-userData-saved"); + elUserDataSaved.innerText = "Saved!"; + elDiv.childNodes[2].appendChild(elUserDataSaved); + elDiv.insertBefore(elStaticUserData, elUserData); + elDiv.insertBefore(elUserDataEditor, elUserData); + + // Material Data + let elMaterialData = elPropertyElements["materialData"]; + elDiv = elMaterialData.parentNode; + let elStaticMaterialData = document.createElement('div'); + elStaticMaterialData.setAttribute("id", "property-materialData-static"); + let elMaterialDataEditor = document.createElement('div'); + elMaterialDataEditor.setAttribute("id", "property-materialData-editor"); + let elMaterialDataSaved = document.createElement('span'); + elMaterialDataSaved.setAttribute("id", "property-materialData-saved"); + elMaterialDataSaved.innerText = "Saved!"; + elDiv.childNodes[2].appendChild(elMaterialDataSaved); + elDiv.insertBefore(elStaticMaterialData, elMaterialData); + elDiv.insertBefore(elMaterialDataEditor, elMaterialData); + + // User Data Fields + let elGrabbable = elPropertyElements["grabbable"]; + let elTriggerable = elPropertyElements["triggerable"]; + let elIgnoreIK = elPropertyElements["ignoreIK"]; + elGrabbable.addEventListener('change', function() { userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, true); }); - - elCloneable.addEventListener('change', createEmitCheckedPropertyUpdateFunction('cloneable')); - elCloneableDynamic.addEventListener('change', createEmitCheckedPropertyUpdateFunction('cloneDynamic')); - elCloneableAvatarEntity.addEventListener('change', createEmitCheckedPropertyUpdateFunction('cloneAvatarEntity')); - elCloneableLifetime.addEventListener('change', createEmitNumberPropertyUpdateFunction('cloneLifetime')); - elCloneableLimit.addEventListener('change', createEmitNumberPropertyUpdateFunction('cloneLimit')); - - elTriggerable.addEventListener('change', function() { + elTriggerable.addEventListener('change', function() { userDataChanger("grabbableKey", "triggerable", elTriggerable, elUserData, false, ['wantsTrigger']); }); elIgnoreIK.addEventListener('change', function() { userDataChanger("grabbableKey", "ignoreIK", elIgnoreIK, elUserData, true); }); - - elCollisionSoundURL.addEventListener('change', createEmitTextPropertyUpdateFunction('collisionSoundURL')); - - elLifetime.addEventListener('change', createEmitNumberPropertyUpdateFunction('lifetime')); - elScriptURL.addEventListener('change', createEmitTextPropertyUpdateFunction('script')); - elScriptTimestamp.addEventListener('change', createEmitNumberPropertyUpdateFunction('scriptTimestamp')); - elServerScripts.addEventListener('change', createEmitTextPropertyUpdateFunction('serverScripts')); - elServerScripts.addEventListener('change', function() { - // invalidate the current status (so that same-same updates can still be observed visually) - elServerScriptStatus.innerText = PENDING_SCRIPT_STATUS; - }); - - elClearUserData.addEventListener("click", function() { - deleteJSONEditor(); - elUserData.value = ""; - showUserDataTextArea(); - showNewJSONEditorButton(); - hideSaveUserDataButton(); - updateProperty('userData', elUserData.value); - }); - - elSaveUserData.addEventListener("click", function() { - saveJSONUserData(true); - }); - - elUserData.addEventListener('change', createEmitTextPropertyUpdateFunction('userData')); - - elNewJSONEditor.addEventListener('click', function() { - deleteJSONEditor(); - createJSONEditor(); - var data = {}; - setEditorJSON(data); - hideUserDataTextArea(); - hideNewJSONEditorButton(); - showSaveUserDataButton(); - }); - - elClearMaterialData.addEventListener("click", function() { - deleteJSONMaterialEditor(); - elMaterialData.value = ""; - showMaterialDataTextArea(); - showNewJSONMaterialEditorButton(); - hideSaveMaterialDataButton(); - updateProperty('materialData', elMaterialData.value); - }); - - elSaveMaterialData.addEventListener("click", function() { - saveJSONMaterialData(true); - }); - - elMaterialData.addEventListener('change', createEmitTextPropertyUpdateFunction('materialData')); - - elNewJSONMaterialEditor.addEventListener('click', function() { - deleteJSONMaterialEditor(); - createJSONMaterialEditor(); - var data = {}; - setMaterialEditorJSON(data); - hideMaterialDataTextArea(); - hideNewJSONMaterialEditorButton(); - showSaveMaterialDataButton(); - }); - - var colorChangeFunction = createEmitColorPropertyUpdateFunction( - 'color', elColorRed, elColorGreen, elColorBlue); - elColorRed.addEventListener('change', colorChangeFunction); - elColorGreen.addEventListener('change', colorChangeFunction); - elColorBlue.addEventListener('change', colorChangeFunction); - colorPickers['#property-color-control2'] = $('#property-color-control2').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-color-control2').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-color-control2'].colpickSetColor({ - "r": elColorRed.value, - "g": elColorGreen.value, - "b": elColorBlue.value}); - }, - onHide: function(colpick) { - $('#property-color-control2').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); - } - }); - - elLightSpotLight.addEventListener('change', createEmitCheckedPropertyUpdateFunction('isSpotlight')); - - var lightColorChangeFunction = createEmitColorPropertyUpdateFunction( - 'color', elLightColorRed, elLightColorGreen, elLightColorBlue); - elLightColorRed.addEventListener('change', lightColorChangeFunction); - elLightColorGreen.addEventListener('change', lightColorChangeFunction); - elLightColorBlue.addEventListener('change', lightColorChangeFunction); - colorPickers['#property-light-color'] = $('#property-light-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-light-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-light-color'].colpickSetColor({ - "r": elLightColorRed.value, - "g": elLightColorGreen.value, - "b": elLightColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-light-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); - } - }); - - elLightIntensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('intensity', 1)); - elLightFalloffRadius.addEventListener('change', createEmitNumberPropertyUpdateFunction('falloffRadius', 1)); - elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent', 2)); - elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff', 2)); - - elShape.addEventListener('change', createEmitTextPropertyUpdateFunction('shape')); - - elCanCastShadow.addEventListener('change', createEmitCheckedPropertyUpdateFunction('canCastShadow')); - - elImageURL.addEventListener('change', createImageURLUpdateFunction('textures')); - - elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); - elWebDPI.addEventListener('change', createEmitNumberPropertyUpdateFunction('dpi', 0)); - - elModelURL.addEventListener('change', createEmitTextPropertyUpdateFunction('modelURL')); - elShapeType.addEventListener('change', createEmitTextPropertyUpdateFunction('shapeType')); - elCompoundShapeURL.addEventListener('change', createEmitTextPropertyUpdateFunction('compoundShapeURL')); - - elModelAnimationURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('animation', 'url')); - elModelAnimationPlaying.addEventListener('change',createEmitGroupCheckedPropertyUpdateFunction('animation', 'running')); - elModelAnimationFPS.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'fps')); - elModelAnimationFrame.addEventListener('change', - createEmitGroupNumberPropertyUpdateFunction('animation', 'currentFrame')); - elModelAnimationFirstFrame.addEventListener('change', - createEmitGroupNumberPropertyUpdateFunction('animation', 'firstFrame')); - elModelAnimationLastFrame.addEventListener('change', - createEmitGroupNumberPropertyUpdateFunction('animation', 'lastFrame')); - elModelAnimationLoop.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'loop')); - elModelAnimationHold.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'hold')); - elModelAnimationAllowTranslation.addEventListener('change', - createEmitGroupCheckedPropertyUpdateFunction('animation', 'allowTranslation')); - - elModelTextures.addEventListener('change', createEmitTextPropertyUpdateFunction('textures')); - - elMaterialURL.addEventListener('change', createEmitTextPropertyUpdateFunction('materialURL')); - //elMaterialMappingMode.addEventListener('change', createEmitTextPropertyUpdateFunction('materialMappingMode')); - elPriority.addEventListener('change', createEmitNumberPropertyUpdateFunction('priority', 0)); - - elParentMaterialNameString.addEventListener('change', function () { updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + this.value); }); + + // Special Property Callbacks + let elParentMaterialNameString = elPropertyElements["materialNameToReplace"]; + let elParentMaterialNameNumber = elPropertyElements["submeshToReplace"]; + let elParentMaterialNameCheckbox = elPropertyElements["selectSubmesh"]; + elParentMaterialNameString.addEventListener('change', function () { updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + this.value); }); elParentMaterialNameNumber.addEventListener('change', function () { updateProperty("parentMaterialName", this.value); }); elParentMaterialNameCheckbox.addEventListener('change', function () { if (this.checked) { @@ -1858,339 +2504,43 @@ function loaded() { showParentMaterialNameBox(false, elParentMaterialNameNumber, elParentMaterialNameString); } }); + + elPropertyElements["image"].addEventListener('change', createImageURLUpdateFunction('textures')); + + // Collapsible sections + let elCollapsible = document.getElementsByClassName("section-header"); - var materialMappingPosChangeFunction = createEmitVec2PropertyUpdateFunction('materialMappingPos', elMaterialMappingPosX, elMaterialMappingPosY); - elMaterialMappingPosX.addEventListener('change', materialMappingPosChangeFunction); - elMaterialMappingPosY.addEventListener('change', materialMappingPosChangeFunction); - var materialMappingScaleChangeFunction = createEmitVec2PropertyUpdateFunction('materialMappingScale', elMaterialMappingScaleX, elMaterialMappingScaleY); - elMaterialMappingScaleX.addEventListener('change', materialMappingScaleChangeFunction); - elMaterialMappingScaleY.addEventListener('change', materialMappingScaleChangeFunction); - elMaterialMappingRot.addEventListener('change', createEmitNumberPropertyUpdateFunction('materialMappingRot', 2)); + let toggleCollapsedEvent = function(event) { + let element = event.target.parentNode.parentNode; + let isCollapsed = element.dataset.collapsed !== "true"; + element.dataset.collapsed = isCollapsed ? "true" : false; + element.setAttribute("collapsed", isCollapsed ? "true" : "false"); + element.getElementsByClassName(".collapse-icon")[0].textContent = isCollapsed ? "L" : "M"; + }; - elTextText.addEventListener('change', createEmitTextPropertyUpdateFunction('text')); - elTextFaceCamera.addEventListener('change', createEmitCheckedPropertyUpdateFunction('faceCamera')); - elTextLineHeight.addEventListener('change', createEmitNumberPropertyUpdateFunction('lineHeight')); - var textTextColorChangeFunction = createEmitColorPropertyUpdateFunction( - 'textColor', elTextTextColorRed, elTextTextColorGreen, elTextTextColorBlue); - elTextTextColorRed.addEventListener('change', textTextColorChangeFunction); - elTextTextColorGreen.addEventListener('change', textTextColorChangeFunction); - elTextTextColorBlue.addEventListener('change', textTextColorChangeFunction); - colorPickers['#property-text-text-color'] = $('#property-text-text-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-text-text-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-text-text-color'].colpickSetColor({ - "r": elTextTextColorRed.value, - "g": elTextTextColorGreen.value, - "b": elTextTextColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-text-text-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - $(el).attr('active', 'false'); - emitColorPropertyUpdate('textColor', rgb.r, rgb.g, rgb.b); - } - }); + for (let collapseIndex = 0, numCollapsibles = elCollapsible.length; collapseIndex < numCollapsibles; ++collapseIndex) { + let curCollapsibleElement = elCollapsible[collapseIndex]; + curCollapsibleElement.getElementsByTagName('span')[0].addEventListener("click", toggleCollapsedEvent, true); + } + + // Textarea scrollbars + let elTextareas = document.getElementsByTagName("TEXTAREA"); - var textBackgroundColorChangeFunction = createEmitColorPropertyUpdateFunction( - 'backgroundColor', elTextBackgroundColorRed, elTextBackgroundColorGreen, elTextBackgroundColorBlue); + let textareaOnChangeEvent = function(event) { + setTextareaScrolling(event.target); + }; - elTextBackgroundColorRed.addEventListener('change', textBackgroundColorChangeFunction); - elTextBackgroundColorGreen.addEventListener('change', textBackgroundColorChangeFunction); - elTextBackgroundColorBlue.addEventListener('change', textBackgroundColorChangeFunction); - colorPickers['#property-text-background-color'] = $('#property-text-background-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-text-background-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-text-background-color'].colpickSetColor({ - "r": elTextBackgroundColorRed.value, - "g": elTextBackgroundColorGreen.value, - "b": elTextBackgroundColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-text-background-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('backgroundColor', rgb.r, rgb.g, rgb.b); - } - }); - - // Key light - var keyLightModeChanged = createZoneComponentModeChangedFunction('keyLightMode', - elZoneKeyLightModeInherit, elZoneKeyLightModeDisabled, elZoneKeyLightModeEnabled); - - elZoneKeyLightModeInherit.addEventListener('change', keyLightModeChanged); - elZoneKeyLightModeDisabled.addEventListener('change', keyLightModeChanged); - elZoneKeyLightModeEnabled.addEventListener('change', keyLightModeChanged); - - colorPickers['#property-zone-key-light-color'] = $('#property-zone-key-light-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-zone-key-light-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-zone-key-light-color'].colpickSetColor({ - "r": elZoneKeyLightColorRed.value, - "g": elZoneKeyLightColorGreen.value, - "b": elZoneKeyLightColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-zone-key-light-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'keyLight'); - } - }); - var zoneKeyLightColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('keyLight', 'color', - elZoneKeyLightColorRed, elZoneKeyLightColorGreen, elZoneKeyLightColorBlue); - - elZoneKeyLightColorRed.addEventListener('change', zoneKeyLightColorChangeFunction); - elZoneKeyLightColorGreen.addEventListener('change', zoneKeyLightColorChangeFunction); - elZoneKeyLightColorBlue.addEventListener('change', zoneKeyLightColorChangeFunction); - elZoneKeyLightIntensity.addEventListener('change', - createEmitGroupNumberPropertyUpdateFunction('keyLight', 'intensity')); - - var zoneKeyLightDirectionChangeFunction = createEmitGroupVec3PropertyUpdateFunction('keyLight', 'direction', - elZoneKeyLightDirectionX, elZoneKeyLightDirectionY); - - elZoneKeyLightDirectionX.addEventListener('change', zoneKeyLightDirectionChangeFunction); - elZoneKeyLightDirectionY.addEventListener('change', zoneKeyLightDirectionChangeFunction); - - elZoneKeyLightCastShadows.addEventListener('change', - createEmitGroupCheckedPropertyUpdateFunction('keyLight', 'castShadows')); - - // Skybox - var skyboxModeChanged = createZoneComponentModeChangedFunction('skyboxMode', - elZoneSkyboxModeInherit, elZoneSkyboxModeDisabled, elZoneSkyboxModeEnabled); - - elZoneSkyboxModeInherit.addEventListener('change', skyboxModeChanged); - elZoneSkyboxModeDisabled.addEventListener('change', skyboxModeChanged); - elZoneSkyboxModeEnabled.addEventListener('change', skyboxModeChanged); - - // Ambient light - elCopySkyboxURLToAmbientURL.addEventListener("click", function () { - document.getElementById("property-zone-key-ambient-url").value = properties.skybox.url; - properties.ambientLight.ambientURL = properties.skybox.url; - updateProperties(properties); - }); - - var ambientLightModeChanged = createZoneComponentModeChangedFunction('ambientLightMode', - elZoneAmbientLightModeInherit, elZoneAmbientLightModeDisabled, elZoneAmbientLightModeEnabled); - - elZoneAmbientLightModeInherit.addEventListener('change', ambientLightModeChanged); - elZoneAmbientLightModeDisabled.addEventListener('change', ambientLightModeChanged); - elZoneAmbientLightModeEnabled.addEventListener('change', ambientLightModeChanged); - - elZoneAmbientLightIntensity.addEventListener('change', - createEmitGroupNumberPropertyUpdateFunction('ambientLight', 'ambientIntensity')); - - elZoneAmbientLightURL.addEventListener('change', - createEmitGroupTextPropertyUpdateFunction('ambientLight', 'ambientURL')); - - // Haze - var hazeModeChanged = createZoneComponentModeChangedFunction('hazeMode', - elZoneHazeModeInherit, elZoneHazeModeDisabled, elZoneHazeModeEnabled); - - elZoneHazeModeInherit.addEventListener('change', hazeModeChanged); - elZoneHazeModeDisabled.addEventListener('change', hazeModeChanged); - elZoneHazeModeEnabled.addEventListener('change', hazeModeChanged); - - elZoneHazeRange.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeRange')); - - colorPickers['#property-zone-haze-color'] = $('#property-zone-haze-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-zone-haze-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-zone-haze-color'].colpickSetColor({ - "r": elZoneHazeColorRed.value, - "g": elZoneHazeColorGreen.value, - "b": elZoneHazeColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-zone-haze-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('hazeColor', rgb.r, rgb.g, rgb.b, 'haze'); - } - }); - var zoneHazeColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('haze', 'hazeColor', - elZoneHazeColorRed, - elZoneHazeColorGreen, - elZoneHazeColorBlue); - - elZoneHazeColorRed.addEventListener('change', zoneHazeColorChangeFunction); - elZoneHazeColorGreen.addEventListener('change', zoneHazeColorChangeFunction); - elZoneHazeColorBlue.addEventListener('change', zoneHazeColorChangeFunction); - - colorPickers['#property-zone-haze-glare-color'] = $('#property-zone-haze-glare-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-zone-haze-glare-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-zone-haze-glare-color'].colpickSetColor({ - "r": elZoneHazeGlareColorRed.value, - "g": elZoneHazeGlareColorGreen.value, - "b": elZoneHazeGlareColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-zone-haze-glare-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('hazeGlareColor', rgb.r, rgb.g, rgb.b, 'haze'); - } - }); - var zoneHazeGlareColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('haze', 'hazeGlareColor', - elZoneHazeGlareColorRed, - elZoneHazeGlareColorGreen, - elZoneHazeGlareColorBlue); - - elZoneHazeGlareColorRed.addEventListener('change', zoneHazeGlareColorChangeFunction); - elZoneHazeGlareColorGreen.addEventListener('change', zoneHazeGlareColorChangeFunction); - elZoneHazeGlareColorBlue.addEventListener('change', zoneHazeGlareColorChangeFunction); - - elZoneHazeEnableGlare.addEventListener('change', - createEmitGroupCheckedPropertyUpdateFunction('haze', 'hazeEnableGlare')); - elZoneHazeGlareAngle.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeGlareAngle')); - - elZoneHazeAltitudeEffect.addEventListener('change', - createEmitGroupCheckedPropertyUpdateFunction('haze', 'hazeAltitudeEffect')); - elZoneHazeCeiling.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeCeiling')); - elZoneHazeBaseRef.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeBaseRef')); - - elZoneHazeBackgroundBlend.addEventListener('change', - createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeBackgroundBlend')); - - // Bloom - var bloomModeChanged = createZoneComponentModeChangedFunction('bloomMode', - elZoneBloomModeInherit, elZoneBloomModeDisabled, elZoneBloomModeEnabled); - - elZoneBloomModeInherit.addEventListener('change', bloomModeChanged); - elZoneBloomModeDisabled.addEventListener('change', bloomModeChanged); - elZoneBloomModeEnabled.addEventListener('change', bloomModeChanged); - - elZoneBloomIntensity.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('bloom', 'bloomIntensity')); - elZoneBloomThreshold.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('bloom', 'bloomThreshold')); - elZoneBloomSize.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('bloom', 'bloomSize')); - - var zoneSkyboxColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('skybox', 'color', - elZoneSkyboxColorRed, elZoneSkyboxColorGreen, elZoneSkyboxColorBlue); - elZoneSkyboxColorRed.addEventListener('change', zoneSkyboxColorChangeFunction); - elZoneSkyboxColorGreen.addEventListener('change', zoneSkyboxColorChangeFunction); - elZoneSkyboxColorBlue.addEventListener('change', zoneSkyboxColorChangeFunction); - colorPickers['#property-zone-skybox-color'] = $('#property-zone-skybox-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-zone-skybox-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-zone-skybox-color'].colpickSetColor({ - "r": elZoneSkyboxColorRed.value, - "g": elZoneSkyboxColorGreen.value, - "b": elZoneSkyboxColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-zone-skybox-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'skybox'); - } - }); - - elZoneSkyboxURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('skybox', 'url')); - - elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed')); - elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed')); - elZoneFilterURL.addEventListener('change', createEmitTextPropertyUpdateFunction('filterURL')); - - var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction( - 'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ); - elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction); - elVoxelVolumeSizeY.addEventListener('change', voxelVolumeSizeChangeFunction); - elVoxelVolumeSizeZ.addEventListener('change', voxelVolumeSizeChangeFunction); - elVoxelSurfaceStyle.addEventListener('change', createEmitTextPropertyUpdateFunction('voxelSurfaceStyle')); - elXTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('xTextureURL')); - elYTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('yTextureURL')); - elZTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('zTextureURL')); - - elMoveSelectionToGrid.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "moveSelectionToGrid" - })); - }); - elMoveAllToGrid.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "moveAllToGrid" - })); - }); - elResetToNaturalDimensions.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "resetToNaturalDimensions" - })); - }); - elRescaleDimensionsButton.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "rescaleDimensions", - percentage: parseFloat(elRescaleDimensionsPct.value) - })); - }); - elReloadScriptsButton.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "reloadClientScripts" - })); - }); - elReloadServerScriptsButton.addEventListener("click", function() { - // invalidate the current status (so that same-same updates can still be observed visually) - elServerScriptStatus.innerText = PENDING_SCRIPT_STATUS; - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "reloadServerScripts" - })); - }); - - document.addEventListener("keydown", function (keyDown) { + for (let textAreaIndex = 0, numTextAreas = elTextareas.length; textAreaIndex < numTextAreas; ++textAreaIndex) { + let curTextAreaElement = elTextareas[textAreaIndex]; + setTextareaScrolling(curTextAreaElement); + curTextAreaElement.addEventListener("input", textareaOnChangeEvent, false); + curTextAreaElement.addEventListener("change", textareaOnChangeEvent, false); + /* FIXME: Detect and update textarea scrolling attribute on resize. Unfortunately textarea doesn't have a resize + event; mouseup is a partial stand-in but doesn't handle resizing if mouse moves outside textarea rectangle. */ + curTextAreaElement.addEventListener("mouseup", textareaOnChangeEvent, false); + } + + document.addEventListener("keydown", function (keyDown) { if (keyDown.keyCode === KEY_P && keyDown.ctrlKey) { if (keyDown.shiftKey) { EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); @@ -2199,155 +2549,28 @@ function loaded() { } } }); + window.onblur = function() { // Fake a change event - var ev = document.createEvent("HTMLEvents"); + let ev = document.createEvent("HTMLEvents"); ev.initEvent("change", true, true); document.activeElement.dispatchEvent(ev); }; - - // For input and textarea elements, select all of the text on focus - var els = document.querySelectorAll("input, textarea"); - for (var i = 0; i < els.length; i++) { + + // For input and textarea elements, select all of the text on focus + let els = document.querySelectorAll("input, textarea"); + for (let i = 0; i < els.length; i++) { els[i].onfocus = function (e) { e.target.select(); }; } + + bindAllNonJSONEditorElements(); - bindAllNonJSONEditorElements(); - }); - - // Collapsible sections - var elCollapsible = document.getElementsByClassName("section-header"); - - var toggleCollapsedEvent = function(event) { - var element = event.target.parentNode.parentNode; - var isCollapsed = element.dataset.collapsed !== "true"; - element.dataset.collapsed = isCollapsed ? "true" : false; - element.setAttribute("collapsed", isCollapsed ? "true" : "false"); - element.getElementsByClassName(".collapse-icon")[0].textContent = isCollapsed ? "L" : "M"; - }; - - for (var collapseIndex = 0, numCollapsibles = elCollapsible.length; collapseIndex < numCollapsibles; ++collapseIndex) { - var curCollapsibleElement = elCollapsible[collapseIndex]; - curCollapsibleElement.getElementsByTagName('span')[0].addEventListener("click", toggleCollapsedEvent, true); - } - - - // Textarea scrollbars - var elTextareas = document.getElementsByTagName("TEXTAREA"); - - var textareaOnChangeEvent = function(event) { - setTextareaScrolling(event.target); - }; - - for (var textAreaIndex = 0, numTextAreas = elTextareas.length; textAreaIndex < numTextAreas; ++textAreaIndex) { - var curTextAreaElement = elTextareas[textAreaIndex]; - setTextareaScrolling(curTextAreaElement); - curTextAreaElement.addEventListener("input", textareaOnChangeEvent, false); - curTextAreaElement.addEventListener("change", textareaOnChangeEvent, false); - /* FIXME: Detect and update textarea scrolling attribute on resize. Unfortunately textarea doesn't have a resize - event; mouseup is a partial stand-in but doesn't handle resizing if mouse moves outside textarea rectangle. */ - curTextAreaElement.addEventListener("mouseup", textareaOnChangeEvent, false); - } - - // Dropdowns - // For each dropdown the following replacement is created in place of the original dropdown... - // Structure created: - //
- //
display textcarat
- //
- //
    - //
  • 0) { - var el = elDropdowns[0]; - el.parentNode.removeChild(el); - elDropdowns = document.getElementsByTagName("select"); - } + showGroupsForType("None"); + resetProperties(); + disableProperties(); + }); augmentSpinButtons(); From 7aba0a9082499ed98ae1359ef7b329566afaa295 Mon Sep 17 00:00:00 2001 From: David Back Date: Tue, 9 Oct 2018 10:17:39 -0700 Subject: [PATCH 054/362] cleanup --- scripts/system/html/css/edit-style.css | 24 +- scripts/system/html/js/entityProperties.js | 2353 ++++++++++---------- 2 files changed, 1187 insertions(+), 1190 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index b7aac02c7c..e700b50839 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -614,6 +614,10 @@ hr { margin-top: 0; } +.checkbox-sub-props { + margin-top: 18px; +} + .property .number { float: left; } @@ -918,6 +922,14 @@ tuple, .blue:focus, .tuple .z:focus, .tuple .roll:focus { outline: none; } +fieldset .checkbox-sub-props { + margin-top: 0; +} + +fieldset .checkbox-sub-props .property:first-child { + margin-top: 0; +} + .column { vertical-align: top; } @@ -1269,7 +1281,7 @@ th#entity-hasScript { #base #property-type { padding: 5px 24px 5px 0; - border-right: 1px solid #808080; + border-right: 1px solid #808080; width: auto; display: inline-block; } @@ -1277,13 +1289,13 @@ th#entity-hasScript { #base #div-locked { position: absolute; top: 0px; - right: 140px; + right: 140px; } #base #div-visible { position: absolute; top: 20px; - right: 20px; + right: 20px; } #base .checkbox { @@ -1361,9 +1373,9 @@ input#property-scale-button-reset { } #property-serverScripts-status { - position: relative; - top: -3px; - right: -20px; + position: relative; + top: -3px; + right: -20px; } #properties-list #collision-info > fieldset:first-of-type { diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 03dad80be6..d8ffc2ee25 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1,5 +1,4 @@ // entityProperties.js -// entityProperties.js // // Created by Ryan Huffman on 13 Nov 2014 // Copyright 2014 High Fidelity, Inc. @@ -9,7 +8,7 @@ /* global alert, augmentSpinButtons, clearTimeout, console, document, Element, EventBridge, HifiEntityUI, JSONEditor, openEventBridge, setTimeout, window, _ $ */ - + const ICON_FOR_TYPE = { Box: "V", Sphere: "n", @@ -25,7 +24,7 @@ const ICON_FOR_TYPE = { Multiple: "", PolyLine: "", Material: "" -}; +}; const PI = 3.14159265358979; const DEGREES_TO_RADIANS = PI / 180.0; @@ -37,420 +36,406 @@ const GROUPS = [ { id: "base", properties: [ - { + { label: NO_SELECTION, type: "icon", - icons: ICON_FOR_TYPE, + icons: ICON_FOR_TYPE, propertyName: "type", }, - { + { label: "Name", type: "string", propertyName: "name", }, - { + { label: "ID", type: "string", propertyName: "id", readOnly: true, }, - { + { label: "Parent", type: "string", propertyName: "parentID", }, - { + { label: "Locked", - glyph: "", + glyph: "", type: "bool", propertyName: "locked", }, - { + { label: "Visible", - glyph: "", + glyph: "", type: "bool", propertyName: "visible", }, ] }, - { + { id: "shape", - addToGroup: "base", + addToGroup: "base", properties: [ { label: "Shape", type: "dropdown", - options: { Cube: "Box", Sphere: "Sphere", Tetrahedron: "Tetrahedron", Octahedron: "Octahedron", Icosahedron: "Icosahedron", Dodecahedron: "Dodecahedron", Hexagon: "Hexagon", Triangle: "Triangle", Octagon: "Octagon", Cylinder: "Cylinder", Cone: "Cone", Circle: "Circle", Quad: "Quad" }, + options: { Cube: "Box", Sphere: "Sphere", Tetrahedron: "Tetrahedron", Octahedron: "Octahedron", Icosahedron: "Icosahedron", Dodecahedron: "Dodecahedron", Hexagon: "Hexagon", Triangle: "Triangle", Octagon: "Octagon", Cylinder: "Cylinder", Cone: "Cone", Circle: "Circle", Quad: "Quad" }, propertyName: "shape", }, - { + { label: "Color", type: "color", propertyName: "color", }, - /* - { - label: "Material", - type: "string", - propertyName: "", - }, - */ ] }, - { + { id: "text", - addToGroup: "base", + addToGroup: "base", properties: [ { label: "Text", type: "string", propertyName: "text", }, - { + { label: "Text Color", type: "color", propertyName: "textColor", }, - { + { label: "Background Color", type: "color", propertyName: "backgroundColor", }, - /* - { - label: "Transparent Background", - type: "bool", - propertyName: "" - }, - */ - { - label: "Line Height", + { + label: "Line Height", type: "number", - min: 0, - step: 0.005, - fixedDecimals: 4, - unit: "m", + min: 0, + step: 0.005, + fixedDecimals: 4, + unit: "m", propertyName: "lineHeight" }, - { + { label: "Face Camera", type: "bool", propertyName: "faceCamera" }, ] }, - { + { id: "zone", - addToGroup: "base", + addToGroup: "base", properties: [ { label: "Flying Allowed", type: "bool", propertyName: "flyingAllowed", }, - { + { label: "Ghosting Allowed", type: "bool", propertyName: "ghostingAllowed", }, - { + { label: "Filter", type: "string", propertyName: "filterURL", }, - { + { label: "Key Light", type: "dropdown", - options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, propertyName: "keyLightMode", - + }, - { + { label: "Key Light Color", type: "color", propertyName: "keyLight.color", - showPropertyRule: { "keyLightMode": "enabled" }, + showPropertyRule: { "keyLightMode": "enabled" }, }, - { + { label: "Light Intensity", type: "number", - min: 0, - max: 10, - step: 0.1, - fixedDecimals: 2, + min: 0, + max: 10, + step: 0.1, + fixedDecimals: 2, propertyName: "keyLight.intensity", - showPropertyRule: { "keyLightMode": "enabled" }, + showPropertyRule: { "keyLightMode": "enabled" }, }, - { + { label: "Light Altitude", type: "number", - fixedDecimals: 2, - unit: "deg", + fixedDecimals: 2, + unit: "deg", propertyName: "keyLight.direction.y", - showPropertyRule: { "keyLightMode": "enabled" }, + showPropertyRule: { "keyLightMode": "enabled" }, }, - { + { label: "Light Azimuth", type: "number", - fixedDecimals: 2, - unit: "deg", + fixedDecimals: 2, + unit: "deg", propertyName: "keyLight.direction.x", - showPropertyRule: { "keyLightMode": "enabled" }, + showPropertyRule: { "keyLightMode": "enabled" }, }, - { + { label: "Cast Shadows", type: "bool", propertyName: "keyLight.castShadows", - showPropertyRule: { "keyLightMode": "enabled" }, + showPropertyRule: { "keyLightMode": "enabled" }, }, - { + { label: "Skybox", type: "dropdown", - options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, propertyName: "skyboxMode", }, - { + { label: "Skybox Color", type: "color", propertyName: "skybox.color", - showPropertyRule: { "skyboxMode": "enabled" }, + showPropertyRule: { "skyboxMode": "enabled" }, }, - { + { label: "Skybox URL", type: "string", propertyName: "skybox.url", - showPropertyRule: { "skyboxMode": "enabled" }, + showPropertyRule: { "skyboxMode": "enabled" }, }, - { + { type: "buttons", - buttons: [ { id: "copy", label: "Copy URL To Ambient", className: "black", onClick: copySkyboxURLToAmbientURL } ], + buttons: [ { id: "copy", label: "Copy URL To Ambient", className: "black", onClick: copySkyboxURLToAmbientURL } ], propertyName: "copyURLToAmbient", - showPropertyRule: { "skyboxMode": "enabled" }, + showPropertyRule: { "skyboxMode": "enabled" }, }, - { + { label: "Ambient Light", type: "dropdown", - options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, propertyName: "ambientLightMode", }, - { + { label: "Ambient Intensity", type: "number", - min: 0, - max: 10, - step: 0.1, - fixedDecimals: 2, + min: 0, + max: 10, + step: 0.1, + fixedDecimals: 2, propertyName: "ambientLight.ambientIntensity", - showPropertyRule: { "ambientLightMode": "enabled" }, + showPropertyRule: { "ambientLightMode": "enabled" }, }, - { + { label: "Ambient URL", type: "string", propertyName: "ambientLight.ambientURL", - showPropertyRule: { "ambientLightMode": "enabled" }, + showPropertyRule: { "ambientLightMode": "enabled" }, }, - { + { label: "Haze", type: "dropdown", - options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, propertyName: "hazeMode", }, - { + { label: "Range", type: "number", - min: 5, - max: 10000, - step: 5, - fixedDecimals: 0, - unit: "m", + min: 5, + max: 10000, + step: 5, + fixedDecimals: 0, + unit: "m", propertyName: "haze.hazeRange", - showPropertyRule: { "hazeMode": "enabled" }, + showPropertyRule: { "hazeMode": "enabled" }, }, - { + { label: "Use Altitude", type: "bool", propertyName: "haze.hazeAltitudeEffect", - showPropertyRule: { "hazeMode": "enabled" }, + showPropertyRule: { "hazeMode": "enabled" }, }, - { + { label: "Base", type: "number", - min: -1000, - max: 1000, - step: 10, - fixedDecimals: 0, - unit: "m", + min: -1000, + max: 1000, + step: 10, + fixedDecimals: 0, + unit: "m", propertyName: "haze.hazeBaseRef", - showPropertyRule: { "hazeMode": "enabled" }, + showPropertyRule: { "hazeMode": "enabled" }, }, - { + { label: "Ceiling", type: "number", - min: -1000, - max: 5000, - step: 10, - fixedDecimals: 0, - unit: "m", + min: -1000, + max: 5000, + step: 10, + fixedDecimals: 0, + unit: "m", propertyName: "haze.hazeCeiling", - showPropertyRule: { "hazeMode": "enabled" }, + showPropertyRule: { "hazeMode": "enabled" }, }, - { + { label: "Haze Color", type: "color", propertyName: "haze.hazeColor", - showPropertyRule: { "hazeMode": "enabled" }, + showPropertyRule: { "hazeMode": "enabled" }, }, - { + { label: "Background Blend", type: "number", - min: 0.0, - max: 1.0, - step: 0.01, - fixedDecimals: 2, + min: 0.0, + max: 1.0, + step: 0.01, + fixedDecimals: 2, propertyName: "haze.hazeBackgroundBlend", - showPropertyRule: { "hazeMode": "enabled" }, + showPropertyRule: { "hazeMode": "enabled" }, }, - { + { label: "Enable Glare", type: "bool", propertyName: "haze.hazeEnableGlare", - showPropertyRule: { "hazeMode": "enabled" }, + showPropertyRule: { "hazeMode": "enabled" }, }, - { + { label: "Glare Color", type: "color", propertyName: "haze.hazeGlareColor", - showPropertyRule: { "hazeMode": "enabled" }, + showPropertyRule: { "hazeMode": "enabled" }, }, - { + { label: "Glare Angle", type: "number", - min: 0, - max: 180, - step: 1, - fixedDecimals: 0, + min: 0, + max: 180, + step: 1, + fixedDecimals: 0, propertyName: "haze.hazeGlareAngle", - showPropertyRule: { "hazeMode": "enabled" }, + showPropertyRule: { "hazeMode": "enabled" }, }, - { + { label: "Bloom", type: "dropdown", - options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, propertyName: "bloomMode", }, - { + { label: "Bloom Intensity", type: "number", - min: 0, - max: 1, - step: 0.01, - fixedDecimals: 2, + min: 0, + max: 1, + step: 0.01, + fixedDecimals: 2, propertyName: "bloom.bloomIntensity", - showPropertyRule: { "bloomMode": "enabled" }, + showPropertyRule: { "bloomMode": "enabled" }, }, - { + { label: "Bloom Threshold", type: "number", - min: 0, - min: 1, - step: 0.01, - fixedDecimals: 2, + min: 0, + min: 1, + step: 0.01, + fixedDecimals: 2, propertyName: "bloom.bloomThreshold", - showPropertyRule: { "bloomMode": "enabled" }, + showPropertyRule: { "bloomMode": "enabled" }, }, - { + { label: "Bloom Size", type: "number", - min: 0, - min: 2, - step: 0.01, - fixedDecimals: 2, + min: 0, + min: 2, + step: 0.01, + fixedDecimals: 2, propertyName: "bloom.bloomSize", - showPropertyRule: { "bloomMode": "enabled" }, + showPropertyRule: { "bloomMode": "enabled" }, }, ] }, - { + { id: "model", - addToGroup: "base", + addToGroup: "base", properties: [ { label: "Model", type: "string", propertyName: "modelURL", }, - { + { label: "Collision Shape", type: "dropdown", - options: { "none": "No Collision", "box": "Box", "sphere": "Sphere", "compound": "Compound" , "simple-hull": "Basic - Whole model", "simple-compound": "Good - Sub-meshes" , "static-mesh": "Exact - All polygons (non-dynamic only)" }, + options: { "none": "No Collision", "box": "Box", "sphere": "Sphere", "compound": "Compound" , "simple-hull": "Basic - Whole model", "simple-compound": "Good - Sub-meshes" , "static-mesh": "Exact - All polygons (non-dynamic only)" }, propertyName: "shapeType", }, - { + { label: "Compound Shape", type: "string", propertyName: "compoundShapeURL", }, - { + { label: "Animation", type: "string", propertyName: "animation.url", }, - { + { label: "Play Automatically", type: "bool", propertyName: "animation.running", }, - { + { label: "Allow Transition", type: "bool", propertyName: "animation.allowTranslation", }, - { + { label: "Loop", type: "bool", propertyName: "animation.loop", }, - { + { label: "Hold", type: "bool", propertyName: "animation.hold", }, - { + { label: "Animation Frame", type: "number", propertyName: "animation.currentFrame", }, - { + { label: "First Frame", type: "number", propertyName: "animation.firstFrame", }, - { + { label: "Last Frame", type: "number", propertyName: "animation.lastFrame", }, - { + { label: "Animation FPS", type: "number", propertyName: "animation.fps", }, - { + { label: "Texture", type: "textarea", propertyName: "textures", }, - { + { label: "Original Texture", type: "textarea", propertyName: "originalTextures", - readOnly: true, + readOnly: true, }, ] }, - { + { id: "image", - addToGroup: "base", + addToGroup: "base", properties: [ { label: "Image", @@ -459,135 +444,135 @@ const GROUPS = [ }, ] }, - { + { id: "web", - addToGroup: "base", + addToGroup: "base", properties: [ { label: "Source", type: "string", propertyName: "sourceUrl", }, - { + { label: "Source Resolution", type: "number", propertyName: "dpi", }, ] }, - { + { id: "light", - addToGroup: "base", + addToGroup: "base", properties: [ { label: "Light Color", type: "color", propertyName: "lightColor", // this actually shares "color" property with shape Color - // but separating naming here to separate property fields + // but separating naming here to separate property fields }, - { + { label: "Intensity", type: "number", - min: 0, - step: 0.1, - fixedDecimals: 1, + min: 0, + step: 0.1, + fixedDecimals: 1, propertyName: "intensity", }, - { + { label: "Fall-Off Radius", type: "number", - min: 0, - step: 0.1, - fixedDecimals: 1, - unit: "m", + min: 0, + step: 0.1, + fixedDecimals: 1, + unit: "m", propertyName: "falloffRadius", }, - { + { label: "Spotlight", type: "bool", propertyName: "isSpotlight", }, - { + { label: "Spotlight Exponent", type: "number", - step: 0.01, - fixedDecimals: 2, + step: 0.01, + fixedDecimals: 2, propertyName: "exponent", }, - { + { label: "Spotlight Cut-Off", type: "number", - step: 0.01, - fixedDecimals: 2, + step: 0.01, + fixedDecimals: 2, propertyName: "cutoff", }, ] }, - { + { id: "material", - addToGroup: "base", + addToGroup: "base", properties: [ { label: "Material URL", type: "string", propertyName: "materialURL", }, - { + { label: "Material Data", type: "textarea", - buttons: [ { id: "clear", label: "Clear Material Data", className: "red", onClick: clearMaterialData }, - { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONMaterialEditor }, - { id: "save", label: "Save Material Data", className: "black", onClick: saveMaterialData } ], + buttons: [ { id: "clear", label: "Clear Material Data", className: "red", onClick: clearMaterialData }, + { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONMaterialEditor }, + { id: "save", label: "Save Material Data", className: "black", onClick: saveMaterialData } ], propertyName: "materialData", }, - { + { label: "Submesh to Replace", type: "number", - min: 0, - step: 1, + min: 0, + step: 1, propertyName: "submeshToReplace", }, - { + { label: "Material Name to Replace", type: "string", propertyName: "materialNameToReplace", }, - { + { label: "Select Submesh", type: "bool", propertyName: "selectSubmesh", }, - { + { label: "Priority", type: "number", - min: 0, + min: 0, propertyName: "priority", }, - { + { label: "Material Position", type: "vec2", - min: 0, - min: 1, - step: 0.1, - vec2Type: "xy", - subLabels: [ "x", "y" ], + min: 0, + min: 1, + step: 0.1, + vec2Type: "xy", + subLabels: [ "x", "y" ], propertyName: "materialMappingPos", }, - { + { label: "Material Scale", type: "vec2", - min: 0, - step: 0.1, - vec2Type: "wh", - subLabels: [ "width", "height" ], + min: 0, + step: 0.1, + vec2Type: "wh", + subLabels: [ "width", "height" ], propertyName: "materialMappingScale", }, - { + { label: "Material Rotation", type: "number", - step: 0.1, - fixedDecimals: 2, - unit: "deg", + step: 0.1, + fixedDecimals: 2, + unit: "deg", propertyName: "materialMappingRot", }, ] @@ -599,292 +584,292 @@ const GROUPS = [ { label: "Position", type: "vec3", - vec3Type: "xyz", - subLabels: [ "x", "y", "z" ], - unit: "m", + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + unit: "m", propertyName: "position", }, - { + { label: "Rotation", type: "vec3", - step: 0.1, - vec3Type: "pyr", - subLabels: [ "pitch", "yaw", "roll" ], - unit: "deg", + step: 0.1, + vec3Type: "pyr", + subLabels: [ "pitch", "yaw", "roll" ], + unit: "deg", propertyName: "rotation", }, - { + { label: "Dimension", type: "vec3", - step: 0.1, - vec3Type: "xyz", - subLabels: [ "x", "y", "z" ], - unit: "m", + step: 0.1, + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + unit: "m", propertyName: "dimensions", }, - { + { label: "Scale", type: "number", - defaultValue: 100, - unit: "%", - buttons: [ { id: "rescale", label: "Rescale", className: "blue", onClick: rescaleDimensions }, - { id: "reset", label: "Reset Dimensions", className: "red", onClick: resetToNaturalDimensions } ], + defaultValue: 100, + unit: "%", + buttons: [ { id: "rescale", label: "Rescale", className: "blue", onClick: rescaleDimensions }, + { id: "reset", label: "Reset Dimensions", className: "red", onClick: resetToNaturalDimensions } ], propertyName: "scale", }, - { + { label: "Pivot", type: "vec3", - step: 0.1, - vec3Type: "xyz", - subLabels: [ "x", "y", "z" ], - unit: "(ratio of dimension)", + step: 0.1, + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + unit: "(ratio of dimension)", propertyName: "registrationPoint", }, - { + { label: "Align", type: "buttons", - buttons: [ { id: "selection", label: "Selection to Grid", className: "black", onClick: moveSelectionToGrid }, - { id: "all", label: "All to Grid", className: "black", onClick: moveAllToGrid } ], + buttons: [ { id: "selection", label: "Selection to Grid", className: "black", onClick: moveSelectionToGrid }, + { id: "all", label: "All to Grid", className: "black", onClick: moveAllToGrid } ], propertyName: "alignToGrid", }, ] }, - { + { id: "collision", label: "COLLISION", - twoColumn: true, + twoColumn: true, properties: [ { label: "Collides", type: "bool", propertyName: "collisionless", - inverse: true, - column: -1, // before two columns div + inverse: true, + column: -1, // before two columns div }, - { + { label: "Dynamic", type: "bool", propertyName: "dynamic", - column: -1, // before two columns div + column: -1, // before two columns div }, - { + { label: "Collides With", type: "sub-header", - propertyName: "collidesWithHeader", - showPropertyRule: { "collisionless": "false" }, - column: 1, + propertyName: "collidesWithHeader", + showPropertyRule: { "collisionless": "false" }, + column: 1, }, - { + { label: "", type: "sub-header", - propertyName: "collidesWithHeaderHelper", - showPropertyRule: { "collisionless": "false" }, - column: 2, + propertyName: "collidesWithHeaderHelper", + showPropertyRule: { "collisionless": "false" }, + column: 2, }, - { + { label: "Static Entities", type: "bool", propertyName: "static", - subPropertyOf: "collidesWith", - showPropertyRule: { "collisionless": "false" }, - column: 1, + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + column: 1, }, - { + { label: "Dynamic Entities", type: "bool", propertyName: "dynamic", - subPropertyOf: "collidesWith", - showPropertyRule: { "collisionless": "false" }, - column: 2, + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + column: 2, }, - { + { label: "Kinematic Entities", type: "bool", propertyName: "kinematic", - subPropertyOf: "collidesWith", - showPropertyRule: { "collisionless": "false" }, - column: 1, + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + column: 1, }, - { + { label: "My Avatar", type: "bool", propertyName: "myAvatar", - subPropertyOf: "collidesWith", - showPropertyRule: { "collisionless": "false" }, - column: 2, + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + column: 2, }, - { + { label: "Other Avatars", type: "bool", propertyName: "otherAvatar", - subPropertyOf: "collidesWith", - showPropertyRule: { "collisionless": "false" }, - column: 1, + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + column: 1, }, - { + { label: "Collision sound URL", type: "string", propertyName: "collisionSoundURL", - showPropertyRule: { "collisionless": "false" }, + showPropertyRule: { "collisionless": "false" }, }, ] }, - { + { id: "behavior", label: "BEHAVIOR", - twoColumn: true, + twoColumn: true, properties: [ { label: "Grabbable", type: "bool", propertyName: "grabbable", - column: 1, + column: 1, }, - { + { label: "Triggerable", type: "bool", propertyName: "triggerable", - column: 2, + column: 2, }, - { + { label: "Cloneable", type: "bool", propertyName: "cloneable", - column: 1, + column: 1, }, - { + { label: "Ignore inverse kinematics", type: "bool", propertyName: "ignoreIK", - column: 2, + column: 2, }, - { + { label: "Clone Lifetime", type: "number", - unit: "s", + unit: "s", propertyName: "cloneLifetime", - showPropertyRule: { "cloneable": "true" }, - column: 1, + showPropertyRule: { "cloneable": "true" }, + column: 1, }, - { + { label: "Clone Limit", type: "number", propertyName: "cloneLimit", - showPropertyRule: { "cloneable": "true" }, - column: 1, + showPropertyRule: { "cloneable": "true" }, + column: 1, }, - { + { label: "Clone Dynamic", type: "bool", propertyName: "cloneDynamic", - showPropertyRule: { "cloneable": "true" }, - column: 1, + showPropertyRule: { "cloneable": "true" }, + column: 1, }, - { + { label: "Clone Avatar Entity", type: "bool", propertyName: "cloneAvatarEntity", - showPropertyRule: { "cloneable": "true" }, - column: 1, + showPropertyRule: { "cloneable": "true" }, + column: 1, }, - { + { label: "Can cast shadow", type: "bool", propertyName: "castShadow", }, - { + { label: "Script", type: "string", - buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadScripts } ], + buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadScripts } ], propertyName: "script", }, - { + { label: "Server Script", type: "string", - buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadServerScripts } ], + buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadServerScripts } ], propertyName: "serverScripts", }, - { + { label: "Lifetime", type: "number", - unit: "s", + unit: "s", propertyName: "lifetime", }, - { + { label: "User Data", type: "textarea", - buttons: [ { id: "clear", label: "Clear User Data", className: "red", onClick: clearUserData }, - { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONEditor }, - { id: "save", label: "Save User Data", className: "black", onClick: saveUserData } ], + buttons: [ { id: "clear", label: "Clear User Data", className: "red", onClick: clearUserData }, + { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONEditor }, + { id: "save", label: "Save User Data", className: "black", onClick: saveUserData } ], propertyName: "userData", }, - ] + ] }, - { + { id: "physics", label: "PHYSICS", properties: [ { label: "Linear Velocity", type: "vec3", - vec3Type: "xyz", - subLabels: [ "x", "y", "z" ], - unit: "m/s", + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + unit: "m/s", propertyName: "velocity", }, - { + { label: "Linear Damping", type: "number", - fixedDecimals: 2, + fixedDecimals: 2, propertyName: "damping", }, - { + { label: "Angular Velocity", type: "vec3", - multiplier: DEGREES_TO_RADIANS, - vec3Type: "pyr", - subLabels: [ "pitch", "yaw", "roll" ], - unit: "deg/s", + multiplier: DEGREES_TO_RADIANS, + vec3Type: "pyr", + subLabels: [ "pitch", "yaw", "roll" ], + unit: "deg/s", propertyName: "angularVelocity", }, - { + { label: "Angular Damping", type: "number", - fixedDecimals: 4, + fixedDecimals: 4, propertyName: "angularDamping", }, - { + { label: "Bounciness", type: "number", - fixedDecimals: 4, + fixedDecimals: 4, propertyName: "restitution", }, - { + { label: "Friction", type: "number", - fixedDecimals: 4, + fixedDecimals: 4, propertyName: "friction", }, - { + { label: "Density", type: "number", - fixedDecimals: 4, + fixedDecimals: 4, propertyName: "density", }, - { + { label: "Gravity", type: "vec3", - vec3Type: "xyz", - subLabels: [ "x", "y", "z" ], - unit: "m/s2", + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + unit: "m/s2", propertyName: "gravity", }, - { + { label: "Acceleration", type: "vec3", - vec3Type: "xyz", - subLabels: [ "x", "y", "z" ], - unit: "m/s2", + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + unit: "m/s2", propertyName: "acceleration", }, ] @@ -980,21 +965,21 @@ function showElements(els, show) { function updateProperty(propertyName, propertyValue) { var properties = {}; - let splitPropertyName = propertyName.split('.'); - if (splitPropertyName.length > 1) { - let propertyGroupName = splitPropertyName[0]; - let subPropertyName = splitPropertyName[1]; - properties[propertyGroupName] = {}; - if (splitPropertyName.length === 3) { - let subSubPropertyName = splitPropertyName[2]; - properties[propertyGroupName][subPropertyName] = {}; - properties[propertyGroupName][subPropertyName][subSubPropertyName] = propertyValue; - } else { - properties[propertyGroupName][subPropertyName] = propertyValue; - } - } else { - properties[propertyName] = propertyValue; - } + let splitPropertyName = propertyName.split('.'); + if (splitPropertyName.length > 1) { + let propertyGroupName = splitPropertyName[0]; + let subPropertyName = splitPropertyName[1]; + properties[propertyGroupName] = {}; + if (splitPropertyName.length === 3) { + let subSubPropertyName = splitPropertyName[2]; + properties[propertyGroupName][subPropertyName] = {}; + properties[propertyGroupName][subPropertyName][subSubPropertyName] = propertyValue; + } else { + properties[propertyGroupName][subPropertyName] = propertyValue; + } + } else { + properties[propertyName] = propertyValue; + } updateProperties(properties); } @@ -1117,27 +1102,27 @@ function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElemen } function clearUserData() { - let elUserData = elPropertyElements["userData"]; - deleteJSONEditor(); - elUserData.value = ""; - showUserDataTextArea(); - showNewJSONEditorButton(); - hideSaveUserDataButton(); - updateProperty('userData', elUserData.value); + let elUserData = elPropertyElements["userData"]; + deleteJSONEditor(); + elUserData.value = ""; + showUserDataTextArea(); + showNewJSONEditorButton(); + hideSaveUserDataButton(); + updateProperty('userData', elUserData.value); } function newJSONEditor() { - deleteJSONEditor(); - createJSONEditor(); - var data = {}; - setEditorJSON(data); - hideUserDataTextArea(); - hideNewJSONEditorButton(); - showSaveUserDataButton(); + deleteJSONEditor(); + createJSONEditor(); + var data = {}; + setEditorJSON(data); + hideUserDataTextArea(); + hideNewJSONEditorButton(); + showSaveUserDataButton(); } function saveUserData() { - saveJSONUserData(true); + saveJSONUserData(true); } function setUserDataFromEditor(noUpdate) { @@ -1287,7 +1272,7 @@ function hideUserDataTextArea() { } function hideUserDataSaved() { - $('#property-userData-saved').hide(); + $('#property-userData-saved').hide(); } function showStaticUserData() { @@ -1335,27 +1320,27 @@ function saveJSONUserData(noUpdate) { } function clearMaterialData() { - let elMaterialData = elPropertyElements["materialData"]; - deleteJSONMaterialEditor(); - elMaterialData.value = ""; - showMaterialDataTextArea(); - showNewJSONMaterialEditorButton(); - hideSaveMaterialDataButton(); - updateProperty('materialData', elMaterialData.value); + let elMaterialData = elPropertyElements["materialData"]; + deleteJSONMaterialEditor(); + elMaterialData.value = ""; + showMaterialDataTextArea(); + showNewJSONMaterialEditorButton(); + hideSaveMaterialDataButton(); + updateProperty('materialData', elMaterialData.value); } function newJSONMaterialEditor() { - deleteJSONMaterialEditor(); - createJSONMaterialEditor(); - var data = {}; - setMaterialEditorJSON(data); - hideMaterialDataTextArea(); - hideNewJSONMaterialEditorButton(); - showSaveMaterialDataButton(); + deleteJSONMaterialEditor(); + createJSONMaterialEditor(); + var data = {}; + setMaterialEditorJSON(data); + hideMaterialDataTextArea(); + hideNewJSONMaterialEditorButton(); + showSaveMaterialDataButton(); } function saveMaterialData() { - saveJSONMaterialData(true); + saveJSONMaterialData(true); } function setMaterialDataFromEditor(noUpdate) { @@ -1424,7 +1409,7 @@ function hideSaveMaterialDataButton() { } function disableSaveMaterialDataButton() { - $('#property-materialData-button-save').attr('disabled', true); + $('#property-materialData-button-save').attr('disabled', true); } function showNewJSONMaterialEditorButton() { @@ -1444,7 +1429,7 @@ function hideMaterialDataTextArea() { } function hideMaterialDataSaved() { - $('#property-materialData-saved').hide(); + $('#property-materialData-saved').hide(); } function showStaticMaterialData() { @@ -1525,8 +1510,8 @@ function unbindAllInputs() { } function setTextareaScrolling(element) { - var isScrolling = element.scrollHeight > element.offsetHeight; - element.setAttribute("scrolling", isScrolling ? "true" : "false"); + var isScrolling = element.scrollHeight > element.offsetHeight; + element.setAttribute("scrolling", isScrolling ? "true" : "false"); } function showParentMaterialNameBox(number, elNumber, elString) { @@ -1542,557 +1527,557 @@ function showParentMaterialNameBox(number, elNumber, elString) { } function rescaleDimensions() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "rescaleDimensions", - percentage: parseFloat(document.getElementById("property-scale").value) - })); + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "rescaleDimensions", + percentage: parseFloat(document.getElementById("property-scale").value) + })); } function moveSelectionToGrid() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "moveSelectionToGrid" - })); + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveSelectionToGrid" + })); } function moveAllToGrid() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "moveAllToGrid" - })); + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveAllToGrid" + })); } function resetToNaturalDimensions() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "resetToNaturalDimensions" - })); + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "resetToNaturalDimensions" + })); } function reloadScripts() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "reloadClientScripts" - })); + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "reloadClientScripts" + })); } function reloadServerScripts() { - // invalidate the current status (so that same-same updates can still be observed visually) - document.getElementById("property-serverScripts-status").innerText = PENDING_SCRIPT_STATUS; - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "reloadServerScripts" - })); + // invalidate the current status (so that same-same updates can still be observed visually) + document.getElementById("property-serverScripts-status").innerText = PENDING_SCRIPT_STATUS; + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "reloadServerScripts" + })); } function copySkyboxURLToAmbientURL() { - let skyboxURL = elPropertyElements["skybox.url"].value; - elPropertyElements["ambientLight.ambientURL"].value = skyboxURL; - updateProperty("ambientLight.ambientURL", skyboxURL); + let skyboxURL = elPropertyElements["skybox.url"].value; + elPropertyElements["ambientLight.ambientURL"].value = skyboxURL; + updateProperty("ambientLight.ambientURL", skyboxURL); } function createTupleNumberInput(elTuple, propertyID, subLabel, min, max, step) { - let elementPropertyID = propertyID + "-" + subLabel.toLowerCase(); - let elDiv = document.createElement('div'); - let elLabel = document.createElement('label'); - elLabel.innerText = subLabel[0].toUpperCase() + subLabel.slice(1) + ":"; - elLabel.setAttribute("for", elementPropertyID); - let elInput = document.createElement('input'); - elInput.setAttribute("id", elementPropertyID); - elInput.setAttribute("type", "number"); - elInput.setAttribute("class", subLabel); - if (min !== undefined) { - elInput.setAttribute("min", min); - } - if (max !== undefined) { - elInput.setAttribute("max", max); - } - if (step !== undefined) { - elInput.setAttribute("step", step); - } - elDiv.appendChild(elInput); - elDiv.appendChild(elLabel); - elTuple.appendChild(elDiv); - return elInput; + let elementPropertyID = propertyID + "-" + subLabel.toLowerCase(); + let elDiv = document.createElement('div'); + let elLabel = document.createElement('label'); + elLabel.innerText = subLabel[0].toUpperCase() + subLabel.slice(1) + ":"; + elLabel.setAttribute("for", elementPropertyID); + let elInput = document.createElement('input'); + elInput.setAttribute("id", elementPropertyID); + elInput.setAttribute("type", "number"); + elInput.setAttribute("class", subLabel); + if (min !== undefined) { + elInput.setAttribute("min", min); + } + if (max !== undefined) { + elInput.setAttribute("max", max); + } + if (step !== undefined) { + elInput.setAttribute("step", step); + } + elDiv.appendChild(elInput); + elDiv.appendChild(elLabel); + elTuple.appendChild(elDiv); + return elInput; } function addUnit(unit, elLabel) { - if (unit !== undefined) { - let elSpan = document.createElement('span'); - elSpan.setAttribute("class", "unit"); - elSpan.innerHTML = unit; - elLabel.appendChild(elSpan); - } + if (unit !== undefined) { + let elSpan = document.createElement('span'); + elSpan.setAttribute("class", "unit"); + elSpan.innerHTML = unit; + elLabel.appendChild(elSpan); + } } function addButtons(elProperty, propertyID, buttons, newRow) { - let elDiv = document.createElement('div'); - elDiv.setAttribute("class", "row"); - - buttons.forEach(function(button) { - let elButton = document.createElement('input'); - elButton.setAttribute("type", "button"); - elButton.setAttribute("class", button.className); - elButton.setAttribute("id", propertyID + "-button-" + button.id); - elButton.setAttribute("value", button.label); - elButton.addEventListener("click", button.onClick); - if (newRow) { - elDiv.appendChild(elButton); - } else { - elProperty.appendChild(elButton); - } - }); - - if (newRow) { - elProperty.appendChild(document.createElement('br')); - elProperty.appendChild(elDiv); - } + let elDiv = document.createElement('div'); + elDiv.setAttribute("class", "row"); + + buttons.forEach(function(button) { + let elButton = document.createElement('input'); + elButton.setAttribute("type", "button"); + elButton.setAttribute("class", button.className); + elButton.setAttribute("id", propertyID + "-button-" + button.id); + elButton.setAttribute("value", button.label); + elButton.addEventListener("click", button.onClick); + if (newRow) { + elDiv.appendChild(elButton); + } else { + elProperty.appendChild(elButton); + } + }); + + if (newRow) { + elProperty.appendChild(document.createElement('br')); + elProperty.appendChild(elDiv); + } } function showGroupsForType(type) { - if (type === "Box" || type === "Sphere") { - type = "Shape"; - } - - let typeGroups = GROUPS_PER_TYPE[type]; + if (type === "Box" || type === "Sphere") { + type = "Shape"; + } + + let typeGroups = GROUPS_PER_TYPE[type]; - for (let groupKey in elGroups) { - let elGroup = elGroups[groupKey]; - if (typeGroups && typeGroups.indexOf(groupKey) > -1) { - elGroup.style.display = "block"; - } else { - elGroup.style.display = "none"; - } - } + for (let groupKey in elGroups) { + let elGroup = elGroups[groupKey]; + if (typeGroups && typeGroups.indexOf(groupKey) > -1) { + elGroup.style.display = "block"; + } else { + elGroup.style.display = "none"; + } + } } function resetProperties() { - for (let propertyName in elPropertyElements) { - let elProperty = elPropertyElements[propertyName]; - if (elProperty instanceof Array) { - if (elProperty.length === 2 || elProperty.length === 3) { - // vec2/vec3 are array of 2/3 input numbers - elProperty[0].value = ""; - elProperty[1].value = ""; - if (elProperty[2] !== undefined) { - elProperty[2].value = ""; - } - } else if (elProperty.length === 4) { - // color is array of color picker and 3 input numbers - elProperty[0].style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elProperty[1].value = ""; - elProperty[2].value = ""; - elProperty[3].value = ""; - } - } else if (elProperty.getAttribute("type") === "number") { - if (elProperty.getAttribute("defaultValue")) { - elProperty.value = elProperty.getAttribute("defaultValue"); - } else { - elProperty.value = ""; - } - } else if (elProperty.getAttribute("type") === "checkbox") { - elProperty.checked = false; - } else { - elProperty.value = ""; - } - } - - for (let showPropertyRule in showPropertyRules) { - let propertyShowRules = showPropertyRules[showPropertyRule]; - for (let propertyToHide in propertyShowRules) { - let elPropertyToHide = elPropertyElements[propertyToHide]; - if (elPropertyToHide) { - let parentNode = elPropertyToHide.parentNode; - if (parentNode === undefined && elPropertyToHide instanceof Array) { - parentNode = elPropertyToHide[0].parentNode; - } - parentNode.style.display = "none"; - } - } - } + for (let propertyName in elPropertyElements) { + let elProperty = elPropertyElements[propertyName]; + if (elProperty instanceof Array) { + if (elProperty.length === 2 || elProperty.length === 3) { + // vec2/vec3 are array of 2/3 input numbers + elProperty[0].value = ""; + elProperty[1].value = ""; + if (elProperty[2] !== undefined) { + elProperty[2].value = ""; + } + } else if (elProperty.length === 4) { + // color is array of color picker and 3 input numbers + elProperty[0].style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; + elProperty[1].value = ""; + elProperty[2].value = ""; + elProperty[3].value = ""; + } + } else if (elProperty.getAttribute("type") === "number") { + if (elProperty.getAttribute("defaultValue")) { + elProperty.value = elProperty.getAttribute("defaultValue"); + } else { + elProperty.value = ""; + } + } else if (elProperty.getAttribute("type") === "checkbox") { + elProperty.checked = false; + } else { + elProperty.value = ""; + } + } + + for (let showPropertyRule in showPropertyRules) { + let propertyShowRules = showPropertyRules[showPropertyRule]; + for (let propertyToHide in propertyShowRules) { + let elPropertyToHide = elPropertyElements[propertyToHide]; + if (elPropertyToHide) { + let parentNode = elPropertyToHide.parentNode; + if (parentNode === undefined && elPropertyToHide instanceof Array) { + parentNode = elPropertyToHide[0].parentNode; + } + parentNode.style.display = "none"; + } + } + } } function loaded() { - openEventBridge(function() { - let elPropertiesList = document.getElementById("properties-list"); - - GROUPS.forEach(function(group) { - let elGroup; - if (group.addToGroup !== undefined) { - let fieldset = document.getElementById(group.addToGroup); - elGroup = document.createElement('div'); - fieldset.appendChild(elGroup); - } else { - elGroup = document.createElement('fieldset'); - elGroup.setAttribute("class", "major"); - elGroup.setAttribute("id", group.id); - elPropertiesList.appendChild(elGroup); - } + openEventBridge(function() { + let elPropertiesList = document.getElementById("properties-list"); + + GROUPS.forEach(function(group) { + let elGroup; + if (group.addToGroup !== undefined) { + let fieldset = document.getElementById(group.addToGroup); + elGroup = document.createElement('div'); + fieldset.appendChild(elGroup); + } else { + elGroup = document.createElement('fieldset'); + elGroup.setAttribute("class", "major"); + elGroup.setAttribute("id", group.id); + elPropertiesList.appendChild(elGroup); + } - if (group.label !== undefined) { - let elLegend = document.createElement('legend'); - elLegend.innerText = group.label; - elLegend.setAttribute("class", "section-header"); - let elSpan = document.createElement('span'); - elSpan.setAttribute("class", ".collapse-icon"); - elSpan.innerText = "M"; - elLegend.appendChild(elSpan); - elGroup.appendChild(elLegend); - } - - group.properties.forEach(function(property) { - let propertyType = property.type; - let propertyName = property.propertyName; - let propertyID = "property-" + propertyName; - propertyID = propertyID.replace(".", "-"); - - let elProperty; - if (propertyType === "sub-header") { - elProperty = document.createElement('legend'); - elProperty.innerText = property.label; - elProperty.setAttribute("class", "sub-section-header"); - } else { - elProperty = document.createElement('div'); - elProperty.setAttribute("id", "div-" + propertyName); - } - - if (group.twoColumn && property.column !== undefined && property.column !== -1) { - let columnName = group.id + "column" + property.column; - let elColumn = document.getElementById(columnName); - if (!elColumn) { - let columnDivName = group.id + "columnDiv"; - let elColumnDiv = document.getElementById(columnDivName); - if (!elColumnDiv) { - elColumnDiv = document.createElement('div'); - elColumnDiv.setAttribute("class", "two-column"); - elColumnDiv.setAttribute("id", group.id + "columnDiv"); - elGroup.appendChild(elColumnDiv); - } - elColumn = document.createElement('fieldset'); - elColumn.setAttribute("class", "column"); - elColumn.setAttribute("id", columnName); - elColumnDiv.appendChild(elColumn); - } - elColumn.appendChild(elProperty); - } else { - elGroup.appendChild(elProperty); - } - - let elLabel = document.createElement('label'); - elLabel.innerText = property.label; - elLabel.setAttribute("for", propertyID); - - switch (propertyType) { - case 'vec3': { - elProperty.setAttribute("class", "property " + property.vec3Type + " fstuple"); - - let elTuple = document.createElement('div'); - elTuple.setAttribute("class", "tuple"); - - addUnit(property.unit, elLabel); - - elProperty.appendChild(elLabel); - elProperty.appendChild(elTuple); - - let elInputX = createTupleNumberInput(elTuple, propertyID, property.subLabels[0], property.min, property.max, property.step); - let elInputY = createTupleNumberInput(elTuple, propertyID, property.subLabels[1], property.min, property.max, property.step); - let elInputZ = createTupleNumberInput(elTuple, propertyID, property.subLabels[2], property.min, property.max, property.step); - - let inputChangeFunction; - let multiplier = 1; - if (property.multiplier !== undefined) { - multiplier = property.multiplier; - inputChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elInputX, elInputY, elInputZ); - } else { - inputChangeFunction = createEmitVec3PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY, elInputZ, property.multiplier); - } - elInputX.setAttribute("multiplier", multiplier); - elInputY.setAttribute("multiplier", multiplier); - elInputZ.setAttribute("multiplier", multiplier); - elInputX.addEventListener('change', inputChangeFunction); - elInputY.addEventListener('change', inputChangeFunction); - elInputZ.addEventListener('change', inputChangeFunction); - - elPropertyElements[propertyName] = [elInputX, elInputY, elInputZ]; - break; - } - case 'vec2': { - elProperty.setAttribute("class", "property " + property.vec2Type + " fstuple"); - - let elTuple = document.createElement('div'); - elTuple.setAttribute("class", "tuple"); - - addUnit(property.unit, elLabel); - - elProperty.appendChild(elLabel); - elProperty.appendChild(elTuple); - - let elInputX = createTupleNumberInput(elTuple, propertyID, property.subLabels[0], property.min, property.max, property.step); - let elInputY = createTupleNumberInput(elTuple, propertyID, property.subLabels[1], property.min, property.max, property.step); - - let inputChangeFunction; - let multiplier = 1; - if (property.multiplier !== undefined) { - multiplier = property.multiplier; - inputChangeFunction = createEmitVec2PropertyUpdateFunction(propertyName, elInputX, elInputY); - } else { - inputChangeFunction = createEmitVec2PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY, property.multiplier); - } - elInputX.setAttribute("multiplier", multiplier); - elInputY.setAttribute("multiplier", multiplier); - elInputX.addEventListener('change', inputChangeFunction); - elInputY.addEventListener('change', inputChangeFunction); - - elPropertyElements[propertyName] = [elInputX, elInputY]; - break; - } - case 'color': { - elProperty.setAttribute("class", "property rgb fstuple"); - - let elColorPicker = document.createElement('div'); - elColorPicker.setAttribute("class", "color-picker"); - elColorPicker.setAttribute("id", propertyID); - - let elTuple = document.createElement('div'); - elTuple.setAttribute("class", "tuple"); - - elProperty.appendChild(elColorPicker); - elProperty.appendChild(elLabel); - elProperty.appendChild(elTuple); - - let elInputR = createTupleNumberInput(elTuple, propertyID, "red", 0, 255, 1); - let elInputG = createTupleNumberInput(elTuple, propertyID, "green", 0, 255, 1); - let elInputB = createTupleNumberInput(elTuple, propertyID, "blue", 0, 255, 1); - - let inputChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elInputR, elInputG, elInputB); - elInputR.addEventListener('change', inputChangeFunction); - elInputG.addEventListener('change', inputChangeFunction); - elInputB.addEventListener('change', inputChangeFunction); - - let colorPickerID = "#" + propertyID; - colorPickers[colorPickerID] = $(colorPickerID).colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $(colorPickerID).attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers[colorPickerID].colpickSetColor({ - "r": elInputR.value, - "g": elInputG.value, - "b": elInputB.value - }); - }, - onHide: function(colpick) { - $(colorPickerID).attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); - } - }); - - elPropertyElements[propertyName] = [elColorPicker, elInputR, elInputG, elInputB]; - break; - } - case 'string': { - elProperty.setAttribute("class", "property text"); - - let elInput = document.createElement('input'); - elInput.setAttribute("id", propertyID); - elInput.setAttribute("type", "text"); - if (property.readOnly) { - elInput.readOnly = true; - } - - elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); - - elProperty.appendChild(elLabel); - elProperty.appendChild(elInput); - - if (property.buttons !== undefined) { - addButtons(elProperty, propertyID, property.buttons, false); - } - - elPropertyElements[propertyName] = elInput; - break; - } - case 'bool': { - elProperty.setAttribute("class", "property checkbox"); - - if (property.glyph !== undefined) { - elLabel.innerText = " " + property.label; - let elSpan = document.createElement('span'); - elSpan.innerHTML = property.glyph; - elLabel.insertBefore(elSpan, elLabel.firstChild); - } - - let elInput = document.createElement('input'); - elInput.setAttribute("id", propertyID); - elInput.setAttribute("type", "checkbox"); - - let inverse = property.inverse; - elInput.setAttribute("inverse", inverse ? "true" : "false"); - - elProperty.appendChild(elInput); - elProperty.appendChild(elLabel); - - let subPropertyOf = property.subPropertyOf; - if (subPropertyOf !== undefined) { - elInput.setAttribute("subPropertyOf", subPropertyOf); - elPropertyElements[propertyName] = elInput; - subProperties.push(propertyName); - elInput.addEventListener('change', function() { - updateCheckedSubProperty(subPropertyOf, properties[subPropertyOf], elInput, propertyName); - }); - } else { - elPropertyElements[propertyName] = elInput; - elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(propertyName, inverse)); - } - break; - } - case 'dropdown': { - elProperty.setAttribute("class", "property dropdown"); - - let elInput = document.createElement('select'); - elInput.setAttribute("id", propertyID); - - for (let optionKey in property.options) { - let option = document.createElement('option'); - option.value = optionKey; - option.text = property.options[optionKey]; - elInput.add(option); - } - - elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); - - elProperty.appendChild(elLabel); - elProperty.appendChild(elInput); - elPropertyElements[propertyName] = elInput; - break; - } - case 'number': { - elProperty.setAttribute("class", "property number"); - - addUnit(property.unit, elLabel); - - let elInput = document.createElement('input'); - elInput.setAttribute("id", propertyID); - elInput.setAttribute("type", "number"); - if (property.min !== undefined) { - elInput.setAttribute("min", property.min); - } - if (property.max !== undefined) { - elInput.setAttribute("max", property.max); - } - if (property.step !== undefined) { - elInput.setAttribute("step", property.step); - } - - let fixedDecimals = property.fixedDecimals !== undefined ? property.fixedDecimals : 0; - elInput.setAttribute("fixedDecimals", fixedDecimals); - - let defaultValue = property.defaultValue; - if (defaultValue !== undefined) { - elInput.setAttribute("defaultValue", defaultValue); - elInput.value = defaultValue; - } - - elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(propertyName, fixedDecimals)); - - elProperty.appendChild(elLabel); - elProperty.appendChild(elInput); - - if (property.buttons !== undefined) { - addButtons(elProperty, propertyID, property.buttons, true); - } - - elPropertyElements[propertyName] = elInput; - break; - } - case 'textarea': { - elProperty.setAttribute("class", "property textarea"); - - elProperty.appendChild(elLabel); - - if (property.buttons !== undefined) { - addButtons(elProperty, propertyID, property.buttons, true); - } - - let elInput = document.createElement('textarea'); - elInput.setAttribute("id", propertyID); - if (property.readOnly) { - elInput.readOnly = true; - } - - elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); - - elProperty.appendChild(elInput); - elPropertyElements[propertyName] = elInput; - break; - } - case 'icon': { - elProperty.setAttribute("class", "property value"); - - elLabel.setAttribute("id", propertyID); - elLabel.innerHTML = " " + property.label; - - let elSpan = document.createElement('span'); - elSpan.setAttribute("id", propertyID + "-icon"); - icons[propertyName] = property.icons; + if (group.label !== undefined) { + let elLegend = document.createElement('legend'); + elLegend.innerText = group.label; + elLegend.setAttribute("class", "section-header"); + let elSpan = document.createElement('span'); + elSpan.setAttribute("class", ".collapse-icon"); + elSpan.innerText = "M"; + elLegend.appendChild(elSpan); + elGroup.appendChild(elLegend); + } + + group.properties.forEach(function(property) { + let propertyType = property.type; + let propertyName = property.propertyName; + let propertyID = "property-" + propertyName; + propertyID = propertyID.replace(".", "-"); + + let elProperty; + if (propertyType === "sub-header") { + elProperty = document.createElement('legend'); + elProperty.innerText = property.label; + elProperty.setAttribute("class", "sub-section-header"); + } else { + elProperty = document.createElement('div'); + elProperty.setAttribute("id", "div-" + propertyName); + } + + if (group.twoColumn && property.column !== undefined && property.column !== -1) { + let columnName = group.id + "column" + property.column; + let elColumn = document.getElementById(columnName); + if (!elColumn) { + let columnDivName = group.id + "columnDiv"; + let elColumnDiv = document.getElementById(columnDivName); + if (!elColumnDiv) { + elColumnDiv = document.createElement('div'); + elColumnDiv.setAttribute("class", "two-column"); + elColumnDiv.setAttribute("id", group.id + "columnDiv"); + elGroup.appendChild(elColumnDiv); + } + elColumn = document.createElement('fieldset'); + elColumn.setAttribute("class", "column"); + elColumn.setAttribute("id", columnName); + elColumnDiv.appendChild(elColumn); + } + elColumn.appendChild(elProperty); + } else { + elGroup.appendChild(elProperty); + } + + let elLabel = document.createElement('label'); + elLabel.innerText = property.label; + elLabel.setAttribute("for", propertyID); + + switch (propertyType) { + case 'vec3': { + elProperty.setAttribute("class", "property " + property.vec3Type + " fstuple"); + + let elTuple = document.createElement('div'); + elTuple.setAttribute("class", "tuple"); + + addUnit(property.unit, elLabel); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elTuple); + + let elInputX = createTupleNumberInput(elTuple, propertyID, property.subLabels[0], property.min, property.max, property.step); + let elInputY = createTupleNumberInput(elTuple, propertyID, property.subLabels[1], property.min, property.max, property.step); + let elInputZ = createTupleNumberInput(elTuple, propertyID, property.subLabels[2], property.min, property.max, property.step); + + let inputChangeFunction; + let multiplier = 1; + if (property.multiplier !== undefined) { + multiplier = property.multiplier; + inputChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elInputX, elInputY, elInputZ); + } else { + inputChangeFunction = createEmitVec3PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY, elInputZ, property.multiplier); + } + elInputX.setAttribute("multiplier", multiplier); + elInputY.setAttribute("multiplier", multiplier); + elInputZ.setAttribute("multiplier", multiplier); + elInputX.addEventListener('change', inputChangeFunction); + elInputY.addEventListener('change', inputChangeFunction); + elInputZ.addEventListener('change', inputChangeFunction); + + elPropertyElements[propertyName] = [elInputX, elInputY, elInputZ]; + break; + } + case 'vec2': { + elProperty.setAttribute("class", "property " + property.vec2Type + " fstuple"); + + let elTuple = document.createElement('div'); + elTuple.setAttribute("class", "tuple"); + + addUnit(property.unit, elLabel); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elTuple); + + let elInputX = createTupleNumberInput(elTuple, propertyID, property.subLabels[0], property.min, property.max, property.step); + let elInputY = createTupleNumberInput(elTuple, propertyID, property.subLabels[1], property.min, property.max, property.step); + + let inputChangeFunction; + let multiplier = 1; + if (property.multiplier !== undefined) { + multiplier = property.multiplier; + inputChangeFunction = createEmitVec2PropertyUpdateFunction(propertyName, elInputX, elInputY); + } else { + inputChangeFunction = createEmitVec2PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY, property.multiplier); + } + elInputX.setAttribute("multiplier", multiplier); + elInputY.setAttribute("multiplier", multiplier); + elInputX.addEventListener('change', inputChangeFunction); + elInputY.addEventListener('change', inputChangeFunction); + + elPropertyElements[propertyName] = [elInputX, elInputY]; + break; + } + case 'color': { + elProperty.setAttribute("class", "property rgb fstuple"); + + let elColorPicker = document.createElement('div'); + elColorPicker.setAttribute("class", "color-picker"); + elColorPicker.setAttribute("id", propertyID); + + let elTuple = document.createElement('div'); + elTuple.setAttribute("class", "tuple"); + + elProperty.appendChild(elColorPicker); + elProperty.appendChild(elLabel); + elProperty.appendChild(elTuple); + + let elInputR = createTupleNumberInput(elTuple, propertyID, "red", 0, 255, 1); + let elInputG = createTupleNumberInput(elTuple, propertyID, "green", 0, 255, 1); + let elInputB = createTupleNumberInput(elTuple, propertyID, "blue", 0, 255, 1); + + let inputChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elInputR, elInputG, elInputB); + elInputR.addEventListener('change', inputChangeFunction); + elInputG.addEventListener('change', inputChangeFunction); + elInputB.addEventListener('change', inputChangeFunction); + + let colorPickerID = "#" + propertyID; + colorPickers[colorPickerID] = $(colorPickerID).colpick({ + colorScheme: 'dark', + layout: 'hex', + color: '000000', + submit: false, // We don't want to have a submission button + onShow: function(colpick) { + $(colorPickerID).attr('active', 'true'); + // The original color preview within the picker needs to be updated on show because + // prior to the picker being shown we don't have access to the selections' starting color. + colorPickers[colorPickerID].colpickSetColor({ + "r": elInputR.value, + "g": elInputG.value, + "b": elInputB.value + }); + }, + onHide: function(colpick) { + $(colorPickerID).attr('active', 'false'); + }, + onChange: function(hsb, hex, rgb, el) { + $(el).css('background-color', '#' + hex); + emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); + } + }); + + elPropertyElements[propertyName] = [elColorPicker, elInputR, elInputG, elInputB]; + break; + } + case 'string': { + elProperty.setAttribute("class", "property text"); + + let elInput = document.createElement('input'); + elInput.setAttribute("id", propertyID); + elInput.setAttribute("type", "text"); + if (property.readOnly) { + elInput.readOnly = true; + } + + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elInput); + + if (property.buttons !== undefined) { + addButtons(elProperty, propertyID, property.buttons, false); + } + + elPropertyElements[propertyName] = elInput; + break; + } + case 'bool': { + elProperty.setAttribute("class", "property checkbox"); + + if (property.glyph !== undefined) { + elLabel.innerText = " " + property.label; + let elSpan = document.createElement('span'); + elSpan.innerHTML = property.glyph; + elLabel.insertBefore(elSpan, elLabel.firstChild); + } + + let elInput = document.createElement('input'); + elInput.setAttribute("id", propertyID); + elInput.setAttribute("type", "checkbox"); + + let inverse = property.inverse; + elInput.setAttribute("inverse", inverse ? "true" : "false"); + + elProperty.appendChild(elInput); + elProperty.appendChild(elLabel); + + let subPropertyOf = property.subPropertyOf; + if (subPropertyOf !== undefined) { + elInput.setAttribute("subPropertyOf", subPropertyOf); + elPropertyElements[propertyName] = elInput; + subProperties.push(propertyName); + elInput.addEventListener('change', function() { + updateCheckedSubProperty(subPropertyOf, properties[subPropertyOf], elInput, propertyName); + }); + } else { + elPropertyElements[propertyName] = elInput; + elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(propertyName, inverse)); + } + break; + } + case 'dropdown': { + elProperty.setAttribute("class", "property dropdown"); + + let elInput = document.createElement('select'); + elInput.setAttribute("id", propertyID); + + for (let optionKey in property.options) { + let option = document.createElement('option'); + option.value = optionKey; + option.text = property.options[optionKey]; + elInput.add(option); + } + + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elInput); + elPropertyElements[propertyName] = elInput; + break; + } + case 'number': { + elProperty.setAttribute("class", "property number"); + + addUnit(property.unit, elLabel); + + let elInput = document.createElement('input'); + elInput.setAttribute("id", propertyID); + elInput.setAttribute("type", "number"); + if (property.min !== undefined) { + elInput.setAttribute("min", property.min); + } + if (property.max !== undefined) { + elInput.setAttribute("max", property.max); + } + if (property.step !== undefined) { + elInput.setAttribute("step", property.step); + } + + let fixedDecimals = property.fixedDecimals !== undefined ? property.fixedDecimals : 0; + elInput.setAttribute("fixedDecimals", fixedDecimals); + + let defaultValue = property.defaultValue; + if (defaultValue !== undefined) { + elInput.setAttribute("defaultValue", defaultValue); + elInput.value = defaultValue; + } + + elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(propertyName, fixedDecimals)); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elInput); + + if (property.buttons !== undefined) { + addButtons(elProperty, propertyID, property.buttons, true); + } + + elPropertyElements[propertyName] = elInput; + break; + } + case 'textarea': { + elProperty.setAttribute("class", "property textarea"); + + elProperty.appendChild(elLabel); + + if (property.buttons !== undefined) { + addButtons(elProperty, propertyID, property.buttons, true); + } + + let elInput = document.createElement('textarea'); + elInput.setAttribute("id", propertyID); + if (property.readOnly) { + elInput.readOnly = true; + } + + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + + elProperty.appendChild(elInput); + elPropertyElements[propertyName] = elInput; + break; + } + case 'icon': { + elProperty.setAttribute("class", "property value"); + + elLabel.setAttribute("id", propertyID); + elLabel.innerHTML = " " + property.label; + + let elSpan = document.createElement('span'); + elSpan.setAttribute("id", propertyID + "-icon"); + icons[propertyName] = property.icons; - elProperty.appendChild(elSpan); - elProperty.appendChild(elLabel); - elPropertyElements[propertyName] = [ elSpan, elLabel ]; - break; - } - case 'buttons': { - elProperty.setAttribute("class", "property Text"); - - let hasLabel = property.label !== undefined; - if (hasLabel) { - elProperty.appendChild(elLabel); - } - - if (property.buttons !== undefined) { - addButtons(elProperty, propertyID, property.buttons, hasLabel); - } - - elPropertyElements[propertyName] = elProperty; - break; - } - case 'sub-header': { - if (propertyName !== undefined) { - elPropertyElements[propertyName] = elProperty; - } - break; - } - } - - let showPropertyRule = property.showPropertyRule; - if (showPropertyRule !== undefined) { - let dependentProperty = Object.keys(showPropertyRule)[0]; - let dependentPropertyValue = showPropertyRule[dependentProperty]; - if (!showPropertyRules[dependentProperty]) { - showPropertyRules[dependentProperty] = []; - } - showPropertyRules[dependentProperty][propertyName] = dependentPropertyValue; - } - }); - - elGroups[group.id] = elGroup; - }); - - if (window.EventBridge !== undefined) { + elProperty.appendChild(elSpan); + elProperty.appendChild(elLabel); + elPropertyElements[propertyName] = [ elSpan, elLabel ]; + break; + } + case 'buttons': { + elProperty.setAttribute("class", "property Text"); + + let hasLabel = property.label !== undefined; + if (hasLabel) { + elProperty.appendChild(elLabel); + } + + if (property.buttons !== undefined) { + addButtons(elProperty, propertyID, property.buttons, hasLabel); + } + + elPropertyElements[propertyName] = elProperty; + break; + } + case 'sub-header': { + if (propertyName !== undefined) { + elPropertyElements[propertyName] = elProperty; + } + break; + } + } + + let showPropertyRule = property.showPropertyRule; + if (showPropertyRule !== undefined) { + let dependentProperty = Object.keys(showPropertyRule)[0]; + let dependentPropertyValue = showPropertyRule[dependentProperty]; + if (!showPropertyRules[dependentProperty]) { + showPropertyRules[dependentProperty] = []; + } + showPropertyRules[dependentProperty][propertyName] = dependentPropertyValue; + } + }); + + elGroups[group.id] = elGroup; + }); + + if (window.EventBridge !== undefined) { var properties; EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); if (data.type === "server_script_status") { - let elServerScriptError = document.getElementById("property-serverScripts-error"); - let elServerScriptStatus = document.getElementById("property-serverScripts-status"); + let elServerScriptError = document.getElementById("property-serverScripts-error"); + let elServerScriptStatus = document.getElementById("property-serverScripts-status"); elServerScriptError.value = data.errorInfo; // If we just set elServerScriptError's diplay to block or none, we still end up with // it's parent contributing 21px bottom padding even when elServerScriptError is display:none. @@ -2125,22 +2110,22 @@ function loaded() { deleteJSONMaterialEditor(); } } - - elPropertyElements["type"][0].style.display = "none"; + + elPropertyElements["type"][0].style.display = "none"; elPropertyElements["type"][1].innerHTML = NO_SELECTION; - elPropertiesList.className = ''; - showGroupsForType("None"); - - resetProperties(); - - deleteJSONEditor(); + elPropertiesList.className = ''; + showGroupsForType("None"); + + resetProperties(); + + deleteJSONEditor(); elPropertyElements["userData"].value = ""; showUserDataTextArea(); showSaveUserDataButton(); showNewJSONEditorButton(); - - deleteJSONMaterialEditor(); - elPropertyElements["materialData"].value = ""; + + deleteJSONMaterialEditor(); + elPropertyElements["materialData"].value = ""; showMaterialDataTextArea(); showSaveMaterialDataButton(); showNewJSONMaterialEditorButton(); @@ -2149,8 +2134,8 @@ function loaded() { } else if (data.selections.length > 1) { deleteJSONEditor(); deleteJSONMaterialEditor(); - - let selections = data.selections; + + let selections = data.selections; let ids = []; let types = {}; @@ -2170,19 +2155,19 @@ function loaded() { if (numTypes === 1) { type = selections[0].properties.type; } - + elPropertyElements["type"][0].innerHTML = ICON_FOR_TYPE[type]; elPropertyElements["type"][0].style.display = "inline-block"; - elPropertyElements["type"][1].innerHTML = type + " (" + data.selections.length + ")"; - elPropertiesList.className = ''; - showGroupsForType(type); - - resetProperties(); - disableProperties(); + elPropertyElements["type"][1].innerHTML = type + " (" + data.selections.length + ")"; + elPropertiesList.className = ''; + showGroupsForType(type); + + resetProperties(); + disableProperties(); } else { properties = data.selections[0].properties; - - if (lastEntityID !== '"' + properties.id + '"' && lastEntityID !== null) { + + if (lastEntityID !== '"' + properties.id + '"' && lastEntityID !== null) { if (editor !== null) { saveUserData(); } @@ -2206,148 +2191,148 @@ function loaded() { properties.type = "Image"; } } - + // Create class name for css ruleset filtering elPropertiesList.className = properties.type + 'Menu'; - showGroupsForType(properties.type); - - for (let propertyName in elPropertyElements) { - let elProperty = elPropertyElements[propertyName]; - - let propertyValue; - let splitPropertyName = propertyName.split('.'); - if (splitPropertyName.length > 1) { - let propertyGroupName = splitPropertyName[0]; - let subPropertyName = splitPropertyName[1]; - if (properties[propertyGroupName] === undefined || properties[propertyGroupName][subPropertyName] === undefined) { - continue; - } - if (splitPropertyName.length === 3) { - let subSubPropertyName = splitPropertyName[2]; - propertyValue = properties[propertyGroupName][subPropertyName][subSubPropertyName]; - } else { - propertyValue = properties[propertyGroupName][subPropertyName]; - } - } else { - propertyValue = properties[propertyName]; - } + showGroupsForType(properties.type); + + for (let propertyName in elPropertyElements) { + let elProperty = elPropertyElements[propertyName]; + + let propertyValue; + let splitPropertyName = propertyName.split('.'); + if (splitPropertyName.length > 1) { + let propertyGroupName = splitPropertyName[0]; + let subPropertyName = splitPropertyName[1]; + if (properties[propertyGroupName] === undefined || properties[propertyGroupName][subPropertyName] === undefined) { + continue; + } + if (splitPropertyName.length === 3) { + let subSubPropertyName = splitPropertyName[2]; + propertyValue = properties[propertyGroupName][subPropertyName][subSubPropertyName]; + } else { + propertyValue = properties[propertyGroupName][subPropertyName]; + } + } else { + propertyValue = properties[propertyName]; + } - // workaround for shape Color & Light Color property fields sharing same property value "color" - if (propertyValue === undefined && propertyName === "lightColor") { - propertyValue = properties["color"] - } - - let isSubProperty = subProperties.indexOf(propertyName) > -1; - if (elProperty === undefined || (propertyValue === undefined && !isSubProperty)) { - continue; - } - - if (elProperty instanceof Array) { - if (elProperty[1].getAttribute("type") === "number") { // vectors - if (elProperty.length === 2 || elProperty.length === 3) { - // vec2/vec3 are array of 2/3 input numbers - elProperty[0].value = (propertyValue.x * 1 / elProperty[0].getAttribute("multiplier")).toFixed(4); - elProperty[1].value = (propertyValue.y * 1 / elProperty[1].getAttribute("multiplier")).toFixed(4); - if (elProperty[2] !== undefined) { - elProperty[2].value = (propertyValue.z * 1 / elProperty[1].getAttribute("multiplier")).toFixed(4); - } - } else if (elProperty.length === 4) { - // color is array of color picker and 3 input numbers - elProperty[0].style.backgroundColor = "rgb(" + propertyValue.red + "," + propertyValue.green + "," + propertyValue.blue + ")"; - elProperty[1].value = propertyValue.red; - elProperty[2].value = propertyValue.green; - elProperty[3].value = propertyValue.blue; - } - } else if (elProperty[0].nodeName === "SPAN") { // icons - // icon is array of elSpan (icon glyph) and elLabel - elProperty[0].innerHTML = icons[propertyName][propertyValue]; - elProperty[0].style.display = "inline-block"; - elProperty[1].innerHTML = propertyValue; //+ " (" + data.selections.length + ")"; - } - } else if (elProperty.getAttribute("subPropertyOf")) { - let propertyValue = properties[elProperty.getAttribute("subPropertyOf")]; - let subProperties = propertyValue.split(","); - elProperty.checked = subProperties.indexOf(propertyName) > -1; - } else if (elProperty.getAttribute("type") === "number") { - let fixedDecimals = elProperty.getAttribute("fixedDecimals"); - elProperty.value = propertyValue.toFixed(fixedDecimals); - } else if (elProperty.getAttribute("type") === "checkbox") { - let inverse = elProperty.getAttribute("inverse") === "true"; - elProperty.checked = inverse ? !propertyValue : propertyValue; - } else if (elProperty.nodeName === "TEXTAREA") { - elProperty.value = propertyValue; - setTextareaScrolling(elProperty); - } else if (elProperty.nodeName === "SELECT") { // dropdown - elProperty.value = propertyValue; - } else { - elProperty.value = propertyValue; - } - - let propertyShowRules = showPropertyRules[propertyName]; - if (propertyShowRules !== undefined) { - for (let propertyToShow in propertyShowRules) { - let showIfThisPropertyValue = propertyShowRules[propertyToShow]; - let show = String(propertyValue) === String(showIfThisPropertyValue); - let elPropertyToShow = elPropertyElements[propertyToShow]; - if (elPropertyToShow) { - let parentNode = elPropertyToShow.parentNode; - if (parentNode === undefined && elPropertyToShow instanceof Array) { - parentNode = elPropertyToShow[0].parentNode; - } - parentNode.style.display = show ? "block" : "none"; - } - } - } - } - - let elGrabbable = elPropertyElements["grabbable"]; - let elTriggerable = elPropertyElements["triggerable"]; - let elIgnoreIK = elPropertyElements["ignoreIK"]; - elGrabbable.checked = elPropertyElements["dynamic"].checked; - elTriggerable.checked = false; - elIgnoreIK.checked = true; - let grabbablesSet = false; - let parsedUserData = {}; - try { - parsedUserData = JSON.parse(properties.userData); - if ("grabbableKey" in parsedUserData) { - grabbablesSet = true; - let grabbableData = parsedUserData.grabbableKey; - if ("grabbable" in grabbableData) { - elGrabbable.checked = grabbableData.grabbable; - } else { - elGrabbable.checked = true; - } - if ("triggerable" in grabbableData) { - elTriggerable.checked = grabbableData.triggerable; - } else if ("wantsTrigger" in grabbableData) { - elTriggerable.checked = grabbableData.wantsTrigger; - } else { - elTriggerable.checked = false; - } - if ("ignoreIK" in grabbableData) { - elIgnoreIK.checked = grabbableData.ignoreIK; - } else { - elIgnoreIK.checked = true; - } - } - } catch (e) { - // TODO: What should go here? - } - if (!grabbablesSet) { - elGrabbable.checked = true; - elTriggerable.checked = false; - elIgnoreIK.checked = true; - } - - if (properties.type === "Image") { + // workaround for shape Color & Light Color property fields sharing same property value "color" + if (propertyValue === undefined && propertyName === "lightColor") { + propertyValue = properties["color"] + } + + let isSubProperty = subProperties.indexOf(propertyName) > -1; + if (elProperty === undefined || (propertyValue === undefined && !isSubProperty)) { + continue; + } + + if (elProperty instanceof Array) { + if (elProperty[1].getAttribute("type") === "number") { // vectors + if (elProperty.length === 2 || elProperty.length === 3) { + // vec2/vec3 are array of 2/3 input numbers + elProperty[0].value = (propertyValue.x * 1 / elProperty[0].getAttribute("multiplier")).toFixed(4); + elProperty[1].value = (propertyValue.y * 1 / elProperty[1].getAttribute("multiplier")).toFixed(4); + if (elProperty[2] !== undefined) { + elProperty[2].value = (propertyValue.z * 1 / elProperty[1].getAttribute("multiplier")).toFixed(4); + } + } else if (elProperty.length === 4) { + // color is array of color picker and 3 input numbers + elProperty[0].style.backgroundColor = "rgb(" + propertyValue.red + "," + propertyValue.green + "," + propertyValue.blue + ")"; + elProperty[1].value = propertyValue.red; + elProperty[2].value = propertyValue.green; + elProperty[3].value = propertyValue.blue; + } + } else if (elProperty[0].nodeName === "SPAN") { // icons + // icon is array of elSpan (icon glyph) and elLabel + elProperty[0].innerHTML = icons[propertyName][propertyValue]; + elProperty[0].style.display = "inline-block"; + elProperty[1].innerHTML = propertyValue; //+ " (" + data.selections.length + ")"; + } + } else if (elProperty.getAttribute("subPropertyOf")) { + let propertyValue = properties[elProperty.getAttribute("subPropertyOf")]; + let subProperties = propertyValue.split(","); + elProperty.checked = subProperties.indexOf(propertyName) > -1; + } else if (elProperty.getAttribute("type") === "number") { + let fixedDecimals = elProperty.getAttribute("fixedDecimals"); + elProperty.value = propertyValue.toFixed(fixedDecimals); + } else if (elProperty.getAttribute("type") === "checkbox") { + let inverse = elProperty.getAttribute("inverse") === "true"; + elProperty.checked = inverse ? !propertyValue : propertyValue; + } else if (elProperty.nodeName === "TEXTAREA") { + elProperty.value = propertyValue; + setTextareaScrolling(elProperty); + } else if (elProperty.nodeName === "SELECT") { // dropdown + elProperty.value = propertyValue; + } else { + elProperty.value = propertyValue; + } + + let propertyShowRules = showPropertyRules[propertyName]; + if (propertyShowRules !== undefined) { + for (let propertyToShow in propertyShowRules) { + let showIfThisPropertyValue = propertyShowRules[propertyToShow]; + let show = String(propertyValue) === String(showIfThisPropertyValue); + let elPropertyToShow = elPropertyElements[propertyToShow]; + if (elPropertyToShow) { + let parentNode = elPropertyToShow.parentNode; + if (parentNode === undefined && elPropertyToShow instanceof Array) { + parentNode = elPropertyToShow[0].parentNode; + } + parentNode.style.display = show ? "block" : "none"; + } + } + } + } + + let elGrabbable = elPropertyElements["grabbable"]; + let elTriggerable = elPropertyElements["triggerable"]; + let elIgnoreIK = elPropertyElements["ignoreIK"]; + elGrabbable.checked = elPropertyElements["dynamic"].checked; + elTriggerable.checked = false; + elIgnoreIK.checked = true; + let grabbablesSet = false; + let parsedUserData = {}; + try { + parsedUserData = JSON.parse(properties.userData); + if ("grabbableKey" in parsedUserData) { + grabbablesSet = true; + let grabbableData = parsedUserData.grabbableKey; + if ("grabbable" in grabbableData) { + elGrabbable.checked = grabbableData.grabbable; + } else { + elGrabbable.checked = true; + } + if ("triggerable" in grabbableData) { + elTriggerable.checked = grabbableData.triggerable; + } else if ("wantsTrigger" in grabbableData) { + elTriggerable.checked = grabbableData.wantsTrigger; + } else { + elTriggerable.checked = false; + } + if ("ignoreIK" in grabbableData) { + elIgnoreIK.checked = grabbableData.ignoreIK; + } else { + elIgnoreIK.checked = true; + } + } + } catch (e) { + // TODO: What should go here? + } + if (!grabbablesSet) { + elGrabbable.checked = true; + elTriggerable.checked = false; + elIgnoreIK.checked = true; + } + + if (properties.type === "Image") { var imageLink = JSON.parse(properties.textures)["tex.picture"]; elPropertyElements["image"].value = imageLink; - } else if (properties.type === "Material") { - let elParentMaterialNameString = elPropertyElements["materialNameToReplace"]; - let elParentMaterialNameNumber = elPropertyElements["submeshToReplace"]; - let elParentMaterialNameCheckbox = elPropertyElements["selectSubmesh"]; - if (properties.parentMaterialName.startsWith(MATERIAL_PREFIX_STRING)) { + } else if (properties.type === "Material") { + let elParentMaterialNameString = elPropertyElements["materialNameToReplace"]; + let elParentMaterialNameNumber = elPropertyElements["submeshToReplace"]; + let elParentMaterialNameCheckbox = elPropertyElements["selectSubmesh"]; + if (properties.parentMaterialName.startsWith(MATERIAL_PREFIX_STRING)) { elParentMaterialNameString.value = properties.parentMaterialName.replace(MATERIAL_PREFIX_STRING, ""); showParentMaterialNameBox(false, elParentMaterialNameNumber, elParentMaterialNameString); elParentMaterialNameCheckbox.checked = false; @@ -2356,9 +2341,9 @@ function loaded() { showParentMaterialNameBox(true, elParentMaterialNameNumber, elParentMaterialNameString); elParentMaterialNameCheckbox.checked = true; } - } - - let json = null; + } + + let json = null; try { json = JSON.parse(properties.userData); } catch (e) { @@ -2368,7 +2353,7 @@ function loaded() { showUserDataTextArea(); showNewJSONEditorButton(); hideSaveUserDataButton(); - hideUserDataSaved(); + hideUserDataSaved(); } if (json !== null) { if (editor === null) { @@ -2377,8 +2362,8 @@ function loaded() { setEditorJSON(json); showSaveUserDataButton(); hideUserDataTextArea(); - hideNewJSONEditorButton(); - hideUserDataSaved(); + hideNewJSONEditorButton(); + hideUserDataSaved(); } let materialJson = null; @@ -2391,7 +2376,7 @@ function loaded() { showMaterialDataTextArea(); showNewJSONMaterialEditorButton(); hideSaveMaterialDataButton(); - hideMaterialDataSaved(); + hideMaterialDataSaved(); } if (materialJson !== null) { if (materialEditor === null) { @@ -2401,19 +2386,19 @@ function loaded() { showSaveMaterialDataButton(); hideMaterialDataTextArea(); hideNewJSONMaterialEditorButton(); - hideMaterialDataSaved(); + hideMaterialDataSaved(); } - - if (properties.locked) { - disableProperties(); - elPropertyElements["locked"].removeAttribute('disabled'); - } else { - enableProperties(); - disableSaveUserDataButton(); - disableSaveMaterialDataButton() - } - - var activeElement = document.activeElement; + + if (properties.locked) { + disableProperties(); + elPropertyElements["locked"].removeAttribute('disabled'); + } else { + enableProperties(); + disableSaveUserDataButton(); + disableSaveMaterialDataButton() + } + + var activeElement = document.activeElement; if (doSelectElement && typeof activeElement.select !== "undefined") { activeElement.select(); } @@ -2421,79 +2406,79 @@ function loaded() { } }); } - - // Server Script Status - let elServerScript = elPropertyElements["serverScripts"]; - let elDiv = document.createElement('div'); - elDiv.setAttribute("class", "property"); - let elLabel = document.createElement('label'); - elLabel.setAttribute("for", "property-serverScripts-status"); - elLabel.innerText = "Server Script Status"; - let elServerScriptStatus = document.createElement('span'); - elServerScriptStatus.setAttribute("id", "property-serverScripts-status"); - elDiv.appendChild(elLabel); - elDiv.appendChild(elServerScriptStatus); - elServerScript.parentNode.appendChild(elDiv); - - // Server Script Error - elDiv = document.createElement('div'); - elDiv.setAttribute("class", "property"); - let elServerScriptError = document.createElement('textarea'); - elServerScriptError.setAttribute("id", "property-serverScripts-error"); - elDiv.appendChild(elServerScriptError); - elServerScript.parentNode.appendChild(elDiv); - - let elScript = elPropertyElements["script"]; - elScript.parentNode.setAttribute("class", "property url refresh"); - elServerScript.parentNode.setAttribute("class", "property url refresh"); - - // User Data - let elUserData = elPropertyElements["userData"]; - elDiv = elUserData.parentNode; - let elStaticUserData = document.createElement('div'); - elStaticUserData.setAttribute("id", "property-userData-static"); - let elUserDataEditor = document.createElement('div'); - elUserDataEditor.setAttribute("id", "property-userData-editor"); - let elUserDataSaved = document.createElement('span'); - elUserDataSaved.setAttribute("id", "property-userData-saved"); - elUserDataSaved.innerText = "Saved!"; - elDiv.childNodes[2].appendChild(elUserDataSaved); - elDiv.insertBefore(elStaticUserData, elUserData); - elDiv.insertBefore(elUserDataEditor, elUserData); - - // Material Data - let elMaterialData = elPropertyElements["materialData"]; - elDiv = elMaterialData.parentNode; - let elStaticMaterialData = document.createElement('div'); - elStaticMaterialData.setAttribute("id", "property-materialData-static"); - let elMaterialDataEditor = document.createElement('div'); - elMaterialDataEditor.setAttribute("id", "property-materialData-editor"); - let elMaterialDataSaved = document.createElement('span'); - elMaterialDataSaved.setAttribute("id", "property-materialData-saved"); - elMaterialDataSaved.innerText = "Saved!"; - elDiv.childNodes[2].appendChild(elMaterialDataSaved); - elDiv.insertBefore(elStaticMaterialData, elMaterialData); - elDiv.insertBefore(elMaterialDataEditor, elMaterialData); - - // User Data Fields - let elGrabbable = elPropertyElements["grabbable"]; - let elTriggerable = elPropertyElements["triggerable"]; - let elIgnoreIK = elPropertyElements["ignoreIK"]; - elGrabbable.addEventListener('change', function() { + + // Server Script Status + let elServerScript = elPropertyElements["serverScripts"]; + let elDiv = document.createElement('div'); + elDiv.setAttribute("class", "property"); + let elLabel = document.createElement('label'); + elLabel.setAttribute("for", "property-serverScripts-status"); + elLabel.innerText = "Server Script Status"; + let elServerScriptStatus = document.createElement('span'); + elServerScriptStatus.setAttribute("id", "property-serverScripts-status"); + elDiv.appendChild(elLabel); + elDiv.appendChild(elServerScriptStatus); + elServerScript.parentNode.appendChild(elDiv); + + // Server Script Error + elDiv = document.createElement('div'); + elDiv.setAttribute("class", "property"); + let elServerScriptError = document.createElement('textarea'); + elServerScriptError.setAttribute("id", "property-serverScripts-error"); + elDiv.appendChild(elServerScriptError); + elServerScript.parentNode.appendChild(elDiv); + + let elScript = elPropertyElements["script"]; + elScript.parentNode.setAttribute("class", "property url refresh"); + elServerScript.parentNode.setAttribute("class", "property url refresh"); + + // User Data + let elUserData = elPropertyElements["userData"]; + elDiv = elUserData.parentNode; + let elStaticUserData = document.createElement('div'); + elStaticUserData.setAttribute("id", "property-userData-static"); + let elUserDataEditor = document.createElement('div'); + elUserDataEditor.setAttribute("id", "property-userData-editor"); + let elUserDataSaved = document.createElement('span'); + elUserDataSaved.setAttribute("id", "property-userData-saved"); + elUserDataSaved.innerText = "Saved!"; + elDiv.childNodes[2].appendChild(elUserDataSaved); + elDiv.insertBefore(elStaticUserData, elUserData); + elDiv.insertBefore(elUserDataEditor, elUserData); + + // Material Data + let elMaterialData = elPropertyElements["materialData"]; + elDiv = elMaterialData.parentNode; + let elStaticMaterialData = document.createElement('div'); + elStaticMaterialData.setAttribute("id", "property-materialData-static"); + let elMaterialDataEditor = document.createElement('div'); + elMaterialDataEditor.setAttribute("id", "property-materialData-editor"); + let elMaterialDataSaved = document.createElement('span'); + elMaterialDataSaved.setAttribute("id", "property-materialData-saved"); + elMaterialDataSaved.innerText = "Saved!"; + elDiv.childNodes[2].appendChild(elMaterialDataSaved); + elDiv.insertBefore(elStaticMaterialData, elMaterialData); + elDiv.insertBefore(elMaterialDataEditor, elMaterialData); + + // User Data Fields + let elGrabbable = elPropertyElements["grabbable"]; + let elTriggerable = elPropertyElements["triggerable"]; + let elIgnoreIK = elPropertyElements["ignoreIK"]; + elGrabbable.addEventListener('change', function() { userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, true); }); - elTriggerable.addEventListener('change', function() { + elTriggerable.addEventListener('change', function() { userDataChanger("grabbableKey", "triggerable", elTriggerable, elUserData, false, ['wantsTrigger']); }); elIgnoreIK.addEventListener('change', function() { userDataChanger("grabbableKey", "ignoreIK", elIgnoreIK, elUserData, true); }); - - // Special Property Callbacks - let elParentMaterialNameString = elPropertyElements["materialNameToReplace"]; - let elParentMaterialNameNumber = elPropertyElements["submeshToReplace"]; - let elParentMaterialNameCheckbox = elPropertyElements["selectSubmesh"]; - elParentMaterialNameString.addEventListener('change', function () { updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + this.value); }); + + // Special Property Callbacks + let elParentMaterialNameString = elPropertyElements["materialNameToReplace"]; + let elParentMaterialNameNumber = elPropertyElements["submeshToReplace"]; + let elParentMaterialNameCheckbox = elPropertyElements["selectSubmesh"]; + elParentMaterialNameString.addEventListener('change', function () { updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + this.value); }); elParentMaterialNameNumber.addEventListener('change', function () { updateProperty("parentMaterialName", this.value); }); elParentMaterialNameCheckbox.addEventListener('change', function () { if (this.checked) { @@ -2504,43 +2489,43 @@ function loaded() { showParentMaterialNameBox(false, elParentMaterialNameNumber, elParentMaterialNameString); } }); - - elPropertyElements["image"].addEventListener('change', createImageURLUpdateFunction('textures')); - - // Collapsible sections - let elCollapsible = document.getElementsByClassName("section-header"); + + elPropertyElements["image"].addEventListener('change', createImageURLUpdateFunction('textures')); + + // Collapsible sections + let elCollapsible = document.getElementsByClassName("section-header"); - let toggleCollapsedEvent = function(event) { - let element = event.target.parentNode.parentNode; - let isCollapsed = element.dataset.collapsed !== "true"; - element.dataset.collapsed = isCollapsed ? "true" : false; - element.setAttribute("collapsed", isCollapsed ? "true" : "false"); - element.getElementsByClassName(".collapse-icon")[0].textContent = isCollapsed ? "L" : "M"; - }; + let toggleCollapsedEvent = function(event) { + let element = event.target.parentNode.parentNode; + let isCollapsed = element.dataset.collapsed !== "true"; + element.dataset.collapsed = isCollapsed ? "true" : false; + element.setAttribute("collapsed", isCollapsed ? "true" : "false"); + element.getElementsByClassName(".collapse-icon")[0].textContent = isCollapsed ? "L" : "M"; + }; - for (let collapseIndex = 0, numCollapsibles = elCollapsible.length; collapseIndex < numCollapsibles; ++collapseIndex) { - let curCollapsibleElement = elCollapsible[collapseIndex]; - curCollapsibleElement.getElementsByTagName('span')[0].addEventListener("click", toggleCollapsedEvent, true); - } - - // Textarea scrollbars - let elTextareas = document.getElementsByTagName("TEXTAREA"); + for (let collapseIndex = 0, numCollapsibles = elCollapsible.length; collapseIndex < numCollapsibles; ++collapseIndex) { + let curCollapsibleElement = elCollapsible[collapseIndex]; + curCollapsibleElement.getElementsByTagName('span')[0].addEventListener("click", toggleCollapsedEvent, true); + } + + // Textarea scrollbars + let elTextareas = document.getElementsByTagName("TEXTAREA"); - let textareaOnChangeEvent = function(event) { - setTextareaScrolling(event.target); - }; + let textareaOnChangeEvent = function(event) { + setTextareaScrolling(event.target); + }; - for (let textAreaIndex = 0, numTextAreas = elTextareas.length; textAreaIndex < numTextAreas; ++textAreaIndex) { - let curTextAreaElement = elTextareas[textAreaIndex]; - setTextareaScrolling(curTextAreaElement); - curTextAreaElement.addEventListener("input", textareaOnChangeEvent, false); - curTextAreaElement.addEventListener("change", textareaOnChangeEvent, false); - /* FIXME: Detect and update textarea scrolling attribute on resize. Unfortunately textarea doesn't have a resize - event; mouseup is a partial stand-in but doesn't handle resizing if mouse moves outside textarea rectangle. */ - curTextAreaElement.addEventListener("mouseup", textareaOnChangeEvent, false); - } - - document.addEventListener("keydown", function (keyDown) { + for (let textAreaIndex = 0, numTextAreas = elTextareas.length; textAreaIndex < numTextAreas; ++textAreaIndex) { + let curTextAreaElement = elTextareas[textAreaIndex]; + setTextareaScrolling(curTextAreaElement); + curTextAreaElement.addEventListener("input", textareaOnChangeEvent, false); + curTextAreaElement.addEventListener("change", textareaOnChangeEvent, false); + /* FIXME: Detect and update textarea scrolling attribute on resize. Unfortunately textarea doesn't have a resize + event; mouseup is a partial stand-in but doesn't handle resizing if mouse moves outside textarea rectangle. */ + curTextAreaElement.addEventListener("mouseup", textareaOnChangeEvent, false); + } + + document.addEventListener("keydown", function (keyDown) { if (keyDown.keyCode === KEY_P && keyDown.ctrlKey) { if (keyDown.shiftKey) { EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); @@ -2549,28 +2534,28 @@ function loaded() { } } }); - + window.onblur = function() { // Fake a change event let ev = document.createEvent("HTMLEvents"); ev.initEvent("change", true, true); document.activeElement.dispatchEvent(ev); }; - - // For input and textarea elements, select all of the text on focus + + // For input and textarea elements, select all of the text on focus let els = document.querySelectorAll("input, textarea"); for (let i = 0; i < els.length; i++) { els[i].onfocus = function (e) { e.target.select(); }; } - - bindAllNonJSONEditorElements(); + + bindAllNonJSONEditorElements(); - showGroupsForType("None"); - resetProperties(); - disableProperties(); - }); + showGroupsForType("None"); + resetProperties(); + disableProperties(); + }); augmentSpinButtons(); From 8642edd144242f061b493e843ed68d2805c5ddcf Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Thu, 13 Sep 2018 18:18:23 -0700 Subject: [PATCH 055/362] Added acceleration limit filter to controller system --- interface/resources/controllers/vive.json | 9 +- .../src/controllers/impl/Filter.cpp | 2 + .../filters/AccelerationLimiterFilter.cpp | 163 ++++++++++++++++++ .../impl/filters/AccelerationLimiterFilter.h | 40 +++++ 4 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp create mode 100644 libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 8a7744efb3..442584a2ce 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -51,8 +51,13 @@ { "from": "Vive.RSCenter", "to": "Standard.RightPrimaryThumb" }, { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, - { "from": "Vive.LeftHand", "to": "Standard.LeftHand"}, - { "from": "Vive.RightHand", "to": "Standard.RightHand"}, + { "from": "Vive.LeftHand", "to": "Standard.LeftHand", + "filters" : [{"type" : "accelerationLimiter", "rotationLimit" : 4000.0, "translationLimit": 200.0}] + }, + + { "from": "Vive.RightHand", "to": "Standard.RightHand", + "filters" : [{"type" : "accelerationLimiter", "rotationLimit" : 2000.0, "translationLimit": 100.0}] + }, { "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot", diff --git a/libraries/controllers/src/controllers/impl/Filter.cpp b/libraries/controllers/src/controllers/impl/Filter.cpp index 6e6dc816d0..f230fb83dc 100644 --- a/libraries/controllers/src/controllers/impl/Filter.cpp +++ b/libraries/controllers/src/controllers/impl/Filter.cpp @@ -31,6 +31,7 @@ #include "filters/RotateFilter.h" #include "filters/LowVelocityFilter.h" #include "filters/ExponentialSmoothingFilter.h" +#include "filters/AccelerationLimiterFilter.h" using namespace controller; @@ -51,6 +52,7 @@ REGISTER_FILTER_CLASS_INSTANCE(PostTransformFilter, "postTransform") REGISTER_FILTER_CLASS_INSTANCE(RotateFilter, "rotate") REGISTER_FILTER_CLASS_INSTANCE(LowVelocityFilter, "lowVelocity") REGISTER_FILTER_CLASS_INSTANCE(ExponentialSmoothingFilter, "exponentialSmoothing") +REGISTER_FILTER_CLASS_INSTANCE(AccelerationLimiterFilter, "accelerationLimiter") const QString JSON_FILTER_TYPE = QStringLiteral("type"); const QString JSON_FILTER_PARAMS = QStringLiteral("params"); diff --git a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp new file mode 100644 index 0000000000..1f63b28786 --- /dev/null +++ b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp @@ -0,0 +1,163 @@ +// +// Created by Anthony Thibault 2018/11/09 +// 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 +// + + +#include "AccelerationLimiterFilter.h" + +#include +#include +#include "../../UserInputMapper.h" +#include "../../Input.h" +#include +#include +#include + +static const QString JSON_ROTATION_LIMIT = QStringLiteral("rotationLimit"); +static const QString JSON_TRANSLATION_LIMIT = QStringLiteral("translationLimit"); + +static glm::vec3 angularVelFromDeltaRot(const glm::quat& deltaQ, float dt) { + // Measure the angular velocity of a delta rotation quaternion by using quaternion logarithm. + // The logarithm of a unit quternion returns the axis of rotation with a length of one half the angle of rotation in the imaginary part. + // The real part will be 0. Then we multiply it by 2 / dt. turning it into the angular velocity, (except for the extra w = 0 part). + glm::quat omegaQ((2.0f / dt) * glm::log(deltaQ)); + return glm::vec3(omegaQ.x, omegaQ.y, omegaQ.z); +} + +static glm::quat deltaRotFromAngularVel(const glm::vec3& omega, float dt) { + // Convert angular velocity into a delta quaternion by using quaternion exponent. + // The exponent of quaternion will return a delta rotation around the axis of the imaginary part, by twice the angle as determined by the length of that imaginary part. + // It is the inverse of the logarithm step in angularVelFromDeltaRot + glm::quat omegaQ(0.0f, omega.x, omega.y, omega.z); + return glm::exp((dt / 2.0f) * omegaQ); +} + +static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, const glm::vec3& x2, const glm::vec3& x3, float dt, const float aLimit) { + + // measure the linear velocities of this step and the previoius step + glm::vec3 v1 = (x3 - x1) / (2.0f * dt); + glm::vec3 v0 = (x2 - x0) / (2.0f * dt); + + // compute the acceleration + const glm::vec3 a = (v1 - v0) / dt; + + // clamp the acceleration if it is over the limit + float aLen = glm::length(a); + + if (aLen > aLimit) { + // Solve for a new `v1`, such that `a` does not exceed `aLimit` + // This combines two steps: + // 1) Computing a limited accelration in the direction of `a`, but with a magnitute of `aLimit`: + // `newA = a * (aLimit / aLen)` + // 2) Computing new `v1` + // `v1 = newA * dt + v0` + // We combine the scalars from step 1 and step 2 into a single term to avoid having to do multiple scalar-vec3 multiplies. + v1 = a * ((aLimit * dt) / aLen) + v0; + + // apply limited v1 to compute filtered x3 + return v1 * dt + x2; + } else { + // did not exceed limit, no filtering necesary + return x3; + } +} + +static glm::quat filterRotation(const glm::quat& q0In, const glm::quat& q1In, const glm::quat& q2In, const glm::quat& q3In, float dt, const float aLimit) { + + // ensure quaternions have the same polarity + glm::quat q0 = q0In; + glm::quat q1 = glm::dot(q0In, q1In) < 0.0f ? -q1In : q1In; + glm::quat q2 = glm::dot(q1In, q2In) < 0.0f ? -q2In : q2In; + glm::quat q3 = glm::dot(q2In, q3In) < 0.0f ? -q3In : q3In; + + // measure the angular velocities of this step and the previous step + glm::vec3 w1 = angularVelFromDeltaRot(q3 * glm::inverse(q1), 2.0f * dt); + glm::vec3 w0 = angularVelFromDeltaRot(q2 * glm::inverse(q0), 2.0f * dt); + + const glm::vec3 a = (w1 - w0) / dt; + + // clamp the acceleration if it is over the limit + float aLen = glm::length(a); + if (aLen > aLimit) { + // solve for a new w1, such that a does not exceed the accLimit + w1 = a * ((aLimit * dt) / aLen) + w0; + + // apply limited w1 to compute filtered q3 + return deltaRotFromAngularVel(w1, dt) * q2; + } else { + // did not exceed limit, no filtering necesary + return q3; + } +} + +namespace controller { + + Pose AccelerationLimiterFilter::apply(Pose value) const { + + if (value.isValid()) { + + // to perform filtering in sensor space, we need to compute the transformations. + auto userInputMapper = DependencyManager::get(); + const InputCalibrationData calibrationData = userInputMapper->getInputCalibrationData(); + glm::mat4 sensorToAvatarMat = glm::inverse(calibrationData.avatarMat) * calibrationData.sensorToWorldMat; + glm::mat4 avatarToSensorMat = glm::inverse(calibrationData.sensorToWorldMat) * calibrationData.avatarMat; + + // transform pose into sensor space. + Pose sensorValue = value.transform(avatarToSensorMat); + + if (_prevValid) { + + const float DELTA_TIME = 0.01111111f; + + sensorValue.translation = filterTranslation(_prevPos[0], _prevPos[1], _prevPos[2], sensorValue.translation, DELTA_TIME, _translationLimit); + sensorValue.rotation = filterRotation(_prevRot[0], _prevRot[1], _prevRot[2], sensorValue.rotation, DELTA_TIME, _rotationLimit); + + // remember previous values. + _prevPos[0] = _prevPos[1]; + _prevPos[1] = _prevPos[2]; + _prevPos[2] = sensorValue.translation; + _prevRot[0] = _prevRot[1]; + _prevRot[1] = _prevRot[2]; + _prevRot[2] = sensorValue.rotation; + + // transform back into avatar space + return sensorValue.transform(sensorToAvatarMat); + } else { + // initialize previous values with the current sample. + _prevPos[0] = sensorValue.translation; + _prevPos[1] = sensorValue.translation; + _prevPos[2] = sensorValue.translation; + _prevRot[0] = sensorValue.rotation; + _prevRot[1] = sensorValue.rotation; + _prevRot[2] = sensorValue.rotation; + _prevValid = true; + + // no previous value to smooth with, so return value unchanged + return value; + } + } else { + // mark previous poses as invalid. + _prevValid = false; + + // return invalid value unchanged + return value; + } + } + + bool AccelerationLimiterFilter::parseParameters(const QJsonValue& parameters) { + if (parameters.isObject()) { + auto obj = parameters.toObject(); + if (obj.contains(JSON_ROTATION_LIMIT) && obj.contains(JSON_TRANSLATION_LIMIT)) { + _rotationLimit = (float)obj[JSON_ROTATION_LIMIT].toDouble(); + _translationLimit = (float)obj[JSON_TRANSLATION_LIMIT].toDouble(); + return true; + } + } + return false; + } + +} diff --git a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h new file mode 100644 index 0000000000..22a1ca530b --- /dev/null +++ b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h @@ -0,0 +1,40 @@ +// +// Created by Anthony Thibault 2018/11/09 +// 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 +// + +#ifndef hifi_Controllers_Filters_Acceleration_Limiter_h +#define hifi_Controllers_Filters_Acceleration_Limiter_h + +#include "../Filter.h" + +namespace controller { + + class AccelerationLimiterFilter : public Filter { + REGISTER_FILTER_CLASS(AccelerationLimiterFilter); + + public: + AccelerationLimiterFilter() {} + AccelerationLimiterFilter(float rotationLimit, float translationLimit) : + _rotationLimit(rotationLimit), + _translationLimit(translationLimit) {} + + float apply(float value) const override { return value; } + Pose apply(Pose value) const override; + bool parseParameters(const QJsonValue& parameters) override; + + private: + float _rotationLimit { FLT_MAX }; + float _translationLimit { FLT_MAX }; + + mutable glm::vec3 _prevPos[3]; // sensor space + mutable glm::quat _prevRot[3]; // sensor space + mutable bool _prevValid { false }; + }; + +} + +#endif From d15cc86735cc65a2fafaef4e2cd71040efb07db2 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 18 Sep 2018 11:53:17 -0700 Subject: [PATCH 056/362] Added deceleration limit to AccelerationLimiterFilter --- interface/resources/controllers/vive.json | 8 +++- .../filters/AccelerationLimiterFilter.cpp | 41 ++++++++++++------- .../impl/filters/AccelerationLimiterFilter.h | 9 ++-- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 442584a2ce..e737fec594 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -52,11 +52,15 @@ { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, { "from": "Vive.LeftHand", "to": "Standard.LeftHand", - "filters" : [{"type" : "accelerationLimiter", "rotationLimit" : 4000.0, "translationLimit": 200.0}] + "filters" : [{"type" : "accelerationLimiter", + "rotationAccelerationLimit" : 4000.0, "rotationDecelerationLimit" : 8000.0, + "translationAccelerationLimit": 200.0, "translationDecelerationLimit": 400.0}] }, { "from": "Vive.RightHand", "to": "Standard.RightHand", - "filters" : [{"type" : "accelerationLimiter", "rotationLimit" : 2000.0, "translationLimit": 100.0}] + "filters" : [{"type" : "accelerationLimiter", + "rotationAccelerationLimit" : 2000.0, "rotationDecelerationLimit" : 4000.0, + "translationAccelerationLimit": 100.0, "translationDecelerationLimit": 200.0}] }, { diff --git a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp index 1f63b28786..234dff1c65 100644 --- a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp @@ -17,8 +17,10 @@ #include #include -static const QString JSON_ROTATION_LIMIT = QStringLiteral("rotationLimit"); -static const QString JSON_TRANSLATION_LIMIT = QStringLiteral("translationLimit"); +static const QString JSON_ROTATION_ACCELERATION_LIMIT = QStringLiteral("rotationAccelerationLimit"); +static const QString JSON_ROTATION_DECELERATION_LIMIT = QStringLiteral("rotationDecelerationLimit"); +static const QString JSON_TRANSLATION_ACCELERATION_LIMIT = QStringLiteral("translationAccelerationLimit"); +static const QString JSON_TRANSLATION_DECELERATION_LIMIT = QStringLiteral("translationDecelerationLimit"); static glm::vec3 angularVelFromDeltaRot(const glm::quat& deltaQ, float dt) { // Measure the angular velocity of a delta rotation quaternion by using quaternion logarithm. @@ -36,7 +38,7 @@ static glm::quat deltaRotFromAngularVel(const glm::vec3& omega, float dt) { return glm::exp((dt / 2.0f) * omegaQ); } -static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, const glm::vec3& x2, const glm::vec3& x3, float dt, const float aLimit) { +static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, const glm::vec3& x2, const glm::vec3& x3, float dt, const float accLimit, const float decLimit) { // measure the linear velocities of this step and the previoius step glm::vec3 v1 = (x3 - x1) / (2.0f * dt); @@ -48,7 +50,10 @@ static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, con // clamp the acceleration if it is over the limit float aLen = glm::length(a); - if (aLen > aLimit) { + // pick limit based on if we are accelerating or decelerating. + float limit = glm::length(v1) > glm::length(v0) ? accLimit : decLimit; + + if (aLen > limit) { // Solve for a new `v1`, such that `a` does not exceed `aLimit` // This combines two steps: // 1) Computing a limited accelration in the direction of `a`, but with a magnitute of `aLimit`: @@ -56,7 +61,7 @@ static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, con // 2) Computing new `v1` // `v1 = newA * dt + v0` // We combine the scalars from step 1 and step 2 into a single term to avoid having to do multiple scalar-vec3 multiplies. - v1 = a * ((aLimit * dt) / aLen) + v0; + v1 = a * ((limit * dt) / aLen) + v0; // apply limited v1 to compute filtered x3 return v1 * dt + x2; @@ -66,7 +71,7 @@ static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, con } } -static glm::quat filterRotation(const glm::quat& q0In, const glm::quat& q1In, const glm::quat& q2In, const glm::quat& q3In, float dt, const float aLimit) { +static glm::quat filterRotation(const glm::quat& q0In, const glm::quat& q1In, const glm::quat& q2In, const glm::quat& q3In, float dt, const float accLimit, const float decLimit) { // ensure quaternions have the same polarity glm::quat q0 = q0In; @@ -79,12 +84,15 @@ static glm::quat filterRotation(const glm::quat& q0In, const glm::quat& q1In, co glm::vec3 w0 = angularVelFromDeltaRot(q2 * glm::inverse(q0), 2.0f * dt); const glm::vec3 a = (w1 - w0) / dt; + float aLen = glm::length(a); + + // pick limit based on if we are accelerating or decelerating. + float limit = glm::length(w1) > glm::length(w0) ? accLimit : decLimit; // clamp the acceleration if it is over the limit - float aLen = glm::length(a); - if (aLen > aLimit) { + if (aLen > limit) { // solve for a new w1, such that a does not exceed the accLimit - w1 = a * ((aLimit * dt) / aLen) + w0; + w1 = a * ((limit * dt) / aLen) + w0; // apply limited w1 to compute filtered q3 return deltaRotFromAngularVel(w1, dt) * q2; @@ -113,8 +121,10 @@ namespace controller { const float DELTA_TIME = 0.01111111f; - sensorValue.translation = filterTranslation(_prevPos[0], _prevPos[1], _prevPos[2], sensorValue.translation, DELTA_TIME, _translationLimit); - sensorValue.rotation = filterRotation(_prevRot[0], _prevRot[1], _prevRot[2], sensorValue.rotation, DELTA_TIME, _rotationLimit); + sensorValue.translation = filterTranslation(_prevPos[0], _prevPos[1], _prevPos[2], sensorValue.translation, + DELTA_TIME, _translationAccelerationLimit, _translationDecelerationLimit); + sensorValue.rotation = filterRotation(_prevRot[0], _prevRot[1], _prevRot[2], sensorValue.rotation, + DELTA_TIME, _rotationAccelerationLimit, _rotationDecelerationLimit); // remember previous values. _prevPos[0] = _prevPos[1]; @@ -151,9 +161,12 @@ namespace controller { bool AccelerationLimiterFilter::parseParameters(const QJsonValue& parameters) { if (parameters.isObject()) { auto obj = parameters.toObject(); - if (obj.contains(JSON_ROTATION_LIMIT) && obj.contains(JSON_TRANSLATION_LIMIT)) { - _rotationLimit = (float)obj[JSON_ROTATION_LIMIT].toDouble(); - _translationLimit = (float)obj[JSON_TRANSLATION_LIMIT].toDouble(); + if (obj.contains(JSON_ROTATION_ACCELERATION_LIMIT) && obj.contains(JSON_ROTATION_DECELERATION_LIMIT) && + obj.contains(JSON_TRANSLATION_ACCELERATION_LIMIT) && obj.contains(JSON_TRANSLATION_DECELERATION_LIMIT)) { + _rotationAccelerationLimit = (float)obj[JSON_ROTATION_ACCELERATION_LIMIT].toDouble(); + _rotationDecelerationLimit = (float)obj[JSON_ROTATION_DECELERATION_LIMIT].toDouble(); + _translationAccelerationLimit = (float)obj[JSON_TRANSLATION_ACCELERATION_LIMIT].toDouble(); + _translationDecelerationLimit = (float)obj[JSON_TRANSLATION_DECELERATION_LIMIT].toDouble(); return true; } } diff --git a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h index 22a1ca530b..6a6c7f8c33 100644 --- a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h @@ -18,17 +18,16 @@ namespace controller { public: AccelerationLimiterFilter() {} - AccelerationLimiterFilter(float rotationLimit, float translationLimit) : - _rotationLimit(rotationLimit), - _translationLimit(translationLimit) {} float apply(float value) const override { return value; } Pose apply(Pose value) const override; bool parseParameters(const QJsonValue& parameters) override; private: - float _rotationLimit { FLT_MAX }; - float _translationLimit { FLT_MAX }; + float _rotationAccelerationLimit { FLT_MAX }; + float _rotationDecelerationLimit { FLT_MAX }; + float _translationAccelerationLimit { FLT_MAX }; + float _translationDecelerationLimit { FLT_MAX }; mutable glm::vec3 _prevPos[3]; // sensor space mutable glm::quat _prevRot[3]; // sensor space From 7777f3edd0233f8850a25edc9c963ce4517c4c7f Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Wed, 19 Sep 2018 16:08:16 -0700 Subject: [PATCH 057/362] Added OutOfRangeDataStrategy parameter to Controller Settings The openvr SDK provides a way to gauge the quality of tracking on a given device via the eTrackingResult enum. * Drop - Only Running_OK is considered valid, all other eTrackingResults will return invalid poses. * Freeze - Only Running_OK is considered valid, but other valid TrackingResults will return the last Running_OK pose. In esseces this will freeze the puck in place at the last good value. * None - All valid eTrackingResults will be valid, including OutOfRange and Calibrating results. This is the default. --- interface/resources/controllers/vive.json | 13 +-- .../qml/hifi/tablet/OpenVrConfiguration.qml | 41 +++++++- plugins/openvr/src/ViveControllerManager.cpp | 95 ++++++++++++++----- plugins/openvr/src/ViveControllerManager.h | 8 ++ 4 files changed, 118 insertions(+), 39 deletions(-) diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index e737fec594..8a7744efb3 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -51,17 +51,8 @@ { "from": "Vive.RSCenter", "to": "Standard.RightPrimaryThumb" }, { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, - { "from": "Vive.LeftHand", "to": "Standard.LeftHand", - "filters" : [{"type" : "accelerationLimiter", - "rotationAccelerationLimit" : 4000.0, "rotationDecelerationLimit" : 8000.0, - "translationAccelerationLimit": 200.0, "translationDecelerationLimit": 400.0}] - }, - - { "from": "Vive.RightHand", "to": "Standard.RightHand", - "filters" : [{"type" : "accelerationLimiter", - "rotationAccelerationLimit" : 2000.0, "rotationDecelerationLimit" : 4000.0, - "translationAccelerationLimit": 100.0, "translationDecelerationLimit": 200.0}] - }, + { "from": "Vive.LeftHand", "to": "Standard.LeftHand"}, + { "from": "Vive.RightHand", "to": "Standard.RightHand"}, { "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot", diff --git a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml index c2aff08e35..f91642105f 100644 --- a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml +++ b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml @@ -822,11 +822,44 @@ Flickable { } } + Row { + id: outOfRangeDataStrategyRow + anchors.top: viveInDesktop.bottom + anchors.topMargin: 5 + anchors.left: openVrConfiguration.left + anchors.leftMargin: leftMargin + 10 + spacing: 15 + + RalewayRegular { + id: outOfRangeDataStrategyLabel + size: 12 + text: "Out Of Range Data Strategy:" + color: hifi.colors.lightGrayText + topPadding: 5 + } + + HifiControls.ComboBox { + id: outOfRangeDataStrategyComboBox + + height: 25 + width: 100 + + editable: true + colorScheme: hifi.colorSchemes.dark + model: ["None", "Freeze", "Drop"] + label: "" + + onCurrentIndexChanged: { + sendConfigurationSettings(); + } + } + } + RalewayBold { id: viveDesktopText - size: 10 + size: 12 text: "Use " + stack.selectedPlugin + " devices in desktop mode" - color: hifi.colors.white + color: hifi.colors.lightGrayText anchors { left: viveInDesktop.right @@ -946,6 +979,7 @@ Flickable { viveInDesktop.checked = desktopMode; hmdInDesktop.checked = hmdDesktopPosition; + outOfRangeDataStrategyComboBox.currentIndex = outOfRangeDataStrategyComboBox.model.indexOf(settings.outOfRangeDataStrategy); initializeButtonState(); updateCalibrationText(); @@ -1107,7 +1141,8 @@ Flickable { "armCircumference": armCircumference.realValue, "shoulderWidth": shoulderWidth.realValue, "desktopMode": viveInDesktop.checked, - "hmdDesktopTracking": hmdInDesktop.checked + "hmdDesktopTracking": hmdInDesktop.checked, + "outOfRangeDataStrategy": outOfRangeDataStrategyComboBox.model[outOfRangeDataStrategyComboBox.currentIndex] } return settingsObject; diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 3e26f304f8..cc3ca523df 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -129,6 +129,28 @@ static glm::mat4 calculateResetMat() { return glm::mat4(); } +static QString outOfRangeDataStrategyToString(ViveControllerManager::OutOfRangeDataStrategy strategy) { + switch (strategy) { + default: + case ViveControllerManager::OutOfRangeDataStrategy::None: + return "None"; + case ViveControllerManager::OutOfRangeDataStrategy::Freeze: + return "Freeze"; + case ViveControllerManager::OutOfRangeDataStrategy::Drop: + return "Drop"; + } +} + +static ViveControllerManager::OutOfRangeDataStrategy stringToOutOfRangeDataStrategy(const QString& string) { + if (string == "Drop") { + return ViveControllerManager::OutOfRangeDataStrategy::Drop; + } else if (string == "Freeze") { + return ViveControllerManager::OutOfRangeDataStrategy::Freeze; + } else { + return ViveControllerManager::OutOfRangeDataStrategy::None; + } +} + bool ViveControllerManager::isDesktopMode() { if (_container) { return !_container->getActiveDisplayPlugin()->isHmd(); @@ -288,8 +310,10 @@ void ViveControllerManager::loadSettings() { if (_inputDevice) { const double DEFAULT_ARM_CIRCUMFERENCE = 0.33; const double DEFAULT_SHOULDER_WIDTH = 0.48; + const QString DEFAULT_OUT_OF_RANGE_STRATEGY = "None"; _inputDevice->_armCircumference = settings.value("armCircumference", QVariant(DEFAULT_ARM_CIRCUMFERENCE)).toDouble(); _inputDevice->_shoulderWidth = settings.value("shoulderWidth", QVariant(DEFAULT_SHOULDER_WIDTH)).toDouble(); + _inputDevice->_outOfRangeDataStrategy = stringToOutOfRangeDataStrategy(settings.value("outOfRangeDataStrategy", QVariant(DEFAULT_OUT_OF_RANGE_STRATEGY)).toString()); } } settings.endGroup(); @@ -303,6 +327,7 @@ void ViveControllerManager::saveSettings() const { if (_inputDevice) { settings.setValue(QString("armCircumference"), _inputDevice->_armCircumference); settings.setValue(QString("shoulderWidth"), _inputDevice->_shoulderWidth); + settings.setValue(QString("outOfRangeDataStrategy"), outOfRangeDataStrategyToString(_inputDevice->_outOfRangeDataStrategy)); } } settings.endGroup(); @@ -446,6 +471,8 @@ void ViveControllerManager::InputDevice::configureCalibrationSettings(const QJso hmdDesktopTracking = iter.value().toBool(); } else if (iter.key() == "desktopMode") { hmdDesktopMode = iter.value().toBool(); + } else if (iter.key() == "outOfRangeDataStrategy") { + _outOfRangeDataStrategy = stringToOutOfRangeDataStrategy(iter.value().toString()); } iter++; } @@ -468,6 +495,7 @@ QJsonObject ViveControllerManager::InputDevice::configurationSettings() { configurationSettings["puckCount"] = (int)_validTrackedObjects.size(); configurationSettings["armCircumference"] = (double)_armCircumference * M_TO_CM; configurationSettings["shoulderWidth"] = (double)_shoulderWidth * M_TO_CM; + configurationSettings["outOfRangeDataStrategy"] = outOfRangeDataStrategyToString(_outOfRangeDataStrategy); return configurationSettings; } @@ -484,6 +512,10 @@ void ViveControllerManager::InputDevice::emitCalibrationStatus() { emit inputConfiguration->calibrationStatus(status); } +static controller::Pose buildPose(const glm::mat4& mat, const glm::vec3& linearVelocity, const glm::vec3& angularVelocity) { + return controller::Pose(extractTranslation(mat), glmExtractRotation(mat), linearVelocity, angularVelocity); +} + void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData) { uint32_t poseIndex = controller::TRACKED_OBJECT_00 + deviceIndex; printDeviceTrackingResultChange(deviceIndex); @@ -492,35 +524,48 @@ void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceInde _nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid && poseIndex <= controller::TRACKED_OBJECT_15) { - mat4& mat = mat4(); - vec3 linearVelocity = vec3(); - vec3 angularVelocity = vec3(); - // check if the device is tracking out of range, then process the correct pose depending on the result. - if (_nextSimPoseData.vrPoses[deviceIndex].eTrackingResult != vr::TrackingResult_Running_OutOfRange) { - mat = _nextSimPoseData.poses[deviceIndex]; - linearVelocity = _nextSimPoseData.linearVelocities[deviceIndex]; - angularVelocity = _nextSimPoseData.angularVelocities[deviceIndex]; - } else { - mat = _lastSimPoseData.poses[deviceIndex]; - linearVelocity = _lastSimPoseData.linearVelocities[deviceIndex]; - angularVelocity = _lastSimPoseData.angularVelocities[deviceIndex]; - - // make sure that we do not overwrite the pose in the _lastSimPose with incorrect data. - _nextSimPoseData.poses[deviceIndex] = _lastSimPoseData.poses[deviceIndex]; - _nextSimPoseData.linearVelocities[deviceIndex] = _lastSimPoseData.linearVelocities[deviceIndex]; - _nextSimPoseData.angularVelocities[deviceIndex] = _lastSimPoseData.angularVelocities[deviceIndex]; + controller::Pose pose; + switch (_outOfRangeDataStrategy) { + case OutOfRangeDataStrategy::Drop: + default: + // Drop - Mark all non Running_OK results as invald + if (_nextSimPoseData.vrPoses[deviceIndex].eTrackingResult == vr::TrackingResult_Running_OK) { + pose = buildPose(_nextSimPoseData.poses[deviceIndex], _nextSimPoseData.linearVelocities[deviceIndex], _nextSimPoseData.angularVelocities[deviceIndex]); + } else { + pose.valid = false; + } + break; + case OutOfRangeDataStrategy::None: + // None - Ignore eTrackingResult all together + pose = buildPose(_nextSimPoseData.poses[deviceIndex], _nextSimPoseData.linearVelocities[deviceIndex], _nextSimPoseData.angularVelocities[deviceIndex]); + break; + case OutOfRangeDataStrategy::Freeze: + // Freeze - Dont invalide non Running_OK poses, instead just return the last good pose. + if (_nextSimPoseData.vrPoses[deviceIndex].eTrackingResult == vr::TrackingResult_Running_OK) { + pose = buildPose(_nextSimPoseData.poses[deviceIndex], _nextSimPoseData.linearVelocities[deviceIndex], _nextSimPoseData.angularVelocities[deviceIndex]); + } else { + pose = buildPose(_lastSimPoseData.poses[deviceIndex], _lastSimPoseData.linearVelocities[deviceIndex], _lastSimPoseData.angularVelocities[deviceIndex]); + // make sure that we do not overwrite the pose in the _lastSimPose with incorrect data. + _nextSimPoseData.poses[deviceIndex] = _lastSimPoseData.poses[deviceIndex]; + _nextSimPoseData.linearVelocities[deviceIndex] = _lastSimPoseData.linearVelocities[deviceIndex]; + _nextSimPoseData.angularVelocities[deviceIndex] = _lastSimPoseData.angularVelocities[deviceIndex]; + } + break; } - controller::Pose pose(extractTranslation(mat), glmExtractRotation(mat), linearVelocity, angularVelocity); + if (pose.valid) { + // transform into avatar frame + glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; + _poseStateMap[poseIndex] = pose.transform(controllerToAvatar); - // transform into avatar frame - glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; - _poseStateMap[poseIndex] = pose.transform(controllerToAvatar); - - // but _validTrackedObjects remain in sensor frame - _validTrackedObjects.push_back(std::make_pair(poseIndex, pose)); - _trackedControllers++; + // but _validTrackedObjects remain in sensor frame + _validTrackedObjects.push_back(std::make_pair(poseIndex, pose)); + _trackedControllers++; + } else { + // insert invalid pose into state map + _poseStateMap[poseIndex] = pose; + } } else { controller::Pose invalidPose; _poseStateMap[poseIndex] = invalidPose; diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 30f8590062..f59ed9d62a 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -60,11 +60,18 @@ public: virtual void saveSettings() const override; virtual void loadSettings() override; + enum class OutOfRangeDataStrategy { + None, + Freeze, + Drop + }; + private: class InputDevice : public controller::InputDevice { public: InputDevice(vr::IVRSystem*& system); bool isHeadControllerMounted() const { return _overrideHead; } + private: // Device functions controller::Input::NamedVector getAvailableInputs() const override; @@ -162,6 +169,7 @@ private: FilteredStick _filteredLeftStick; FilteredStick _filteredRightStick; std::string _headsetName {""}; + OutOfRangeDataStrategy _outOfRangeDataStrategy { OutOfRangeDataStrategy::None }; std::vector _validTrackedObjects; std::map _pucksPostOffset; From 527c0d41952336c23428f2ddfb534072fdf95ac5 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Thu, 20 Sep 2018 11:34:15 -0700 Subject: [PATCH 058/362] Added accelerationFitlerApp. Attempts to make deceleration limit filter work better. --- .../filters/AccelerationLimiterFilter.cpp | 38 +++- .../impl/filters/AccelerationLimiterFilter.h | 2 + scripts/developer/accelerationFilterApp.js | 214 ++++++++++++++++++ 3 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 scripts/developer/accelerationFilterApp.js diff --git a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp index 234dff1c65..677e0af75a 100644 --- a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp @@ -38,7 +38,8 @@ static glm::quat deltaRotFromAngularVel(const glm::vec3& omega, float dt) { return glm::exp((dt / 2.0f) * omegaQ); } -static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, const glm::vec3& x2, const glm::vec3& x3, float dt, const float accLimit, const float decLimit) { +static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, const glm::vec3& x2, const glm::vec3& x3, + const glm::vec3& unfilteredVelocity, float dt, const float accLimit, const float decLimit) { // measure the linear velocities of this step and the previoius step glm::vec3 v1 = (x3 - x1) / (2.0f * dt); @@ -50,8 +51,8 @@ static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, con // clamp the acceleration if it is over the limit float aLen = glm::length(a); - // pick limit based on if we are accelerating or decelerating. - float limit = glm::length(v1) > glm::length(v0) ? accLimit : decLimit; + // pick limit based on if we are moving faster then our target + float limit = glm::length(v1) > glm::length(unfilteredVelocity) ? accLimit : decLimit; if (aLen > limit) { // Solve for a new `v1`, such that `a` does not exceed `aLimit` @@ -71,7 +72,8 @@ static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, con } } -static glm::quat filterRotation(const glm::quat& q0In, const glm::quat& q1In, const glm::quat& q2In, const glm::quat& q3In, float dt, const float accLimit, const float decLimit) { +static glm::quat filterRotation(const glm::quat& q0In, const glm::quat& q1In, const glm::quat& q2In, const glm::quat& q3In, + const glm::vec3& unfilteredVelocity, float dt, const float accLimit, const float decLimit) { // ensure quaternions have the same polarity glm::quat q0 = q0In; @@ -86,8 +88,8 @@ static glm::quat filterRotation(const glm::quat& q0In, const glm::quat& q1In, co const glm::vec3 a = (w1 - w0) / dt; float aLen = glm::length(a); - // pick limit based on if we are accelerating or decelerating. - float limit = glm::length(w1) > glm::length(w0) ? accLimit : decLimit; + // pick limit based on if we are moving faster then our target + float limit = glm::length(w1) > glm::length(unfilteredVelocity) ? accLimit : decLimit; // clamp the acceleration if it is over the limit if (aLen > limit) { @@ -121,9 +123,14 @@ namespace controller { const float DELTA_TIME = 0.01111111f; - sensorValue.translation = filterTranslation(_prevPos[0], _prevPos[1], _prevPos[2], sensorValue.translation, + glm::vec3 unfilteredTranslation = sensorValue.translation; + glm::vec3 unfilteredLinearVel = (unfilteredTranslation - _unfilteredPrevPos[1]) / (2.0f * DELTA_TIME); + sensorValue.translation = filterTranslation(_prevPos[0], _prevPos[1], _prevPos[2], sensorValue.translation, unfilteredLinearVel, DELTA_TIME, _translationAccelerationLimit, _translationDecelerationLimit); - sensorValue.rotation = filterRotation(_prevRot[0], _prevRot[1], _prevRot[2], sensorValue.rotation, + glm::quat unfilteredRot = sensorValue.rotation; + glm::quat unfilteredPrevRot = glm::dot(unfilteredRot, _unfilteredPrevRot[1]) < 0.0f ? -_unfilteredPrevRot[1] : _unfilteredPrevRot[1]; + glm::vec3 unfilteredAngularVel = angularVelFromDeltaRot(unfilteredRot * glm::inverse(unfilteredPrevRot), 2.0f * DELTA_TIME); + sensorValue.rotation = filterRotation(_prevRot[0], _prevRot[1], _prevRot[2], sensorValue.rotation, unfilteredAngularVel, DELTA_TIME, _rotationAccelerationLimit, _rotationDecelerationLimit); // remember previous values. @@ -134,6 +141,13 @@ namespace controller { _prevRot[1] = _prevRot[2]; _prevRot[2] = sensorValue.rotation; + _unfilteredPrevPos[0] = _unfilteredPrevPos[1]; + _unfilteredPrevPos[1] = _unfilteredPrevPos[2]; + _unfilteredPrevPos[2] = unfilteredTranslation; + _unfilteredPrevRot[0] = _unfilteredPrevRot[1]; + _unfilteredPrevRot[1] = _unfilteredPrevRot[2]; + _unfilteredPrevRot[2] = unfilteredRot; + // transform back into avatar space return sensorValue.transform(sensorToAvatarMat); } else { @@ -144,6 +158,14 @@ namespace controller { _prevRot[0] = sensorValue.rotation; _prevRot[1] = sensorValue.rotation; _prevRot[2] = sensorValue.rotation; + + _unfilteredPrevPos[0] = sensorValue.translation; + _unfilteredPrevPos[1] = sensorValue.translation; + _unfilteredPrevPos[2] = sensorValue.translation; + _unfilteredPrevRot[0] = sensorValue.rotation; + _unfilteredPrevRot[1] = sensorValue.rotation; + _unfilteredPrevRot[2] = sensorValue.rotation; + _prevValid = true; // no previous value to smooth with, so return value unchanged diff --git a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h index 6a6c7f8c33..06eeef1579 100644 --- a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h @@ -31,6 +31,8 @@ namespace controller { mutable glm::vec3 _prevPos[3]; // sensor space mutable glm::quat _prevRot[3]; // sensor space + mutable glm::vec3 _unfilteredPrevPos[3]; // sensor space + mutable glm::quat _unfilteredPrevRot[3]; // sensor space mutable bool _prevValid { false }; }; diff --git a/scripts/developer/accelerationFilterApp.js b/scripts/developer/accelerationFilterApp.js new file mode 100644 index 0000000000..52109c0f5d --- /dev/null +++ b/scripts/developer/accelerationFilterApp.js @@ -0,0 +1,214 @@ +var LEFT_HAND_INDEX = 0; +var RIGHT_HAND_INDEX = 1; +var LEFT_FOOT_INDEX = 2; +var RIGHT_FOOT_INDEX = 3; +var HIPS_INDEX = 4; +var SPINE2_INDEX = 5; + +var mappingJson = { + name: "com.highfidelity.testing.accelerationTest", + channels: [ + { + from: "Vive.LeftHand", + to: "Standard.LeftHand", + filters: [ + { + type: "accelerationLimiter", + rotationAccelerationLimit: 2000.0, + rotationDecelerationLimit: 4000.0, + translationAccelerationLimit: 100.0, + translationDecelerationLimit: 200.0 + } + ] + }, + { + from: "Vive.RightHand", + to: "Standard.RightHand", + filters: [ + { + type: "accelerationLimiter", + rotationAccelerationLimit: 2000.0, + rotationDecelerationLimit: 4000.0, + translationAccelerationLimit: 100.0, + translationDecelerationLimit: 200.0 + } + ] + }, + { + from: "Vive.LeftFoot", + to: "Standard.LeftFoot", + filters: [ + { + type: "exponentialSmoothing", + rotation: 0.15, + translation: 0.3 + } + ] + }, + { + from: "Vive.RightFoot", + to: "Standard.RightFoot", + filters: [ + { + type: "exponentialSmoothing", + rotation: 0.15, + translation: 0.3 + } + ] + }, + { + from: "Vive.Hips", + to: "Standard.Hips", + filters: [ + { + type: "exponentialSmoothing", + rotation: 0.15, + translation: 0.3 + } + ] + }, + { + from: "Vive.Spine2", + to: "Standard.Spine2", + filters: [ + { + type: "exponentialSmoothing", + rotation: 0.15, + translation: 0.3 + } + ] + } + ] +}; + +// +// tablet app boiler plate +// + +var TABLET_BUTTON_NAME = "ACCFILT"; +var HTML_URL = "https://s3.amazonaws.com/hifi-public/tony/html/accelerationFilterApp.html"; + +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); +var tabletButton = tablet.addButton({ + text: TABLET_BUTTON_NAME, + icon: "https://s3.amazonaws.com/hifi-public/tony/icons/tpose-i.svg", + activeIcon: "https://s3.amazonaws.com/hifi-public/tony/icons/tpose-a.svg" +}); + +tabletButton.clicked.connect(function () { + if (shown) { + tablet.gotoHomeScreen(); + } else { + tablet.gotoWebScreen(HTML_URL); + } +}); + +var shown = false; + +function onScreenChanged(type, url) { + if (type === "Web" && url === HTML_URL) { + tabletButton.editProperties({isActive: true}); + if (!shown) { + // hook up to event bridge + tablet.webEventReceived.connect(onWebEventReceived); + shownChanged(true); + } + shown = true; + } else { + tabletButton.editProperties({isActive: false}); + if (shown) { + // disconnect from event bridge + tablet.webEventReceived.disconnect(onWebEventReceived); + shownChanged(false); + } + shown = false; + } +} + +function getTranslationAccelerationLimit(i) { + return mappingJson.channels[i].filters[0].translationAccelerationLimit; +} +function setTranslationAccelerationLimit(i, value) { + mappingJson.channels[i].filters[0].translationAccelerationLimit = value; + mappingChanged(); +} +function getTranslationDecelerationLimit(i) { + return mappingJson.channels[i].filters[0].translationDecelerationLimit; +} +function setTranslationDecelerationLimit(i, value) { + mappingJson.channels[i].filters[0].translationDecelerationLimit = value; mappingChanged(); +} +function getRotationAccelerationLimit(i) { + return mappingJson.channels[i].filters[0].rotationAccelerationLimit; +} +function setRotationAccelerationLimit(i, value) { + mappingJson.channels[i].filters[0].rotationAccelerationLimit = value; mappingChanged(); +} +function getRotationDecelerationLimit(i) { + return mappingJson.channels[i].filters[0].rotationDecelerationLimit; +} +function setRotationDecelerationLimit(i, value) { + mappingJson.channels[i].filters[0].rotationDecelerationLimit = value; mappingChanged(); +} + +function onWebEventReceived(msg) { + if (msg.name === "init-complete") { + var values = [ + {name: "left-hand-translation-acceleration-limit", val: getTranslationAccelerationLimit(LEFT_HAND_INDEX), checked: false}, + {name: "left-hand-translation-deceleration-limit", val: getTranslationDecelerationLimit(LEFT_HAND_INDEX), checked: false}, + {name: "left-hand-rotation-acceleration-limit", val: getRotationAccelerationLimit(LEFT_HAND_INDEX), checked: false}, + {name: "left-hand-rotation-deceleration-limit", val: getRotationDecelerationLimit(LEFT_HAND_INDEX), checked: false} + ]; + tablet.emitScriptEvent(JSON.stringify(values)); + } else if (msg.name === "left-hand-translation-acceleration-limit") { + setTranslationAccelerationLimit(LEFT_HAND_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "left-hand-translation-deceleration-limit") { + setTranslationDecelerationLimit(LEFT_HAND_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "left-hand-rotation-acceleration-limit") { + setRotationAccelerationLimit(LEFT_HAND_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "left-hand-rotation-deceleration-limit") { + setRotationDecelerationLimit(LEFT_HAND_INDEX, parseInt(msg.val, 10)); + } +} + +tablet.screenChanged.connect(onScreenChanged); + +function shutdownTabletApp() { + tablet.removeButton(tabletButton); + if (shown) { + tablet.webEventReceived.disconnect(onWebEventReceived); + tablet.gotoHomeScreen(); + } + tablet.screenChanged.disconnect(onScreenChanged); +} + +// +// end tablet app boiler plate +// + +var mapping; +function mappingChanged() { + if (mapping) { + mapping.disable(); + } + mapping = Controller.parseMapping(JSON.stringify(mappingJson)); + mapping.enable(); +} + +function shownChanged(newShown) { + if (newShown) { + mappingChanged(); + } else { + mapping.disable(); + } +} + +mappingChanged(); + +Script.scriptEnding.connect(function() { + if (mapping) { + mapping.disable(); + } + tablet.removeButton(tabletButton); +}); + From 8c8b30c0f9e16ae89139e5670741cd728dc80dd9 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Thu, 20 Sep 2018 13:03:17 -0700 Subject: [PATCH 059/362] Removed deceleration updated app --- .../filters/AccelerationLimiterFilter.cpp | 33 ++---- scripts/developer/accelerationFilterApp.js | 102 ++++++++++-------- 2 files changed, 66 insertions(+), 69 deletions(-) diff --git a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp index 677e0af75a..aacbdd2cea 100644 --- a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp @@ -39,7 +39,7 @@ static glm::quat deltaRotFromAngularVel(const glm::vec3& omega, float dt) { } static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, const glm::vec3& x2, const glm::vec3& x3, - const glm::vec3& unfilteredVelocity, float dt, const float accLimit, const float decLimit) { + float dt, const float accLimit) { // measure the linear velocities of this step and the previoius step glm::vec3 v1 = (x3 - x1) / (2.0f * dt); @@ -52,9 +52,7 @@ static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, con float aLen = glm::length(a); // pick limit based on if we are moving faster then our target - float limit = glm::length(v1) > glm::length(unfilteredVelocity) ? accLimit : decLimit; - - if (aLen > limit) { + if (aLen > accLimit) { // Solve for a new `v1`, such that `a` does not exceed `aLimit` // This combines two steps: // 1) Computing a limited accelration in the direction of `a`, but with a magnitute of `aLimit`: @@ -62,7 +60,7 @@ static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, con // 2) Computing new `v1` // `v1 = newA * dt + v0` // We combine the scalars from step 1 and step 2 into a single term to avoid having to do multiple scalar-vec3 multiplies. - v1 = a * ((limit * dt) / aLen) + v0; + v1 = a * ((accLimit * dt) / aLen) + v0; // apply limited v1 to compute filtered x3 return v1 * dt + x2; @@ -73,7 +71,7 @@ static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, con } static glm::quat filterRotation(const glm::quat& q0In, const glm::quat& q1In, const glm::quat& q2In, const glm::quat& q3In, - const glm::vec3& unfilteredVelocity, float dt, const float accLimit, const float decLimit) { + float dt, const float accLimit) { // ensure quaternions have the same polarity glm::quat q0 = q0In; @@ -88,13 +86,10 @@ static glm::quat filterRotation(const glm::quat& q0In, const glm::quat& q1In, co const glm::vec3 a = (w1 - w0) / dt; float aLen = glm::length(a); - // pick limit based on if we are moving faster then our target - float limit = glm::length(w1) > glm::length(unfilteredVelocity) ? accLimit : decLimit; - // clamp the acceleration if it is over the limit - if (aLen > limit) { + if (aLen > accLimit) { // solve for a new w1, such that a does not exceed the accLimit - w1 = a * ((limit * dt) / aLen) + w0; + w1 = a * ((accLimit * dt) / aLen) + w0; // apply limited w1 to compute filtered q3 return deltaRotFromAngularVel(w1, dt) * q2; @@ -124,14 +119,11 @@ namespace controller { const float DELTA_TIME = 0.01111111f; glm::vec3 unfilteredTranslation = sensorValue.translation; - glm::vec3 unfilteredLinearVel = (unfilteredTranslation - _unfilteredPrevPos[1]) / (2.0f * DELTA_TIME); - sensorValue.translation = filterTranslation(_prevPos[0], _prevPos[1], _prevPos[2], sensorValue.translation, unfilteredLinearVel, - DELTA_TIME, _translationAccelerationLimit, _translationDecelerationLimit); + sensorValue.translation = filterTranslation(_prevPos[0], _prevPos[1], _prevPos[2], sensorValue.translation, + DELTA_TIME, _translationAccelerationLimit); glm::quat unfilteredRot = sensorValue.rotation; - glm::quat unfilteredPrevRot = glm::dot(unfilteredRot, _unfilteredPrevRot[1]) < 0.0f ? -_unfilteredPrevRot[1] : _unfilteredPrevRot[1]; - glm::vec3 unfilteredAngularVel = angularVelFromDeltaRot(unfilteredRot * glm::inverse(unfilteredPrevRot), 2.0f * DELTA_TIME); - sensorValue.rotation = filterRotation(_prevRot[0], _prevRot[1], _prevRot[2], sensorValue.rotation, unfilteredAngularVel, - DELTA_TIME, _rotationAccelerationLimit, _rotationDecelerationLimit); + sensorValue.rotation = filterRotation(_prevRot[0], _prevRot[1], _prevRot[2], sensorValue.rotation, + DELTA_TIME, _rotationAccelerationLimit); // remember previous values. _prevPos[0] = _prevPos[1]; @@ -183,12 +175,9 @@ namespace controller { bool AccelerationLimiterFilter::parseParameters(const QJsonValue& parameters) { if (parameters.isObject()) { auto obj = parameters.toObject(); - if (obj.contains(JSON_ROTATION_ACCELERATION_LIMIT) && obj.contains(JSON_ROTATION_DECELERATION_LIMIT) && - obj.contains(JSON_TRANSLATION_ACCELERATION_LIMIT) && obj.contains(JSON_TRANSLATION_DECELERATION_LIMIT)) { + if (obj.contains(JSON_ROTATION_ACCELERATION_LIMIT) && obj.contains(JSON_TRANSLATION_ACCELERATION_LIMIT)) { _rotationAccelerationLimit = (float)obj[JSON_ROTATION_ACCELERATION_LIMIT].toDouble(); - _rotationDecelerationLimit = (float)obj[JSON_ROTATION_DECELERATION_LIMIT].toDouble(); _translationAccelerationLimit = (float)obj[JSON_TRANSLATION_ACCELERATION_LIMIT].toDouble(); - _translationDecelerationLimit = (float)obj[JSON_TRANSLATION_DECELERATION_LIMIT].toDouble(); return true; } } diff --git a/scripts/developer/accelerationFilterApp.js b/scripts/developer/accelerationFilterApp.js index 52109c0f5d..a2ef937e52 100644 --- a/scripts/developer/accelerationFilterApp.js +++ b/scripts/developer/accelerationFilterApp.js @@ -9,72 +9,68 @@ var mappingJson = { name: "com.highfidelity.testing.accelerationTest", channels: [ { - from: "Vive.LeftHand", - to: "Standard.LeftHand", + from: "Standard.LeftHand", + to: "Actions.LeftHand", filters: [ { type: "accelerationLimiter", rotationAccelerationLimit: 2000.0, - rotationDecelerationLimit: 4000.0, translationAccelerationLimit: 100.0, - translationDecelerationLimit: 200.0 } ] }, { - from: "Vive.RightHand", - to: "Standard.RightHand", + from: "Standard.RightHand", + to: "Actions.RightHand", filters: [ { type: "accelerationLimiter", rotationAccelerationLimit: 2000.0, - rotationDecelerationLimit: 4000.0, translationAccelerationLimit: 100.0, - translationDecelerationLimit: 200.0 } ] }, { - from: "Vive.LeftFoot", - to: "Standard.LeftFoot", + from: "Standard.LeftFoot", + to: "Actions.LeftFoot", filters: [ { - type: "exponentialSmoothing", - rotation: 0.15, - translation: 0.3 + type: "accelerationLimiter", + rotationAccelerationLimit: 2000.0, + translationAccelerationLimit: 100.0, } ] }, { - from: "Vive.RightFoot", - to: "Standard.RightFoot", + from: "Standard.RightFoot", + to: "Actions.RightFoot", filters: [ { - type: "exponentialSmoothing", - rotation: 0.15, - translation: 0.3 + type: "accelerationLimiter", + rotationAccelerationLimit: 2000.0, + translationAccelerationLimit: 100.0, } ] }, { - from: "Vive.Hips", - to: "Standard.Hips", + from: "Standard.Hips", + to: "Actions.Hips", filters: [ { - type: "exponentialSmoothing", - rotation: 0.15, - translation: 0.3 + type: "accelerationLimiter", + rotationAccelerationLimit: 2000.0, + translationAccelerationLimit: 100.0, } ] }, { - from: "Vive.Spine2", - to: "Standard.Spine2", + from: "Standard.Spine2", + to: "Actions.Spine2", filters: [ { - type: "exponentialSmoothing", - rotation: 0.15, - translation: 0.3 + type: "accelerationLimiter", + rotationAccelerationLimit: 2000.0, + translationAccelerationLimit: 100.0, } ] } @@ -86,7 +82,7 @@ var mappingJson = { // var TABLET_BUTTON_NAME = "ACCFILT"; -var HTML_URL = "https://s3.amazonaws.com/hifi-public/tony/html/accelerationFilterApp.html"; +var HTML_URL = "https://s3.amazonaws.com/hifi-public/tony/html/accelerationFilterApp.html?2"; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var tabletButton = tablet.addButton({ @@ -132,42 +128,54 @@ function setTranslationAccelerationLimit(i, value) { mappingJson.channels[i].filters[0].translationAccelerationLimit = value; mappingChanged(); } -function getTranslationDecelerationLimit(i) { - return mappingJson.channels[i].filters[0].translationDecelerationLimit; -} -function setTranslationDecelerationLimit(i, value) { - mappingJson.channels[i].filters[0].translationDecelerationLimit = value; mappingChanged(); -} function getRotationAccelerationLimit(i) { return mappingJson.channels[i].filters[0].rotationAccelerationLimit; } function setRotationAccelerationLimit(i, value) { mappingJson.channels[i].filters[0].rotationAccelerationLimit = value; mappingChanged(); } -function getRotationDecelerationLimit(i) { - return mappingJson.channels[i].filters[0].rotationDecelerationLimit; -} -function setRotationDecelerationLimit(i, value) { - mappingJson.channels[i].filters[0].rotationDecelerationLimit = value; mappingChanged(); -} function onWebEventReceived(msg) { if (msg.name === "init-complete") { var values = [ {name: "left-hand-translation-acceleration-limit", val: getTranslationAccelerationLimit(LEFT_HAND_INDEX), checked: false}, - {name: "left-hand-translation-deceleration-limit", val: getTranslationDecelerationLimit(LEFT_HAND_INDEX), checked: false}, {name: "left-hand-rotation-acceleration-limit", val: getRotationAccelerationLimit(LEFT_HAND_INDEX), checked: false}, - {name: "left-hand-rotation-deceleration-limit", val: getRotationDecelerationLimit(LEFT_HAND_INDEX), checked: false} + {name: "right-hand-translation-acceleration-limit", val: getTranslationAccelerationLimit(RIGHT_HAND_INDEX), checked: false}, + {name: "right-hand-rotation-acceleration-limit", val: getRotationAccelerationLimit(RIGHT_HAND_INDEX), checked: false}, + {name: "left-foot-translation-acceleration-limit", val: getTranslationAccelerationLimit(LEFT_FOOT_INDEX), checked: false}, + {name: "left-foot-rotation-acceleration-limit", val: getRotationAccelerationLimit(LEFT_FOOT_INDEX), checked: false}, + {name: "right-foot-translation-acceleration-limit", val: getTranslationAccelerationLimit(RIGHT_FOOT_INDEX), checked: false}, + {name: "right-foot-rotation-acceleration-limit", val: getRotationAccelerationLimit(RIGHT_FOOT_INDEX), checked: false}, + {name: "hips-translation-acceleration-limit", val: getTranslationAccelerationLimit(HIPS_INDEX), checked: false}, + {name: "hips-rotation-acceleration-limit", val: getRotationAccelerationLimit(HIPS_INDEX), checked: false}, + {name: "spine2-translation-acceleration-limit", val: getTranslationAccelerationLimit(SPINE2_INDEX), checked: false}, + {name: "spine2-rotation-acceleration-limit", val: getRotationAccelerationLimit(SPINE2_INDEX), checked: false} ]; tablet.emitScriptEvent(JSON.stringify(values)); } else if (msg.name === "left-hand-translation-acceleration-limit") { setTranslationAccelerationLimit(LEFT_HAND_INDEX, parseInt(msg.val, 10)); - } else if (msg.name === "left-hand-translation-deceleration-limit") { - setTranslationDecelerationLimit(LEFT_HAND_INDEX, parseInt(msg.val, 10)); } else if (msg.name === "left-hand-rotation-acceleration-limit") { setRotationAccelerationLimit(LEFT_HAND_INDEX, parseInt(msg.val, 10)); - } else if (msg.name === "left-hand-rotation-deceleration-limit") { - setRotationDecelerationLimit(LEFT_HAND_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "right-hand-translation-acceleration-limit") { + setTranslationAccelerationLimit(RIGHT_HAND_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "right-hand-rotation-acceleration-limit") { + setRotationAccelerationLimit(RIGHT_HAND_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "left-foot-translation-acceleration-limit") { + setTranslationAccelerationLimit(LEFT_FOOT_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "left-foot-rotation-acceleration-limit") { + setRotationAccelerationLimit(LEFT_FOOT_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "right-foot-translation-acceleration-limit") { + setTranslationAccelerationLimit(RIGHT_FOOT_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "right-foot-rotation-acceleration-limit") { + setRotationAccelerationLimit(RIGHT_FOOT_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "hips-translation-acceleration-limit") { + setTranslationAccelerationLimit(HIPS_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "hips-rotation-acceleration-limit") { + setRotationAccelerationLimit(HIPS_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "spine2-translation-acceleration-limit") { + setTranslationAccelerationLimit(SPINE2_INDEX, parseInt(msg.val, 10)); + } else if (msg.name === "spine2-rotation-acceleration-limit") { + setRotationAccelerationLimit(SPINE2_INDEX, parseInt(msg.val, 10)); } } From a776f7e55a98bd09c729a2ca888d6b33b0d5ada6 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 25 Sep 2018 10:24:30 -0700 Subject: [PATCH 060/362] Changed default for OutOfRange data to Drop. --- plugins/openvr/src/ViveControllerManager.cpp | 2 +- plugins/openvr/src/ViveControllerManager.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index cc3ca523df..69797340dd 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -310,7 +310,7 @@ void ViveControllerManager::loadSettings() { if (_inputDevice) { const double DEFAULT_ARM_CIRCUMFERENCE = 0.33; const double DEFAULT_SHOULDER_WIDTH = 0.48; - const QString DEFAULT_OUT_OF_RANGE_STRATEGY = "None"; + const QString DEFAULT_OUT_OF_RANGE_STRATEGY = "Drop"; _inputDevice->_armCircumference = settings.value("armCircumference", QVariant(DEFAULT_ARM_CIRCUMFERENCE)).toDouble(); _inputDevice->_shoulderWidth = settings.value("shoulderWidth", QVariant(DEFAULT_SHOULDER_WIDTH)).toDouble(); _inputDevice->_outOfRangeDataStrategy = stringToOutOfRangeDataStrategy(settings.value("outOfRangeDataStrategy", QVariant(DEFAULT_OUT_OF_RANGE_STRATEGY)).toString()); diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index f59ed9d62a..06e13e1c49 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -169,7 +169,7 @@ private: FilteredStick _filteredLeftStick; FilteredStick _filteredRightStick; std::string _headsetName {""}; - OutOfRangeDataStrategy _outOfRangeDataStrategy { OutOfRangeDataStrategy::None }; + OutOfRangeDataStrategy _outOfRangeDataStrategy { OutOfRangeDataStrategy::Drop }; std::vector _validTrackedObjects; std::map _pucksPostOffset; From 0283c9fbdc34fcee1b81232f215afee02c146e17 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 2 Oct 2018 16:20:00 -0700 Subject: [PATCH 061/362] Added CriticallyDampedSpringPoseHelper to help smooth out hips. --- interface/src/avatar/MySkeletonModel.cpp | 43 ++++++++----------- interface/src/avatar/MySkeletonModel.h | 6 ++- libraries/animation/src/AnimUtil.h | 53 ++++++++++++++++++++++++ libraries/animation/src/Rig.cpp | 3 ++ libraries/animation/src/Rig.h | 2 + 5 files changed, 79 insertions(+), 28 deletions(-) diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 3084542472..f13a4c8e10 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -36,6 +36,7 @@ Rig::CharacterControllerState convertCharacterControllerState(CharacterControlle static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { glm::mat4 worldToSensorMat = glm::inverse(myAvatar->getSensorToWorldMatrix()); + // check for pinned hips. auto hipsIndex = myAvatar->getJointIndex("Hips"); if (myAvatar->isJointPinned(hipsIndex)) { @@ -199,49 +200,38 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { if (avatarHeadPose.isValid() && !(params.primaryControllerFlags[Rig::PrimaryControllerType_Hips] & (uint8_t)Rig::ControllerFlags::Enabled)) { bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS); - if (!_prevHipsValid) { - AnimPose hips = computeHipsInSensorFrame(myAvatar, isFlying); - _prevHips = hips; - } - - AnimPose hips = computeHipsInSensorFrame(myAvatar, isFlying); - // timescale in seconds const float TRANS_HORIZ_TIMESCALE = 0.15f; const float TRANS_VERT_TIMESCALE = 0.01f; // We want the vertical component of the hips to follow quickly to prevent spine squash/stretch. const float ROT_TIMESCALE = 0.15f; const float FLY_IDLE_TRANSITION_TIMESCALE = 0.25f; - float transHorizAlpha, transVertAlpha, rotAlpha; if (_flyIdleTimer < 0.0f) { - transHorizAlpha = glm::min(deltaTime / TRANS_HORIZ_TIMESCALE, 1.0f); - transVertAlpha = glm::min(deltaTime / TRANS_VERT_TIMESCALE, 1.0f); - rotAlpha = glm::min(deltaTime / ROT_TIMESCALE, 1.0f); + _smoothHipsHelper.setHorizontalTranslationTimescale(TRANS_HORIZ_TIMESCALE); + _smoothHipsHelper.setVerticalTranslationTimescale(TRANS_VERT_TIMESCALE); + _smoothHipsHelper.setRotationTimescale(ROT_TIMESCALE); } else { - transHorizAlpha = glm::min(deltaTime / FLY_IDLE_TRANSITION_TIMESCALE, 1.0f); - transVertAlpha = glm::min(deltaTime / FLY_IDLE_TRANSITION_TIMESCALE, 1.0f); - rotAlpha = glm::min(deltaTime / FLY_IDLE_TRANSITION_TIMESCALE, 1.0f); + _smoothHipsHelper.setHorizontalTranslationTimescale(FLY_IDLE_TRANSITION_TIMESCALE); + _smoothHipsHelper.setVerticalTranslationTimescale(FLY_IDLE_TRANSITION_TIMESCALE); + _smoothHipsHelper.setRotationTimescale(FLY_IDLE_TRANSITION_TIMESCALE); } - // smootly lerp hips, in sensorframe, with different coeff for horiz and vertical translation. - float hipsY = hips.trans().y; - hips.trans() = lerp(_prevHips.trans(), hips.trans(), transHorizAlpha); - hips.trans().y = lerp(_prevHips.trans().y, hipsY, transVertAlpha); - hips.rot() = safeLerp(_prevHips.rot(), hips.rot(), rotAlpha); - - _prevHips = hips; - _prevHipsValid = true; + AnimPose sensorHips = computeHipsInSensorFrame(myAvatar, isFlying); + if (!_prevIsEstimatingHips) { + _smoothHipsHelper.teleport(sensorHips); + } + sensorHips = _smoothHipsHelper.update(sensorHips, deltaTime); glm::mat4 invRigMat = glm::inverse(myAvatar->getTransform().getMatrix() * Matrices::Y_180); AnimPose sensorToRigPose(invRigMat * myAvatar->getSensorToWorldMatrix()); - params.primaryControllerPoses[Rig::PrimaryControllerType_Hips] = sensorToRigPose * hips; + params.primaryControllerPoses[Rig::PrimaryControllerType_Hips] = sensorToRigPose * sensorHips; params.primaryControllerFlags[Rig::PrimaryControllerType_Hips] = (uint8_t)Rig::ControllerFlags::Enabled | (uint8_t)Rig::ControllerFlags::Estimated; // set spine2 if we have hand controllers if (myAvatar->getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid() && - myAvatar->getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && - !(params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] & (uint8_t)Rig::ControllerFlags::Enabled)) { + myAvatar->getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && + !(params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] & (uint8_t)Rig::ControllerFlags::Enabled)) { AnimPose currentSpine2Pose; AnimPose currentHeadPose; @@ -267,8 +257,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { } } + _prevIsEstimatingHips = true; } else { - _prevHipsValid = false; + _prevIsEstimatingHips = false; } params.isTalking = head->getTimeWithoutTalking() <= 1.5f; diff --git a/interface/src/avatar/MySkeletonModel.h b/interface/src/avatar/MySkeletonModel.h index ebef9796a4..9a3559ddf7 100644 --- a/interface/src/avatar/MySkeletonModel.h +++ b/interface/src/avatar/MySkeletonModel.h @@ -10,6 +10,7 @@ #define hifi_MySkeletonModel_h #include +#include #include "MyAvatar.h" /// A skeleton loaded from a model. @@ -26,11 +27,12 @@ public: private: void updateFingers(); - AnimPose _prevHips; // sensor frame - bool _prevHipsValid { false }; + CriticallyDampedSpringPoseHelper _smoothHipsHelper; // sensor frame bool _prevIsFlying { false }; float _flyIdleTimer { 0.0f }; + float _prevIsEstimatingHips { false }; + std::map _jointRotationFrameOffsetMap; }; diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h index 9300f1a7a0..1880550435 100644 --- a/libraries/animation/src/AnimUtil.h +++ b/libraries/animation/src/AnimUtil.h @@ -38,4 +38,57 @@ AnimPose boneLookAt(const glm::vec3& target, const AnimPose& bone); // and returns a bodyRot that is also z-forward and y-up glm::quat computeBodyFacingFromHead(const glm::quat& headRot, const glm::vec3& up); + +// Uses a approximation of a critically damped spring to smooth full AnimPoses. +// It provides seperate timescales for horizontal, vertical and rotation components. +// The timescale is roughly how much time it will take the spring will reach halfway toward it's target. +class CriticallyDampedSpringPoseHelper { +public: + CriticallyDampedSpringPoseHelper() : _prevPoseValid(false) {} + + void setHorizontalTranslationTimescale(float timescale) { + _horizontalTranslationTimescale = timescale; + } + void setVerticalTranslationTimescale(float timescale) { + _verticalTranslationTimescale = timescale; + } + void setRotationTimescale(float timescale) { + _rotationTimescale = timescale; + } + + AnimPose update(const AnimPose& pose, float deltaTime) { + if (!_prevPoseValid) { + _prevPose = pose; + _prevPoseValid = true; + } + + const float horizontalTranslationAlpha = glm::min(deltaTime / _horizontalTranslationTimescale, 1.0f); + const float verticalTranslationAlpha = glm::min(deltaTime / _verticalTranslationTimescale, 1.0f); + const float rotationAlpha = glm::min(deltaTime / _rotationTimescale, 1.0f); + + const float poseY = pose.trans().y; + AnimPose newPose = _prevPose; + newPose.trans() = lerp(_prevPose.trans(), pose.trans(), horizontalTranslationAlpha); + newPose.trans().y = lerp(_prevPose.trans().y, poseY, verticalTranslationAlpha); + newPose.rot() = safeLerp(_prevPose.rot(), pose.rot(), rotationAlpha); + + _prevPose = newPose; + _prevPoseValid = true; + + return newPose; + } + + void teleport(const AnimPose& pose) { + _prevPoseValid = true; + _prevPose = pose; + } + +protected: + AnimPose _prevPose; + float _horizontalTranslationTimescale { 0.15f }; + float _verticalTranslationTimescale { 0.15f }; + float _rotationTimescale { 0.15f }; + bool _prevPoseValid; +}; + #endif diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 91d4e0f9d3..a1f3fbe8c8 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1609,6 +1609,7 @@ glm::vec3 Rig::calculateKneePoleVector(int footJointIndex, int kneeIndex, int up void Rig::updateFromControllerParameters(const ControllerParameters& params, float dt) { if (!_animSkeleton || !_animNode) { + _previousControllerParameters = params; return; } @@ -1700,6 +1701,8 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo } } } + + _previousControllerParameters = params; } void Rig::initAnimGraph(const QUrl& url) { diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 48f00d4e5d..4b1c994605 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -394,6 +394,8 @@ protected: AnimContext _lastContext; AnimVariantMap _lastAnimVars; + + ControllerParameters _previousControllerParameters; }; #endif /* defined(__hifi__Rig__) */ From 4e751237401e076b374950b00b34a9b4a7171311 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 2 Oct 2018 18:07:26 -0700 Subject: [PATCH 062/362] Blend between non-estimated and estimated hips in Rig --- libraries/animation/src/AnimUtil.h | 37 ++++++++++++++++++++++++++++++ libraries/animation/src/Rig.cpp | 23 +++++++++++++++++-- libraries/animation/src/Rig.h | 2 ++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h index 1880550435..cf190e8dbf 100644 --- a/libraries/animation/src/AnimUtil.h +++ b/libraries/animation/src/AnimUtil.h @@ -91,4 +91,41 @@ protected: bool _prevPoseValid; }; +class SnapshotBlendPoseHelper { +public: + SnapshotBlendPoseHelper() : _snapshotValid(false) {} + + void setBlendDuration(float duration) { + _duration = duration; + } + + void setSnapshot(const AnimPose& pose) { + _snapshotValid = true; + _snapshotPose = pose; + _timer = _duration; + } + + AnimPose update(const AnimPose& targetPose, float deltaTime) { + _timer -= deltaTime; + if (_timer > 0.0f) { + float alpha = (_duration - _timer) / _duration; + + // ease in expo + alpha = 1.0f - powf(2.0f, -10.0f * alpha); + + AnimPose newPose = targetPose; + newPose.blend(_snapshotPose, alpha); + return newPose; + } else { + return targetPose; + } + } + +protected: + AnimPose _snapshotPose; + float _duration { 1.0f }; + float _timer { 0.0f }; + bool _snapshotValid { false }; +}; + #endif diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index a1f3fbe8c8..d076ce5029 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1620,7 +1620,9 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo bool leftHandEnabled = params.primaryControllerFlags[PrimaryControllerType_LeftHand] & (uint8_t)ControllerFlags::Enabled; bool rightHandEnabled = params.primaryControllerFlags[PrimaryControllerType_RightHand] & (uint8_t)ControllerFlags::Enabled; bool hipsEnabled = params.primaryControllerFlags[PrimaryControllerType_Hips] & (uint8_t)ControllerFlags::Enabled; + bool prevHipsEnabled = _previousControllerParameters.primaryControllerFlags[PrimaryControllerType_Hips] & (uint8_t)ControllerFlags::Enabled; bool hipsEstimated = params.primaryControllerFlags[PrimaryControllerType_Hips] & (uint8_t)ControllerFlags::Estimated; + bool prevHipsEstimated = _previousControllerParameters.primaryControllerFlags[PrimaryControllerType_Hips] & (uint8_t)ControllerFlags::Estimated; bool leftFootEnabled = params.primaryControllerFlags[PrimaryControllerType_LeftFoot] & (uint8_t)ControllerFlags::Enabled; bool rightFootEnabled = params.primaryControllerFlags[PrimaryControllerType_RightFoot] & (uint8_t)ControllerFlags::Enabled; bool spine2Enabled = params.primaryControllerFlags[PrimaryControllerType_Spine2] & (uint8_t)ControllerFlags::Enabled; @@ -1659,9 +1661,26 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo } if (hipsEnabled) { + + // Apply a bit of smoothing when the hips toggle between estimated and non-estimated poses. + // This should help smooth out problems with the vive tracker when the sensor is occluded. + if (prevHipsEnabled && hipsEstimated != prevHipsEstimated) { + // blend from a snapshot of the previous hips. + const float HIPS_BLEND_DURATION = 0.3f; + _hipsBlendHelper.setBlendDuration(HIPS_BLEND_DURATION); + _hipsBlendHelper.setSnapshot(_previousControllerParameters.primaryControllerPoses[PrimaryControllerType_Hips]); + } else if (!prevHipsEnabled) { + // we have no sensible value to blend from. + const float HIPS_BLEND_DURATION = 0.0f; + _hipsBlendHelper.setBlendDuration(HIPS_BLEND_DURATION); + _hipsBlendHelper.setSnapshot(params.primaryControllerPoses[PrimaryControllerType_Hips]); + } + + AnimPose hips = _hipsBlendHelper.update(params.primaryControllerPoses[PrimaryControllerType_Hips], dt); + _animVars.set("hipsType", (int)IKTarget::Type::RotationAndPosition); - _animVars.set("hipsPosition", params.primaryControllerPoses[PrimaryControllerType_Hips].trans()); - _animVars.set("hipsRotation", params.primaryControllerPoses[PrimaryControllerType_Hips].rot()); + _animVars.set("hipsPosition", hips.trans()); + _animVars.set("hipsRotation", hips.rot()); } else { _animVars.set("hipsType", (int)IKTarget::Type::Unknown); } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 4b1c994605..3a2fa2d036 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -24,6 +24,7 @@ #include "AnimNode.h" #include "AnimNodeLoader.h" #include "SimpleMovingAverage.h" +#include "AnimUtil.h" class Rig; class AnimInverseKinematics; @@ -395,6 +396,7 @@ protected: AnimContext _lastContext; AnimVariantMap _lastAnimVars; + SnapshotBlendPoseHelper _hipsBlendHelper; ControllerParameters _previousControllerParameters; }; From 60d102c9bfa7ffa542f827d6869f7ae76a5c524c Mon Sep 17 00:00:00 2001 From: David Back Date: Tue, 9 Oct 2018 12:42:57 -0700 Subject: [PATCH 063/362] styling fixes --- scripts/system/html/css/edit-style.css | 39 ++---- scripts/system/html/js/entityProperties.js | 151 ++++++++++++++++++--- 2 files changed, 142 insertions(+), 48 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index e700b50839..f69ace2401 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -478,13 +478,11 @@ input[type=checkbox]:checked + label:hover { box-shadow: none; } -#properties-list > fieldset#properties-header { +#properties-list > fieldset#properties-base { margin-top: 0px; padding-bottom: 0px; } - - #properties-list > fieldset > legend { position: relative; display: table; @@ -896,7 +894,7 @@ tuple, .blue:focus, .tuple .z:focus, .tuple .roll:focus { } #properties-list fieldset .two-column { - padding-top:21px; + padding-top: 21px; display: flex; } @@ -917,7 +915,7 @@ tuple, .blue:focus, .tuple .z:focus, .tuple .roll:focus { font-family: Raleway-Regular; font-size: 12px; color: #afafaf; - height: 28px; + height: 20px; text-transform: uppercase; outline: none; } @@ -1270,7 +1268,7 @@ th#entity-hasScript { color: #afafaf; } -#base #property-type-icon { +#properties-base #property-type-icon { font-family: hifi-glyphs; font-size: 31px; color: #00b4ef; @@ -1279,52 +1277,39 @@ th#entity-hasScript { display: none; } -#base #property-type { +#properties-base #property-type { padding: 5px 24px 5px 0; border-right: 1px solid #808080; width: auto; display: inline-block; } -#base #div-locked { +#properties-base #div-property-locked { position: absolute; top: 0px; right: 140px; } -#base #div-visible { +#properties-base #div-property-visible { position: absolute; top: 20px; right: 20px; } -#base .checkbox { - position: relative; - top: -1px; -} - -#base .checkbox:last-child { - padding-left: 24px; -} - -#base .checkbox label { - background-position-y: 1px; -} - -#base .checkbox label span { +#properties-base .checkbox label span { font-family: HiFi-Glyphs; font-size: 20px; padding-right: 6px; vertical-align: top; position: relative; - top: -4px; + top: -2px; } -#base input[type=checkbox]:checked + label span { +#properties-base input[type=checkbox]:checked + label span { color: #ffffff; } -#base + hr { +#properties-base + hr { margin-top: 12px; } @@ -1387,4 +1372,4 @@ input#property-scale-button-reset { #properties-list { display: flex; flex-direction: column; -} \ No newline at end of file +} diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index d8ffc2ee25..46f16dd0a0 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -775,7 +775,7 @@ const GROUPS = [ { label: "Can cast shadow", type: "bool", - propertyName: "castShadow", + propertyName: "canCastShadow", }, { label: "Script", @@ -957,12 +957,6 @@ function disableProperties() { } } -function showElements(els, show) { - for (var i = 0; i < els.length; i++) { - els[i].style.display = (show) ? 'table' : 'none'; - } -} - function updateProperty(propertyName, propertyValue) { var properties = {}; let splitPropertyName = propertyName.split('.'); @@ -1688,11 +1682,17 @@ function resetProperties() { for (let propertyToHide in propertyShowRules) { let elPropertyToHide = elPropertyElements[propertyToHide]; if (elPropertyToHide) { - let parentNode = elPropertyToHide.parentNode; - if (parentNode === undefined && elPropertyToHide instanceof Array) { - parentNode = elPropertyToHide[0].parentNode; + let nodeToHide = elPropertyToHide; + if (elPropertyToHide.nodeName !== "DIV") { + let parentNode = elPropertyToHide.parentNode; + if (parentNode === undefined && elPropertyToHide instanceof Array) { + parentNode = elPropertyToHide[0].parentNode; + } + if (parentNode !== undefined) { + nodeToHide = parentNode; + } } - parentNode.style.display = "none"; + nodeToHide.style.display = "none"; } } } @@ -1705,13 +1705,13 @@ function loaded() { GROUPS.forEach(function(group) { let elGroup; if (group.addToGroup !== undefined) { - let fieldset = document.getElementById(group.addToGroup); + let fieldset = document.getElementById("properties-" + group.addToGroup); elGroup = document.createElement('div'); fieldset.appendChild(elGroup); } else { elGroup = document.createElement('fieldset'); elGroup.setAttribute("class", "major"); - elGroup.setAttribute("id", group.id); + elGroup.setAttribute("id", "properties-" + group.id); elPropertiesList.appendChild(elGroup); } @@ -1739,7 +1739,7 @@ function loaded() { elProperty.setAttribute("class", "sub-section-header"); } else { elProperty = document.createElement('div'); - elProperty.setAttribute("id", "div-" + propertyName); + elProperty.setAttribute("id", "div-" + propertyID); } if (group.twoColumn && property.column !== undefined && property.column !== -1) { @@ -1944,6 +1944,7 @@ function loaded() { let elInput = document.createElement('select'); elInput.setAttribute("id", propertyID); + elInput.setAttribute("propertyName", propertyName); for (let optionKey in property.options) { let option = document.createElement('option'); @@ -2035,7 +2036,7 @@ function loaded() { break; } case 'buttons': { - elProperty.setAttribute("class", "property Text"); + elProperty.setAttribute("class", "property text"); let hasLabel = property.label !== undefined; if (hasLabel) { @@ -2234,7 +2235,7 @@ function loaded() { elProperty[0].value = (propertyValue.x * 1 / elProperty[0].getAttribute("multiplier")).toFixed(4); elProperty[1].value = (propertyValue.y * 1 / elProperty[1].getAttribute("multiplier")).toFixed(4); if (elProperty[2] !== undefined) { - elProperty[2].value = (propertyValue.z * 1 / elProperty[1].getAttribute("multiplier")).toFixed(4); + elProperty[2].value = (propertyValue.z * 1 / elProperty[2].getAttribute("multiplier")).toFixed(4); } } else if (elProperty.length === 4) { // color is array of color picker and 3 input numbers @@ -2262,8 +2263,9 @@ function loaded() { } else if (elProperty.nodeName === "TEXTAREA") { elProperty.value = propertyValue; setTextareaScrolling(elProperty); - } else if (elProperty.nodeName === "SELECT") { // dropdown + } else if (elProperty.nodeName === "DT") { // dropdown elProperty.value = propertyValue; + setDropdownText(elProperty); } else { elProperty.value = propertyValue; } @@ -2275,11 +2277,17 @@ function loaded() { let show = String(propertyValue) === String(showIfThisPropertyValue); let elPropertyToShow = elPropertyElements[propertyToShow]; if (elPropertyToShow) { - let parentNode = elPropertyToShow.parentNode; - if (parentNode === undefined && elPropertyToShow instanceof Array) { - parentNode = elPropertyToShow[0].parentNode; + let nodeToShow = elPropertyToShow; + if (elPropertyToShow.nodeName !== "DIV") { + let parentNode = elPropertyToShow.parentNode; + if (parentNode === undefined && elPropertyToShow instanceof Array) { + parentNode = elPropertyToShow[0].parentNode; + } + if (parentNode !== undefined) { + nodeToShow = parentNode; + } } - parentNode.style.display = show ? "block" : "none"; + nodeToShow.style.display = show ? "table" : "none"; } } } @@ -2524,6 +2532,107 @@ function loaded() { event; mouseup is a partial stand-in but doesn't handle resizing if mouse moves outside textarea rectangle. */ curTextAreaElement.addEventListener("mouseup", textareaOnChangeEvent, false); } + + // Dropdowns + // For each dropdown the following replacement is created in place of the original dropdown... + // Structure created: + //
    + //
    display textcarat
    + //
    + //
      + //
    • 0) { + var el = elDropdowns[0]; + el.parentNode.removeChild(el); + elDropdowns = document.getElementsByTagName("select"); + } document.addEventListener("keydown", function (keyDown) { if (keyDown.keyCode === KEY_P && keyDown.ctrlKey) { From 8d56a313e891664af3ba118dbc573eda5a05c811 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 9 Oct 2018 14:05:26 -0700 Subject: [PATCH 064/362] renamed viveTrackedObjects.js to drawTrackedObjects.js Removed requirement of having a Vive hooked up. Input recordings can have trackedObjects within them. --- ...TrackedObjects.js => drawTrackedObjects.js} | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) rename scripts/developer/tests/{viveTrackedObjects.js => drawTrackedObjects.js} (62%) diff --git a/scripts/developer/tests/viveTrackedObjects.js b/scripts/developer/tests/drawTrackedObjects.js similarity index 62% rename from scripts/developer/tests/viveTrackedObjects.js rename to scripts/developer/tests/drawTrackedObjects.js index 1d60f658d9..c7d886c319 100644 --- a/scripts/developer/tests/viveTrackedObjects.js +++ b/scripts/developer/tests/drawTrackedObjects.js @@ -21,16 +21,14 @@ function shutdown() { var BLUE = {x: 0, y: 0, z: 1, w: 1}; function update(dt) { - if (Controller.Hardware.Vive) { - TRACKED_OBJECT_POSES.forEach(function (key) { - var pose = Controller.getPoseValue(Controller.Standard[key]); - if (pose.valid) { - DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, BLUE); - } else { - DebugDraw.removeMyAvatarMarker(key); - } - }); - } + TRACKED_OBJECT_POSES.forEach(function (key) { + var pose = Controller.getPoseValue(Controller.Standard[key]); + if (pose.valid) { + DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, BLUE); + } else { + DebugDraw.removeMyAvatarMarker(key); + } + }); } init(); From c0ceb713ec6315f24feb5bf52cd92c2cb522af3f Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 9 Oct 2018 16:48:28 -0700 Subject: [PATCH 065/362] Spine2 IKTarget now smoothly interpolates when ik target is disabled. Also all smooth ik interpolations use easeInExpo instead of linear curves. --- .../animation/src/AnimInverseKinematics.cpp | 20 ++++++++++++++++--- libraries/animation/src/AnimTwoBoneIK.cpp | 7 +++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 71094cc6e1..399eaf3fab 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -253,11 +253,25 @@ void AnimInverseKinematics::solve(const AnimContext& context, const std::vector< if (numLoops == MAX_IK_LOOPS) { for (size_t i = 0; i < _prevJointChainInfoVec.size(); i++) { if (_prevJointChainInfoVec[i].timer > 0.0f) { + float alpha = (JOINT_CHAIN_INTERP_TIME - _prevJointChainInfoVec[i].timer) / JOINT_CHAIN_INTERP_TIME; + + // ease in expo + alpha = 1.0f - powf(2.0f, -10.0f * alpha); + size_t chainSize = std::min(_prevJointChainInfoVec[i].jointInfoVec.size(), jointChainInfoVec[i].jointInfoVec.size()); - for (size_t j = 0; j < chainSize; j++) { - jointChainInfoVec[i].jointInfoVec[j].rot = safeMix(_prevJointChainInfoVec[i].jointInfoVec[j].rot, jointChainInfoVec[i].jointInfoVec[j].rot, alpha); - jointChainInfoVec[i].jointInfoVec[j].trans = lerp(_prevJointChainInfoVec[i].jointInfoVec[j].trans, jointChainInfoVec[i].jointInfoVec[j].trans, alpha); + + if (jointChainInfoVec[i].target.getType() != IKTarget::Type::Unknown) { + // if we are interping into an enabled target type, i.e. not off, lerp the rot and the trans. + for (size_t j = 0; j < chainSize; j++) { + jointChainInfoVec[i].jointInfoVec[j].rot = safeMix(_prevJointChainInfoVec[i].jointInfoVec[j].rot, jointChainInfoVec[i].jointInfoVec[j].rot, alpha); + jointChainInfoVec[i].jointInfoVec[j].trans = lerp(_prevJointChainInfoVec[i].jointInfoVec[j].trans, jointChainInfoVec[i].jointInfoVec[j].trans, alpha); + } + } else { + // if we are interping into a disabled target type, keep the rot & trans the same, but lerp the weight down to zero. + jointChainInfoVec[i].target.setType((int)_prevJointChainInfoVec[i].target.getType()); + jointChainInfoVec[i].target.setWeight(_prevJointChainInfoVec[i].target.getWeight() * (1.0f - alpha)); + jointChainInfoVec[i].jointInfoVec = _prevJointChainInfoVec[i].jointInfoVec; } } } diff --git a/libraries/animation/src/AnimTwoBoneIK.cpp b/libraries/animation/src/AnimTwoBoneIK.cpp index d68240d176..8960b15940 100644 --- a/libraries/animation/src/AnimTwoBoneIK.cpp +++ b/libraries/animation/src/AnimTwoBoneIK.cpp @@ -201,14 +201,17 @@ const AnimPoseVec& AnimTwoBoneIK::evaluate(const AnimVariantMap& animVars, const if (_interpType != InterpType::None) { _interpAlpha += _interpAlphaVel * dt; + // ease in expo + float easeInAlpha = 1.0f - powf(2.0f, -10.0f * _interpAlpha); + if (_interpAlpha < 1.0f) { AnimChain interpChain; if (_interpType == InterpType::SnapshotToUnderPoses) { interpChain = underChain; - interpChain.blend(_snapshotChain, _interpAlpha); + interpChain.blend(_snapshotChain, easeInAlpha); } else if (_interpType == InterpType::SnapshotToSolve) { interpChain = ikChain; - interpChain.blend(_snapshotChain, _interpAlpha); + interpChain.blend(_snapshotChain, easeInAlpha); } // copy interpChain into _poses interpChain.outputRelativePoses(_poses); From 296ea8a5af821e5f8bfeaac3e85c6eb002813189 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 10 Oct 2018 12:00:58 -0700 Subject: [PATCH 066/362] fix overlay issues during interstitial mode --- .../controllerModules/webSurfaceLaserInput.js | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index 2412e2fa1c..cf61693dfd 100644 --- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js +++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -20,6 +20,7 @@ Script.include("/~/system/libraries/controllers.js"); this.hand = hand; this.otherHand = this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND; this.running = false; + this.ignoredOverlays = []; this.parameters = makeDispatcherModuleParameters( 160, @@ -67,6 +68,42 @@ Script.include("/~/system/libraries/controllers.js"); return this.hand === RIGHT_HAND ? leftOverlayLaserInput : rightOverlayLaserInput; }; + this.addOverlayToIgnoreList = 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.ignoredOverlays.indexOf(objectID); + + if (overlayIndex === -1) { + 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.ignoredOverlays.push(objectID); + } + } + } + } + }; + + this.restoreIgnoredOverlays = function() { + for (var index = 0; index < this.ignoredOverlays.length; index++) { + var data = { + action: 'remove', + id: this.ignoredOverlays[index] + }; + Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)); + } + + this.ignoredOverlays = []; + }; + this.isPointingAtTriggerable = 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 @@ -143,6 +180,7 @@ Script.include("/~/system/libraries/controllers.js"); var allowThisModule = !otherModuleRunning && !grabModuleNeedsToRun; var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE; var laserOn = isTriggerPressed || this.parameters.handLaser.allwaysOn; + this.addOverlayToIgnoreList(controllerData); if (allowThisModule) { if (isTriggerPressed && !this.isPointingAtTriggerable(controllerData, isTriggerPressed, true)) { // if trigger is down + not pointing at a web entity, keep running web surface laser @@ -156,6 +194,7 @@ Script.include("/~/system/libraries/controllers.js"); this.deleteContextOverlay(); this.running = false; this.dominantHandOverride = false; + this.restoreIgnoredOverlays(); return makeRunningValues(false, [], []); } } @@ -163,6 +202,7 @@ Script.include("/~/system/libraries/controllers.js"); this.deleteContextOverlay(); this.running = false; this.dominantHandOverride = false; + this.restoreIgnoredOverlays(); return makeRunningValues(false, [], []); }; } From 82918a5c31c64767fbbcf3b77332564e193b4134 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 10 Oct 2018 12:31:16 -0700 Subject: [PATCH 067/362] Add animation support --- interface/src/avatar/AvatarManager.cpp | 41 +++++++++++++++++++ interface/src/avatar/AvatarManager.h | 1 + .../src/avatars-renderer/Avatar.h | 14 +++++++ 3 files changed, 56 insertions(+) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index d31b201dc7..1f2c9e462d 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -83,10 +83,18 @@ AvatarManager::AvatarManager(QObject* parent) : const int AVATAR_TRANSIT_FRAME_COUNT = 11; // Based on testing const int AVATAR_TRANSIT_FRAMES_PER_METER = 1; // Based on testing + const QString START_ANIMATION_URL = "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx"; + const QString MIDDLE_ANIMATION_URL = "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx"; + const QString END_ANIMATION_URL = "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx"; + _transitConfig._totalFrames = AVATAR_TRANSIT_FRAME_COUNT; _transitConfig._triggerDistance = AVATAR_TRANSIT_TRIGGER_DISTANCE; _transitConfig._framesPerMeter = AVATAR_TRANSIT_FRAMES_PER_METER; _transitConfig._isDistanceBased = true; + + _transitConfig._startTransitAnimation = AvatarTransit::TransitAnimation(START_ANIMATION_URL, 0, 14); + _transitConfig._middleTransitAnimation = AvatarTransit::TransitAnimation(MIDDLE_ANIMATION_URL, 15, 0); + _transitConfig._endTransitAnimation = AvatarTransit::TransitAnimation(END_ANIMATION_URL, 16, 38); } AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { @@ -134,6 +142,39 @@ void AvatarManager::setSpace(workload::SpacePointer& space ) { _space = space; } +void AvatarManager::playTransitAnimations(AvatarTransit::Status status) { + auto startAnimation = _transitConfig._startTransitAnimation; + auto middleAnimation = _transitConfig._middleTransitAnimation; + auto endAnimation = _transitConfig._endTransitAnimation; + + const float REFERENCE_FPS = 30.0f; + switch (status) { + case AvatarTransit::Status::START_FRAME: + qDebug() << "START_FRAME"; + _myAvatar->overrideAnimation(startAnimation._animationUrl, REFERENCE_FPS, false, startAnimation._firstFrame, + startAnimation._firstFrame + startAnimation._frameCount); + break; + case AvatarTransit::Status::START_TRANSIT: + qDebug() << "START_TRANSIT"; + _myAvatar->overrideAnimation(middleAnimation._animationUrl, REFERENCE_FPS, false, middleAnimation._firstFrame, + middleAnimation._firstFrame + middleAnimation._frameCount); + break; + case AvatarTransit::Status::END_TRANSIT: + qDebug() << "END_TRANSIT"; + _myAvatar->overrideAnimation(endAnimation._animationUrl, REFERENCE_FPS, false, endAnimation._firstFrame, + endAnimation._firstFrame + endAnimation._frameCount); + break; + case AvatarTransit::Status::END_FRAME: + qDebug() << "END_FRAME"; + _myAvatar->restoreAnimation(); + break; + case AvatarTransit::Status::IDLE: + break; + case AvatarTransit::Status::TRANSITING: + break; + } +} + void AvatarManager::updateMyAvatar(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "AvatarManager::updateMyAvatar()"); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 209b976c44..0dc39e991b 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -213,6 +213,7 @@ private: // frequently grabs a read lock on the hash to get a given avatar by ID void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override; + void playTransitAnimations(AvatarTransit::Status status); QVector _avatarsToFade; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 1087f74c07..21e359051f 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -54,9 +54,11 @@ class AvatarTransit { public: enum Status { IDLE = 0, + START_FRAME, START_TRANSIT, TRANSITING, END_TRANSIT, + END_FRAME, ABORT_TRANSIT }; @@ -67,6 +69,15 @@ public: EASE_IN_OUT }; + struct TransitAnimation { + TransitAnimation(){}; + TransitAnimation(const QString& animationUrl, int firstFrame, int frameCount) : + _firstFrame(firstFrame), _frameCount(frameCount), _animationUrl(animationUrl){}; + int _firstFrame; + int _frameCount; + QString _animationUrl; + }; + struct TransitConfig { TransitConfig() {}; int _totalFrames { 0 }; @@ -74,6 +85,9 @@ public: bool _isDistanceBased { false }; float _triggerDistance { 0 }; EaseType _easeType { EaseType::EASE_OUT }; + TransitAnimation _startTransitAnimation; + TransitAnimation _middleTransitAnimation; + TransitAnimation _endTransitAnimation; }; AvatarTransit() {}; From 7de784ce27d6c9d41e3847e74caf778d90b71c87 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 10 Oct 2018 12:32:55 -0700 Subject: [PATCH 068/362] First steps (definitely not working) --- assignment-client/src/Agent.cpp | 2 +- interface/src/Application.cpp | 14 ++- interface/src/assets/ATPAssetMigrator.cpp | 4 +- .../scripting/SpeechScriptingInterface.cpp | 96 +++++++++++++++++++ .../src/scripting/SpeechScriptingInterface.h | 76 +++++++++++++++ .../src/RenderableWebEntityItem.cpp | 2 +- libraries/entities/src/EntityEditFilters.cpp | 2 +- .../src/model-networking/TextureCache.cpp | 4 +- libraries/networking/src/AddressManager.cpp | 10 +- libraries/networking/src/DomainHandler.cpp | 2 +- .../networking/src/NetworkingConstants.h | 10 +- libraries/networking/src/ResourceCache.cpp | 2 +- libraries/networking/src/ResourceManager.cpp | 18 ++-- 13 files changed, 209 insertions(+), 33 deletions(-) create mode 100644 interface/src/scripting/SpeechScriptingInterface.cpp create mode 100644 interface/src/scripting/SpeechScriptingInterface.h diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 639e9f924b..ee21fff8c0 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -216,7 +216,7 @@ void Agent::requestScript() { } // make sure this is not a script request for the file scheme - if (scriptURL.scheme() == URL_SCHEME_FILE) { + if (scriptURL.scheme() == HIFI_URL_SCHEME_FILE) { qWarning() << "Cannot load script for Agent from local filesystem."; scriptRequestFinished(); return; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index aa2b382c58..74532ef53a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -182,6 +182,7 @@ #include "scripting/RatesScriptingInterface.h" #include "scripting/SelectionScriptingInterface.h" #include "scripting/WalletScriptingInterface.h" +#include "scripting/SpeechScriptingInterface.h" #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif @@ -528,11 +529,11 @@ bool isDomainURL(QUrl url) { if (url.scheme() == URL_SCHEME_HIFI) { return true; } - if (url.scheme() != URL_SCHEME_FILE) { + if (url.scheme() != HIFI_URL_SCHEME_FILE) { // TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can // be loaded over http(s) - // && url.scheme() != URL_SCHEME_HTTP && - // url.scheme() != URL_SCHEME_HTTPS + // && url.scheme() != HIFI_URL_SCHEME_HTTP && + // url.scheme() != HIFI_URL_SCHEME_HTTPS return false; } if (url.path().endsWith(".json", Qt::CaseInsensitive) || @@ -943,6 +944,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); @@ -1024,8 +1026,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // If the URL scheme is http(s) or ftp, then use as is, else - treat it as a local file // This is done so as not break previous command line scripts - if (testScriptPath.left(URL_SCHEME_HTTP.length()) == URL_SCHEME_HTTP || - testScriptPath.left(URL_SCHEME_FTP.length()) == URL_SCHEME_FTP) { + if (testScriptPath.left(HIFI_URL_SCHEME_HTTP.length()) == HIFI_URL_SCHEME_HTTP || + testScriptPath.left(HIFI_URL_SCHEME_FTP.length()) == HIFI_URL_SCHEME_FTP) { setProperty(hifi::properties::TEST, QUrl::fromUserInput(testScriptPath)); } else if (QFileInfo(testScriptPath).exists()) { @@ -3127,6 +3129,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); surfaceContext->setContextProperty("Wallet", DependencyManager::get().data()); surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); + surfaceContext->setContextProperty("Speech", DependencyManager::get().data()); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); @@ -6797,6 +6800,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("Wallet", DependencyManager::get().data()); scriptEngine->registerGlobalObject("AddressManager", DependencyManager::get().data()); scriptEngine->registerGlobalObject("HifiAbout", AboutUtil::getInstance()); + scriptEngine->registerGlobalObject("Speech", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine.data(), OverlayIDtoScriptValue, OverlayIDfromScriptValue); diff --git a/interface/src/assets/ATPAssetMigrator.cpp b/interface/src/assets/ATPAssetMigrator.cpp index 45ac80b054..423a4b8509 100644 --- a/interface/src/assets/ATPAssetMigrator.cpp +++ b/interface/src/assets/ATPAssetMigrator.cpp @@ -121,8 +121,8 @@ void ATPAssetMigrator::loadEntityServerFile() { QUrl migrationURL = QUrl(migrationURLString); if (!_ignoredUrls.contains(migrationURL) - && (migrationURL.scheme() == URL_SCHEME_HTTP || migrationURL.scheme() == URL_SCHEME_HTTPS - || migrationURL.scheme() == URL_SCHEME_FILE || migrationURL.scheme() == URL_SCHEME_FTP)) { + && (migrationURL.scheme() == HIFI_URL_SCHEME_HTTP || migrationURL.scheme() == HIFI_URL_SCHEME_HTTPS + || migrationURL.scheme() == HIFI_URL_SCHEME_FILE || migrationURL.scheme() == HIFI_URL_SCHEME_FTP)) { if (_pendingReplacements.contains(migrationURL)) { // we already have a request out for this asset, just store the QJsonValueRef diff --git a/interface/src/scripting/SpeechScriptingInterface.cpp b/interface/src/scripting/SpeechScriptingInterface.cpp new file mode 100644 index 0000000000..a38e1aa824 --- /dev/null +++ b/interface/src/scripting/SpeechScriptingInterface.cpp @@ -0,0 +1,96 @@ +// +// SpeechScriptingInterface.cpp +// interface/src/scripting +// +// Created by Zach Fox on 2018-10-10. +// 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 +// + +#include "SpeechScriptingInterface.h" +#include "avatar/AvatarManager.h" +#include + +SpeechScriptingInterface::SpeechScriptingInterface() { + // + // Create text to speech engine + // + HRESULT hr = m_tts.CoCreateInstance(CLSID_SpVoice); + if (FAILED(hr)) { + ATLTRACE(TEXT("Text-to-speech creation failed.\n")); + AtlThrow(hr); + } + + // + // Get token corresponding to default voice + // + hr = SpGetDefaultTokenFromCategoryId(SPCAT_VOICES, &m_voiceToken, FALSE); + if (FAILED(hr)) { + ATLTRACE(TEXT("Can't get default voice token.\n")); + AtlThrow(hr); + } + + // + // Set default voice + // + hr = m_tts->SetVoice(m_voiceToken); + if (FAILED(hr)) { + ATLTRACE(TEXT("Can't set default voice.\n")); + AtlThrow(hr); + } + + WAVEFORMATEX fmt; + fmt.wFormatTag = WAVE_FORMAT_PCM; + fmt.nSamplesPerSec = 48000; + fmt.wBitsPerSample = 16; + fmt.nChannels = 1; + fmt.nBlockAlign = fmt.nChannels * fmt.wBitsPerSample / 8; + fmt.nAvgBytesPerSec = fmt.nSamplesPerSec * fmt.nBlockAlign; + fmt.cbSize = 0; + + BYTE* pcontent = new BYTE[1024 * 1000]; + + cpIStream = SHCreateMemStream(NULL, 0); + hr = outputStream->SetBaseStream(cpIStream, SPDFID_WaveFormatEx, &fmt); + + hr = m_tts->SetOutput(outputStream, true); + if (FAILED(hr)) { + ATLTRACE(TEXT("Can't set output stream.\n")); + AtlThrow(hr); + } +} + +SpeechScriptingInterface::~SpeechScriptingInterface() { + +} + +void SpeechScriptingInterface::speakText(const QString& textToSpeak) { + ULONG streamNumber; + HRESULT hr = m_tts->Speak(reinterpret_cast(textToSpeak.utf16()), + SPF_IS_NOT_XML | SPF_ASYNC | SPF_PURGEBEFORESPEAK, + &streamNumber); + if (FAILED(hr)) { + ATLTRACE(TEXT("Speak failed.\n")); + AtlThrow(hr); + } + + m_tts->WaitUntilDone(-1); + + outputStream->GetBaseStream(&cpIStream); + ULARGE_INTEGER StreamSize; + StreamSize.LowPart = 0; + hr = IStream_Size(cpIStream, &StreamSize); + + DWORD dwSize = StreamSize.QuadPart; + char* buf1 = new char[dwSize + 1]; + hr = IStream_Read(cpIStream, buf1, dwSize); + + QByteArray byteArray = QByteArray::QByteArray(buf1, (int)dwSize); + AudioInjectorOptions options; + + options.position = DependencyManager::get()->getMyAvatarPosition(); + + AudioInjector::playSound(byteArray, options); +} diff --git a/interface/src/scripting/SpeechScriptingInterface.h b/interface/src/scripting/SpeechScriptingInterface.h new file mode 100644 index 0000000000..311bd80605 --- /dev/null +++ b/interface/src/scripting/SpeechScriptingInterface.h @@ -0,0 +1,76 @@ +// SpeechScriptingInterface.h +// interface/src/scripting +// +// Created by Zach Fox on 2018-10-10. +// 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 +// + +#ifndef hifi_SpeechScriptingInterface_h +#define hifi_SpeechScriptingInterface_h + +#include +#include + +#include // SAPI +#include // SAPI Helper + +class SpeechScriptingInterface : public QObject, public Dependency { + Q_OBJECT + +public: + SpeechScriptingInterface(); + ~SpeechScriptingInterface(); + + Q_INVOKABLE void speakText(const QString& textToSpeak); + +private: + + class CComAutoInit { + public: + // Initializes COM using CoInitialize. + // On failure, signals error using AtlThrow. + CComAutoInit() { + HRESULT hr = ::CoInitialize(NULL); + if (FAILED(hr)) { + ATLTRACE(TEXT("CoInitialize() failed in CComAutoInit constructor (hr=0x%08X).\n"), hr); + AtlThrow(hr); + } + } + + // Initializes COM using CoInitializeEx. + // On failure, signals error using AtlThrow. + explicit CComAutoInit(__in DWORD dwCoInit) { + HRESULT hr = ::CoInitializeEx(NULL, dwCoInit); + if (FAILED(hr)) { + ATLTRACE(TEXT("CoInitializeEx() failed in CComAutoInit constructor (hr=0x%08X).\n"), hr); + AtlThrow(hr); + } + } + + // Uninitializes COM using CoUninitialize. + ~CComAutoInit() { ::CoUninitialize(); } + + // + // Ban copy + // + private: + CComAutoInit(const CComAutoInit&); + }; + + // COM initialization and cleanup (must precede other COM related data members) + CComAutoInit m_comInit; + + // Text to speech engine + CComPtr m_tts; + + // Default voice token + CComPtr m_voiceToken; + + CComPtr outputStream; + CComPtr cpIStream; +}; + +#endif // hifi_SpeechScriptingInterface_h diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index bc9ac84c91..ac5e43e558 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -54,7 +54,7 @@ WebEntityRenderer::ContentType WebEntityRenderer::getContentType(const QString& const QUrl url(urlString); auto scheme = url.scheme(); - if (scheme == URL_SCHEME_ABOUT || scheme == URL_SCHEME_HTTP || scheme == URL_SCHEME_HTTPS || + if (scheme == HIFI_URL_SCHEME_ABOUT || scheme == HIFI_URL_SCHEME_HTTP || scheme == HIFI_URL_SCHEME_HTTPS || urlString.toLower().endsWith(".htm") || urlString.toLower().endsWith(".html")) { return ContentType::HtmlContent; } diff --git a/libraries/entities/src/EntityEditFilters.cpp b/libraries/entities/src/EntityEditFilters.cpp index 94df7eb465..9a3f056e04 100644 --- a/libraries/entities/src/EntityEditFilters.cpp +++ b/libraries/entities/src/EntityEditFilters.cpp @@ -183,7 +183,7 @@ void EntityEditFilters::addFilter(EntityItemID entityID, QString filterURL) { } // The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp) - if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == URL_SCHEME_FILE)) { + if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == HIFI_URL_SCHEME_FILE)) { qWarning() << "Cannot load script from local filesystem, because assignment may be on a different computer."; scriptRequestFinished(entityID); return; diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index e8aec5e60e..11a5b2f167 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -329,7 +329,7 @@ _maxNumPixels(100) static bool isLocalUrl(const QUrl& url) { auto scheme = url.scheme(); - return (scheme == URL_SCHEME_FILE || scheme == URL_SCHEME_QRC || scheme == RESOURCE_SCHEME); + return (scheme == HIFI_URL_SCHEME_FILE || scheme == URL_SCHEME_QRC || scheme == RESOURCE_SCHEME); } NetworkTexture::NetworkTexture(const QUrl& url, image::TextureUsage::Type type, const QByteArray& content, int maxNumPixels) : @@ -502,7 +502,7 @@ void NetworkTexture::handleLocalRequestCompleted() { void NetworkTexture::makeLocalRequest() { const QString scheme = _activeUrl.scheme(); QString path; - if (scheme == URL_SCHEME_FILE) { + if (scheme == HIFI_URL_SCHEME_FILE) { path = PathUtils::expandToLocalDataAbsolutePath(_activeUrl).toLocalFile(); } else { path = ":" + _activeUrl.path(); diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index f8ab8ceaec..e6957728e8 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -155,12 +155,12 @@ void AddressManager::goForward() { void AddressManager::storeCurrentAddress() { auto url = currentAddress(); - if (url.scheme() == URL_SCHEME_FILE || + if (url.scheme() == HIFI_URL_SCHEME_FILE || (url.scheme() == URL_SCHEME_HIFI && !url.host().isEmpty())) { // TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can // be loaded over http(s) - // url.scheme() == URL_SCHEME_HTTP || - // url.scheme() == URL_SCHEME_HTTPS || + // url.scheme() == HIFI_URL_SCHEME_HTTP || + // url.scheme() == HIFI_URL_SCHEME_HTTPS || bool isInErrorState = DependencyManager::get()->getDomainHandler().isInErrorState(); if (isConnected()) { if (isInErrorState) { @@ -331,11 +331,11 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { emit lookupResultsFinished(); return true; - } else if (lookupUrl.scheme() == URL_SCHEME_FILE) { + } else if (lookupUrl.scheme() == HIFI_URL_SCHEME_FILE) { // TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can // be loaded over http(s) // lookupUrl.scheme() == URL_SCHEME_HTTP || - // lookupUrl.scheme() == URL_SCHEME_HTTPS || + // lookupUrl.scheme() == HIFI_URL_SCHEME_HTTPS || // TODO once a file can return a connection refusal if there were to be some kind of load error, we'd // need to store the previous domain tried in _lastVisitedURL. For now , do not store it. diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 615546b410..3dda182989 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -194,7 +194,7 @@ void DomainHandler::setURLAndID(QUrl domainURL, QUuid domainID) { _sockAddr.clear(); // if this is a file URL we need to see if it has a ~ for us to expand - if (domainURL.scheme() == URL_SCHEME_FILE) { + if (domainURL.scheme() == HIFI_URL_SCHEME_FILE) { domainURL = PathUtils::expandToLocalDataAbsolutePath(domainURL); } } diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index 839e269fd4..302e0efa02 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -30,14 +30,14 @@ namespace NetworkingConstants { QUrl METAVERSE_SERVER_URL(); } -const QString URL_SCHEME_ABOUT = "about"; +const QString HIFI_URL_SCHEME_ABOUT = "about"; const QString URL_SCHEME_HIFI = "hifi"; const QString URL_SCHEME_HIFIAPP = "hifiapp"; const QString URL_SCHEME_QRC = "qrc"; -const QString URL_SCHEME_FILE = "file"; -const QString URL_SCHEME_HTTP = "http"; -const QString URL_SCHEME_HTTPS = "https"; -const QString URL_SCHEME_FTP = "ftp"; +const QString HIFI_URL_SCHEME_FILE = "file"; +const QString HIFI_URL_SCHEME_HTTP = "http"; +const QString HIFI_URL_SCHEME_HTTPS = "https"; +const QString HIFI_URL_SCHEME_FTP = "ftp"; const QString URL_SCHEME_ATP = "atp"; #endif // hifi_NetworkingConstants_h diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index aed9f3b0e5..1328606be4 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -114,7 +114,7 @@ QSharedPointer ResourceCacheSharedItems::getHighestPendingRequest() { // Check load priority float priority = resource->getLoadPriority(); - bool isFile = resource->getURL().scheme() == URL_SCHEME_FILE; + bool isFile = resource->getURL().scheme() == HIFI_URL_SCHEME_FILE; if (priority >= highestPriority && (isFile || !currentHighestIsFile)) { highestPriority = priority; highestIndex = i; diff --git a/libraries/networking/src/ResourceManager.cpp b/libraries/networking/src/ResourceManager.cpp index 553f0d0a61..40d6570f48 100644 --- a/libraries/networking/src/ResourceManager.cpp +++ b/libraries/networking/src/ResourceManager.cpp @@ -82,10 +82,10 @@ const QSet& getKnownUrls() { static std::once_flag once; std::call_once(once, [] { knownUrls.insert(URL_SCHEME_QRC); - knownUrls.insert(URL_SCHEME_FILE); - knownUrls.insert(URL_SCHEME_HTTP); - knownUrls.insert(URL_SCHEME_HTTPS); - knownUrls.insert(URL_SCHEME_FTP); + knownUrls.insert(HIFI_URL_SCHEME_FILE); + knownUrls.insert(HIFI_URL_SCHEME_HTTP); + knownUrls.insert(HIFI_URL_SCHEME_HTTPS); + knownUrls.insert(HIFI_URL_SCHEME_FTP); knownUrls.insert(URL_SCHEME_ATP); }); return knownUrls; @@ -97,7 +97,7 @@ QUrl ResourceManager::normalizeURL(const QUrl& originalUrl) { if (!getKnownUrls().contains(scheme)) { // check the degenerative file case: on windows we can often have urls of the form c:/filename // this checks for and works around that case. - QUrl urlWithFileScheme{ URL_SCHEME_FILE + ":///" + url.toString() }; + QUrl urlWithFileScheme{ HIFI_URL_SCHEME_FILE + ":///" + url.toString() }; if (!urlWithFileScheme.toLocalFile().isEmpty()) { return urlWithFileScheme; } @@ -118,9 +118,9 @@ ResourceRequest* ResourceManager::createResourceRequest(QObject* parent, const Q ResourceRequest* request = nullptr; - if (scheme == URL_SCHEME_FILE || scheme == URL_SCHEME_QRC) { + if (scheme == HIFI_URL_SCHEME_FILE || scheme == URL_SCHEME_QRC) { request = new FileResourceRequest(normalizedURL); - } else if (scheme == URL_SCHEME_HTTP || scheme == URL_SCHEME_HTTPS || scheme == URL_SCHEME_FTP) { + } else if (scheme == HIFI_URL_SCHEME_HTTP || scheme == HIFI_URL_SCHEME_HTTPS || scheme == HIFI_URL_SCHEME_FTP) { request = new HTTPResourceRequest(normalizedURL); } else if (scheme == URL_SCHEME_ATP) { if (!_atpSupportEnabled) { @@ -143,10 +143,10 @@ ResourceRequest* ResourceManager::createResourceRequest(QObject* parent, const Q bool ResourceManager::resourceExists(const QUrl& url) { auto scheme = url.scheme(); - if (scheme == URL_SCHEME_FILE) { + if (scheme == HIFI_URL_SCHEME_FILE) { QFileInfo file{ url.toString() }; return file.exists(); - } else if (scheme == URL_SCHEME_HTTP || scheme == URL_SCHEME_HTTPS || scheme == URL_SCHEME_FTP) { + } else if (scheme == HIFI_URL_SCHEME_HTTP || scheme == HIFI_URL_SCHEME_HTTPS || scheme == HIFI_URL_SCHEME_FTP) { auto& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest request{ url }; From 09c3b39f0d14202a6efb17425a68e4ea46efd0c1 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Wed, 10 Oct 2018 14:00:16 -0700 Subject: [PATCH 069/362] Update puck-attach.js with model that matches the orientation of the pucks --- scripts/developer/tests/puck-attach.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/developer/tests/puck-attach.js b/scripts/developer/tests/puck-attach.js index 04d5db5710..019a911535 100644 --- a/scripts/developer/tests/puck-attach.js +++ b/scripts/developer/tests/puck-attach.js @@ -85,7 +85,7 @@ function entityExists(entityID) { return Object.keys(Entities.getEntityProperties(entityID)).length > 0; } -var VIVE_PUCK_MODEL = "http://content.highfidelity.com/seefo/production/puck-attach/vive_tracker_puck.obj"; +var VIVE_PUCK_MODEL = "https://s3.amazonaws.com/hifi-public/tony/vive_tracker_puck_y180z180.obj"; var VIVE_PUCK_DIMENSIONS = { x: 0.0945, y: 0.0921, z: 0.0423 }; // 1/1000th scale of model var VIVE_PUCK_SEARCH_DISTANCE = 1.5; // metres var VIVE_PUCK_SPAWN_DISTANCE = 0.5; // metres @@ -304,4 +304,4 @@ tabletButton.clicked.connect(function () { tablet.gotoWebScreen(TABLET_APP_URL); } }); -}()); // END LOCAL_SCOPE \ No newline at end of file +}()); // END LOCAL_SCOPE From a47a1c03e6b68492003aac9c3aa237bbc40dc6c3 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 10 Oct 2018 14:08:47 -0700 Subject: [PATCH 070/362] ignore web entities --- .../controllerModules/webSurfaceLaserInput.js | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index cf61693dfd..898164dc99 100644 --- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js +++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -20,7 +20,7 @@ Script.include("/~/system/libraries/controllers.js"); this.hand = hand; this.otherHand = this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND; this.running = false; - this.ignoredOverlays = []; + this.ignoredObjects = []; this.parameters = makeDispatcherModuleParameters( 160, @@ -68,35 +68,43 @@ Script.include("/~/system/libraries/controllers.js"); return this.hand === RIGHT_HAND ? leftOverlayLaserInput : rightOverlayLaserInput; }; - this.addOverlayToIgnoreList = function(controllerData) { + 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.ignoredOverlays.indexOf(objectID); + var overlayIndex = this.ignoredObjects.indexOf(objectID); - if (overlayIndex === -1) { - 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.ignoredOverlays.push(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 + }; + print("ignoreing entity " + entityIndex); + Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)); + this.ignoredObjects.push(objectID); } } }; - this.restoreIgnoredOverlays = function() { - for (var index = 0; index < this.ignoredOverlays.length; index++) { + this.restoreIgnoredObjects = function() { + for (var index = 0; index < this.ignoredObjects.length; index++) { + print("removing"); var data = { action: 'remove', - id: this.ignoredOverlays[index] + id: this.ignoredObjects[index] }; Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)); } @@ -168,6 +176,10 @@ Script.include("/~/system/libraries/controllers.js"); return makeRunningValues(true, [], []); } } + + if (Window.interstitialModeEnabled && Window.isPhysicsEnabled()) { + this.restoreIgnoredObjects(); + } return makeRunningValues(false, [], []); }; @@ -180,7 +192,7 @@ Script.include("/~/system/libraries/controllers.js"); var allowThisModule = !otherModuleRunning && !grabModuleNeedsToRun; var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE; var laserOn = isTriggerPressed || this.parameters.handLaser.allwaysOn; - this.addOverlayToIgnoreList(controllerData); + this.addObjectToIgnoreList(controllerData); if (allowThisModule) { if (isTriggerPressed && !this.isPointingAtTriggerable(controllerData, isTriggerPressed, true)) { // if trigger is down + not pointing at a web entity, keep running web surface laser @@ -194,7 +206,6 @@ Script.include("/~/system/libraries/controllers.js"); this.deleteContextOverlay(); this.running = false; this.dominantHandOverride = false; - this.restoreIgnoredOverlays(); return makeRunningValues(false, [], []); } } @@ -202,7 +213,6 @@ Script.include("/~/system/libraries/controllers.js"); this.deleteContextOverlay(); this.running = false; this.dominantHandOverride = false; - this.restoreIgnoredOverlays(); return makeRunningValues(false, [], []); }; } From f1446532d02eca2ee160e83bb5d2b122a1b48d38 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 10 Oct 2018 16:13:23 -0700 Subject: [PATCH 071/362] Almost working --- .../scripting/SpeechScriptingInterface.cpp | 106 ++++++++++++------ .../src/scripting/SpeechScriptingInterface.h | 7 +- 2 files changed, 75 insertions(+), 38 deletions(-) diff --git a/interface/src/scripting/SpeechScriptingInterface.cpp b/interface/src/scripting/SpeechScriptingInterface.cpp index a38e1aa824..b9c7718075 100644 --- a/interface/src/scripting/SpeechScriptingInterface.cpp +++ b/interface/src/scripting/SpeechScriptingInterface.cpp @@ -19,8 +19,7 @@ SpeechScriptingInterface::SpeechScriptingInterface() { // HRESULT hr = m_tts.CoCreateInstance(CLSID_SpVoice); if (FAILED(hr)) { - ATLTRACE(TEXT("Text-to-speech creation failed.\n")); - AtlThrow(hr); + qDebug() << "Text-to-speech engine creation failed."; } // @@ -28,8 +27,7 @@ SpeechScriptingInterface::SpeechScriptingInterface() { // hr = SpGetDefaultTokenFromCategoryId(SPCAT_VOICES, &m_voiceToken, FALSE); if (FAILED(hr)) { - ATLTRACE(TEXT("Can't get default voice token.\n")); - AtlThrow(hr); + qDebug() << "Can't get default voice token."; } // @@ -37,28 +35,7 @@ SpeechScriptingInterface::SpeechScriptingInterface() { // hr = m_tts->SetVoice(m_voiceToken); if (FAILED(hr)) { - ATLTRACE(TEXT("Can't set default voice.\n")); - AtlThrow(hr); - } - - WAVEFORMATEX fmt; - fmt.wFormatTag = WAVE_FORMAT_PCM; - fmt.nSamplesPerSec = 48000; - fmt.wBitsPerSample = 16; - fmt.nChannels = 1; - fmt.nBlockAlign = fmt.nChannels * fmt.wBitsPerSample / 8; - fmt.nAvgBytesPerSec = fmt.nSamplesPerSec * fmt.nBlockAlign; - fmt.cbSize = 0; - - BYTE* pcontent = new BYTE[1024 * 1000]; - - cpIStream = SHCreateMemStream(NULL, 0); - hr = outputStream->SetBaseStream(cpIStream, SPDFID_WaveFormatEx, &fmt); - - hr = m_tts->SetOutput(outputStream, true); - if (FAILED(hr)) { - ATLTRACE(TEXT("Can't set output stream.\n")); - AtlThrow(hr); + qDebug() << "Can't set default voice."; } } @@ -66,30 +43,91 @@ SpeechScriptingInterface::~SpeechScriptingInterface() { } +class ReleaseOnExit { +public: + ReleaseOnExit(IUnknown* p) : m_p(p) {} + ~ReleaseOnExit() { + if (m_p) { + m_p->Release(); + } + } + +private: + IUnknown* m_p; +}; + void SpeechScriptingInterface::speakText(const QString& textToSpeak) { + WAVEFORMATEX fmt; + fmt.wFormatTag = WAVE_FORMAT_PCM; + fmt.nSamplesPerSec = 44100; + fmt.wBitsPerSample = 16; + fmt.nChannels = 1; + fmt.nBlockAlign = fmt.nChannels * fmt.wBitsPerSample / 8; + fmt.nAvgBytesPerSec = fmt.nSamplesPerSec * fmt.nBlockAlign; + fmt.cbSize = 0; + + IStream* pStream = NULL; + + ISpStream* pSpStream = nullptr; + HRESULT hr = CoCreateInstance(CLSID_SpStream, nullptr, CLSCTX_ALL, __uuidof(ISpStream), (void**)&pSpStream); + if (FAILED(hr)) { + qDebug() << "CoCreateInstance failed."; + } + ReleaseOnExit rSpStream(pSpStream); + + pStream = SHCreateMemStream(NULL, 0); + if (nullptr == pStream) { + qDebug() << "SHCreateMemStream failed."; + } + + hr = pSpStream->SetBaseStream(pStream, SPDFID_WaveFormatEx, &fmt); + if (FAILED(hr)) { + qDebug() << "Can't set base stream."; + } + + hr = m_tts->SetOutput(pSpStream, true); + if (FAILED(hr)) { + qDebug() << "Can't set output stream."; + } + + ReleaseOnExit rStream(pStream); + ULONG streamNumber; - HRESULT hr = m_tts->Speak(reinterpret_cast(textToSpeak.utf16()), + hr = m_tts->Speak(reinterpret_cast(textToSpeak.utf16()), SPF_IS_NOT_XML | SPF_ASYNC | SPF_PURGEBEFORESPEAK, &streamNumber); if (FAILED(hr)) { - ATLTRACE(TEXT("Speak failed.\n")); - AtlThrow(hr); + qDebug() << "Speak failed."; } m_tts->WaitUntilDone(-1); - outputStream->GetBaseStream(&cpIStream); + hr = pSpStream->GetBaseStream(&pStream); + if (FAILED(hr)) { + qDebug() << "Couldn't get base stream."; + } + + hr = IStream_Reset(pStream); + if (FAILED(hr)) { + qDebug() << "Couldn't reset stream."; + } + ULARGE_INTEGER StreamSize; StreamSize.LowPart = 0; - hr = IStream_Size(cpIStream, &StreamSize); + hr = IStream_Size(pStream, &StreamSize); DWORD dwSize = StreamSize.QuadPart; char* buf1 = new char[dwSize + 1]; - hr = IStream_Read(cpIStream, buf1, dwSize); + memset(buf1, 0, dwSize + 1); + + hr = IStream_Read(pStream, buf1, dwSize); + if (FAILED(hr)) { + qDebug() << "Couldn't read from stream."; + } + + QByteArray byteArray = QByteArray::QByteArray(buf1, dwSize); - QByteArray byteArray = QByteArray::QByteArray(buf1, (int)dwSize); AudioInjectorOptions options; - options.position = DependencyManager::get()->getMyAvatarPosition(); AudioInjector::playSound(byteArray, options); diff --git a/interface/src/scripting/SpeechScriptingInterface.h b/interface/src/scripting/SpeechScriptingInterface.h index 311bd80605..ad6777e339 100644 --- a/interface/src/scripting/SpeechScriptingInterface.h +++ b/interface/src/scripting/SpeechScriptingInterface.h @@ -13,7 +13,9 @@ #include #include - +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif #include // SAPI #include // SAPI Helper @@ -68,9 +70,6 @@ private: // Default voice token CComPtr m_voiceToken; - - CComPtr outputStream; - CComPtr cpIStream; }; #endif // hifi_SpeechScriptingInterface_h From d8c9712dd2cfb404878eb830f7060015fbfc37c6 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 10 Oct 2018 16:19:11 -0700 Subject: [PATCH 072/362] It's working! --- interface/src/scripting/SpeechScriptingInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/SpeechScriptingInterface.cpp b/interface/src/scripting/SpeechScriptingInterface.cpp index b9c7718075..3b3ecf728d 100644 --- a/interface/src/scripting/SpeechScriptingInterface.cpp +++ b/interface/src/scripting/SpeechScriptingInterface.cpp @@ -59,7 +59,7 @@ private: void SpeechScriptingInterface::speakText(const QString& textToSpeak) { WAVEFORMATEX fmt; fmt.wFormatTag = WAVE_FORMAT_PCM; - fmt.nSamplesPerSec = 44100; + fmt.nSamplesPerSec = 24000; fmt.wBitsPerSample = 16; fmt.nChannels = 1; fmt.nBlockAlign = fmt.nChannels * fmt.wBitsPerSample / 8; From 31e665ab8bc737bfa248d25434f8256ceb44ad48 Mon Sep 17 00:00:00 2001 From: David Back Date: Wed, 10 Oct 2018 16:49:20 -0700 Subject: [PATCH 073/362] CR changes - runtime property data rework --- scripts/system/html/js/entityProperties.js | 1554 +++++++++++--------- 1 file changed, 850 insertions(+), 704 deletions(-) diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 46f16dd0a0..2841414d87 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -6,8 +6,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global alert, augmentSpinButtons, clearTimeout, console, document, Element, EventBridge, - HifiEntityUI, JSONEditor, openEventBridge, setTimeout, window, _ $ */ +/* global alert, augmentSpinButtons, clearTimeout, console, document, Element, + EventBridge, JSONEditor, openEventBridge, setTimeout, window, _ $ */ const ICON_FOR_TYPE = { Box: "V", @@ -40,6 +40,7 @@ const GROUPS = [ label: NO_SELECTION, type: "icon", icons: ICON_FOR_TYPE, + defaultValue: "Box", propertyName: "type", }, { @@ -79,7 +80,10 @@ const GROUPS = [ { label: "Shape", type: "dropdown", - options: { Cube: "Box", Sphere: "Sphere", Tetrahedron: "Tetrahedron", Octahedron: "Octahedron", Icosahedron: "Icosahedron", Dodecahedron: "Dodecahedron", Hexagon: "Hexagon", Triangle: "Triangle", Octagon: "Octagon", Cylinder: "Cylinder", Cone: "Cone", Circle: "Circle", Quad: "Quad" }, + options: { Cube: "Box", Sphere: "Sphere", Tetrahedron: "Tetrahedron", Octahedron: "Octahedron", + Icosahedron: "Icosahedron", Dodecahedron: "Dodecahedron", Hexagon: "Hexagon", + Triangle: "Triangle", Octagon: "Octagon", Cylinder: "Cylinder", Cone: "Cone", + Circle: "Circle", Quad: "Quad" }, propertyName: "shape", }, { @@ -208,7 +212,8 @@ const GROUPS = [ }, { type: "buttons", - buttons: [ { id: "copy", label: "Copy URL To Ambient", className: "black", onClick: copySkyboxURLToAmbientURL } ], + buttons: [ { id: "copy", label: "Copy URL To Ambient", + className: "black", onClick: copySkyboxURLToAmbientURL } ], propertyName: "copyURLToAmbient", showPropertyRule: { "skyboxMode": "enabled" }, }, @@ -367,7 +372,9 @@ const GROUPS = [ { label: "Collision Shape", type: "dropdown", - options: { "none": "No Collision", "box": "Box", "sphere": "Sphere", "compound": "Compound" , "simple-hull": "Basic - Whole model", "simple-compound": "Good - Sub-meshes" , "static-mesh": "Exact - All polygons (non-dynamic only)" }, + options: { "none": "No Collision", "box": "Box", "sphere": "Sphere", "compound": "Compound" , + "simple-hull": "Basic - Whole model", "simple-compound": "Good - Sub-meshes" , + "static-mesh": "Exact - All polygons (non-dynamic only)" }, propertyName: "shapeType", }, { @@ -467,8 +474,8 @@ const GROUPS = [ { label: "Light Color", type: "color", - propertyName: "lightColor", // this actually shares "color" property with shape Color - // but separating naming here to separate property fields + propertyName: "lightColor", // this actually shares "color" property with shape Color but + // separating naming here to distinguish property element/data }, { label: "Intensity", @@ -521,8 +528,8 @@ const GROUPS = [ label: "Material Data", type: "textarea", buttons: [ { id: "clear", label: "Clear Material Data", className: "red", onClick: clearMaterialData }, - { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONMaterialEditor }, - { id: "save", label: "Save Material Data", className: "black", onClick: saveMaterialData } ], + { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONMaterialEditor }, + { id: "save", label: "Save Material Data", className: "black", onClick: saveMaterialData } ], propertyName: "materialData", }, { @@ -613,7 +620,7 @@ const GROUPS = [ defaultValue: 100, unit: "%", buttons: [ { id: "rescale", label: "Rescale", className: "blue", onClick: rescaleDimensions }, - { id: "reset", label: "Reset Dimensions", className: "red", onClick: resetToNaturalDimensions } ], + { id: "reset", label: "Reset Dimensions", className: "red", onClick: resetToNaturalDimensions } ], propertyName: "scale", }, { @@ -628,8 +635,8 @@ const GROUPS = [ { label: "Align", type: "buttons", - buttons: [ { id: "selection", label: "Selection to Grid", className: "black", onClick: moveSelectionToGrid }, - { id: "all", label: "All to Grid", className: "black", onClick: moveAllToGrid } ], + buttons: [ { id: "selection", label: "Selection to Grid", className: "black", onClick: moveSelectionToGrid }, + { id: "all", label: "All to Grid", className: "black", onClick: moveAllToGrid } ], propertyName: "alignToGrid", }, ] @@ -642,8 +649,8 @@ const GROUPS = [ { label: "Collides", type: "bool", - propertyName: "collisionless", inverse: true, + propertyName: "collisionless", column: -1, // before two columns div }, { @@ -655,14 +662,14 @@ const GROUPS = [ { label: "Collides With", type: "sub-header", - propertyName: "collidesWithHeader", + propertyName: "collidesWithHeader", // not actually a property but used for naming/storing this element showPropertyRule: { "collisionless": "false" }, column: 1, }, { label: "", type: "sub-header", - propertyName: "collidesWithHeaderHelper", + propertyName: "collidesWithHeaderHelper", // not actually a property but used for naming/storing this element showPropertyRule: { "collisionless": "false" }, column: 2, }, @@ -711,6 +718,7 @@ const GROUPS = [ type: "string", propertyName: "collisionSoundURL", showPropertyRule: { "collisionless": "false" }, + // having no column number means place this after two columns div }, ] }, @@ -772,7 +780,7 @@ const GROUPS = [ showPropertyRule: { "cloneable": "true" }, column: 1, }, - { + { // below properties having no column number means place them after two columns div label: "Can cast shadow", type: "bool", propertyName: "canCastShadow", @@ -799,8 +807,8 @@ const GROUPS = [ label: "User Data", type: "textarea", buttons: [ { id: "clear", label: "Clear User Data", className: "red", onClick: clearUserData }, - { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONEditor }, - { id: "save", label: "Save User Data", className: "black", onClick: saveUserData } ], + { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONEditor }, + { id: "save", label: "Save User Data", className: "black", onClick: saveUserData } ], propertyName: "userData", }, ] @@ -898,11 +906,9 @@ const MATERIAL_PREFIX_STRING = "mat::"; const PENDING_SCRIPT_STATUS = "[ Fetching status ]"; var elGroups = {}; -var elPropertyElements = {}; -var showPropertyRules = {}; -var subProperties = []; -var icons = {}; +var properties = {}; var colorPickers = {}; +var selectedEntityProperties; var lastEntityID = null; function debugPrint(message) { @@ -914,6 +920,17 @@ function debugPrint(message) { ); } + +// GENERAL PROPERTY/GROUP FUNCTIONS + +function getPropertyElement(propertyName) { + return properties[propertyName].el; +} + +function getPropertyElementID(propertyName) { + return "property-" + propertyName; +} + function enableChildren(el, selector) { var elSelectors = el.querySelectorAll(selector); for (var selectorIndex = 0; selectorIndex < elSelectors.length; ++selectorIndex) { @@ -931,7 +948,7 @@ function disableChildren(el, selector) { function enableProperties() { enableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); enableChildren(document, ".colpick"); - var elLocked = elPropertyElements["locked"]; + var elLocked = getPropertyElement("locked"); if (elLocked.checked === false) { removeStaticUserData(); @@ -945,7 +962,7 @@ function disableProperties() { for (var pickKey in colorPickers) { colorPickers[pickKey].colpickHide(); } - var elLocked = elPropertyElements["locked"]; + var elLocked = getPropertyElement("locked"); if (elLocked.checked === true) { if ($('#property-userData-editor').css('display') === "block") { @@ -957,34 +974,148 @@ function disableProperties() { } } +function showPropertyElement(propertyName, show) { + let elProperty = properties[propertyName].el; + let elNode = elProperty; + if (elNode.nodeName !== "DIV") { + let elParent = elProperty.parentNode; + if (elParent === undefined && elProperty instanceof Array) { + elParent = elProperty[0].parentNode; + } + if (elParent !== undefined) { + elNode = elParent; + } + } + elNode.style.display = show ? "table" : "none"; +} + +function resetProperties() { + for (let propertyName in properties) { + let elProperty = properties[propertyName].el; + let propertyData = properties[propertyName].data; + + switch (propertyData.type) { + case 'string': { + elProperty.value = ""; + break; + } + case 'bool': { + elProperty.checked = false; + break; + } + case 'number': { + if (propertyData.defaultValue !== undefined) { + elProperty.value = propertyData.defaultValue; + } else { + elProperty.value = ""; + } + break; + } + case 'vec3': + case 'vec2': { + // vec2/vec3 are array of 2/3 elInput numbers + elProperty[0].value = ""; + elProperty[1].value = ""; + if (elProperty[2] !== undefined) { + elProperty[2].value = ""; + } + break; + } + case 'color': { + // color is array of color picker and 3 elInput numbers + elProperty[0].style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; + elProperty[1].value = ""; + elProperty[2].value = ""; + elProperty[3].value = ""; + break; + } + case 'dropdown': { + elProperty.value = ""; + setDropdownText(elProperty); + break; + } + case 'textarea': { + elProperty.value = ""; + setTextareaScrolling(elProperty); + break; + } + case 'icon': { + // icon is array of elSpan (icon glyph) and elLabel + if (propertyData.defaultValue !== undefined) { + elProperty[0].innerHTML = propertyData.icons[propertyData.defaultValue]; + elProperty[0].style.display = "inline-block"; + elProperty[1].innerHTML = propertyData.defaultValue; + } + break; + } + } + + let showPropertyRules = properties[propertyName].showPropertyRules; + if (showPropertyRules !== undefined) { + for (let propertyToHide in showPropertyRules) { + showPropertyElement(propertyToHide, false); + } + } + } +} + +function showGroupsForType(type) { + if (type === "Box" || type === "Sphere") { + type = "Shape"; + } + + let typeGroups = GROUPS_PER_TYPE[type]; + + for (let groupKey in elGroups) { + let elGroup = elGroups[groupKey]; + if (typeGroups && typeGroups.indexOf(groupKey) > -1) { + elGroup.style.display = "block"; + } else { + elGroup.style.display = "none"; + } + } +} + + + +// PROPERTY UPDATE FUNCTIONS + function updateProperty(propertyName, propertyValue) { - var properties = {}; + let propertyUpdate = {}; + // if this is a compound property name (i.e. animation.running) then split it by . up to 3 times let splitPropertyName = propertyName.split('.'); if (splitPropertyName.length > 1) { let propertyGroupName = splitPropertyName[0]; let subPropertyName = splitPropertyName[1]; - properties[propertyGroupName] = {}; + propertyUpdate[propertyGroupName] = {}; if (splitPropertyName.length === 3) { let subSubPropertyName = splitPropertyName[2]; - properties[propertyGroupName][subPropertyName] = {}; - properties[propertyGroupName][subPropertyName][subSubPropertyName] = propertyValue; + propertyUpdate[propertyGroupName][subPropertyName] = {}; + propertyUpdate[propertyGroupName][subPropertyName][subSubPropertyName] = propertyValue; } else { - properties[propertyGroupName][subPropertyName] = propertyValue; + propertyUpdate[propertyGroupName][subPropertyName] = propertyValue; } } else { - properties[propertyName] = propertyValue; + propertyUpdate[propertyName] = propertyValue; } - updateProperties(properties); + updateProperties(propertyUpdate); } -function updateProperties(properties) { +function updateProperties(propertiesToUpdate) { EventBridge.emitWebEvent(JSON.stringify({ id: lastEntityID, type: "update", - properties: properties + properties: propertiesToUpdate })); } + +function createEmitTextPropertyUpdateFunction(propertyName) { + return function() { + updateProperty(propertyName, this.value); + }; +} + function createEmitCheckedPropertyUpdateFunction(propertyName, inverse) { return function() { updateProperty(propertyName, inverse ? !this.checked : this.checked); @@ -994,67 +1125,50 @@ function createEmitCheckedPropertyUpdateFunction(propertyName, inverse) { function createEmitNumberPropertyUpdateFunction(propertyName, decimals) { decimals = ((decimals === undefined) ? 4 : decimals); return function() { - var value = parseFloat(this.value).toFixed(decimals); + let value = parseFloat(this.value).toFixed(decimals); updateProperty(propertyName, value); }; } -function createImageURLUpdateFunction(propertyName) { - return function () { - var newTextures = JSON.stringify({ "tex.picture": this.value }); - updateProperty(propertyName, newTextures); - }; -} - -function createEmitTextPropertyUpdateFunction(propertyName) { - return function() { - updateProperty(propertyName, this.value); - }; -} - function createEmitVec2PropertyUpdateFunction(property, elX, elY) { return function () { - var properties = {}; - properties[property] = { + let newValue = { x: elX.value, y: elY.value }; - updateProperties(properties); + updateProperty(property, newValue); }; } function createEmitVec2PropertyUpdateFunctionWithMultiplier(property, elX, elY, multiplier) { return function () { - var properties = {}; - properties[property] = { + let newValue = { x: elX.value * multiplier, y: elY.value * multiplier }; - updateProperties(properties); + updateProperty(property, newValue); }; } function createEmitVec3PropertyUpdateFunction(property, elX, elY, elZ) { return function() { - var properties = {}; - properties[property] = { + let newValue = { x: elX.value, y: elY.value, z: elZ ? elZ.value : 0 }; - updateProperties(properties); + updateProperty(property, newValue); }; } function createEmitVec3PropertyUpdateFunctionWithMultiplier(property, elX, elY, elZ, multiplier) { return function() { - var properties = {}; - properties[property] = { + let newValue = { x: elX.value * multiplier, y: elY.value * multiplier, z: elZ.value * multiplier }; - updateProperties(properties); + updateProperty(property, newValue); }; } @@ -1064,23 +1178,13 @@ function createEmitColorPropertyUpdateFunction(property, elRed, elGreen, elBlue) }; } -function emitColorPropertyUpdate(property, red, green, blue, group) { - var properties = {}; - if (group) { - properties[group] = {}; - properties[group][property] = { - red: red, - green: green, - blue: blue - }; - } else { - properties[property] = { - red: red, - green: green, - blue: blue - }; - } - updateProperties(properties); +function emitColorPropertyUpdate(property, red, green, blue) { + let newValue = { + red: red, + green: green, + blue: blue + }; + updateProperty(property, newValue); } function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString) { @@ -1095,8 +1199,440 @@ function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElemen updateProperty(propertyName, propertyValue); } +function createImageURLUpdateFunction(propertyName) { + return function () { + var newTextures = JSON.stringify({ "tex.picture": this.value }); + updateProperty(propertyName, newTextures); + }; +} + + + +// PROPERTY ELEMENT CREATION FUNCTIONS + +function createStringProperty(elProperty, elLabel, propertyData) { + let propertyName = propertyData.propertyName; + let propertyElementID = getPropertyElementID(propertyName); + + elProperty.setAttribute("class", "property text"); + + let elInput = document.createElement('input'); + elInput.setAttribute("id", propertyElementID); + elInput.setAttribute("type", "text"); + if (propertyData.readOnly) { + elInput.readOnly = true; + } + + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elInput); + + if (propertyData.buttons !== undefined) { + addButtons(elProperty, propertyElementID, propertyData.buttons, false); + } + + properties[propertyName].el = elInput; +} + +function createBoolProperty(elProperty, elLabel, propertyData) { + let propertyName = propertyData.propertyName; + let propertyElementID = getPropertyElementID(propertyName); + + elProperty.setAttribute("class", "property checkbox"); + + if (propertyData.glyph !== undefined) { + elLabel.innerText = " " + propertyData.label; + let elSpan = document.createElement('span'); + elSpan.innerHTML = propertyData.glyph; + elLabel.insertBefore(elSpan, elLabel.firstChild); + } + + let elInput = document.createElement('input'); + elInput.setAttribute("id", propertyElementID); + elInput.setAttribute("type", "checkbox"); + + elProperty.appendChild(elInput); + elProperty.appendChild(elLabel); + + let subPropertyOf = propertyData.subPropertyOf; + if (subPropertyOf !== undefined) { + elInput.addEventListener('change', function() { + updateCheckedSubProperty(subPropertyOf, selectedEntityProperties[subPropertyOf], elInput, propertyName); + }); + } else { + elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(propertyName, propertyData.inverse)); + } + + properties[propertyName].el = elInput; +} + +function createVec3Property(elProperty, elLabel, propertyData) { + let propertyName = propertyData.propertyName; + let propertyElementID = getPropertyElementID(propertyName); + + elProperty.setAttribute("class", "property " + propertyData.vec3Type + " fstuple"); + + let elTuple = document.createElement('div'); + elTuple.setAttribute("class", "tuple"); + + addUnit(propertyData.unit, elLabel); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elTuple); + + let elInputX = createTupleNumberInput(elTuple, propertyElementID, propertyData.subLabels[0], + propertyData.min, propertyData.max, propertyData.step); + let elInputY = createTupleNumberInput(elTuple, propertyElementID, propertyData.subLabels[1], + propertyData.min, propertyData.max, propertyData.step); + let elInputZ = createTupleNumberInput(elTuple, propertyElementID, propertyData.subLabels[2], + propertyData.min, propertyData.max, propertyData.step); + + let inputChangeFunction; + if (propertyData.multiplier !== undefined) { + inputChangeFunction = createEmitVec3PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY, + elInputZ, propertyData.multiplier); + } else { + inputChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elInputX, elInputY, elInputZ); + } + elInputX.addEventListener('change', inputChangeFunction); + elInputY.addEventListener('change', inputChangeFunction); + elInputZ.addEventListener('change', inputChangeFunction); + + properties[propertyName].el = [ elInputX, elInputY, elInputZ ]; +} + +function createVec2Property(elProperty, elLabel, propertyData) { + let propertyName = propertyData.propertyName; + let propertyElementID = getPropertyElementID(propertyName); + + elProperty.setAttribute("class", "property " + propertyData.vec2Type + " fstuple"); + + let elTuple = document.createElement('div'); + elTuple.setAttribute("class", "tuple"); + + addUnit(propertyData.unit, elLabel); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elTuple); + + let elInputX = createTupleNumberInput(elTuple, propertyElementID, propertyData.subLabels[0], + propertyData.min, propertyData.max, propertyData.step); + let elInputY = createTupleNumberInput(elTuple, propertyElementID, propertyData.subLabels[1], + propertyData.min, propertyData.max, propertyData.step); + + let inputChangeFunction; + if (propertyData.multiplier !== undefined) { + inputChangeFunction = createEmitVec2PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY, + propertyData.multiplier); + } else { + inputChangeFunction = createEmitVec2PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY); + } + elInputX.addEventListener('change', inputChangeFunction); + elInputY.addEventListener('change', inputChangeFunction); + + properties[propertyName].el = [elInputX, elInputY]; +} + +function createColorProperty(elProperty, elLabel, propertyData) { + let propertyName = propertyData.propertyName; + let propertyElementID = getPropertyElementID(propertyName); + + elProperty.setAttribute("class", "property rgb fstuple"); + + let elColorPicker = document.createElement('div'); + elColorPicker.setAttribute("class", "color-picker"); + elColorPicker.setAttribute("id", propertyElementID); + + let elTuple = document.createElement('div'); + elTuple.setAttribute("class", "tuple"); + + elProperty.appendChild(elColorPicker); + elProperty.appendChild(elLabel); + elProperty.appendChild(elTuple); + + let elInputR = createTupleNumberInput(elTuple, propertyElementID, "red", 0, 255, 1); + let elInputG = createTupleNumberInput(elTuple, propertyElementID, "green", 0, 255, 1); + let elInputB = createTupleNumberInput(elTuple, propertyElementID, "blue", 0, 255, 1); + + let inputChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elInputR, elInputG, elInputB); + elInputR.addEventListener('change', inputChangeFunction); + elInputG.addEventListener('change', inputChangeFunction); + elInputB.addEventListener('change', inputChangeFunction); + + let colorPickerID = "#" + propertyElementID; + colorPickers[colorPickerID] = $(colorPickerID).colpick({ + colorScheme: 'dark', + layout: 'hex', + color: '000000', + submit: false, // We don't want to have a submission button + onShow: function(colpick) { + $(colorPickerID).attr('active', 'true'); + // The original color preview within the picker needs to be updated on show because + // prior to the picker being shown we don't have access to the selections' starting color. + colorPickers[colorPickerID].colpickSetColor({ + "r": elInputR.value, + "g": elInputG.value, + "b": elInputB.value + }); + }, + onHide: function(colpick) { + $(colorPickerID).attr('active', 'false'); + }, + onChange: function(hsb, hex, rgb, el) { + $(el).css('background-color', '#' + hex); + emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); + } + }); + + properties[propertyName].el = [elColorPicker, elInputR, elInputG, elInputB]; +} + +function createDropdownProperty(elProperty, elLabel, propertyData) { + let propertyName = propertyData.propertyName; + let propertyElementID = getPropertyElementID(propertyName); + + elProperty.setAttribute("class", "property dropdown"); + + let elInput = document.createElement('select'); + elInput.setAttribute("id", propertyElementID); + elInput.setAttribute("propertyName", propertyName); + + for (let optionKey in propertyData.options) { + let option = document.createElement('option'); + option.value = optionKey; + option.text = propertyData.options[optionKey]; + elInput.add(option); + } + + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elInput); + + properties[propertyName].el = elInput; +} + +function createNumberProperty(elProperty, elLabel, propertyData) { + let propertyName = propertyData.propertyName; + let propertyElementID = getPropertyElementID(propertyName); + + elProperty.setAttribute("class", "property number"); + + addUnit(propertyData.unit, elLabel); + + let elInput = document.createElement('input'); + elInput.setAttribute("id", propertyElementID); + elInput.setAttribute("type", "number"); + if (propertyData.min !== undefined) { + elInput.setAttribute("min", propertyData.min); + } + if (propertyData.max !== undefined) { + elInput.setAttribute("max", propertyData.max); + } + if (propertyData.step !== undefined) { + elInput.setAttribute("step", propertyData.step); + } + + let defaultValue = propertyData.defaultValue; + if (defaultValue !== undefined) { + elInput.setAttribute("defaultValue", defaultValue); + elInput.value = defaultValue; + } + + let fixedDecimals = propertyData.fixedDecimals !== undefined ? propertyData.fixedDecimals : 0; + elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(propertyName, fixedDecimals)); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elInput); + + if (propertyData.buttons !== undefined) { + addButtons(elProperty, propertyElementID, propertyData.buttons, true); + } + + properties[propertyName].el = elInput; +} + +function createTextareaProperty(elProperty, elLabel, propertyData) { + let propertyName = propertyData.propertyName; + let propertyElementID = getPropertyElementID(propertyName); + + elProperty.setAttribute("class", "property textarea"); + + elProperty.appendChild(elLabel); + + if (propertyData.buttons !== undefined) { + addButtons(elProperty, propertyElementID, propertyData.buttons, true); + } + + let elInput = document.createElement('textarea'); + elInput.setAttribute("id", propertyElementID); + if (propertyData.readOnly) { + elInput.readOnly = true; + } + + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + + elProperty.appendChild(elInput); + + properties[propertyName].el = elInput; +} + +function createIconProperty(elProperty, elLabel, propertyData) { + let propertyName = propertyData.propertyName; + let propertyElementID = getPropertyElementID(propertyName); + + elProperty.setAttribute("class", "property value"); + + elLabel.setAttribute("id", propertyElementID); + elLabel.innerHTML = " " + propertyData.label; + + let elSpan = document.createElement('span'); + elSpan.setAttribute("id", propertyElementID + "-icon"); + + elProperty.appendChild(elSpan); + elProperty.appendChild(elLabel); + + properties[propertyName].el = [ elSpan, elLabel ]; +} + +function createButtonsProperty(elProperty, elLabel, propertyData) { + let propertyName = propertyData.propertyName; + let propertyElementID = getPropertyElementID(propertyName); + + elProperty.setAttribute("class", "property text"); + + let hasLabel = propertyData.label !== undefined; + if (hasLabel) { + elProperty.appendChild(elLabel); + } + + if (propertyData.buttons !== undefined) { + addButtons(elProperty, propertyElementID, propertyData.buttons, hasLabel); + } + + properties[propertyName].el = elProperty; +} + +function createTupleNumberInput(elTuple, propertyID, subLabel, min, max, step) { + let elementPropertyID = propertyID + "-" + subLabel.toLowerCase(); + let elDiv = document.createElement('div'); + let elLabel = document.createElement('label'); + elLabel.innerText = subLabel[0].toUpperCase() + subLabel.slice(1) + ":"; + elLabel.setAttribute("for", elementPropertyID); + let elInput = document.createElement('input'); + elInput.setAttribute("id", elementPropertyID); + elInput.setAttribute("type", "number"); + elInput.setAttribute("class", subLabel); + if (min !== undefined) { + elInput.setAttribute("min", min); + } + if (max !== undefined) { + elInput.setAttribute("max", max); + } + if (step !== undefined) { + elInput.setAttribute("step", step); + } + elDiv.appendChild(elInput); + elDiv.appendChild(elLabel); + elTuple.appendChild(elDiv); + return elInput; +} + +function addUnit(unit, elLabel) { + if (unit !== undefined) { + let elSpan = document.createElement('span'); + elSpan.setAttribute("class", "unit"); + elSpan.innerHTML = unit; + elLabel.appendChild(elSpan); + } +} + +function addButtons(elProperty, propertyID, buttons, newRow) { + let elDiv = document.createElement('div'); + elDiv.setAttribute("class", "row"); + + buttons.forEach(function(button) { + let elButton = document.createElement('input'); + elButton.setAttribute("type", "button"); + elButton.setAttribute("class", button.className); + elButton.setAttribute("id", propertyID + "-button-" + button.id); + elButton.setAttribute("value", button.label); + elButton.addEventListener("click", button.onClick); + if (newRow) { + elDiv.appendChild(elButton); + } else { + elProperty.appendChild(elButton); + } + }); + + if (newRow) { + elProperty.appendChild(document.createElement('br')); + elProperty.appendChild(elDiv); + } +} + + + +// BUTTON CALLBACKS + +function rescaleDimensions() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "rescaleDimensions", + percentage: parseFloat(document.getElementById("property-scale").value) + })); +} + +function moveSelectionToGrid() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveSelectionToGrid" + })); +} + +function moveAllToGrid() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveAllToGrid" + })); +} + +function resetToNaturalDimensions() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "resetToNaturalDimensions" + })); +} + +function reloadScripts() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "reloadClientScripts" + })); +} + +function reloadServerScripts() { + // invalidate the current status (so that same-same updates can still be observed visually) + document.getElementById("property-serverScripts-status").innerText = PENDING_SCRIPT_STATUS; + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "reloadServerScripts" + })); +} + +function copySkyboxURLToAmbientURL() { + let skyboxURL = getPropertyElement("skybox.url").value; + getPropertyElement("ambientLight.ambientURL").value = skyboxURL; + updateProperty("ambientLight.ambientURL", skyboxURL); +} + + + +// USER DATA FUNCTIONS + function clearUserData() { - let elUserData = elPropertyElements["userData"]; + let elUserData = getPropertyElement("userData"); deleteJSONEditor(); elUserData.value = ""; showUserDataTextArea(); @@ -1148,7 +1684,7 @@ function setUserDataFromEditor(noUpdate) { } function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults, removeKeys) { - var properties = {}; + var propertyUpdate = {}; var parsedData = {}; var keysToBeRemoved = removeKeys ? removeKeys : []; try { @@ -1192,14 +1728,14 @@ function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults, r delete parsedData[groupName]; } if (Object.keys(parsedData).length > 0) { - properties.userData = JSON.stringify(parsedData); + propertyUpdate.userData = JSON.stringify(parsedData); } else { - properties.userData = ''; + propertyUpdate.userData = ''; } - userDataElement.value = properties.userData; + userDataElement.value = propertyUpdate.userData; - updateProperties(properties); + updateProperties(propertyUpdate); } function userDataChanger(groupName, keyName, values, userDataElement, defaultValue, removeKeys) { var val = {}, def = {}; @@ -1313,8 +1849,12 @@ function saveJSONUserData(noUpdate) { }, EDITOR_TIMEOUT_DURATION); } + + +// MATERIAL DATA FUNCTIONS + function clearMaterialData() { - let elMaterialData = elPropertyElements["materialData"]; + let elMaterialData = getPropertyElement("materialData"); deleteJSONMaterialEditor(); elMaterialData.value = ""; showMaterialDataTextArea(); @@ -1479,7 +2019,8 @@ function bindAllNonJSONEditorElements() { // TODO FIXME: (JSHint) Functions declared within loops referencing // an outer scoped variable may lead to confusing semantics. field.on('focus', function(e) { - if (e.target.id === "property-userData-button-edit" || e.target.id === "property-userData-button-clear" || e.target.id === "property-materialData-button-edit" || e.target.id === "property-materialData-button-clear") { + if (e.target.id === "property-userData-button-edit" || e.target.id === "property-userData-button-clear" || + e.target.id === "property-materialData-button-edit" || e.target.id === "property-materialData-button-clear") { return; } else { if ($('#property-userData-editor').css('height') !== "0px") { @@ -1493,14 +2034,41 @@ function bindAllNonJSONEditorElements() { } } -function unbindAllInputs() { - var inputs = $('input'); - var i; - for (i = 0; i < inputs.length; i++) { - var input = inputs[i]; - var field = $(input); - field.unbind(); + + +// DROPDOWNS / TEXTAREAS / PARENT MATERIAL NAME FUNCTIONS + +function setDropdownText(dropdown) { + let lis = dropdown.parentNode.getElementsByTagName("li"); + let text = ""; + for (let i = 0; i < lis.length; i++) { + if (String(lis[i].getAttribute("value")) === String(dropdown.value)) { + text = lis[i].textContent; + } } + dropdown.firstChild.textContent = text; +} + +function toggleDropdown(event) { + let element = event.target; + if (element.nodeName !== "DT") { + element = element.parentNode; + } + element = element.parentNode; + let isDropped = element.getAttribute("dropped"); + element.setAttribute("dropped", isDropped !== "true" ? "true" : "false"); +} + +function setDropdownValue(event) { + let dt = event.target.parentNode.parentNode.previousSibling; + dt.value = event.target.getAttribute("value"); + dt.firstChild.textContent = event.target.textContent; + + dt.parentNode.setAttribute("dropped", "false"); + + let evt = document.createEvent("HTMLEvents"); + evt.initEvent("change", true, true); + dt.dispatchEvent(evt); } function setTextareaScrolling(element) { @@ -1520,183 +2088,7 @@ function showParentMaterialNameBox(number, elNumber, elString) { } } -function rescaleDimensions() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "rescaleDimensions", - percentage: parseFloat(document.getElementById("property-scale").value) - })); -} -function moveSelectionToGrid() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "moveSelectionToGrid" - })); -} - -function moveAllToGrid() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "moveAllToGrid" - })); -} - -function resetToNaturalDimensions() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "resetToNaturalDimensions" - })); -} - -function reloadScripts() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "reloadClientScripts" - })); -} - -function reloadServerScripts() { - // invalidate the current status (so that same-same updates can still be observed visually) - document.getElementById("property-serverScripts-status").innerText = PENDING_SCRIPT_STATUS; - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "reloadServerScripts" - })); -} - -function copySkyboxURLToAmbientURL() { - let skyboxURL = elPropertyElements["skybox.url"].value; - elPropertyElements["ambientLight.ambientURL"].value = skyboxURL; - updateProperty("ambientLight.ambientURL", skyboxURL); -} - -function createTupleNumberInput(elTuple, propertyID, subLabel, min, max, step) { - let elementPropertyID = propertyID + "-" + subLabel.toLowerCase(); - let elDiv = document.createElement('div'); - let elLabel = document.createElement('label'); - elLabel.innerText = subLabel[0].toUpperCase() + subLabel.slice(1) + ":"; - elLabel.setAttribute("for", elementPropertyID); - let elInput = document.createElement('input'); - elInput.setAttribute("id", elementPropertyID); - elInput.setAttribute("type", "number"); - elInput.setAttribute("class", subLabel); - if (min !== undefined) { - elInput.setAttribute("min", min); - } - if (max !== undefined) { - elInput.setAttribute("max", max); - } - if (step !== undefined) { - elInput.setAttribute("step", step); - } - elDiv.appendChild(elInput); - elDiv.appendChild(elLabel); - elTuple.appendChild(elDiv); - return elInput; -} - -function addUnit(unit, elLabel) { - if (unit !== undefined) { - let elSpan = document.createElement('span'); - elSpan.setAttribute("class", "unit"); - elSpan.innerHTML = unit; - elLabel.appendChild(elSpan); - } -} - -function addButtons(elProperty, propertyID, buttons, newRow) { - let elDiv = document.createElement('div'); - elDiv.setAttribute("class", "row"); - - buttons.forEach(function(button) { - let elButton = document.createElement('input'); - elButton.setAttribute("type", "button"); - elButton.setAttribute("class", button.className); - elButton.setAttribute("id", propertyID + "-button-" + button.id); - elButton.setAttribute("value", button.label); - elButton.addEventListener("click", button.onClick); - if (newRow) { - elDiv.appendChild(elButton); - } else { - elProperty.appendChild(elButton); - } - }); - - if (newRow) { - elProperty.appendChild(document.createElement('br')); - elProperty.appendChild(elDiv); - } -} - -function showGroupsForType(type) { - if (type === "Box" || type === "Sphere") { - type = "Shape"; - } - - let typeGroups = GROUPS_PER_TYPE[type]; - - for (let groupKey in elGroups) { - let elGroup = elGroups[groupKey]; - if (typeGroups && typeGroups.indexOf(groupKey) > -1) { - elGroup.style.display = "block"; - } else { - elGroup.style.display = "none"; - } - } -} - -function resetProperties() { - for (let propertyName in elPropertyElements) { - let elProperty = elPropertyElements[propertyName]; - if (elProperty instanceof Array) { - if (elProperty.length === 2 || elProperty.length === 3) { - // vec2/vec3 are array of 2/3 input numbers - elProperty[0].value = ""; - elProperty[1].value = ""; - if (elProperty[2] !== undefined) { - elProperty[2].value = ""; - } - } else if (elProperty.length === 4) { - // color is array of color picker and 3 input numbers - elProperty[0].style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elProperty[1].value = ""; - elProperty[2].value = ""; - elProperty[3].value = ""; - } - } else if (elProperty.getAttribute("type") === "number") { - if (elProperty.getAttribute("defaultValue")) { - elProperty.value = elProperty.getAttribute("defaultValue"); - } else { - elProperty.value = ""; - } - } else if (elProperty.getAttribute("type") === "checkbox") { - elProperty.checked = false; - } else { - elProperty.value = ""; - } - } - - for (let showPropertyRule in showPropertyRules) { - let propertyShowRules = showPropertyRules[showPropertyRule]; - for (let propertyToHide in propertyShowRules) { - let elPropertyToHide = elPropertyElements[propertyToHide]; - if (elPropertyToHide) { - let nodeToHide = elPropertyToHide; - if (elPropertyToHide.nodeName !== "DIV") { - let parentNode = elPropertyToHide.parentNode; - if (parentNode === undefined && elPropertyToHide instanceof Array) { - parentNode = elPropertyToHide[0].parentNode; - } - if (parentNode !== undefined) { - nodeToHide = parentNode; - } - } - nodeToHide.style.display = "none"; - } - } - } -} function loaded() { openEventBridge(function() { @@ -1729,8 +2121,8 @@ function loaded() { group.properties.forEach(function(property) { let propertyType = property.type; let propertyName = property.propertyName; - let propertyID = "property-" + propertyName; - propertyID = propertyID.replace(".", "-"); + let propertyElementID = getPropertyElementID(propertyName); + propertyElementID = propertyElementID.replace(".", "-"); let elProperty; if (propertyType === "sub-header") { @@ -1739,7 +2131,7 @@ function loaded() { elProperty.setAttribute("class", "sub-section-header"); } else { elProperty = document.createElement('div'); - elProperty.setAttribute("id", "div-" + propertyID); + elProperty.setAttribute("id", "div-" + propertyElementID); } if (group.twoColumn && property.column !== undefined && property.column !== -1) { @@ -1766,294 +2158,58 @@ function loaded() { let elLabel = document.createElement('label'); elLabel.innerText = property.label; - elLabel.setAttribute("for", propertyID); + elLabel.setAttribute("for", propertyElementID); + + properties[propertyName] = { data: property }; switch (propertyType) { - case 'vec3': { - elProperty.setAttribute("class", "property " + property.vec3Type + " fstuple"); - - let elTuple = document.createElement('div'); - elTuple.setAttribute("class", "tuple"); - - addUnit(property.unit, elLabel); - - elProperty.appendChild(elLabel); - elProperty.appendChild(elTuple); - - let elInputX = createTupleNumberInput(elTuple, propertyID, property.subLabels[0], property.min, property.max, property.step); - let elInputY = createTupleNumberInput(elTuple, propertyID, property.subLabels[1], property.min, property.max, property.step); - let elInputZ = createTupleNumberInput(elTuple, propertyID, property.subLabels[2], property.min, property.max, property.step); - - let inputChangeFunction; - let multiplier = 1; - if (property.multiplier !== undefined) { - multiplier = property.multiplier; - inputChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elInputX, elInputY, elInputZ); - } else { - inputChangeFunction = createEmitVec3PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY, elInputZ, property.multiplier); - } - elInputX.setAttribute("multiplier", multiplier); - elInputY.setAttribute("multiplier", multiplier); - elInputZ.setAttribute("multiplier", multiplier); - elInputX.addEventListener('change', inputChangeFunction); - elInputY.addEventListener('change', inputChangeFunction); - elInputZ.addEventListener('change', inputChangeFunction); - - elPropertyElements[propertyName] = [elInputX, elInputY, elInputZ]; - break; - } - case 'vec2': { - elProperty.setAttribute("class", "property " + property.vec2Type + " fstuple"); - - let elTuple = document.createElement('div'); - elTuple.setAttribute("class", "tuple"); - - addUnit(property.unit, elLabel); - - elProperty.appendChild(elLabel); - elProperty.appendChild(elTuple); - - let elInputX = createTupleNumberInput(elTuple, propertyID, property.subLabels[0], property.min, property.max, property.step); - let elInputY = createTupleNumberInput(elTuple, propertyID, property.subLabels[1], property.min, property.max, property.step); - - let inputChangeFunction; - let multiplier = 1; - if (property.multiplier !== undefined) { - multiplier = property.multiplier; - inputChangeFunction = createEmitVec2PropertyUpdateFunction(propertyName, elInputX, elInputY); - } else { - inputChangeFunction = createEmitVec2PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY, property.multiplier); - } - elInputX.setAttribute("multiplier", multiplier); - elInputY.setAttribute("multiplier", multiplier); - elInputX.addEventListener('change', inputChangeFunction); - elInputY.addEventListener('change', inputChangeFunction); - - elPropertyElements[propertyName] = [elInputX, elInputY]; - break; - } - case 'color': { - elProperty.setAttribute("class", "property rgb fstuple"); - - let elColorPicker = document.createElement('div'); - elColorPicker.setAttribute("class", "color-picker"); - elColorPicker.setAttribute("id", propertyID); - - let elTuple = document.createElement('div'); - elTuple.setAttribute("class", "tuple"); - - elProperty.appendChild(elColorPicker); - elProperty.appendChild(elLabel); - elProperty.appendChild(elTuple); - - let elInputR = createTupleNumberInput(elTuple, propertyID, "red", 0, 255, 1); - let elInputG = createTupleNumberInput(elTuple, propertyID, "green", 0, 255, 1); - let elInputB = createTupleNumberInput(elTuple, propertyID, "blue", 0, 255, 1); - - let inputChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elInputR, elInputG, elInputB); - elInputR.addEventListener('change', inputChangeFunction); - elInputG.addEventListener('change', inputChangeFunction); - elInputB.addEventListener('change', inputChangeFunction); - - let colorPickerID = "#" + propertyID; - colorPickers[colorPickerID] = $(colorPickerID).colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $(colorPickerID).attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers[colorPickerID].colpickSetColor({ - "r": elInputR.value, - "g": elInputG.value, - "b": elInputB.value - }); - }, - onHide: function(colpick) { - $(colorPickerID).attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); - } - }); - - elPropertyElements[propertyName] = [elColorPicker, elInputR, elInputG, elInputB]; - break; - } case 'string': { - elProperty.setAttribute("class", "property text"); - - let elInput = document.createElement('input'); - elInput.setAttribute("id", propertyID); - elInput.setAttribute("type", "text"); - if (property.readOnly) { - elInput.readOnly = true; - } - - elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); - - elProperty.appendChild(elLabel); - elProperty.appendChild(elInput); - - if (property.buttons !== undefined) { - addButtons(elProperty, propertyID, property.buttons, false); - } - - elPropertyElements[propertyName] = elInput; + createStringProperty(elProperty, elLabel, property); break; } case 'bool': { - elProperty.setAttribute("class", "property checkbox"); - - if (property.glyph !== undefined) { - elLabel.innerText = " " + property.label; - let elSpan = document.createElement('span'); - elSpan.innerHTML = property.glyph; - elLabel.insertBefore(elSpan, elLabel.firstChild); - } - - let elInput = document.createElement('input'); - elInput.setAttribute("id", propertyID); - elInput.setAttribute("type", "checkbox"); - - let inverse = property.inverse; - elInput.setAttribute("inverse", inverse ? "true" : "false"); - - elProperty.appendChild(elInput); - elProperty.appendChild(elLabel); - - let subPropertyOf = property.subPropertyOf; - if (subPropertyOf !== undefined) { - elInput.setAttribute("subPropertyOf", subPropertyOf); - elPropertyElements[propertyName] = elInput; - subProperties.push(propertyName); - elInput.addEventListener('change', function() { - updateCheckedSubProperty(subPropertyOf, properties[subPropertyOf], elInput, propertyName); - }); - } else { - elPropertyElements[propertyName] = elInput; - elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(propertyName, inverse)); - } - break; - } - case 'dropdown': { - elProperty.setAttribute("class", "property dropdown"); - - let elInput = document.createElement('select'); - elInput.setAttribute("id", propertyID); - elInput.setAttribute("propertyName", propertyName); - - for (let optionKey in property.options) { - let option = document.createElement('option'); - option.value = optionKey; - option.text = property.options[optionKey]; - elInput.add(option); - } - - elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); - - elProperty.appendChild(elLabel); - elProperty.appendChild(elInput); - elPropertyElements[propertyName] = elInput; + createBoolProperty(elProperty, elLabel, property); break; } case 'number': { - elProperty.setAttribute("class", "property number"); - - addUnit(property.unit, elLabel); - - let elInput = document.createElement('input'); - elInput.setAttribute("id", propertyID); - elInput.setAttribute("type", "number"); - if (property.min !== undefined) { - elInput.setAttribute("min", property.min); - } - if (property.max !== undefined) { - elInput.setAttribute("max", property.max); - } - if (property.step !== undefined) { - elInput.setAttribute("step", property.step); - } - - let fixedDecimals = property.fixedDecimals !== undefined ? property.fixedDecimals : 0; - elInput.setAttribute("fixedDecimals", fixedDecimals); - - let defaultValue = property.defaultValue; - if (defaultValue !== undefined) { - elInput.setAttribute("defaultValue", defaultValue); - elInput.value = defaultValue; - } - - elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(propertyName, fixedDecimals)); - - elProperty.appendChild(elLabel); - elProperty.appendChild(elInput); - - if (property.buttons !== undefined) { - addButtons(elProperty, propertyID, property.buttons, true); - } - - elPropertyElements[propertyName] = elInput; + createNumberProperty(elProperty, elLabel, property); + break; + } + case 'vec3': { + createVec3Property(elProperty, elLabel, property); + break; + } + case 'vec2': { + createVec2Property(elProperty, elLabel, property); + break; + } + case 'color': { + createColorProperty(elProperty, elLabel, property); + break; + } + case 'dropdown': { + createDropdownProperty(elProperty, elLabel, property); break; } case 'textarea': { - elProperty.setAttribute("class", "property textarea"); - - elProperty.appendChild(elLabel); - - if (property.buttons !== undefined) { - addButtons(elProperty, propertyID, property.buttons, true); - } - - let elInput = document.createElement('textarea'); - elInput.setAttribute("id", propertyID); - if (property.readOnly) { - elInput.readOnly = true; - } - - elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); - - elProperty.appendChild(elInput); - elPropertyElements[propertyName] = elInput; + createTextareaProperty(elProperty, elLabel, property); break; } case 'icon': { - elProperty.setAttribute("class", "property value"); - - elLabel.setAttribute("id", propertyID); - elLabel.innerHTML = " " + property.label; - - let elSpan = document.createElement('span'); - elSpan.setAttribute("id", propertyID + "-icon"); - icons[propertyName] = property.icons; - - elProperty.appendChild(elSpan); - elProperty.appendChild(elLabel); - elPropertyElements[propertyName] = [ elSpan, elLabel ]; + createIconProperty(elProperty, elLabel, property); break; } case 'buttons': { - elProperty.setAttribute("class", "property text"); - - let hasLabel = property.label !== undefined; - if (hasLabel) { - elProperty.appendChild(elLabel); - } - - if (property.buttons !== undefined) { - addButtons(elProperty, propertyID, property.buttons, hasLabel); - } - - elPropertyElements[propertyName] = elProperty; + createButtonsProperty(elProperty, elLabel, property); break; } case 'sub-header': { - if (propertyName !== undefined) { - elPropertyElements[propertyName] = elProperty; - } + properties[propertyName].el = elProperty; + break; + } + default: { + console.log("EntityProperties - Unknown property type " + + propertyType + " set to property " + propertyName); break; } } @@ -2062,10 +2218,13 @@ function loaded() { if (showPropertyRule !== undefined) { let dependentProperty = Object.keys(showPropertyRule)[0]; let dependentPropertyValue = showPropertyRule[dependentProperty]; - if (!showPropertyRules[dependentProperty]) { - showPropertyRules[dependentProperty] = []; + if (properties[dependentProperty] === undefined) { + properties[dependentProperty] = {}; } - showPropertyRules[dependentProperty][propertyName] = dependentPropertyValue; + if (properties[dependentProperty].showPropertyRules === undefined) { + properties[dependentProperty].showPropertyRules = {}; + } + properties[dependentProperty].showPropertyRules[propertyName] = dependentPropertyValue; } }); @@ -2073,7 +2232,6 @@ function loaded() { }); if (window.EventBridge !== undefined) { - var properties; EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); if (data.type === "server_script_status") { @@ -2112,21 +2270,21 @@ function loaded() { } } - elPropertyElements["type"][0].style.display = "none"; - elPropertyElements["type"][1].innerHTML = NO_SELECTION; + resetProperties(); + + getPropertyElement("type")[0].style.display = "none"; + getPropertyElement("type")[1].innerHTML = NO_SELECTION; elPropertiesList.className = ''; showGroupsForType("None"); - - resetProperties(); deleteJSONEditor(); - elPropertyElements["userData"].value = ""; + getPropertyElement("userData").value = ""; showUserDataTextArea(); showSaveUserDataButton(); showNewJSONEditorButton(); deleteJSONMaterialEditor(); - elPropertyElements["materialData"].value = ""; + getPropertyElement("materialData").value = ""; showMaterialDataTextArea(); showSaveMaterialDataButton(); showNewJSONMaterialEditorButton(); @@ -2157,18 +2315,19 @@ function loaded() { type = selections[0].properties.type; } - elPropertyElements["type"][0].innerHTML = ICON_FOR_TYPE[type]; - elPropertyElements["type"][0].style.display = "inline-block"; - elPropertyElements["type"][1].innerHTML = type + " (" + data.selections.length + ")"; + resetProperties(); + + getPropertyElement("type")[0].innerHTML = ICON_FOR_TYPE[type]; + getPropertyElement("type")[0].style.display = "inline-block"; + getPropertyElement("type")[1].innerHTML = type + " (" + data.selections.length + ")"; elPropertiesList.className = ''; showGroupsForType(type); - resetProperties(); disableProperties(); } else { - properties = data.selections[0].properties; + selectedEntityProperties = data.selections[0].properties; - if (lastEntityID !== '"' + properties.id + '"' && lastEntityID !== null) { + if (lastEntityID !== '"' + selectedEntityProperties.id + '"' && lastEntityID !== null) { if (editor !== null) { saveUserData(); } @@ -2177,132 +2336,144 @@ function loaded() { } } - let doSelectElement = lastEntityID === '"' + properties.id + '"'; + let doSelectElement = lastEntityID === '"' + selectedEntityProperties.id + '"'; // the event bridge and json parsing handle our avatar id string differently. - lastEntityID = '"' + properties.id + '"'; + lastEntityID = '"' + selectedEntityProperties.id + '"'; // HTML workaround since image is not yet a separate entity type let IMAGE_MODEL_NAME = 'default-image-model.fbx'; - if (properties.type === "Model") { - let urlParts = properties.modelURL.split('/'); + if (selectedEntityProperties.type === "Model") { + let urlParts = selectedEntityProperties.modelURL.split('/'); let propsFilename = urlParts[urlParts.length - 1]; if (propsFilename === IMAGE_MODEL_NAME) { - properties.type = "Image"; + selectedEntityProperties.type = "Image"; } } // Create class name for css ruleset filtering - elPropertiesList.className = properties.type + 'Menu'; - showGroupsForType(properties.type); + elPropertiesList.className = selectedEntityProperties.type + 'Menu'; + showGroupsForType(selectedEntityProperties.type); - for (let propertyName in elPropertyElements) { - let elProperty = elPropertyElements[propertyName]; + for (let propertyName in properties) { + let property = properties[propertyName]; + let elProperty = property.el; + let propertyData = property.data; + // if this is a compound property name (i.e. animation.running) + // then split it by . up to 3 times to find property value let propertyValue; let splitPropertyName = propertyName.split('.'); if (splitPropertyName.length > 1) { let propertyGroupName = splitPropertyName[0]; let subPropertyName = splitPropertyName[1]; - if (properties[propertyGroupName] === undefined || properties[propertyGroupName][subPropertyName] === undefined) { + let groupProperties = selectedEntityProperties[propertyGroupName]; + if (groupProperties === undefined || groupProperties[subPropertyName] === undefined) { continue; } if (splitPropertyName.length === 3) { let subSubPropertyName = splitPropertyName[2]; - propertyValue = properties[propertyGroupName][subPropertyName][subSubPropertyName]; + propertyValue = groupProperties[subPropertyName][subSubPropertyName]; } else { - propertyValue = properties[propertyGroupName][subPropertyName]; + propertyValue = groupProperties[subPropertyName]; } } else { - propertyValue = properties[propertyName]; + propertyValue = selectedEntityProperties[propertyName]; } // workaround for shape Color & Light Color property fields sharing same property value "color" if (propertyValue === undefined && propertyName === "lightColor") { - propertyValue = properties["color"] + propertyValue = selectedEntityProperties["color"] } - let isSubProperty = subProperties.indexOf(propertyName) > -1; + let isSubProperty = propertyData.subPropertyOf !== undefined; if (elProperty === undefined || (propertyValue === undefined && !isSubProperty)) { continue; } - if (elProperty instanceof Array) { - if (elProperty[1].getAttribute("type") === "number") { // vectors - if (elProperty.length === 2 || elProperty.length === 3) { - // vec2/vec3 are array of 2/3 input numbers - elProperty[0].value = (propertyValue.x * 1 / elProperty[0].getAttribute("multiplier")).toFixed(4); - elProperty[1].value = (propertyValue.y * 1 / elProperty[1].getAttribute("multiplier")).toFixed(4); - if (elProperty[2] !== undefined) { - elProperty[2].value = (propertyValue.z * 1 / elProperty[2].getAttribute("multiplier")).toFixed(4); - } - } else if (elProperty.length === 4) { - // color is array of color picker and 3 input numbers - elProperty[0].style.backgroundColor = "rgb(" + propertyValue.red + "," + propertyValue.green + "," + propertyValue.blue + ")"; - elProperty[1].value = propertyValue.red; - elProperty[2].value = propertyValue.green; - elProperty[3].value = propertyValue.blue; - } - } else if (elProperty[0].nodeName === "SPAN") { // icons - // icon is array of elSpan (icon glyph) and elLabel - elProperty[0].innerHTML = icons[propertyName][propertyValue]; - elProperty[0].style.display = "inline-block"; - elProperty[1].innerHTML = propertyValue; //+ " (" + data.selections.length + ")"; + switch (propertyData.type) { + case 'string': { + elProperty.value = propertyValue; + break; + } + case 'bool': { + let inverse = propertyData.inverse !== undefined ? propertyData.inverse : false; + if (isSubProperty) { + let propertyValue = selectedEntityProperties[propertyData.subPropertyOf]; + let subProperties = propertyValue.split(","); + let subPropertyValue = subProperties.indexOf(propertyName) > -1; + elProperty.checked = inverse ? !subPropertyValue : subPropertyValue; + } else { + elProperty.checked = inverse ? !propertyValue : propertyValue; + } + break; + } + case 'number': { + let fixedDecimals = propertyData.fixedDecimals !== undefined ? propertyData.fixedDecimals : 0; + elProperty.value = propertyValue.toFixed(fixedDecimals); + break; + } + case 'vec3': + case 'vec2': { + // vec2/vec3 are array of 2/3 elInput numbers + let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; + elProperty[0].value = (propertyValue.x / multiplier).toFixed(4); + elProperty[1].value = (propertyValue.y / multiplier).toFixed(4); + if (elProperty[2] !== undefined) { + elProperty[2].value = (propertyValue.z / multiplier).toFixed(4); + } + break; + } + case 'color': { + // color is array of color picker and 3 elInput numbers + elProperty[0].style.backgroundColor = "rgb(" + propertyValue.red + "," + + propertyValue.green + "," + + propertyValue.blue + ")"; + elProperty[1].value = propertyValue.red; + elProperty[2].value = propertyValue.green; + elProperty[3].value = propertyValue.blue; + break; + } + case 'dropdown': { + elProperty.value = propertyValue; + setDropdownText(elProperty); + break; + } + case 'textarea': { + elProperty.value = propertyValue; + setTextareaScrolling(elProperty); + break; + } + case 'icon': { + // icon is array of elSpan (icon glyph) and elLabel + elProperty[0].innerHTML = propertyData.icons[propertyValue]; + elProperty[0].style.display = "inline-block"; + elProperty[1].innerHTML = propertyValue; + break; } - } else if (elProperty.getAttribute("subPropertyOf")) { - let propertyValue = properties[elProperty.getAttribute("subPropertyOf")]; - let subProperties = propertyValue.split(","); - elProperty.checked = subProperties.indexOf(propertyName) > -1; - } else if (elProperty.getAttribute("type") === "number") { - let fixedDecimals = elProperty.getAttribute("fixedDecimals"); - elProperty.value = propertyValue.toFixed(fixedDecimals); - } else if (elProperty.getAttribute("type") === "checkbox") { - let inverse = elProperty.getAttribute("inverse") === "true"; - elProperty.checked = inverse ? !propertyValue : propertyValue; - } else if (elProperty.nodeName === "TEXTAREA") { - elProperty.value = propertyValue; - setTextareaScrolling(elProperty); - } else if (elProperty.nodeName === "DT") { // dropdown - elProperty.value = propertyValue; - setDropdownText(elProperty); - } else { - elProperty.value = propertyValue; } - let propertyShowRules = showPropertyRules[propertyName]; - if (propertyShowRules !== undefined) { - for (let propertyToShow in propertyShowRules) { - let showIfThisPropertyValue = propertyShowRules[propertyToShow]; + let showPropertyRules = property.showPropertyRules; + if (showPropertyRules !== undefined) { + for (let propertyToShow in showPropertyRules) { + let showIfThisPropertyValue = showPropertyRules[propertyToShow]; let show = String(propertyValue) === String(showIfThisPropertyValue); - let elPropertyToShow = elPropertyElements[propertyToShow]; - if (elPropertyToShow) { - let nodeToShow = elPropertyToShow; - if (elPropertyToShow.nodeName !== "DIV") { - let parentNode = elPropertyToShow.parentNode; - if (parentNode === undefined && elPropertyToShow instanceof Array) { - parentNode = elPropertyToShow[0].parentNode; - } - if (parentNode !== undefined) { - nodeToShow = parentNode; - } - } - nodeToShow.style.display = show ? "table" : "none"; - } + showPropertyElement(propertyToShow, show); } } } - let elGrabbable = elPropertyElements["grabbable"]; - let elTriggerable = elPropertyElements["triggerable"]; - let elIgnoreIK = elPropertyElements["ignoreIK"]; - elGrabbable.checked = elPropertyElements["dynamic"].checked; + let elGrabbable = getPropertyElement("grabbable"); + let elTriggerable = getPropertyElement("triggerable"); + let elIgnoreIK = getPropertyElement("ignoreIK"); + elGrabbable.checked = getPropertyElement("dynamic").checked; elTriggerable.checked = false; elIgnoreIK.checked = true; let grabbablesSet = false; let parsedUserData = {}; try { - parsedUserData = JSON.parse(properties.userData); + parsedUserData = JSON.parse(selectedEntityProperties.userData); if ("grabbableKey" in parsedUserData) { grabbablesSet = true; let grabbableData = parsedUserData.grabbableKey; @@ -2333,19 +2504,20 @@ function loaded() { elIgnoreIK.checked = true; } - if (properties.type === "Image") { - var imageLink = JSON.parse(properties.textures)["tex.picture"]; - elPropertyElements["image"].value = imageLink; - } else if (properties.type === "Material") { - let elParentMaterialNameString = elPropertyElements["materialNameToReplace"]; - let elParentMaterialNameNumber = elPropertyElements["submeshToReplace"]; - let elParentMaterialNameCheckbox = elPropertyElements["selectSubmesh"]; - if (properties.parentMaterialName.startsWith(MATERIAL_PREFIX_STRING)) { - elParentMaterialNameString.value = properties.parentMaterialName.replace(MATERIAL_PREFIX_STRING, ""); + if (selectedEntityProperties.type === "Image") { + let imageLink = JSON.parse(selectedEntityProperties.textures)["tex.picture"]; + getPropertyElement("image").value = imageLink; + } else if (selectedEntityProperties.type === "Material") { + let elParentMaterialNameString = getPropertyElement("materialNameToReplace"); + let elParentMaterialNameNumber = getPropertyElement("submeshToReplace"); + let elParentMaterialNameCheckbox = getPropertyElement("selectSubmesh"); + let parentMaterialName = selectedEntityProperties.parentMaterialName; + if (parentMaterialName.startsWith(MATERIAL_PREFIX_STRING)) { + elParentMaterialNameString.value = parentMaterialName.replace(MATERIAL_PREFIX_STRING, ""); showParentMaterialNameBox(false, elParentMaterialNameNumber, elParentMaterialNameString); elParentMaterialNameCheckbox.checked = false; } else { - elParentMaterialNameNumber.value = parseInt(properties.parentMaterialName); + elParentMaterialNameNumber.value = parseInt(parentMaterialName); showParentMaterialNameBox(true, elParentMaterialNameNumber, elParentMaterialNameString); elParentMaterialNameCheckbox.checked = true; } @@ -2353,11 +2525,11 @@ function loaded() { let json = null; try { - json = JSON.parse(properties.userData); + json = JSON.parse(selectedEntityProperties.userData); } catch (e) { // normal text deleteJSONEditor(); - elPropertyElements["userData"].value = properties.userData; + getPropertyElement("userData").value = selectedEntityProperties.userData; showUserDataTextArea(); showNewJSONEditorButton(); hideSaveUserDataButton(); @@ -2376,11 +2548,11 @@ function loaded() { let materialJson = null; try { - materialJson = JSON.parse(properties.materialData); + materialJson = JSON.parse(selectedEntityProperties.materialData); } catch (e) { // normal text deleteJSONMaterialEditor(); - elPropertyElements["materialData"].value = properties.materialData; + getPropertyElement("materialData").value = selectedEntityProperties.materialData; showMaterialDataTextArea(); showNewJSONMaterialEditorButton(); hideSaveMaterialDataButton(); @@ -2397,16 +2569,16 @@ function loaded() { hideMaterialDataSaved(); } - if (properties.locked) { + if (selectedEntityProperties.locked) { disableProperties(); - elPropertyElements["locked"].removeAttribute('disabled'); + getPropertyElement("locked").removeAttribute('disabled'); } else { enableProperties(); disableSaveUserDataButton(); disableSaveMaterialDataButton() } - var activeElement = document.activeElement; + let activeElement = document.activeElement; if (doSelectElement && typeof activeElement.select !== "undefined") { activeElement.select(); } @@ -2416,14 +2588,15 @@ function loaded() { } // Server Script Status - let elServerScript = elPropertyElements["serverScripts"]; + let elServerScript = getPropertyElement("serverScripts"); + let serverScriptElementID = getPropertyElementID("serverScripts"); let elDiv = document.createElement('div'); elDiv.setAttribute("class", "property"); let elLabel = document.createElement('label'); - elLabel.setAttribute("for", "property-serverScripts-status"); + elLabel.setAttribute("for", serverScriptElementID + "-status"); elLabel.innerText = "Server Script Status"; let elServerScriptStatus = document.createElement('span'); - elServerScriptStatus.setAttribute("id", "property-serverScripts-status"); + elServerScriptStatus.setAttribute("id", serverScriptElementID + "-status"); elDiv.appendChild(elLabel); elDiv.appendChild(elServerScriptStatus); elServerScript.parentNode.appendChild(elDiv); @@ -2432,46 +2605,48 @@ function loaded() { elDiv = document.createElement('div'); elDiv.setAttribute("class", "property"); let elServerScriptError = document.createElement('textarea'); - elServerScriptError.setAttribute("id", "property-serverScripts-error"); + elServerScriptError.setAttribute("id", serverScriptElementID + "-error"); elDiv.appendChild(elServerScriptError); elServerScript.parentNode.appendChild(elDiv); - let elScript = elPropertyElements["script"]; + let elScript = getPropertyElement("script"); elScript.parentNode.setAttribute("class", "property url refresh"); elServerScript.parentNode.setAttribute("class", "property url refresh"); // User Data - let elUserData = elPropertyElements["userData"]; + let elUserData = getPropertyElement("userData"); + let userDataElementID = getPropertyElementID("userData"); elDiv = elUserData.parentNode; let elStaticUserData = document.createElement('div'); - elStaticUserData.setAttribute("id", "property-userData-static"); + elStaticUserData.setAttribute("id", userDataElementID + "-static"); let elUserDataEditor = document.createElement('div'); - elUserDataEditor.setAttribute("id", "property-userData-editor"); + elUserDataEditor.setAttribute("id", userDataElementID + "-editor"); let elUserDataSaved = document.createElement('span'); - elUserDataSaved.setAttribute("id", "property-userData-saved"); + elUserDataSaved.setAttribute("id", userDataElementID + "-saved"); elUserDataSaved.innerText = "Saved!"; elDiv.childNodes[2].appendChild(elUserDataSaved); elDiv.insertBefore(elStaticUserData, elUserData); elDiv.insertBefore(elUserDataEditor, elUserData); // Material Data - let elMaterialData = elPropertyElements["materialData"]; + let elMaterialData = getPropertyElement("materialData"); + let materialDataElementID = getPropertyElementID("materialData"); elDiv = elMaterialData.parentNode; let elStaticMaterialData = document.createElement('div'); - elStaticMaterialData.setAttribute("id", "property-materialData-static"); + elStaticMaterialData.setAttribute("id", materialDataElementID + "-static"); let elMaterialDataEditor = document.createElement('div'); - elMaterialDataEditor.setAttribute("id", "property-materialData-editor"); + elMaterialDataEditor.setAttribute("id", materialDataElementID + "-editor"); let elMaterialDataSaved = document.createElement('span'); - elMaterialDataSaved.setAttribute("id", "property-materialData-saved"); + elMaterialDataSaved.setAttribute("id", materialDataElementID + "-saved"); elMaterialDataSaved.innerText = "Saved!"; elDiv.childNodes[2].appendChild(elMaterialDataSaved); elDiv.insertBefore(elStaticMaterialData, elMaterialData); elDiv.insertBefore(elMaterialDataEditor, elMaterialData); // User Data Fields - let elGrabbable = elPropertyElements["grabbable"]; - let elTriggerable = elPropertyElements["triggerable"]; - let elIgnoreIK = elPropertyElements["ignoreIK"]; + let elGrabbable = getPropertyElement("grabbable"); + let elTriggerable = getPropertyElement("triggerable"); + let elIgnoreIK = getPropertyElement("ignoreIK"); elGrabbable.addEventListener('change', function() { userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, true); }); @@ -2483,11 +2658,15 @@ function loaded() { }); // Special Property Callbacks - let elParentMaterialNameString = elPropertyElements["materialNameToReplace"]; - let elParentMaterialNameNumber = elPropertyElements["submeshToReplace"]; - let elParentMaterialNameCheckbox = elPropertyElements["selectSubmesh"]; - elParentMaterialNameString.addEventListener('change', function () { updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + this.value); }); - elParentMaterialNameNumber.addEventListener('change', function () { updateProperty("parentMaterialName", this.value); }); + let elParentMaterialNameString = getPropertyElement("materialNameToReplace"); + let elParentMaterialNameNumber = getPropertyElement("submeshToReplace"); + let elParentMaterialNameCheckbox = getPropertyElement("selectSubmesh"); + elParentMaterialNameString.addEventListener('change', function () { + updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + this.value); + }); + elParentMaterialNameNumber.addEventListener('change', function () { + updateProperty("parentMaterialName", this.value); + }); elParentMaterialNameCheckbox.addEventListener('change', function () { if (this.checked) { updateProperty("parentMaterialName", elParentMaterialNameNumber.value); @@ -2498,7 +2677,7 @@ function loaded() { } }); - elPropertyElements["image"].addEventListener('change', createImageURLUpdateFunction('textures')); + getPropertyElement("image").addEventListener('change', createImageURLUpdateFunction('textures')); // Collapsible sections let elCollapsible = document.getElementsByClassName("section-header"); @@ -2544,40 +2723,7 @@ function loaded() { //
    • ...
    • //
    //
    - //
    - function setDropdownText(dropdown) { - let lis = dropdown.parentNode.getElementsByTagName("li"); - let text = ""; - for (let i = 0; i < lis.length; i++) { - if (String(lis[i].getAttribute("value")) === String(dropdown.value)) { - text = lis[i].textContent; - } - } - dropdown.firstChild.textContent = text; - } - - function toggleDropdown(event) { - let element = event.target; - if (element.nodeName !== "DT") { - element = element.parentNode; - } - element = element.parentNode; - let isDropped = element.getAttribute("dropped"); - element.setAttribute("dropped", isDropped !== "true" ? "true" : "false"); - } - - function setDropdownValue(event) { - let dt = event.target.parentNode.parentNode.previousSibling; - dt.value = event.target.getAttribute("value"); - dt.firstChild.textContent = event.target.textContent; - - dt.parentNode.setAttribute("dropped", "false"); - - let evt = document.createEvent("HTMLEvents"); - evt.initEvent("change", true, true); - dt.dispatchEvent(evt); - } - + //
let elDropdowns = document.getElementsByTagName("select"); for (let dropDownIndex = 0; dropDownIndex < elDropdowns.length; ++dropDownIndex) { let options = elDropdowns[dropDownIndex].getElementsByTagName("option"); @@ -2623,7 +2769,7 @@ function loaded() { } let propertyName = elDropdowns[dropDownIndex].getAttribute("propertyName"); - elPropertyElements[propertyName] = dt; + properties[propertyName].el = dt; dt.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); } From ac0e999629031c696c96e28b3ff6d2a89ebf891f Mon Sep 17 00:00:00 2001 From: David Back Date: Wed, 10 Oct 2018 17:31:06 -0700 Subject: [PATCH 074/362] fix colors --- scripts/system/html/js/entityProperties.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 2841414d87..a58b5ca8cd 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -928,7 +928,9 @@ function getPropertyElement(propertyName) { } function getPropertyElementID(propertyName) { - return "property-" + propertyName; + let propertyElementID = "property-" + propertyName; + propertyElementID = propertyElementID.replace(".", "-"); + return propertyElementID; } function enableChildren(el, selector) { @@ -1077,7 +1079,6 @@ function showGroupsForType(type) { } - // PROPERTY UPDATE FUNCTIONS function updateProperty(propertyName, propertyValue) { @@ -1207,7 +1208,6 @@ function createImageURLUpdateFunction(propertyName) { } - // PROPERTY ELEMENT CREATION FUNCTIONS function createStringProperty(elProperty, elLabel, propertyData) { @@ -1381,7 +1381,7 @@ function createColorProperty(elProperty, elLabel, propertyData) { }, onChange: function(hsb, hex, rgb, el) { $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); + emitColorPropertyUpdate(propertyName, rgb.r, rgb.g, rgb.b); } }); @@ -1573,7 +1573,6 @@ function addButtons(elProperty, propertyID, buttons, newRow) { } - // BUTTON CALLBACKS function rescaleDimensions() { @@ -1628,7 +1627,6 @@ function copySkyboxURLToAmbientURL() { } - // USER DATA FUNCTIONS function clearUserData() { @@ -1850,7 +1848,6 @@ function saveJSONUserData(noUpdate) { } - // MATERIAL DATA FUNCTIONS function clearMaterialData() { @@ -2035,7 +2032,6 @@ function bindAllNonJSONEditorElements() { } - // DROPDOWNS / TEXTAREAS / PARENT MATERIAL NAME FUNCTIONS function setDropdownText(dropdown) { @@ -2122,7 +2118,6 @@ function loaded() { let propertyType = property.type; let propertyName = property.propertyName; let propertyElementID = getPropertyElementID(propertyName); - propertyElementID = propertyElementID.replace(".", "-"); let elProperty; if (propertyType === "sub-header") { From 5d4de3d3b0130aa7c6b00c5aa83fe12af5d808af Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 10 Oct 2018 17:36:38 -0700 Subject: [PATCH 075/362] I love it. --- interface/src/scripting/SpeechScriptingInterface.cpp | 9 ++++++--- interface/src/scripting/SpeechScriptingInterface.h | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/interface/src/scripting/SpeechScriptingInterface.cpp b/interface/src/scripting/SpeechScriptingInterface.cpp index 3b3ecf728d..b8e0f5c3e8 100644 --- a/interface/src/scripting/SpeechScriptingInterface.cpp +++ b/interface/src/scripting/SpeechScriptingInterface.cpp @@ -11,7 +11,6 @@ #include "SpeechScriptingInterface.h" #include "avatar/AvatarManager.h" -#include SpeechScriptingInterface::SpeechScriptingInterface() { // @@ -94,7 +93,7 @@ void SpeechScriptingInterface::speakText(const QString& textToSpeak) { ULONG streamNumber; hr = m_tts->Speak(reinterpret_cast(textToSpeak.utf16()), - SPF_IS_NOT_XML | SPF_ASYNC | SPF_PURGEBEFORESPEAK, + SPF_IS_XML | SPF_ASYNC | SPF_PURGEBEFORESPEAK, &streamNumber); if (FAILED(hr)) { qDebug() << "Speak failed."; @@ -130,5 +129,9 @@ void SpeechScriptingInterface::speakText(const QString& textToSpeak) { AudioInjectorOptions options; options.position = DependencyManager::get()->getMyAvatarPosition(); - AudioInjector::playSound(byteArray, options); + lastSound = AudioInjector::playSound(byteArray, options); +} + +void SpeechScriptingInterface::stopLastSpeech() { + lastSound->stop(); } diff --git a/interface/src/scripting/SpeechScriptingInterface.h b/interface/src/scripting/SpeechScriptingInterface.h index ad6777e339..c683a1a3c6 100644 --- a/interface/src/scripting/SpeechScriptingInterface.h +++ b/interface/src/scripting/SpeechScriptingInterface.h @@ -18,6 +18,7 @@ #endif #include // SAPI #include // SAPI Helper +#include class SpeechScriptingInterface : public QObject, public Dependency { Q_OBJECT @@ -27,9 +28,9 @@ public: ~SpeechScriptingInterface(); Q_INVOKABLE void speakText(const QString& textToSpeak); + Q_INVOKABLE void stopLastSpeech(); private: - class CComAutoInit { public: // Initializes COM using CoInitialize. @@ -70,6 +71,8 @@ private: // Default voice token CComPtr m_voiceToken; + + AudioInjectorPointer lastSound; }; #endif // hifi_SpeechScriptingInterface_h From a3a87ef48b1960b929eafef3adde9ae9c82f7af5 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Wed, 10 Oct 2018 17:52:26 -0700 Subject: [PATCH 076/362] Debug rendering of tracked objects, remove filters from body. --- interface/resources/controllers/vive.json | 29 ++++---------- interface/src/Application.cpp | 38 +++++++++++++++++++ .../filters/ExponentialSmoothingFilter.cpp | 2 +- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 8a7744efb3..42be6f3f04 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -51,30 +51,15 @@ { "from": "Vive.RSCenter", "to": "Standard.RightPrimaryThumb" }, { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, - { "from": "Vive.LeftHand", "to": "Standard.LeftHand"}, - { "from": "Vive.RightHand", "to": "Standard.RightHand"}, + { "from": "Vive.LeftHand", "to": "Standard.LeftHand" }, + { "from": "Vive.RightHand", "to": "Standard.RightHand" }, - { - "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot", - "filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.3}] - }, + { "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot" }, + { "from": "Vive.RightFoot", "to" : "Standard.RightFoot" }, + { "from": "Vive.Hips", "to" : "Standard.Hips" }, + { "from": "Vive.Spine2", "to" : "Standard.Spine2" }, - { - "from": "Vive.RightFoot", "to" : "Standard.RightFoot", - "filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.3}] - }, - - { - "from": "Vive.Hips", "to" : "Standard.Hips", - "filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.3}] - }, - - { - "from": "Vive.Spine2", "to" : "Standard.Spine2", - "filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.3}] - }, - - { "from": "Vive.Head", "to" : "Standard.Head"}, + { "from": "Vive.Head", "to" : "Standard.Head" }, { "from": "Vive.RightArm", "to" : "Standard.RightArm" }, { "from": "Vive.LeftArm", "to" : "Standard.LeftArm" }, diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index aa2b382c58..7457afe091 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5801,6 +5801,44 @@ void Application::update(float deltaTime) { controller::Pose pose = userInputMapper->getPoseState(action); myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix)); } + + // AJT: TODO put a nice menu around this. + // Make sure to remove all markers when menu is turned off. + { + static const std::vector trackedObjectActions = { + controller::Action::TRACKED_OBJECT_00, + controller::Action::TRACKED_OBJECT_01, + controller::Action::TRACKED_OBJECT_02, + controller::Action::TRACKED_OBJECT_03, + controller::Action::TRACKED_OBJECT_04, + controller::Action::TRACKED_OBJECT_05, + controller::Action::TRACKED_OBJECT_06, + controller::Action::TRACKED_OBJECT_07, + controller::Action::TRACKED_OBJECT_08, + controller::Action::TRACKED_OBJECT_09, + controller::Action::TRACKED_OBJECT_10, + controller::Action::TRACKED_OBJECT_11, + controller::Action::TRACKED_OBJECT_12, + controller::Action::TRACKED_OBJECT_13, + controller::Action::TRACKED_OBJECT_14, + controller::Action::TRACKED_OBJECT_15 + }; + + int i = 0; + glm::vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f); + for (auto& action : trackedObjectActions) { + QString key = QString("_TrackedObject%1").arg(i); + controller::Pose pose = userInputMapper->getPoseState(action); + if (pose.valid) { + glm::vec3 pos = transformPoint(myAvatarMatrix, pose.translation); + glm::quat rot = glmExtractRotation(myAvatarMatrix) * pose.rotation; + DebugDraw::getInstance().addMarker(key, rot, pos, BLUE); + } else { + DebugDraw::getInstance().removeMarker(key); + } + i++; + } + } } updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... diff --git a/libraries/controllers/src/controllers/impl/filters/ExponentialSmoothingFilter.cpp b/libraries/controllers/src/controllers/impl/filters/ExponentialSmoothingFilter.cpp index 9cf2673d55..0f204ce15f 100644 --- a/libraries/controllers/src/controllers/impl/filters/ExponentialSmoothingFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/ExponentialSmoothingFilter.cpp @@ -35,7 +35,7 @@ namespace controller { if (_prevSensorValue.isValid()) { // exponential smoothing filter sensorValue.translation = _translationConstant * sensorValue.getTranslation() + (1.0f - _translationConstant) * _prevSensorValue.getTranslation(); - sensorValue.rotation = safeMix(sensorValue.getRotation(), _prevSensorValue.getRotation(), _rotationConstant); + sensorValue.rotation = safeMix(sensorValue.getRotation(), _prevSensorValue.getRotation(), (1.0f - _rotationConstant)); // remember previous sensor space value. _prevSensorValue = sensorValue; From e5c2605ac29097349bd4fb1e1ce792736ab98b38 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Wed, 10 Oct 2018 17:53:14 -0700 Subject: [PATCH 077/362] Added filtered-puck-attach app to test filters on tracked objects. --- .../developer/tests/filtered-puck-attach.js | 425 ++++++++++++++++++ 1 file changed, 425 insertions(+) create mode 100644 scripts/developer/tests/filtered-puck-attach.js diff --git a/scripts/developer/tests/filtered-puck-attach.js b/scripts/developer/tests/filtered-puck-attach.js new file mode 100644 index 0000000000..23217886d5 --- /dev/null +++ b/scripts/developer/tests/filtered-puck-attach.js @@ -0,0 +1,425 @@ +// +// Created by Anthony J. Thibault on 2017/06/20 +// Modified by Robbie Uvanni to support multiple pucks and easier placement of pucks on entities, on 2017/08/01 +// 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 +// +// When this script is running, a new app button, named "PUCKTACH", will be added to the toolbar/tablet. +// Click this app to bring up the puck attachment panel. +// + +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +/* global Xform */ +Script.include("/~/system/libraries/Xform.js"); + +(function() { // BEGIN LOCAL_SCOPE + +var TABLET_BUTTON_NAME = "PUCKATTACH"; +var TABLET_APP_URL = "https://s3.amazonaws.com/hifi-public/tony/html/filtered-puck-attach.html?2"; +var NUM_TRACKED_OBJECTS = 16; + +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); +var tabletButton = tablet.addButton({ + text: TABLET_BUTTON_NAME, + icon: "https://s3.amazonaws.com/hifi-public/tony/icons/puck-i.svg", + activeIcon: "https://s3.amazonaws.com/hifi-public/tony/icons/puck-a.svg" +}); + +var shown = false; +function onScreenChanged(type, url) { + if (type === "Web" && url === TABLET_APP_URL) { + tabletButton.editProperties({isActive: true}); + if (!shown) { + // hook up to event bridge + tablet.webEventReceived.connect(onWebEventReceived); + shownChanged(true); + } + shown = true; + } else { + tabletButton.editProperties({isActive: false}); + if (shown) { + // disconnect from event bridge + tablet.webEventReceived.disconnect(onWebEventReceived); + shownChanged(false); + } + shown = false; + } +} +tablet.screenChanged.connect(onScreenChanged); + +function pad(num, size) { + var tempString = "000000000" + num; + return tempString.substr(tempString.length - size); +} +function indexToTrackedObjectName(index) { + return "TrackedObject" + pad(index, 2); +} +function getAvailableTrackedObjects() { + var available = []; + var i; + for (i = 0; i < NUM_TRACKED_OBJECTS; i++) { + var key = indexToTrackedObjectName(i); + var pose = Controller.getPoseValue(Controller.Standard[key]); + if (pose && pose.valid) { + available.push(i); + } + } + return available; +} +function sendAvailableTrackedObjects() { + tablet.emitScriptEvent(JSON.stringify({ + pucks: getAvailableTrackedObjects(), + selectedPuck: ((lastPuck === undefined) ? -1 : lastPuck.name) + })); +} + +function getRelativePosition(origin, rotation, offset) { + var relativeOffset = Vec3.multiplyQbyV(rotation, offset); + var worldPosition = Vec3.sum(origin, relativeOffset); + return worldPosition; +} +function getPropertyForEntity(entityID, propertyName) { + return Entities.getEntityProperties(entityID, [propertyName])[propertyName]; +} +function entityExists(entityID) { + return Object.keys(Entities.getEntityProperties(entityID)).length > 0; +} + +var VIVE_PUCK_MODEL = "https://s3.amazonaws.com/hifi-public/tony/vive_tracker_puck_y180z180.obj"; +var VIVE_PUCK_DIMENSIONS = { x: 0.0945, y: 0.0921, z: 0.0423 }; // 1/1000th scale of model +var VIVE_PUCK_SEARCH_DISTANCE = 1.5; // metres +var VIVE_PUCK_SPAWN_DISTANCE = 0.5; // metres +var VIVE_PUCK_TRACKED_OBJECT_MAX_DISTANCE = 10.0; // metres +var VIVE_PUCK_NAME = "Tracked Puck"; + +var trackedPucks = { }; +var lastPuck; + +var DEFAULT_ROTATION_SMOOTHING_CONSTANT = 1.0; // no smoothing +var DEFAULT_TRANSLATION_SMOOTHING_CONSTANT = 1.0; // no smoothing +var DEFAULT_TRANSLATION_ACCELERATION_LIMIT = 1000; // only extreme accelerations are smoothed +var DEFAULT_ROTATION_ACCELERATION_LIMIT = 10000; // only extreme accelerations are smoothed + +function buildMappingJson() { + var obj = {name: "com.highfidelity.testing.filteredPuckAttach", channels: []}; + var i; + for (i = 0; i < NUM_TRACKED_OBJECTS; i++) { + obj.channels.push({ + from: "Vive." + indexToTrackedObjectName(i), + to: "Standard." + indexToTrackedObjectName(i), + filters: [ + { + type: "accelerationLimiter", + translationAccelerationLimit: DEFAULT_TRANSLATION_ACCELERATION_LIMIT, + rotationAccelerationLimit: DEFAULT_ROTATION_ACCELERATION_LIMIT + }, + { + type: "exponentialSmoothing", + translation: DEFAULT_TRANSLATION_SMOOTHING_CONSTANT, + rotation: DEFAULT_ROTATION_SMOOTHING_CONSTANT + } + ] + }); + } + return obj; +} + +var mappingJson = buildMappingJson(); + +var mapping; +function mappingChanged() { + if (mapping) { + mapping.disable(); + } + mapping = Controller.parseMapping(JSON.stringify(mappingJson)); + mapping.enable(); +} + +function shownChanged(newShown) { + if (newShown) { + mappingChanged(); + } else { + mapping.disable(); + } +} + +function setTranslationAccelerationLimit(value) { + var i; + for (i = 0; i < NUM_TRACKED_OBJECTS; i++) { + mappingJson.channels[i].filters[0].translationAccelerationLimit = value; + } + mappingChanged(); +} + +function setTranslationSnapThreshold(value) { + // TODO: convert from mm +} + +function setRotationAccelerationLimit(value) { + var i; + for (i = 0; i < NUM_TRACKED_OBJECTS; i++) { + mappingJson.channels[i].filters[0].rotationAccelerationLimit = value; + } + mappingChanged(); +} + +function setRotationSnapThreshold(value) { + // TODO: convert from degrees +} + +function setTranslationSmoothingConstant(value) { + print("AJT: setting translation smoothing constant = " + value); + var i; + for (i = 0; i < NUM_TRACKED_OBJECTS; i++) { + mappingJson.channels[i].filters[1].translation = value; + } + mappingChanged(); + print("AJT: done, value = " + value); +} + +function setRotationSmoothingConstant(value) { + print("AJT: setRotationSmoothingConstant =" + value); + var i; + for (i = 0; i < NUM_TRACKED_OBJECTS; i++) { + mappingJson.channels[i].filters[1].rotation = value; + } + mappingChanged(); +} + + +function createPuck(puck) { + // create a puck entity and add it to our list of pucks + var action = indexToTrackedObjectName(puck.puckno); + var pose = Controller.getPoseValue(Controller.Standard[action]); + + if (pose && pose.valid) { + var spawnOffset = Vec3.multiply(Vec3.FRONT, VIVE_PUCK_SPAWN_DISTANCE); + var spawnPosition = getRelativePosition(MyAvatar.position, MyAvatar.orientation, spawnOffset); + + // should be an overlay + var puckEntityProperties = { + name: "Tracked Puck", + type: "Model", + modelURL: VIVE_PUCK_MODEL, + dimensions: VIVE_PUCK_DIMENSIONS, + position: spawnPosition, + userData: '{ "grabbableKey": { "grabbable": true, "kinematic": false } }' + }; + + var puckEntityID = Entities.addEntity(puckEntityProperties); + + // if we've already created this puck, destroy it + if (trackedPucks.hasOwnProperty(puck.puckno)) { + destroyPuck(puck.puckno); + } + // if we had an unfinalized puck, destroy it + if (lastPuck !== undefined) { + destroyPuck(lastPuck.name); + } + // create our new unfinalized puck + trackedPucks[puck.puckno] = { + puckEntityID: puckEntityID, + trackedEntityID: "" + }; + lastPuck = trackedPucks[puck.puckno]; + lastPuck.name = Number(puck.puckno); + } +} +function finalizePuck(puckName) { + // find nearest entity and change its parent to the puck + + if (!trackedPucks.hasOwnProperty(puckName)) { + print('2'); + return; + } + if (lastPuck === undefined) { + print('3'); + return; + } + if (lastPuck.name !== Number(puckName)) { + print('1'); + return; + } + + var puckPosition = getPropertyForEntity(lastPuck.puckEntityID, "position"); + var foundEntities = Entities.findEntities(puckPosition, VIVE_PUCK_SEARCH_DISTANCE); + + var foundEntity; + var leastDistance = Number.MAX_VALUE; + + for (var i = 0; i < foundEntities.length; i++) { + var entity = foundEntities[i]; + + if (getPropertyForEntity(entity, "name") !== VIVE_PUCK_NAME) { + var entityPosition = getPropertyForEntity(entity, "position"); + var d = Vec3.distance(entityPosition, puckPosition); + + if (d < leastDistance) { + leastDistance = d; + foundEntity = entity; + } + } + } + + if (foundEntity) { + lastPuck.trackedEntityID = foundEntity; + // remember the userdata and collisionless flag for the tracked entity since + // we're about to remove it and make it ungrabbable and collisionless + lastPuck.trackedEntityUserData = getPropertyForEntity(foundEntity, "userData"); + lastPuck.trackedEntityCollisionFlag = getPropertyForEntity(foundEntity, "collisionless"); + // update properties of the tracked entity + Entities.editEntity(lastPuck.trackedEntityID, { + "parentID": lastPuck.puckEntityID, + "userData": '{ "grabbableKey": { "grabbable": false } }', + "collisionless": 1 + }); + // remove reference to puck since it is now calibrated and finalized + lastPuck = undefined; + } +} +function updatePucks() { + // for each puck, update its position and orientation + for (var puckName in trackedPucks) { + if (!trackedPucks.hasOwnProperty(puckName)) { + continue; + } + var action = indexToTrackedObjectName(puckName); + var pose = Controller.getPoseValue(Controller.Standard[action]); + if (pose && pose.valid) { + var puck = trackedPucks[puckName]; + if (puck.trackedEntityID) { + if (entityExists(puck.trackedEntityID)) { + var avatarXform = new Xform(MyAvatar.orientation, MyAvatar.position); + var puckXform = new Xform(pose.rotation, pose.translation); + var finalXform = Xform.mul(avatarXform, puckXform); + + var d = Vec3.distance(MyAvatar.position, finalXform.pos); + if (d > VIVE_PUCK_TRACKED_OBJECT_MAX_DISTANCE) { + print('tried to move tracked object too far away: ' + d); + return; + } + + Entities.editEntity(puck.puckEntityID, { + position: finalXform.pos, + rotation: finalXform.rot + }); + + // in case someone grabbed both entities and destroyed the + // child/parent relationship + Entities.editEntity(puck.trackedEntityID, { + parentID: puck.puckEntityID + }); + } else { + destroyPuck(puckName); + } + } + } + } +} +function destroyPuck(puckName) { + // unparent entity and delete its parent + if (!trackedPucks.hasOwnProperty(puckName)) { + return; + } + + var puck = trackedPucks[puckName]; + var puckEntityID = puck.puckEntityID; + var trackedEntityID = puck.trackedEntityID; + + // remove the puck as a parent entity and restore the tracked entities + // former userdata and collision flag + Entities.editEntity(trackedEntityID, { + "parentID": "{00000000-0000-0000-0000-000000000000}", + "userData": puck.trackedEntityUserData, + "collisionless": puck.trackedEntityCollisionFlag + }); + + delete trackedPucks[puckName]; + + // in some cases, the entity deletion may occur before the parent change + // has been processed, resulting in both the puck and the tracked entity + // to be deleted so we wait 100ms before deleting the puck, assuming + // that the parent change has occured + var DELETE_TIMEOUT = 100; // ms + Script.setTimeout(function() { + // delete the puck + Entities.deleteEntity(puckEntityID); + }, DELETE_TIMEOUT); +} +function destroyPucks() { + // remove all pucks and unparent entities + for (var puckName in trackedPucks) { + if (trackedPucks.hasOwnProperty(puckName)) { + destroyPuck(puckName); + } + } +} + +function onWebEventReceived(msg) { + var obj = {}; + + try { + obj = JSON.parse(msg); + } catch (err) { + return; + } + + print("AJT: onWebEventReceived = " + msg); + + switch (obj.cmd) { + case "ready": + sendAvailableTrackedObjects(); + break; + case "create": + createPuck(obj); + break; + case "finalize": + finalizePuck(obj.puckno); + break; + case "destroy": + destroyPuck(obj.puckno); + break; + case "translation-acceleration-limit": + setTranslationAccelerationLimit(Number(obj.val)); + break; + case "translation-snap-threshold": + setTranslationSnapThreshold(Number(obj.val)); + break; + case "rotation-acceleration-limit": + setRotationAccelerationLimit(Number(obj.val)); + break; + case "rotation-snap-threshold": + setRotationSnapThreshold(Number(obj.val)); + break; + case "translation-smoothing-constant": + setTranslationSmoothingConstant(Number(obj.val)); + break; + case "rotation-smoothing-constant": + setRotationSmoothingConstant(Number(obj.val)); + break; + } +} + +Script.update.connect(updatePucks); +Script.scriptEnding.connect(function () { + tablet.removeButton(tabletButton); + destroyPucks(); + if (shown) { + tablet.webEventReceived.disconnect(onWebEventReceived); + tablet.gotoHomeScreen(); + } + tablet.screenChanged.disconnect(onScreenChanged); + if (mapping) { + mapping.disable(); + } +}); +tabletButton.clicked.connect(function () { + if (shown) { + tablet.gotoHomeScreen(); + } else { + tablet.gotoWebScreen(TABLET_APP_URL); + } +}); +}()); // END LOCAL_SCOPE From 717a5ed31b0a2031cda2f4f9b032af6422dca0b7 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Thu, 11 Oct 2018 09:22:12 -0700 Subject: [PATCH 078/362] Added snap threshold to AccelerationLimiterFilter --- .../filters/AccelerationLimiterFilter.cpp | 23 +++++++++------ .../impl/filters/AccelerationLimiterFilter.h | 4 +-- .../developer/tests/filtered-puck-attach.js | 29 ++++++++++++++----- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp index aacbdd2cea..3db1a9fba6 100644 --- a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.cpp @@ -18,9 +18,9 @@ #include static const QString JSON_ROTATION_ACCELERATION_LIMIT = QStringLiteral("rotationAccelerationLimit"); -static const QString JSON_ROTATION_DECELERATION_LIMIT = QStringLiteral("rotationDecelerationLimit"); static const QString JSON_TRANSLATION_ACCELERATION_LIMIT = QStringLiteral("translationAccelerationLimit"); -static const QString JSON_TRANSLATION_DECELERATION_LIMIT = QStringLiteral("translationDecelerationLimit"); +static const QString JSON_TRANSLATION_SNAP_THRESHOLD = QStringLiteral("translationSnapThreshold"); +static const QString JSON_ROTATION_SNAP_THRESHOLD = QStringLiteral("rotationSnapThreshold"); static glm::vec3 angularVelFromDeltaRot(const glm::quat& deltaQ, float dt) { // Measure the angular velocity of a delta rotation quaternion by using quaternion logarithm. @@ -39,7 +39,7 @@ static glm::quat deltaRotFromAngularVel(const glm::vec3& omega, float dt) { } static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, const glm::vec3& x2, const glm::vec3& x3, - float dt, const float accLimit) { + float dt, float accLimit, float snapThreshold) { // measure the linear velocities of this step and the previoius step glm::vec3 v1 = (x3 - x1) / (2.0f * dt); @@ -52,7 +52,8 @@ static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, con float aLen = glm::length(a); // pick limit based on if we are moving faster then our target - if (aLen > accLimit) { + float distToTarget = glm::length(x3 - x2); + if (aLen > accLimit && distToTarget > snapThreshold) { // Solve for a new `v1`, such that `a` does not exceed `aLimit` // This combines two steps: // 1) Computing a limited accelration in the direction of `a`, but with a magnitute of `aLimit`: @@ -71,7 +72,7 @@ static glm::vec3 filterTranslation(const glm::vec3& x0, const glm::vec3& x1, con } static glm::quat filterRotation(const glm::quat& q0In, const glm::quat& q1In, const glm::quat& q2In, const glm::quat& q3In, - float dt, const float accLimit) { + float dt, float accLimit, float snapThreshold) { // ensure quaternions have the same polarity glm::quat q0 = q0In; @@ -87,7 +88,8 @@ static glm::quat filterRotation(const glm::quat& q0In, const glm::quat& q1In, co float aLen = glm::length(a); // clamp the acceleration if it is over the limit - if (aLen > accLimit) { + float angleToTarget = glm::angle(q3 * glm::inverse(q2)); + if (aLen > accLimit && angleToTarget > snapThreshold) { // solve for a new w1, such that a does not exceed the accLimit w1 = a * ((accLimit * dt) / aLen) + w0; @@ -120,10 +122,10 @@ namespace controller { glm::vec3 unfilteredTranslation = sensorValue.translation; sensorValue.translation = filterTranslation(_prevPos[0], _prevPos[1], _prevPos[2], sensorValue.translation, - DELTA_TIME, _translationAccelerationLimit); + DELTA_TIME, _translationAccelerationLimit, _translationSnapThreshold); glm::quat unfilteredRot = sensorValue.rotation; sensorValue.rotation = filterRotation(_prevRot[0], _prevRot[1], _prevRot[2], sensorValue.rotation, - DELTA_TIME, _rotationAccelerationLimit); + DELTA_TIME, _rotationAccelerationLimit, _rotationSnapThreshold); // remember previous values. _prevPos[0] = _prevPos[1]; @@ -175,9 +177,12 @@ namespace controller { bool AccelerationLimiterFilter::parseParameters(const QJsonValue& parameters) { if (parameters.isObject()) { auto obj = parameters.toObject(); - if (obj.contains(JSON_ROTATION_ACCELERATION_LIMIT) && obj.contains(JSON_TRANSLATION_ACCELERATION_LIMIT)) { + if (obj.contains(JSON_ROTATION_ACCELERATION_LIMIT) && obj.contains(JSON_TRANSLATION_ACCELERATION_LIMIT) && + obj.contains(JSON_ROTATION_SNAP_THRESHOLD) && obj.contains(JSON_TRANSLATION_SNAP_THRESHOLD)) { _rotationAccelerationLimit = (float)obj[JSON_ROTATION_ACCELERATION_LIMIT].toDouble(); _translationAccelerationLimit = (float)obj[JSON_TRANSLATION_ACCELERATION_LIMIT].toDouble(); + _rotationSnapThreshold = (float)obj[JSON_ROTATION_SNAP_THRESHOLD].toDouble(); + _translationSnapThreshold = (float)obj[JSON_TRANSLATION_SNAP_THRESHOLD].toDouble(); return true; } } diff --git a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h index 06eeef1579..269fd54102 100644 --- a/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/AccelerationLimiterFilter.h @@ -25,9 +25,9 @@ namespace controller { private: float _rotationAccelerationLimit { FLT_MAX }; - float _rotationDecelerationLimit { FLT_MAX }; float _translationAccelerationLimit { FLT_MAX }; - float _translationDecelerationLimit { FLT_MAX }; + float _rotationSnapThreshold { 0.0f }; + float _translationSnapThreshold { 0.0f }; mutable glm::vec3 _prevPos[3]; // sensor space mutable glm::quat _prevRot[3]; // sensor space diff --git a/scripts/developer/tests/filtered-puck-attach.js b/scripts/developer/tests/filtered-puck-attach.js index 23217886d5..ad9b17a0e4 100644 --- a/scripts/developer/tests/filtered-puck-attach.js +++ b/scripts/developer/tests/filtered-puck-attach.js @@ -101,6 +101,8 @@ var DEFAULT_ROTATION_SMOOTHING_CONSTANT = 1.0; // no smoothing var DEFAULT_TRANSLATION_SMOOTHING_CONSTANT = 1.0; // no smoothing var DEFAULT_TRANSLATION_ACCELERATION_LIMIT = 1000; // only extreme accelerations are smoothed var DEFAULT_ROTATION_ACCELERATION_LIMIT = 10000; // only extreme accelerations are smoothed +var DEFAULT_TRANSLATION_SNAP_THRESHOLD = 0; // no snapping +var DEFAULT_ROTATION_SNAP_THRESHOLD = 0; // no snapping function buildMappingJson() { var obj = {name: "com.highfidelity.testing.filteredPuckAttach", channels: []}; @@ -113,7 +115,9 @@ function buildMappingJson() { { type: "accelerationLimiter", translationAccelerationLimit: DEFAULT_TRANSLATION_ACCELERATION_LIMIT, - rotationAccelerationLimit: DEFAULT_ROTATION_ACCELERATION_LIMIT + rotationAccelerationLimit: DEFAULT_ROTATION_ACCELERATION_LIMIT, + translationSnapThreshold: DEFAULT_TRANSLATION_SNAP_THRESHOLD, + rotationSnapThreshold: DEFAULT_ROTATION_SNAP_THRESHOLD, }, { type: "exponentialSmoothing", @@ -154,7 +158,14 @@ function setTranslationAccelerationLimit(value) { } function setTranslationSnapThreshold(value) { - // TODO: convert from mm + // convert from mm + var MM_PER_M = 1000; + var meters = value / MM_PER_M; + var i; + for (i = 0; i < NUM_TRACKED_OBJECTS; i++) { + mappingJson.channels[i].filters[0].translationSnapThreshold = meters; + } + mappingChanged(); } function setRotationAccelerationLimit(value) { @@ -166,21 +177,25 @@ function setRotationAccelerationLimit(value) { } function setRotationSnapThreshold(value) { - // TODO: convert from degrees + // convert from degrees + var PI_IN_DEGREES = 180; + var radians = value * (Math.pi / PI_IN_DEGREES); + var i; + for (i = 0; i < NUM_TRACKED_OBJECTS; i++) { + mappingJson.channels[i].filters[0].translationSnapThreshold = radians; + } + mappingChanged(); } function setTranslationSmoothingConstant(value) { - print("AJT: setting translation smoothing constant = " + value); var i; for (i = 0; i < NUM_TRACKED_OBJECTS; i++) { mappingJson.channels[i].filters[1].translation = value; } mappingChanged(); - print("AJT: done, value = " + value); } function setRotationSmoothingConstant(value) { - print("AJT: setRotationSmoothingConstant =" + value); var i; for (i = 0; i < NUM_TRACKED_OBJECTS; i++) { mappingJson.channels[i].filters[1].rotation = value; @@ -366,8 +381,6 @@ function onWebEventReceived(msg) { return; } - print("AJT: onWebEventReceived = " + msg); - switch (obj.cmd) { case "ready": sendAvailableTrackedObjects(); From 5faf26abd104b0db6d4539b5e074aee4f201bf82 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 11 Oct 2018 10:38:59 -0700 Subject: [PATCH 079/362] turned off shoulder rotation when we don't have hmd lean recentering on. this temporarily fixes the behavior of the shoulders in this mode. todo: use the spine2 position to determine azimuth of hands, then they will work in all conditions --- interface/src/avatar/MyAvatar.cpp | 7 +++++++ interface/src/avatar/MySkeletonModel.cpp | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 1f38971ab4..77c66d062f 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -591,8 +591,15 @@ void MyAvatar::update(float deltaTime) { // draw hand azimuth vector glm::vec3 handAzimuthMidpoint = transformPoint(getTransform().getMatrix(), glm::vec3(_hipToHandController.x, 0.0f, _hipToHandController.y)); DebugDraw::getInstance().drawRay(getWorldPosition(), handAzimuthMidpoint, glm::vec4(0.0f, 1.0f, 1.0f, 1.0f)); + + } + // temp: draw spine 2 position for hand azimuth purposes. + int spine2Index = getJointIndex("Spine2"); + glm::vec3 spine2WorldPosition = transformPoint(getTransform().getMatrix(), getAbsoluteJointTranslationInObjectFrame(spine2Index)); + DebugDraw::getInstance().addMarker("spine2 location", Quaternions::IDENTITY, spine2WorldPosition, glm::vec4(1)); + if (_goToPending) { setWorldPosition(_goToPosition); setWorldOrientation(_goToOrientation); diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 42ec582c47..1f97ce03f8 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -239,7 +239,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { params.primaryControllerFlags[Rig::PrimaryControllerType_Hips] = (uint8_t)Rig::ControllerFlags::Enabled | (uint8_t)Rig::ControllerFlags::Estimated; // set spine2 if we have hand controllers - if (myAvatar->getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid() && + if (myAvatar->getHMDLeanRecenterEnabled() && myAvatar->getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid() && myAvatar->getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && !(params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] & (uint8_t)Rig::ControllerFlags::Enabled)) { @@ -250,6 +250,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { bool headExists = _rig.getAbsoluteJointPoseInRigFrame(_rig.indexOfJoint("Head"), currentHeadPose); bool hipsExists = _rig.getAbsoluteJointPoseInRigFrame(_rig.indexOfJoint("Hips"), currentHipsPose); if (spine2Exists && headExists && hipsExists) { + // qCDebug(interfaceapp) << "hips forward direction "<< (currentHipsPose.rot() * glm::vec3(0.0f, 0.0f, 1.0f)); AnimPose rigSpaceYaw(myAvatar->getSpine2RotationRigSpace()); glm::vec3 u, v, w; glm::vec3 fwd = rigSpaceYaw.rot() * glm::vec3(0.0f, 0.0f, 1.0f); From 1adac78828403d0d26eb8e9ad7d0e8b76ee29b2c Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Thu, 11 Oct 2018 10:45:35 -0700 Subject: [PATCH 080/362] fix errors and set new anim parameters --- interface/src/avatar/AvatarManager.cpp | 19 +++++------- .../src/avatars-renderer/Avatar.cpp | 31 +++++++++++++------ .../src/avatars-renderer/Avatar.h | 5 ++- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 1f2c9e462d..0bb1bc2e0a 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -81,7 +81,7 @@ AvatarManager::AvatarManager(QObject* parent) : const float AVATAR_TRANSIT_TRIGGER_DISTANCE = 1.0f; const int AVATAR_TRANSIT_FRAME_COUNT = 11; // Based on testing - const int AVATAR_TRANSIT_FRAMES_PER_METER = 1; // Based on testing + const float AVATAR_TRANSIT_FRAMES_PER_METER = 0.5f; // Based on testing const QString START_ANIMATION_URL = "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx"; const QString MIDDLE_ANIMATION_URL = "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx"; @@ -92,9 +92,9 @@ AvatarManager::AvatarManager(QObject* parent) : _transitConfig._framesPerMeter = AVATAR_TRANSIT_FRAMES_PER_METER; _transitConfig._isDistanceBased = true; - _transitConfig._startTransitAnimation = AvatarTransit::TransitAnimation(START_ANIMATION_URL, 0, 14); + _transitConfig._startTransitAnimation = AvatarTransit::TransitAnimation(START_ANIMATION_URL, 0, 10); _transitConfig._middleTransitAnimation = AvatarTransit::TransitAnimation(MIDDLE_ANIMATION_URL, 15, 0); - _transitConfig._endTransitAnimation = AvatarTransit::TransitAnimation(END_ANIMATION_URL, 16, 38); + _transitConfig._endTransitAnimation = AvatarTransit::TransitAnimation(END_ANIMATION_URL, 22, 49); } AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { @@ -180,8 +180,9 @@ void AvatarManager::updateMyAvatar(float deltaTime) { PerformanceWarning warn(showWarnings, "AvatarManager::updateMyAvatar()"); AvatarTransit::Status status = _myAvatar->updateTransit(deltaTime, _myAvatar->getNextPosition(), _transitConfig); - bool sendFirstTransitPackage = (status == AvatarTransit::Status::START_TRANSIT); - bool blockTransitData = (status == AvatarTransit::Status::TRANSITING); + if (status != AvatarTransit::Status::IDLE) { + playTransitAnimations(status); + } _myAvatar->update(deltaTime); render::Transaction transaction; @@ -191,13 +192,9 @@ void AvatarManager::updateMyAvatar(float deltaTime) { quint64 now = usecTimestampNow(); quint64 dt = now - _lastSendAvatarDataTime; - - if (sendFirstTransitPackage || (dt > MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS && !_myAvatarDataPacketsPaused && !blockTransitData)) { + if (dt > MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS && !_myAvatarDataPacketsPaused) { // send head/hand data to the avatar mixer and voxel server - PerformanceTimer perfTimer("send"); - if (sendFirstTransitPackage) { - _myAvatar->overrideNextPackagePositionData(_myAvatar->getTransit()->getEndPosition()); - } + PerformanceTimer perfTimer("send"); _myAvatar->sendAvatarDataPacket(); _lastSendAvatarDataTime = now; _myAvatarSendRate.increment(); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index f154746707..6ac808ec66 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -144,9 +144,12 @@ void AvatarTransit::start(float deltaTime, const glm::vec3& startPosition, const _totalDistance = glm::length(_transitLine); _easeType = config._easeType; const float REFERENCE_FRAMES_PER_SECOND = 30.0f; - + _preTime = (float)config._startTransitAnimation._frameCount / REFERENCE_FRAMES_PER_SECOND; + _postTime = (float)config._endTransitAnimation._frameCount / REFERENCE_FRAMES_PER_SECOND; + int transitFrames = (!config._isDistanceBased) ? config._totalFrames : config._framesPerMeter * _totalDistance; - _totalTime = (float)transitFrames / REFERENCE_FRAMES_PER_SECOND; + _transitTime = (float)transitFrames / REFERENCE_FRAMES_PER_SECOND; + _totalTime = _transitTime + _preTime + _postTime; _currentTime = 0.0f; _isTransiting = true; } @@ -173,19 +176,27 @@ AvatarTransit::Status AvatarTransit::updatePosition(float deltaTime) { Status status = Status::IDLE; if (_isTransiting) { float nextTime = _currentTime + deltaTime; - glm::vec3 newPosition; - if (nextTime >= _totalTime) { - _currentPosition = _endPosition; - _isTransiting = false; - status = Status::END_TRANSIT; - } else { + if (nextTime < _preTime) { + _currentPosition = _startPosition; if (_currentTime == 0) { + status = Status::START_FRAME; + } + } else if (nextTime < _totalTime - _postTime){ + if (_currentTime <= _preTime) { status = Status::START_TRANSIT; } else { + float percentageIntoTransit = (nextTime - _preTime) / _transitTime; + _currentPosition = _startPosition + getEaseValue(_easeType, percentageIntoTransit) * _transitLine; status = Status::TRANSITING; } - float percentageIntoTransit = nextTime / _totalTime; - _currentPosition = _startPosition + getEaseValue(_easeType, percentageIntoTransit) * _transitLine; + } else { + _currentPosition = _endPosition; + if (nextTime >= _totalTime) { + _isTransiting = false; + status = Status::END_FRAME; + } else if (_currentTime < _totalTime - _postTime) { + status = Status::END_TRANSIT; + } } _currentTime = nextTime; } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 21e359051f..ffe90ecaa2 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -81,7 +81,7 @@ public: struct TransitConfig { TransitConfig() {}; int _totalFrames { 0 }; - int _framesPerMeter { 0 }; + float _framesPerMeter { 0.0f }; bool _isDistanceBased { false }; float _triggerDistance { 0 }; EaseType _easeType { EaseType::EASE_OUT }; @@ -115,7 +115,10 @@ private: glm::vec3 _transitLine; float _totalDistance { 0.0f }; + float _preTime { 0.0f }; float _totalTime { 0.0f }; + float _transitTime { 0.0f }; + float _postTime { 0.0f }; float _currentTime { 0.0f }; EaseType _easeType { EaseType::EASE_OUT }; Status _status { Status::IDLE }; From 2f10327ad36cc5bcd81d55efcb832e2e29a5765c Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Thu, 11 Oct 2018 10:57:39 -0700 Subject: [PATCH 081/362] Fix last anim value --- interface/src/avatar/AvatarManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 0bb1bc2e0a..87c0bc471b 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -94,7 +94,7 @@ AvatarManager::AvatarManager(QObject* parent) : _transitConfig._startTransitAnimation = AvatarTransit::TransitAnimation(START_ANIMATION_URL, 0, 10); _transitConfig._middleTransitAnimation = AvatarTransit::TransitAnimation(MIDDLE_ANIMATION_URL, 15, 0); - _transitConfig._endTransitAnimation = AvatarTransit::TransitAnimation(END_ANIMATION_URL, 22, 49); + _transitConfig._endTransitAnimation = AvatarTransit::TransitAnimation(END_ANIMATION_URL, 22, 27); } AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { From 451142f9a3e15d555a7318a469298d88b1153355 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 11 Oct 2018 11:33:19 -0700 Subject: [PATCH 082/362] fixing some more interstittial issues --- scripts/system/controllers/controllerModules/teleport.js | 4 ++++ scripts/system/interstitialPage.js | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/teleport.js b/scripts/system/controllers/controllerModules/teleport.js index bf5022cdaf..bccdd9007c 100644 --- a/scripts/system/controllers/controllerModules/teleport.js +++ b/scripts/system/controllers/controllerModules/teleport.js @@ -701,6 +701,10 @@ Script.include("/~/system/libraries/controllers.js"); }; this.isReady = function(controllerData, deltaTime) { + if (Window.interstitialModeEnabled && !Window.isPhysicsEnabled()) { + return makeRunningValues(false, [], []); + } + var otherModule = this.getOtherModule(); if (!this.disabled && this.buttonValue !== 0 && !otherModule.active) { this.active = true; diff --git a/scripts/system/interstitialPage.js b/scripts/system/interstitialPage.js index 19e603b4ab..040128ffcf 100644 --- a/scripts/system/interstitialPage.js +++ b/scripts/system/interstitialPage.js @@ -545,7 +545,11 @@ MyAvatar.sensorToWorldScaleChanged.connect(scaleInterstitialPage); MyAvatar.sessionUUIDChanged.connect(function() { var avatarSessionUUID = MyAvatar.sessionUUID; - Overlays.editOverlay(loadingSphereID, { parentID: avatarSessionUUID }); + Overlays.editOverlay(loadingSphereID, { + position: Vec3.sum(Vec3.sum(MyAvatar.position, {x: 0.0, y: -1.0, z: 0.0}), Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.95, z: 0})), + orientation: Quat.multiply(Quat.fromVec3Degrees({x: 0, y: 180, z: 0}), MyAvatar.orientation), + parentID: avatarSessionUUID + }); }); var toggle = true; From de1b6e717fc8f8acb46e61697e9c14431feed210 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Thu, 11 Oct 2018 11:35:56 -0700 Subject: [PATCH 083/362] Cancel animation on chained trasits --- interface/src/avatar/AvatarManager.cpp | 2 +- .../src/avatars-renderer/Avatar.cpp | 15 +++++++-------- libraries/avatars/src/AvatarData.cpp | 2 +- libraries/avatars/src/AvatarData.h | 1 + 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 87c0bc471b..80e4e4ba66 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -313,7 +313,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { if (inView && avatar->hasNewJointData()) { numAvatarsUpdated++; } - auto transitStatus = avatar->_transit.update(deltaTime, avatar->_globalPosition, _transitConfig); + auto transitStatus = avatar->_transit.update(deltaTime, avatar->_lastPosition, _transitConfig); if (avatar->getIsNewAvatar() && (transitStatus == AvatarTransit::Status::START_TRANSIT || transitStatus == AvatarTransit::Status::ABORT_TRANSIT)) { avatar->_transit.reset(); avatar->setIsNewAvatar(false); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 6ac808ec66..53c2019138 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -114,19 +114,18 @@ void Avatar::setShowNamesAboveHeads(bool show) { } AvatarTransit::Status AvatarTransit::update(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config) { - glm::vec3 currentPosition = _isTransiting ? _currentPosition : avatarPosition; - float oneFrameDistance = glm::length(currentPosition - _lastPosition); + float oneFrameDistance = _isTransiting ? glm::length(avatarPosition - _endPosition) : glm::length(avatarPosition - _lastPosition); const float MAX_TRANSIT_DISTANCE = 30.0f; float scaledMaxTransitDistance = MAX_TRANSIT_DISTANCE * _scale; - if (oneFrameDistance > config._triggerDistance && !_isTransiting) { + if (oneFrameDistance > config._triggerDistance) { if (oneFrameDistance < scaledMaxTransitDistance) { - start(deltaTime, _lastPosition, currentPosition, config); + start(deltaTime, _lastPosition, avatarPosition, config); } else { - _lastPosition = currentPosition; - return Status::ABORT_TRANSIT; + _lastPosition = avatarPosition; + _status = Status::ABORT_TRANSIT; } } - _lastPosition = currentPosition; + _lastPosition = avatarPosition; _status = updatePosition(deltaTime); return _status; } @@ -150,7 +149,7 @@ void AvatarTransit::start(float deltaTime, const glm::vec3& startPosition, const int transitFrames = (!config._isDistanceBased) ? config._totalFrames : config._framesPerMeter * _totalDistance; _transitTime = (float)transitFrames / REFERENCE_FRAMES_PER_SECOND; _totalTime = _transitTime + _preTime + _postTime; - _currentTime = 0.0f; + _currentTime = _isTransiting ? _preTime : 0.0f; _isTransiting = true; } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index d78d83bc09..a22cc4a1d3 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -896,7 +896,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { auto newValue = glm::vec3(data->globalPosition[0], data->globalPosition[1], data->globalPosition[2]) + offset; if (_globalPosition != newValue) { - _globalPosition = newValue; + _lastPosition = _globalPosition = newValue; _globalPositionChanged = now; } sourceBuffer += sizeof(AvatarDataPacket::AvatarGlobalPosition); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 46489451f7..dcdaa70ad7 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1378,6 +1378,7 @@ protected: // where Entities are located. This is currently only used by the mixer to decide how often to send // updates about one avatar to another. glm::vec3 _globalPosition { 0, 0, 0 }; + glm::vec3 _lastPosition { 0, 0, 0 }; glm::vec3 _globalPositionOverride { 0, 0, 0 }; bool _overrideGlobalPosition { false }; From 5528050898de75444b1caff4d6b3420beee488f8 Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 11 Oct 2018 12:44:07 -0700 Subject: [PATCH 084/362] CR changes - propertyName vs propertyID, store name/elementID, return property element --- scripts/system/html/js/entityProperties.js | 527 ++++++++++----------- 1 file changed, 257 insertions(+), 270 deletions(-) diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index a58b5ca8cd..fcd9251485 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -40,36 +40,35 @@ const GROUPS = [ label: NO_SELECTION, type: "icon", icons: ICON_FOR_TYPE, - defaultValue: "Box", - propertyName: "type", + propertyID: "type", }, { label: "Name", type: "string", - propertyName: "name", + propertyID: "name", }, { label: "ID", type: "string", - propertyName: "id", + propertyID: "id", readOnly: true, }, { label: "Parent", type: "string", - propertyName: "parentID", + propertyID: "parentID", }, { label: "Locked", glyph: "", type: "bool", - propertyName: "locked", + propertyID: "locked", }, { label: "Visible", glyph: "", type: "bool", - propertyName: "visible", + propertyID: "visible", }, ] }, @@ -84,12 +83,12 @@ const GROUPS = [ Icosahedron: "Icosahedron", Dodecahedron: "Dodecahedron", Hexagon: "Hexagon", Triangle: "Triangle", Octagon: "Octagon", Cylinder: "Cylinder", Cone: "Cone", Circle: "Circle", Quad: "Quad" }, - propertyName: "shape", + propertyID: "shape", }, { label: "Color", type: "color", - propertyName: "color", + propertyID: "color", }, ] }, @@ -100,17 +99,17 @@ const GROUPS = [ { label: "Text", type: "string", - propertyName: "text", + propertyID: "text", }, { label: "Text Color", type: "color", - propertyName: "textColor", + propertyID: "textColor", }, { label: "Background Color", type: "color", - propertyName: "backgroundColor", + propertyID: "backgroundColor", }, { label: "Line Height", @@ -119,12 +118,12 @@ const GROUPS = [ step: 0.005, fixedDecimals: 4, unit: "m", - propertyName: "lineHeight" + propertyID: "lineHeight" }, { label: "Face Camera", type: "bool", - propertyName: "faceCamera" + propertyID: "faceCamera" }, ] }, @@ -135,29 +134,29 @@ const GROUPS = [ { label: "Flying Allowed", type: "bool", - propertyName: "flyingAllowed", + propertyID: "flyingAllowed", }, { label: "Ghosting Allowed", type: "bool", - propertyName: "ghostingAllowed", + propertyID: "ghostingAllowed", }, { label: "Filter", type: "string", - propertyName: "filterURL", + propertyID: "filterURL", }, { label: "Key Light", type: "dropdown", options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, - propertyName: "keyLightMode", + propertyID: "keyLightMode", }, { label: "Key Light Color", type: "color", - propertyName: "keyLight.color", + propertyID: "keyLight.color", showPropertyRule: { "keyLightMode": "enabled" }, }, { @@ -167,7 +166,7 @@ const GROUPS = [ max: 10, step: 0.1, fixedDecimals: 2, - propertyName: "keyLight.intensity", + propertyID: "keyLight.intensity", showPropertyRule: { "keyLightMode": "enabled" }, }, { @@ -175,7 +174,7 @@ const GROUPS = [ type: "number", fixedDecimals: 2, unit: "deg", - propertyName: "keyLight.direction.y", + propertyID: "keyLight.direction.y", showPropertyRule: { "keyLightMode": "enabled" }, }, { @@ -183,45 +182,45 @@ const GROUPS = [ type: "number", fixedDecimals: 2, unit: "deg", - propertyName: "keyLight.direction.x", + propertyID: "keyLight.direction.x", showPropertyRule: { "keyLightMode": "enabled" }, }, { label: "Cast Shadows", type: "bool", - propertyName: "keyLight.castShadows", + propertyID: "keyLight.castShadows", showPropertyRule: { "keyLightMode": "enabled" }, }, { label: "Skybox", type: "dropdown", options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, - propertyName: "skyboxMode", + propertyID: "skyboxMode", }, { label: "Skybox Color", type: "color", - propertyName: "skybox.color", + propertyID: "skybox.color", showPropertyRule: { "skyboxMode": "enabled" }, }, { label: "Skybox URL", type: "string", - propertyName: "skybox.url", + propertyID: "skybox.url", showPropertyRule: { "skyboxMode": "enabled" }, }, { type: "buttons", buttons: [ { id: "copy", label: "Copy URL To Ambient", className: "black", onClick: copySkyboxURLToAmbientURL } ], - propertyName: "copyURLToAmbient", + propertyID: "copyURLToAmbient", showPropertyRule: { "skyboxMode": "enabled" }, }, { label: "Ambient Light", type: "dropdown", options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, - propertyName: "ambientLightMode", + propertyID: "ambientLightMode", }, { label: "Ambient Intensity", @@ -230,20 +229,20 @@ const GROUPS = [ max: 10, step: 0.1, fixedDecimals: 2, - propertyName: "ambientLight.ambientIntensity", + propertyID: "ambientLight.ambientIntensity", showPropertyRule: { "ambientLightMode": "enabled" }, }, { label: "Ambient URL", type: "string", - propertyName: "ambientLight.ambientURL", + propertyID: "ambientLight.ambientURL", showPropertyRule: { "ambientLightMode": "enabled" }, }, { label: "Haze", type: "dropdown", options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, - propertyName: "hazeMode", + propertyID: "hazeMode", }, { label: "Range", @@ -253,13 +252,13 @@ const GROUPS = [ step: 5, fixedDecimals: 0, unit: "m", - propertyName: "haze.hazeRange", + propertyID: "haze.hazeRange", showPropertyRule: { "hazeMode": "enabled" }, }, { label: "Use Altitude", type: "bool", - propertyName: "haze.hazeAltitudeEffect", + propertyID: "haze.hazeAltitudeEffect", showPropertyRule: { "hazeMode": "enabled" }, }, { @@ -270,7 +269,7 @@ const GROUPS = [ step: 10, fixedDecimals: 0, unit: "m", - propertyName: "haze.hazeBaseRef", + propertyID: "haze.hazeBaseRef", showPropertyRule: { "hazeMode": "enabled" }, }, { @@ -281,13 +280,13 @@ const GROUPS = [ step: 10, fixedDecimals: 0, unit: "m", - propertyName: "haze.hazeCeiling", + propertyID: "haze.hazeCeiling", showPropertyRule: { "hazeMode": "enabled" }, }, { label: "Haze Color", type: "color", - propertyName: "haze.hazeColor", + propertyID: "haze.hazeColor", showPropertyRule: { "hazeMode": "enabled" }, }, { @@ -297,19 +296,19 @@ const GROUPS = [ max: 1.0, step: 0.01, fixedDecimals: 2, - propertyName: "haze.hazeBackgroundBlend", + propertyID: "haze.hazeBackgroundBlend", showPropertyRule: { "hazeMode": "enabled" }, }, { label: "Enable Glare", type: "bool", - propertyName: "haze.hazeEnableGlare", + propertyID: "haze.hazeEnableGlare", showPropertyRule: { "hazeMode": "enabled" }, }, { label: "Glare Color", type: "color", - propertyName: "haze.hazeGlareColor", + propertyID: "haze.hazeGlareColor", showPropertyRule: { "hazeMode": "enabled" }, }, { @@ -319,14 +318,14 @@ const GROUPS = [ max: 180, step: 1, fixedDecimals: 0, - propertyName: "haze.hazeGlareAngle", + propertyID: "haze.hazeGlareAngle", showPropertyRule: { "hazeMode": "enabled" }, }, { label: "Bloom", type: "dropdown", options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, - propertyName: "bloomMode", + propertyID: "bloomMode", }, { label: "Bloom Intensity", @@ -335,7 +334,7 @@ const GROUPS = [ max: 1, step: 0.01, fixedDecimals: 2, - propertyName: "bloom.bloomIntensity", + propertyID: "bloom.bloomIntensity", showPropertyRule: { "bloomMode": "enabled" }, }, { @@ -345,7 +344,7 @@ const GROUPS = [ min: 1, step: 0.01, fixedDecimals: 2, - propertyName: "bloom.bloomThreshold", + propertyID: "bloom.bloomThreshold", showPropertyRule: { "bloomMode": "enabled" }, }, { @@ -355,7 +354,7 @@ const GROUPS = [ min: 2, step: 0.01, fixedDecimals: 2, - propertyName: "bloom.bloomSize", + propertyID: "bloom.bloomSize", showPropertyRule: { "bloomMode": "enabled" }, }, ] @@ -367,7 +366,7 @@ const GROUPS = [ { label: "Model", type: "string", - propertyName: "modelURL", + propertyID: "modelURL", }, { label: "Collision Shape", @@ -375,67 +374,67 @@ const GROUPS = [ options: { "none": "No Collision", "box": "Box", "sphere": "Sphere", "compound": "Compound" , "simple-hull": "Basic - Whole model", "simple-compound": "Good - Sub-meshes" , "static-mesh": "Exact - All polygons (non-dynamic only)" }, - propertyName: "shapeType", + propertyID: "shapeType", }, { label: "Compound Shape", type: "string", - propertyName: "compoundShapeURL", + propertyID: "compoundShapeURL", }, { label: "Animation", type: "string", - propertyName: "animation.url", + propertyID: "animation.url", }, { label: "Play Automatically", type: "bool", - propertyName: "animation.running", + propertyID: "animation.running", }, { label: "Allow Transition", type: "bool", - propertyName: "animation.allowTranslation", + propertyID: "animation.allowTranslation", }, { label: "Loop", type: "bool", - propertyName: "animation.loop", + propertyID: "animation.loop", }, { label: "Hold", type: "bool", - propertyName: "animation.hold", + propertyID: "animation.hold", }, { label: "Animation Frame", type: "number", - propertyName: "animation.currentFrame", + propertyID: "animation.currentFrame", }, { label: "First Frame", type: "number", - propertyName: "animation.firstFrame", + propertyID: "animation.firstFrame", }, { label: "Last Frame", type: "number", - propertyName: "animation.lastFrame", + propertyID: "animation.lastFrame", }, { label: "Animation FPS", type: "number", - propertyName: "animation.fps", + propertyID: "animation.fps", }, { label: "Texture", type: "textarea", - propertyName: "textures", + propertyID: "textures", }, { label: "Original Texture", type: "textarea", - propertyName: "originalTextures", + propertyID: "originalTextures", readOnly: true, }, ] @@ -447,7 +446,7 @@ const GROUPS = [ { label: "Image", type: "string", - propertyName: "image", + propertyID: "image", }, ] }, @@ -458,24 +457,26 @@ const GROUPS = [ { label: "Source", type: "string", - propertyName: "sourceUrl", + propertyID: "sourceUrl", }, { label: "Source Resolution", type: "number", - propertyName: "dpi", + propertyID: "dpi", }, ] }, { id: "light", addToGroup: "base", + addToGroup: "base", properties: [ { label: "Light Color", type: "color", - propertyName: "lightColor", // this actually shares "color" property with shape Color but + propertyID: "lightColor", // this actually shares "color" property with shape Color but // separating naming here to distinguish property element/data + propertyName: "color", }, { label: "Intensity", @@ -483,7 +484,7 @@ const GROUPS = [ min: 0, step: 0.1, fixedDecimals: 1, - propertyName: "intensity", + propertyID: "intensity", }, { label: "Fall-Off Radius", @@ -492,26 +493,26 @@ const GROUPS = [ step: 0.1, fixedDecimals: 1, unit: "m", - propertyName: "falloffRadius", + propertyID: "falloffRadius", }, { label: "Spotlight", type: "bool", - propertyName: "isSpotlight", + propertyID: "isSpotlight", }, { label: "Spotlight Exponent", type: "number", step: 0.01, fixedDecimals: 2, - propertyName: "exponent", + propertyID: "exponent", }, { label: "Spotlight Cut-Off", type: "number", step: 0.01, fixedDecimals: 2, - propertyName: "cutoff", + propertyID: "cutoff", }, ] }, @@ -522,7 +523,7 @@ const GROUPS = [ { label: "Material URL", type: "string", - propertyName: "materialURL", + propertyID: "materialURL", }, { label: "Material Data", @@ -530,30 +531,30 @@ const GROUPS = [ buttons: [ { id: "clear", label: "Clear Material Data", className: "red", onClick: clearMaterialData }, { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONMaterialEditor }, { id: "save", label: "Save Material Data", className: "black", onClick: saveMaterialData } ], - propertyName: "materialData", + propertyID: "materialData", }, { label: "Submesh to Replace", type: "number", min: 0, step: 1, - propertyName: "submeshToReplace", + propertyID: "submeshToReplace", }, { label: "Material Name to Replace", type: "string", - propertyName: "materialNameToReplace", + propertyID: "materialNameToReplace", }, { label: "Select Submesh", type: "bool", - propertyName: "selectSubmesh", + propertyID: "selectSubmesh", }, { label: "Priority", type: "number", min: 0, - propertyName: "priority", + propertyID: "priority", }, { label: "Material Position", @@ -563,7 +564,7 @@ const GROUPS = [ step: 0.1, vec2Type: "xy", subLabels: [ "x", "y" ], - propertyName: "materialMappingPos", + propertyID: "materialMappingPos", }, { label: "Material Scale", @@ -572,7 +573,7 @@ const GROUPS = [ step: 0.1, vec2Type: "wh", subLabels: [ "width", "height" ], - propertyName: "materialMappingScale", + propertyID: "materialMappingScale", }, { label: "Material Rotation", @@ -580,7 +581,7 @@ const GROUPS = [ step: 0.1, fixedDecimals: 2, unit: "deg", - propertyName: "materialMappingRot", + propertyID: "materialMappingRot", }, ] }, @@ -594,7 +595,7 @@ const GROUPS = [ vec3Type: "xyz", subLabels: [ "x", "y", "z" ], unit: "m", - propertyName: "position", + propertyID: "position", }, { label: "Rotation", @@ -603,7 +604,7 @@ const GROUPS = [ vec3Type: "pyr", subLabels: [ "pitch", "yaw", "roll" ], unit: "deg", - propertyName: "rotation", + propertyID: "rotation", }, { label: "Dimension", @@ -612,7 +613,7 @@ const GROUPS = [ vec3Type: "xyz", subLabels: [ "x", "y", "z" ], unit: "m", - propertyName: "dimensions", + propertyID: "dimensions", }, { label: "Scale", @@ -621,7 +622,7 @@ const GROUPS = [ unit: "%", buttons: [ { id: "rescale", label: "Rescale", className: "blue", onClick: rescaleDimensions }, { id: "reset", label: "Reset Dimensions", className: "red", onClick: resetToNaturalDimensions } ], - propertyName: "scale", + propertyID: "scale", }, { label: "Pivot", @@ -630,14 +631,14 @@ const GROUPS = [ vec3Type: "xyz", subLabels: [ "x", "y", "z" ], unit: "(ratio of dimension)", - propertyName: "registrationPoint", + propertyID: "registrationPoint", }, { label: "Align", type: "buttons", buttons: [ { id: "selection", label: "Selection to Grid", className: "black", onClick: moveSelectionToGrid }, { id: "all", label: "All to Grid", className: "black", onClick: moveAllToGrid } ], - propertyName: "alignToGrid", + propertyID: "alignToGrid", }, ] }, @@ -650,33 +651,33 @@ const GROUPS = [ label: "Collides", type: "bool", inverse: true, - propertyName: "collisionless", + propertyID: "collisionless", column: -1, // before two columns div }, { label: "Dynamic", type: "bool", - propertyName: "dynamic", + propertyID: "dynamic", column: -1, // before two columns div }, { label: "Collides With", type: "sub-header", - propertyName: "collidesWithHeader", // not actually a property but used for naming/storing this element + propertyID: "collidesWithHeader", // not actually a property but used for naming/storing this element showPropertyRule: { "collisionless": "false" }, column: 1, }, { label: "", type: "sub-header", - propertyName: "collidesWithHeaderHelper", // not actually a property but used for naming/storing this element + propertyID: "collidesWithHeaderHelper", // not actually a property but used for naming/storing this element showPropertyRule: { "collisionless": "false" }, column: 2, }, { label: "Static Entities", type: "bool", - propertyName: "static", + propertyID: "static", subPropertyOf: "collidesWith", showPropertyRule: { "collisionless": "false" }, column: 1, @@ -684,7 +685,7 @@ const GROUPS = [ { label: "Dynamic Entities", type: "bool", - propertyName: "dynamic", + propertyID: "dynamic", subPropertyOf: "collidesWith", showPropertyRule: { "collisionless": "false" }, column: 2, @@ -692,7 +693,7 @@ const GROUPS = [ { label: "Kinematic Entities", type: "bool", - propertyName: "kinematic", + propertyID: "kinematic", subPropertyOf: "collidesWith", showPropertyRule: { "collisionless": "false" }, column: 1, @@ -700,7 +701,7 @@ const GROUPS = [ { label: "My Avatar", type: "bool", - propertyName: "myAvatar", + propertyID: "myAvatar", subPropertyOf: "collidesWith", showPropertyRule: { "collisionless": "false" }, column: 2, @@ -708,7 +709,7 @@ const GROUPS = [ { label: "Other Avatars", type: "bool", - propertyName: "otherAvatar", + propertyID: "otherAvatar", subPropertyOf: "collidesWith", showPropertyRule: { "collisionless": "false" }, column: 1, @@ -716,7 +717,7 @@ const GROUPS = [ { label: "Collision sound URL", type: "string", - propertyName: "collisionSoundURL", + propertyID: "collisionSoundURL", showPropertyRule: { "collisionless": "false" }, // having no column number means place this after two columns div }, @@ -730,78 +731,78 @@ const GROUPS = [ { label: "Grabbable", type: "bool", - propertyName: "grabbable", + propertyID: "grabbable", column: 1, }, { label: "Triggerable", type: "bool", - propertyName: "triggerable", + propertyID: "triggerable", column: 2, }, { label: "Cloneable", type: "bool", - propertyName: "cloneable", + propertyID: "cloneable", column: 1, }, { label: "Ignore inverse kinematics", type: "bool", - propertyName: "ignoreIK", + propertyID: "ignoreIK", column: 2, }, { label: "Clone Lifetime", type: "number", unit: "s", - propertyName: "cloneLifetime", + propertyID: "cloneLifetime", showPropertyRule: { "cloneable": "true" }, column: 1, }, { label: "Clone Limit", type: "number", - propertyName: "cloneLimit", + propertyID: "cloneLimit", showPropertyRule: { "cloneable": "true" }, column: 1, }, { label: "Clone Dynamic", type: "bool", - propertyName: "cloneDynamic", + propertyID: "cloneDynamic", showPropertyRule: { "cloneable": "true" }, column: 1, }, { label: "Clone Avatar Entity", type: "bool", - propertyName: "cloneAvatarEntity", + propertyID: "cloneAvatarEntity", showPropertyRule: { "cloneable": "true" }, column: 1, }, { // below properties having no column number means place them after two columns div label: "Can cast shadow", type: "bool", - propertyName: "canCastShadow", + propertyID: "canCastShadow", }, { label: "Script", type: "string", buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadScripts } ], - propertyName: "script", + propertyID: "script", }, { label: "Server Script", type: "string", buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadServerScripts } ], - propertyName: "serverScripts", + propertyID: "serverScripts", }, { label: "Lifetime", type: "number", unit: "s", - propertyName: "lifetime", + propertyID: "lifetime", }, { label: "User Data", @@ -809,7 +810,7 @@ const GROUPS = [ buttons: [ { id: "clear", label: "Clear User Data", className: "red", onClick: clearUserData }, { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONEditor }, { id: "save", label: "Save User Data", className: "black", onClick: saveUserData } ], - propertyName: "userData", + propertyID: "userData", }, ] }, @@ -823,13 +824,13 @@ const GROUPS = [ vec3Type: "xyz", subLabels: [ "x", "y", "z" ], unit: "m/s", - propertyName: "velocity", + propertyID: "velocity", }, { label: "Linear Damping", type: "number", fixedDecimals: 2, - propertyName: "damping", + propertyID: "damping", }, { label: "Angular Velocity", @@ -838,31 +839,31 @@ const GROUPS = [ vec3Type: "pyr", subLabels: [ "pitch", "yaw", "roll" ], unit: "deg/s", - propertyName: "angularVelocity", + propertyID: "angularVelocity", }, { label: "Angular Damping", type: "number", fixedDecimals: 4, - propertyName: "angularDamping", + propertyID: "angularDamping", }, { label: "Bounciness", type: "number", fixedDecimals: 4, - propertyName: "restitution", + propertyID: "restitution", }, { label: "Friction", type: "number", fixedDecimals: 4, - propertyName: "friction", + propertyID: "friction", }, { label: "Density", type: "number", fixedDecimals: 4, - propertyName: "density", + propertyID: "density", }, { label: "Gravity", @@ -870,7 +871,7 @@ const GROUPS = [ vec3Type: "xyz", subLabels: [ "x", "y", "z" ], unit: "m/s2", - propertyName: "gravity", + propertyID: "gravity", }, { label: "Acceleration", @@ -878,7 +879,7 @@ const GROUPS = [ vec3Type: "xyz", subLabels: [ "x", "y", "z" ], unit: "m/s2", - propertyName: "acceleration", + propertyID: "acceleration", }, ] }, @@ -923,14 +924,8 @@ function debugPrint(message) { // GENERAL PROPERTY/GROUP FUNCTIONS -function getPropertyElement(propertyName) { - return properties[propertyName].el; -} - -function getPropertyElementID(propertyName) { - let propertyElementID = "property-" + propertyName; - propertyElementID = propertyElementID.replace(".", "-"); - return propertyElementID; +function getPropertyElement(propertyID) { + return properties[propertyID].el; } function enableChildren(el, selector) { @@ -976,8 +971,8 @@ function disableProperties() { } } -function showPropertyElement(propertyName, show) { - let elProperty = properties[propertyName].el; +function showPropertyElement(propertyID, show) { + let elProperty = properties[propertyID].el; let elNode = elProperty; if (elNode.nodeName !== "DIV") { let elParent = elProperty.parentNode; @@ -992,9 +987,9 @@ function showPropertyElement(propertyName, show) { } function resetProperties() { - for (let propertyName in properties) { - let elProperty = properties[propertyName].el; - let propertyData = properties[propertyName].data; + for (let propertyID in properties) { + let elProperty = properties[propertyID].el; + let propertyData = properties[propertyID].data; switch (propertyData.type) { case 'string': { @@ -1043,16 +1038,13 @@ function resetProperties() { } case 'icon': { // icon is array of elSpan (icon glyph) and elLabel - if (propertyData.defaultValue !== undefined) { - elProperty[0].innerHTML = propertyData.icons[propertyData.defaultValue]; - elProperty[0].style.display = "inline-block"; - elProperty[1].innerHTML = propertyData.defaultValue; - } + elProperty[0].style.display = "none"; + elProperty[1].innerHTML = propertyData.label; break; } } - let showPropertyRules = properties[propertyName].showPropertyRules; + let showPropertyRules = properties[propertyID].showPropertyRules; if (showPropertyRules !== undefined) { for (let propertyToHide in showPropertyRules) { showPropertyElement(propertyToHide, false); @@ -1131,61 +1123,61 @@ function createEmitNumberPropertyUpdateFunction(propertyName, decimals) { }; } -function createEmitVec2PropertyUpdateFunction(property, elX, elY) { +function createEmitVec2PropertyUpdateFunction(propertyName, elX, elY) { return function () { let newValue = { x: elX.value, y: elY.value }; - updateProperty(property, newValue); + updateProperty(propertyName, newValue); }; } -function createEmitVec2PropertyUpdateFunctionWithMultiplier(property, elX, elY, multiplier) { +function createEmitVec2PropertyUpdateFunctionWithMultiplier(propertyName, elX, elY, multiplier) { return function () { let newValue = { x: elX.value * multiplier, y: elY.value * multiplier }; - updateProperty(property, newValue); + updateProperty(propertyName, newValue); }; } -function createEmitVec3PropertyUpdateFunction(property, elX, elY, elZ) { +function createEmitVec3PropertyUpdateFunction(propertyName, elX, elY, elZ) { return function() { let newValue = { x: elX.value, y: elY.value, z: elZ ? elZ.value : 0 }; - updateProperty(property, newValue); + updateProperty(propertyName, newValue); }; } -function createEmitVec3PropertyUpdateFunctionWithMultiplier(property, elX, elY, elZ, multiplier) { +function createEmitVec3PropertyUpdateFunctionWithMultiplier(propertyName, elX, elY, elZ, multiplier) { return function() { let newValue = { x: elX.value * multiplier, y: elY.value * multiplier, z: elZ.value * multiplier }; - updateProperty(property, newValue); + updateProperty(propertyName, newValue); }; } -function createEmitColorPropertyUpdateFunction(property, elRed, elGreen, elBlue) { +function createEmitColorPropertyUpdateFunction(propertyName, elRed, elGreen, elBlue) { return function() { - emitColorPropertyUpdate(property, elRed.value, elGreen.value, elBlue.value); + emitColorPropertyUpdate(propertyName, elRed.value, elGreen.value, elBlue.value); }; } -function emitColorPropertyUpdate(property, red, green, blue) { +function emitColorPropertyUpdate(propertyName, red, green, blue) { let newValue = { red: red, green: green, blue: blue }; - updateProperty(property, newValue); + updateProperty(propertyName, newValue); } function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString) { @@ -1210,14 +1202,15 @@ function createImageURLUpdateFunction(propertyName) { // PROPERTY ELEMENT CREATION FUNCTIONS -function createStringProperty(elProperty, elLabel, propertyData) { - let propertyName = propertyData.propertyName; - let propertyElementID = getPropertyElementID(propertyName); +function createStringProperty(property, elProperty, elLabel) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; elProperty.setAttribute("class", "property text"); let elInput = document.createElement('input'); - elInput.setAttribute("id", propertyElementID); + elInput.setAttribute("id", elementID); elInput.setAttribute("type", "text"); if (propertyData.readOnly) { elInput.readOnly = true; @@ -1229,15 +1222,16 @@ function createStringProperty(elProperty, elLabel, propertyData) { elProperty.appendChild(elInput); if (propertyData.buttons !== undefined) { - addButtons(elProperty, propertyElementID, propertyData.buttons, false); + addButtons(elProperty, elementID, propertyData.buttons, false); } - properties[propertyName].el = elInput; + return elInput; } -function createBoolProperty(elProperty, elLabel, propertyData) { - let propertyName = propertyData.propertyName; - let propertyElementID = getPropertyElementID(propertyName); +function createBoolProperty(property, elProperty, elLabel) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; elProperty.setAttribute("class", "property checkbox"); @@ -1249,7 +1243,7 @@ function createBoolProperty(elProperty, elLabel, propertyData) { } let elInput = document.createElement('input'); - elInput.setAttribute("id", propertyElementID); + elInput.setAttribute("id", elementID); elInput.setAttribute("type", "checkbox"); elProperty.appendChild(elInput); @@ -1264,12 +1258,13 @@ function createBoolProperty(elProperty, elLabel, propertyData) { elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(propertyName, propertyData.inverse)); } - properties[propertyName].el = elInput; + return elInput; } -function createVec3Property(elProperty, elLabel, propertyData) { - let propertyName = propertyData.propertyName; - let propertyElementID = getPropertyElementID(propertyName); +function createVec3Property(property, elProperty, elLabel) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; elProperty.setAttribute("class", "property " + propertyData.vec3Type + " fstuple"); @@ -1281,11 +1276,11 @@ function createVec3Property(elProperty, elLabel, propertyData) { elProperty.appendChild(elLabel); elProperty.appendChild(elTuple); - let elInputX = createTupleNumberInput(elTuple, propertyElementID, propertyData.subLabels[0], + let elInputX = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[0], propertyData.min, propertyData.max, propertyData.step); - let elInputY = createTupleNumberInput(elTuple, propertyElementID, propertyData.subLabels[1], + let elInputY = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[1], propertyData.min, propertyData.max, propertyData.step); - let elInputZ = createTupleNumberInput(elTuple, propertyElementID, propertyData.subLabels[2], + let elInputZ = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[2], propertyData.min, propertyData.max, propertyData.step); let inputChangeFunction; @@ -1299,12 +1294,13 @@ function createVec3Property(elProperty, elLabel, propertyData) { elInputY.addEventListener('change', inputChangeFunction); elInputZ.addEventListener('change', inputChangeFunction); - properties[propertyName].el = [ elInputX, elInputY, elInputZ ]; + return [ elInputX, elInputY, elInputZ ]; } -function createVec2Property(elProperty, elLabel, propertyData) { - let propertyName = propertyData.propertyName; - let propertyElementID = getPropertyElementID(propertyName); +function createVec2Property(property, elProperty, elLabel) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; elProperty.setAttribute("class", "property " + propertyData.vec2Type + " fstuple"); @@ -1316,9 +1312,9 @@ function createVec2Property(elProperty, elLabel, propertyData) { elProperty.appendChild(elLabel); elProperty.appendChild(elTuple); - let elInputX = createTupleNumberInput(elTuple, propertyElementID, propertyData.subLabels[0], + let elInputX = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[0], propertyData.min, propertyData.max, propertyData.step); - let elInputY = createTupleNumberInput(elTuple, propertyElementID, propertyData.subLabels[1], + let elInputY = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[1], propertyData.min, propertyData.max, propertyData.step); let inputChangeFunction; @@ -1331,18 +1327,18 @@ function createVec2Property(elProperty, elLabel, propertyData) { elInputX.addEventListener('change', inputChangeFunction); elInputY.addEventListener('change', inputChangeFunction); - properties[propertyName].el = [elInputX, elInputY]; + return [elInputX, elInputY]; } -function createColorProperty(elProperty, elLabel, propertyData) { - let propertyName = propertyData.propertyName; - let propertyElementID = getPropertyElementID(propertyName); +function createColorProperty(property, elProperty, elLabel) { + let propertyName = property.name; + let elementID = property.elementID; elProperty.setAttribute("class", "property rgb fstuple"); let elColorPicker = document.createElement('div'); elColorPicker.setAttribute("class", "color-picker"); - elColorPicker.setAttribute("id", propertyElementID); + elColorPicker.setAttribute("id", elementID); let elTuple = document.createElement('div'); elTuple.setAttribute("class", "tuple"); @@ -1351,16 +1347,16 @@ function createColorProperty(elProperty, elLabel, propertyData) { elProperty.appendChild(elLabel); elProperty.appendChild(elTuple); - let elInputR = createTupleNumberInput(elTuple, propertyElementID, "red", 0, 255, 1); - let elInputG = createTupleNumberInput(elTuple, propertyElementID, "green", 0, 255, 1); - let elInputB = createTupleNumberInput(elTuple, propertyElementID, "blue", 0, 255, 1); + let elInputR = createTupleNumberInput(elTuple, elementID, "red", 0, 255, 1); + let elInputG = createTupleNumberInput(elTuple, elementID, "green", 0, 255, 1); + let elInputB = createTupleNumberInput(elTuple, elementID, "blue", 0, 255, 1); let inputChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elInputR, elInputG, elInputB); elInputR.addEventListener('change', inputChangeFunction); elInputG.addEventListener('change', inputChangeFunction); elInputB.addEventListener('change', inputChangeFunction); - let colorPickerID = "#" + propertyElementID; + let colorPickerID = "#" + elementID; colorPickers[colorPickerID] = $(colorPickerID).colpick({ colorScheme: 'dark', layout: 'hex', @@ -1385,18 +1381,19 @@ function createColorProperty(elProperty, elLabel, propertyData) { } }); - properties[propertyName].el = [elColorPicker, elInputR, elInputG, elInputB]; + return [elColorPicker, elInputR, elInputG, elInputB]; } -function createDropdownProperty(elProperty, elLabel, propertyData) { - let propertyName = propertyData.propertyName; - let propertyElementID = getPropertyElementID(propertyName); +function createDropdownProperty(property, elProperty, elLabel) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; elProperty.setAttribute("class", "property dropdown"); let elInput = document.createElement('select'); - elInput.setAttribute("id", propertyElementID); - elInput.setAttribute("propertyName", propertyName); + elInput.setAttribute("id", elementID); + elInput.setAttribute("propertyID", propertyData.propertyID); for (let optionKey in propertyData.options) { let option = document.createElement('option'); @@ -1410,33 +1407,24 @@ function createDropdownProperty(elProperty, elLabel, propertyData) { elProperty.appendChild(elLabel); elProperty.appendChild(elInput); - properties[propertyName].el = elInput; + return elInput; } -function createNumberProperty(elProperty, elLabel, propertyData) { - let propertyName = propertyData.propertyName; - let propertyElementID = getPropertyElementID(propertyName); +function createNumberProperty(property, elProperty, elLabel) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; elProperty.setAttribute("class", "property number"); addUnit(propertyData.unit, elLabel); let elInput = document.createElement('input'); - elInput.setAttribute("id", propertyElementID); + elInput.setAttribute("id", elementID); elInput.setAttribute("type", "number"); - if (propertyData.min !== undefined) { - elInput.setAttribute("min", propertyData.min); - } - if (propertyData.max !== undefined) { - elInput.setAttribute("max", propertyData.max); - } - if (propertyData.step !== undefined) { - elInput.setAttribute("step", propertyData.step); - } let defaultValue = propertyData.defaultValue; if (defaultValue !== undefined) { - elInput.setAttribute("defaultValue", defaultValue); elInput.value = defaultValue; } @@ -1447,26 +1435,27 @@ function createNumberProperty(elProperty, elLabel, propertyData) { elProperty.appendChild(elInput); if (propertyData.buttons !== undefined) { - addButtons(elProperty, propertyElementID, propertyData.buttons, true); + addButtons(elProperty, elementID, propertyData.buttons, true); } - properties[propertyName].el = elInput; + return elInput; } -function createTextareaProperty(elProperty, elLabel, propertyData) { - let propertyName = propertyData.propertyName; - let propertyElementID = getPropertyElementID(propertyName); +function createTextareaProperty(property, elProperty, elLabel) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; elProperty.setAttribute("class", "property textarea"); elProperty.appendChild(elLabel); if (propertyData.buttons !== undefined) { - addButtons(elProperty, propertyElementID, propertyData.buttons, true); + addButtons(elProperty, elementID, propertyData.buttons, true); } let elInput = document.createElement('textarea'); - elInput.setAttribute("id", propertyElementID); + elInput.setAttribute("id", elementID); if (propertyData.readOnly) { elInput.readOnly = true; } @@ -1475,30 +1464,30 @@ function createTextareaProperty(elProperty, elLabel, propertyData) { elProperty.appendChild(elInput); - properties[propertyName].el = elInput; + return elInput; } -function createIconProperty(elProperty, elLabel, propertyData) { - let propertyName = propertyData.propertyName; - let propertyElementID = getPropertyElementID(propertyName); +function createIconProperty(property, elProperty, elLabel) { + let elementID = property.elementID; + let propertyData = property.data; elProperty.setAttribute("class", "property value"); - elLabel.setAttribute("id", propertyElementID); + elLabel.setAttribute("id", elementID); elLabel.innerHTML = " " + propertyData.label; let elSpan = document.createElement('span'); - elSpan.setAttribute("id", propertyElementID + "-icon"); + elSpan.setAttribute("id", elementID + "-icon"); elProperty.appendChild(elSpan); elProperty.appendChild(elLabel); - properties[propertyName].el = [ elSpan, elLabel ]; + return [ elSpan, elLabel ]; } -function createButtonsProperty(elProperty, elLabel, propertyData) { - let propertyName = propertyData.propertyName; - let propertyElementID = getPropertyElementID(propertyName); +function createButtonsProperty(property, elProperty, elLabel) { + let elementID = property.elementID; + let propertyData = property.data; elProperty.setAttribute("class", "property text"); @@ -1508,34 +1497,29 @@ function createButtonsProperty(elProperty, elLabel, propertyData) { } if (propertyData.buttons !== undefined) { - addButtons(elProperty, propertyElementID, propertyData.buttons, hasLabel); + addButtons(elProperty, elementID, propertyData.buttons, hasLabel); } - properties[propertyName].el = elProperty; + return elProperty; } -function createTupleNumberInput(elTuple, propertyID, subLabel, min, max, step) { - let elementPropertyID = propertyID + "-" + subLabel.toLowerCase(); +function createTupleNumberInput(elTuple, propertyElementID, subLabel, min, max, step) { + let elementID = propertyElementID + "-" + subLabel.toLowerCase(); + let elDiv = document.createElement('div'); let elLabel = document.createElement('label'); elLabel.innerText = subLabel[0].toUpperCase() + subLabel.slice(1) + ":"; - elLabel.setAttribute("for", elementPropertyID); + elLabel.setAttribute("for", elementID); + let elInput = document.createElement('input'); - elInput.setAttribute("id", elementPropertyID); + elInput.setAttribute("id", elementID); elInput.setAttribute("type", "number"); elInput.setAttribute("class", subLabel); - if (min !== undefined) { - elInput.setAttribute("min", min); - } - if (max !== undefined) { - elInput.setAttribute("max", max); - } - if (step !== undefined) { - elInput.setAttribute("step", step); - } + elDiv.appendChild(elInput); elDiv.appendChild(elLabel); elTuple.appendChild(elDiv); + return elInput; } @@ -2114,23 +2098,24 @@ function loaded() { elGroup.appendChild(elLegend); } - group.properties.forEach(function(property) { - let propertyType = property.type; - let propertyName = property.propertyName; - let propertyElementID = getPropertyElementID(propertyName); + group.properties.forEach(function(propertyData) { + let propertyType = propertyData.type; + let propertyID = propertyData.propertyID; + let propertyName = propertyData.propertyName !== undefined ? propertyData.propertyName : propertyID; + let propertyElementID = "property-" + propertyID; let elProperty; if (propertyType === "sub-header") { elProperty = document.createElement('legend'); - elProperty.innerText = property.label; + elProperty.innerText = propertyData.label; elProperty.setAttribute("class", "sub-section-header"); } else { elProperty = document.createElement('div'); elProperty.setAttribute("id", "div-" + propertyElementID); } - if (group.twoColumn && property.column !== undefined && property.column !== -1) { - let columnName = group.id + "column" + property.column; + if (group.twoColumn && propertyData.column !== undefined && propertyData.column !== -1) { + let columnName = group.id + "column" + propertyData.column; let elColumn = document.getElementById(columnName); if (!elColumn) { let columnDivName = group.id + "columnDiv"; @@ -2152,64 +2137,66 @@ function loaded() { } let elLabel = document.createElement('label'); - elLabel.innerText = property.label; + elLabel.innerText = propertyData.label; elLabel.setAttribute("for", propertyElementID); - properties[propertyName] = { data: property }; + properties[propertyID] = { data: propertyData, elementID: propertyElementID, name: propertyName }; + + let property = properties[propertyID]; switch (propertyType) { case 'string': { - createStringProperty(elProperty, elLabel, property); + properties[propertyID].el = createStringProperty(property, elProperty, elLabel); break; } case 'bool': { - createBoolProperty(elProperty, elLabel, property); + properties[propertyID].el = createBoolProperty(property, elProperty, elLabel); break; } case 'number': { - createNumberProperty(elProperty, elLabel, property); + properties[propertyID].el = createNumberProperty(property, elProperty, elLabel); break; } case 'vec3': { - createVec3Property(elProperty, elLabel, property); + properties[propertyID].el = createVec3Property(property, elProperty, elLabel); break; } case 'vec2': { - createVec2Property(elProperty, elLabel, property); + properties[propertyID].el = createVec2Property(property, elProperty, elLabel); break; } case 'color': { - createColorProperty(elProperty, elLabel, property); + properties[propertyID].el = createColorProperty(property, elProperty, elLabel); break; } case 'dropdown': { - createDropdownProperty(elProperty, elLabel, property); + properties[propertyID].el = createDropdownProperty(property, elProperty, elLabel); break; } case 'textarea': { - createTextareaProperty(elProperty, elLabel, property); + properties[propertyID].el = createTextareaProperty(property, elProperty, elLabel); break; } case 'icon': { - createIconProperty(elProperty, elLabel, property); + properties[propertyID].el = createIconProperty(property, elProperty, elLabel); break; } case 'buttons': { - createButtonsProperty(elProperty, elLabel, property); + properties[propertyID].el = createButtonsProperty(property, elProperty, elLabel); break; } case 'sub-header': { - properties[propertyName].el = elProperty; + properties[propertyID].el = elProperty; break; } default: { console.log("EntityProperties - Unknown property type " + - propertyType + " set to property " + propertyName); + propertyType + " set to property " + propertyID); break; } } - let showPropertyRule = property.showPropertyRule; + let showPropertyRule = propertyData.showPropertyRule; if (showPropertyRule !== undefined) { let dependentProperty = Object.keys(showPropertyRule)[0]; let dependentPropertyValue = showPropertyRule[dependentProperty]; @@ -2219,7 +2206,7 @@ function loaded() { if (properties[dependentProperty].showPropertyRules === undefined) { properties[dependentProperty].showPropertyRules = {}; } - properties[dependentProperty].showPropertyRules[propertyName] = dependentPropertyValue; + properties[dependentProperty].showPropertyRules[propertyID] = dependentPropertyValue; } }); @@ -2351,10 +2338,11 @@ function loaded() { elPropertiesList.className = selectedEntityProperties.type + 'Menu'; showGroupsForType(selectedEntityProperties.type); - for (let propertyName in properties) { - let property = properties[propertyName]; + for (let propertyID in properties) { + let property = properties[propertyID]; let elProperty = property.el; let propertyData = property.data; + let propertyName = property.name; // if this is a compound property name (i.e. animation.running) // then split it by . up to 3 times to find property value @@ -2376,11 +2364,6 @@ function loaded() { } else { propertyValue = selectedEntityProperties[propertyName]; } - - // workaround for shape Color & Light Color property fields sharing same property value "color" - if (propertyValue === undefined && propertyName === "lightColor") { - propertyValue = selectedEntityProperties["color"] - } let isSubProperty = propertyData.subPropertyOf !== undefined; if (elProperty === undefined || (propertyValue === undefined && !isSubProperty)) { @@ -2397,7 +2380,7 @@ function loaded() { if (isSubProperty) { let propertyValue = selectedEntityProperties[propertyData.subPropertyOf]; let subProperties = propertyValue.split(","); - let subPropertyValue = subProperties.indexOf(propertyName) > -1; + let subPropertyValue = subProperties.indexOf(propertyID) > -1; elProperty.checked = inverse ? !subPropertyValue : subPropertyValue; } else { elProperty.checked = inverse ? !propertyValue : propertyValue; @@ -2583,8 +2566,9 @@ function loaded() { } // Server Script Status - let elServerScript = getPropertyElement("serverScripts"); - let serverScriptElementID = getPropertyElementID("serverScripts"); + let serverScriptProperty = properties["serverScripts"]; + let elServerScript = serverScriptProperty.el; + let serverScriptElementID = serverScriptProperty.elementID; let elDiv = document.createElement('div'); elDiv.setAttribute("class", "property"); let elLabel = document.createElement('label'); @@ -2609,8 +2593,9 @@ function loaded() { elServerScript.parentNode.setAttribute("class", "property url refresh"); // User Data - let elUserData = getPropertyElement("userData"); - let userDataElementID = getPropertyElementID("userData"); + let userDataProperty = properties["userData"]; + let elUserData = userDataProperty.el; + let userDataElementID = userDataProperty.elementID; elDiv = elUserData.parentNode; let elStaticUserData = document.createElement('div'); elStaticUserData.setAttribute("id", userDataElementID + "-static"); @@ -2624,8 +2609,9 @@ function loaded() { elDiv.insertBefore(elUserDataEditor, elUserData); // Material Data - let elMaterialData = getPropertyElement("materialData"); - let materialDataElementID = getPropertyElementID("materialData"); + let materialDataProperty = properties["materialData"]; + let elMaterialData = materialDataProperty.el; + let materialDataElementID = materialDataProperty.elementID; elDiv = elMaterialData.parentNode; let elStaticMaterialData = document.createElement('div'); elStaticMaterialData.setAttribute("id", materialDataElementID + "-static"); @@ -2763,9 +2749,10 @@ function loaded() { ul.appendChild(li); } - let propertyName = elDropdowns[dropDownIndex].getAttribute("propertyName"); - properties[propertyName].el = dt; - dt.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + let propertyID = elDropdowns[dropDownIndex].getAttribute("propertyID"); + let property = properties[propertyID]; + property.el = dt; + dt.addEventListener('change', createEmitTextPropertyUpdateFunction(property.name)); } elDropdowns = document.getElementsByTagName("select"); From 3f08400741d834df27198d79b5cf9f13e2b0d14f Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 11 Oct 2018 12:48:08 -0700 Subject: [PATCH 085/362] comment --- scripts/system/html/js/entityProperties.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index fcd9251485..3ff24d013c 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -474,9 +474,8 @@ const GROUPS = [ { label: "Light Color", type: "color", - propertyID: "lightColor", // this actually shares "color" property with shape Color but - // separating naming here to distinguish property element/data - propertyName: "color", + propertyID: "lightColor", + propertyName: "color", // actual entity property name }, { label: "Intensity", From 62c5fb8e2e34fa53b47814d89360ab5a117ae364 Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 11 Oct 2018 14:25:06 -0700 Subject: [PATCH 086/362] tweaks / dropdown fix --- scripts/system/html/js/entityProperties.js | 32 ++++++++++------------ 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 3ff24d013c..1d8a397f07 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1383,7 +1383,7 @@ function createColorProperty(property, elProperty, elLabel) { return [elColorPicker, elInputR, elInputG, elInputB]; } -function createDropdownProperty(property, elProperty, elLabel) { +function createDropdownProperty(property, propertyID, elProperty, elLabel) { let propertyName = property.name; let elementID = property.elementID; let propertyData = property.data; @@ -1392,7 +1392,7 @@ function createDropdownProperty(property, elProperty, elLabel) { let elInput = document.createElement('select'); elInput.setAttribute("id", elementID); - elInput.setAttribute("propertyID", propertyData.propertyID); + elInput.setAttribute("propertyID", propertyID); for (let optionKey in propertyData.options) { let option = document.createElement('option'); @@ -2015,7 +2015,7 @@ function bindAllNonJSONEditorElements() { } -// DROPDOWNS / TEXTAREAS / PARENT MATERIAL NAME FUNCTIONS +// DROPDOWN FUNCTIONS function setDropdownText(dropdown) { let lis = dropdown.parentNode.getElementsByTagName("li"); @@ -2050,6 +2050,9 @@ function setDropdownValue(event) { dt.dispatchEvent(evt); } + +// TEXTAREA / PARENT MATERIAL NAME FUNCTIONS + function setTextareaScrolling(element) { var isScrolling = element.scrollHeight > element.offsetHeight; element.setAttribute("scrolling", isScrolling ? "true" : "false"); @@ -2169,7 +2172,7 @@ function loaded() { break; } case 'dropdown': { - properties[propertyID].el = createDropdownProperty(property, elProperty, elLabel); + properties[propertyID].el = createDropdownProperty(property, propertyID, elProperty, elLabel); break; } case 'textarea': { @@ -2252,10 +2255,6 @@ function loaded() { } resetProperties(); - - getPropertyElement("type")[0].style.display = "none"; - getPropertyElement("type")[1].innerHTML = NO_SELECTION; - elPropertiesList.className = ''; showGroupsForType("None"); deleteJSONEditor(); @@ -2297,17 +2296,20 @@ function loaded() { } resetProperties(); - - getPropertyElement("type")[0].innerHTML = ICON_FOR_TYPE[type]; - getPropertyElement("type")[0].style.display = "inline-block"; - getPropertyElement("type")[1].innerHTML = type + " (" + data.selections.length + ")"; - elPropertiesList.className = ''; showGroupsForType(type); + let typeProperty = properties["type"]; + let elTypeProperty = typeProperty.el; + elTypeProperty[0].innerHTML = typeProperty.data.icons[type]; + elTypeProperty[0].style.display = "inline-block"; + elTypeProperty[1].innerHTML = type + " (" + data.selections.length + ")"; + disableProperties(); } else { selectedEntityProperties = data.selections[0].properties; + showGroupsForType(selectedEntityProperties.type); + if (lastEntityID !== '"' + selectedEntityProperties.id + '"' && lastEntityID !== null) { if (editor !== null) { saveUserData(); @@ -2333,10 +2335,6 @@ function loaded() { } } - // Create class name for css ruleset filtering - elPropertiesList.className = selectedEntityProperties.type + 'Menu'; - showGroupsForType(selectedEntityProperties.type); - for (let propertyID in properties) { let property = properties[propertyID]; let elProperty = property.el; From f73d974ad77b62a41b4d540e60b1cc1c584db98f Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 11 Oct 2018 15:14:04 -0700 Subject: [PATCH 087/362] added a lock state so you can lock sit or stand state, also made the hand azimuth relative to the spine2 location in avatar space. This stops the arms from behaving badly when the hands are in front of the chest but behind the root position of the avatar --- interface/src/avatar/MyAvatar.cpp | 119 ++++++++++++++--------- interface/src/avatar/MyAvatar.h | 1 + interface/src/avatar/MySkeletonModel.cpp | 3 +- 3 files changed, 75 insertions(+), 48 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 77c66d062f..6b4459e97c 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -531,52 +531,57 @@ void MyAvatar::update(float deltaTime) { glm::vec3 sensorHips = transformPoint(glm::inverse(getSensorToWorldMatrix()), worldHips); // put update sit stand state counts here - if (getIsInSittingState()) { - if (newHeightReading.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { - // if we recenter upwards then no longer in sitting state - _sitStandStateCount++; - if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { - _sitStandStateCount = 0; - _squatCount = 0; - if (newHeightReading.isValid()) { - _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; - _tippingPoint = newHeightReading.getTranslation().y; + if (!_lockSitStandState) { + if (getIsInSittingState()) { + if (newHeightReading.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { + // if we recenter upwards then no longer in sitting state + _sitStandStateCount++; + if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sitStandStateCount = 0; + _squatCount = 0; + if (newHeightReading.isValid()) { + _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; + _tippingPoint = newHeightReading.getTranslation().y; + } + _averageUserHeightCount = 1; + setResetMode(true); + setIsInSittingState(false); + setCenterOfGravityModelEnabled(true); + setSitStandStateChange(true); } - _averageUserHeightCount = 1; - setResetMode(true); - setIsInSittingState(false); - setCenterOfGravityModelEnabled(true); - setSitStandStateChange(true); + qCDebug(interfaceapp) << "going to stand state"; + } else { + _sitStandStateCount = 0; + // tipping point is average height when sitting. + _tippingPoint = averageSensorSpaceHeight; } } else { - _sitStandStateCount = 0; - // tipping point is average height when sitting. - _tippingPoint = averageSensorSpaceHeight; - } - } else { - // in the standing state - if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint))) {// && (angleSpine2 > COSINE_TEN_DEGREES) && !(sensorHips.y > (0.4f * averageSensorSpaceHeight))) { - _sitStandStateCount++; - if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { - _sitStandStateCount = 0; - _squatCount = 0; - if (newHeightReading.isValid()) { - _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; - _tippingPoint = newHeightReading.getTranslation().y; - } - _averageUserHeightCount = 1; - setResetMode(true); - setIsInSittingState(true); - setCenterOfGravityModelEnabled(false); - setSitStandStateChange(true); - } - } else { - // returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); - // use the mode height for the tipping point when we are standing. - _tippingPoint = getCurrentStandingHeight(); - _sitStandStateCount = 0; - } + // in the standing state + if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint))) {// && (angleSpine2 > COSINE_TEN_DEGREES) && !(sensorHips.y > (0.4f * averageSensorSpaceHeight))) { + _sitStandStateCount++; + if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sitStandStateCount = 0; + _squatCount = 0; + if (newHeightReading.isValid()) { + _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; + _tippingPoint = newHeightReading.getTranslation().y; + } + _averageUserHeightCount = 1; + setResetMode(true); + setIsInSittingState(true); + setCenterOfGravityModelEnabled(false); + setSitStandStateChange(true); + } + qCDebug(interfaceapp) << "going to sit state"; + } else { + // returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + // use the mode height for the tipping point when we are standing. + _tippingPoint = getCurrentStandingHeight(); + _sitStandStateCount = 0; + } + + } } @@ -939,6 +944,15 @@ void MyAvatar::updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix) { // Find the vector halfway between the hip to hand azimuth vectors // This midpoint hand azimuth is in Avatar space glm::vec2 MyAvatar::computeHandAzimuth() const { + int spine2Index = _skeletonModel->getRig().indexOfJoint("Spine2"); + glm::vec3 azimuthOrigin(0.0f,0.0f,0.0f); + if (!(spine2Index < 0)) { + // use the spine for the azimuth origin. + azimuthOrigin = getAbsoluteJointTranslationInObjectFrame(spine2Index); + } else { + // use the avatar root as the azimuth origin. + } + controller::Pose leftHandPoseAvatarSpace = getLeftHandPose(); controller::Pose rightHandPoseAvatarSpace = getRightHandPose(); controller::Pose headPoseAvatarSpace = getControllerPoseInAvatarFrame(controller::Action::HEAD); @@ -946,11 +960,13 @@ glm::vec2 MyAvatar::computeHandAzimuth() const { glm::vec2 latestHipToHandController = _hipToHandController; if (leftHandPoseAvatarSpace.isValid() && rightHandPoseAvatarSpace.isValid() && headPoseAvatarSpace.isValid()) { + glm::vec3 rightHandOffset = rightHandPoseAvatarSpace.translation - azimuthOrigin; + glm::vec3 leftHandOffset = leftHandPoseAvatarSpace.translation - azimuthOrigin; // we need the old azimuth reading to prevent flipping the facing direction 180 // in the case where the hands go from being slightly less than 180 apart to slightly more than 180 apart. glm::vec2 oldAzimuthReading = _hipToHandController; - if ((glm::length(glm::vec2(rightHandPoseAvatarSpace.translation.x, rightHandPoseAvatarSpace.translation.z)) > 0.0f) && (glm::length(glm::vec2(leftHandPoseAvatarSpace.translation.x, leftHandPoseAvatarSpace.translation.z)) > 0.0f)) { - latestHipToHandController = lerp(glm::normalize(glm::vec2(rightHandPoseAvatarSpace.translation.x, rightHandPoseAvatarSpace.translation.z)), glm::normalize(glm::vec2(leftHandPoseAvatarSpace.translation.x, leftHandPoseAvatarSpace.translation.z)), HALFWAY); + if ((glm::length(glm::vec2(rightHandOffset.x, rightHandOffset.z)) > 0.0f) && (glm::length(glm::vec2(leftHandOffset.x, leftHandOffset.z)) > 0.0f)) { + latestHipToHandController = lerp(glm::normalize(glm::vec2(rightHandOffset.x, rightHandOffset.z)), glm::normalize(glm::vec2(leftHandOffset.x, leftHandOffset.z)), HALFWAY); } else { latestHipToHandController = glm::vec2(0.0f, -1.0f); } @@ -4134,21 +4150,29 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl const int SQUATTY_COUNT_THRESHOLD = 600; glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); + bool returnValue = false; + returnValue = (offset.y > CYLINDER_TOP);// || (offset.y < CYLINDER_BOTTOM); + if (myAvatar.getSitStandStateChange()) { + qCDebug(interfaceapp) << "sit state change"; returnValue = true; } if (myAvatar.getIsInSittingState()) { if (offset.y < SITTING_BOTTOM) { // we recenter when sitting. + qCDebug(interfaceapp) << "lean back sitting "; returnValue = true; } } else { // in the standing state - returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); if (myAvatar._squatCount > SQUATTY_COUNT_THRESHOLD) { myAvatar._squatCount = 0; - returnValue = true; + qCDebug(interfaceapp) << "squatting "; + // returnValue = true; + } + if (returnValue == true) { + qCDebug(interfaceapp) << "above or below capsule in standing"; } } return returnValue; @@ -4182,7 +4206,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } } } - + if (_velocityCount > 60) { if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Vertical); @@ -4192,6 +4216,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat _velocityCount++; } } + } else { if (!isActive(Rotation) && getForceActivateRotation()) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index f268a05a15..f026f39493 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1824,6 +1824,7 @@ private: float _sumUserHeightSensorSpace{ DEFAULT_AVATAR_HEIGHT }; int _averageUserHeightCount{ 1 }; bool _sitStandStateChange{ false }; + bool _lockSitStandState { true }; // max unscaled forward movement speed ThreadSafeValueCache _walkSpeed { DEFAULT_AVATAR_MAX_WALKING_SPEED }; diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 1f97ce03f8..ce8fefa0c5 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -239,7 +239,8 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { params.primaryControllerFlags[Rig::PrimaryControllerType_Hips] = (uint8_t)Rig::ControllerFlags::Enabled | (uint8_t)Rig::ControllerFlags::Estimated; // set spine2 if we have hand controllers - if (myAvatar->getHMDLeanRecenterEnabled() && myAvatar->getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid() && + // myAvatar->getHMDLeanRecenterEnabled() && + if (myAvatar->getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid() && myAvatar->getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && !(params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] & (uint8_t)Rig::ControllerFlags::Enabled)) { From 1bcbda7ad6454988e4fba25c2e1f389109ba701d Mon Sep 17 00:00:00 2001 From: Clement Date: Thu, 11 Oct 2018 16:16:24 -0700 Subject: [PATCH 088/362] Prevent race on internal client traits members --- assignment-client/src/Agent.h | 1 - interface/src/avatar/MyAvatar.cpp | 1 + interface/src/avatar/MyAvatar.h | 1 - libraries/avatars/src/ClientTraitsHandler.cpp | 23 +++++++++++++++++++ libraries/avatars/src/ClientTraitsHandler.h | 19 +++++++-------- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 2b5ff51b49..7d47c8e713 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -21,7 +21,6 @@ #include #include -#include #include #include #include diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b347963cf1..5418410dd4 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 16b765711a..c99fd3bce6 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index a06b53da7c..f8247d9e52 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -31,7 +31,27 @@ ClientTraitsHandler::ClientTraitsHandler(AvatarData* owningAvatar) : nodeList->getPacketReceiver().registerListener(PacketType::SetAvatarTraits, this, "processTraitOverride"); } +void ClientTraitsHandler::markTraitUpdated(AvatarTraits::TraitType updatedTrait) { + Lock lock(_traitLock); + _traitStatuses[updatedTrait] = Updated; + _hasChangedTraits = true; +} + +void ClientTraitsHandler::markInstancedTraitUpdated(AvatarTraits::TraitType traitType, QUuid updatedInstanceID) { + Lock lock(_traitLock); + _traitStatuses.instanceInsert(traitType, updatedInstanceID, Updated); + _hasChangedTraits = true; +} + +void ClientTraitsHandler::markInstancedTraitDeleted(AvatarTraits::TraitType traitType, QUuid deleteInstanceID) { + Lock lock(_traitLock); + _traitStatuses.instanceInsert(traitType, deleteInstanceID, Deleted); + _hasChangedTraits = true; +} + void ClientTraitsHandler::resetForNewMixer() { + Lock lock(_traitLock); + // re-set the current version to 0 _currentTraitVersion = AvatarTraits::DEFAULT_TRAIT_VERSION; @@ -46,6 +66,8 @@ void ClientTraitsHandler::resetForNewMixer() { } void ClientTraitsHandler::sendChangedTraitsToMixer() { + Lock lock(_traitLock); + if (hasChangedTraits() || _shouldPerformInitialSend) { // we have at least one changed trait to send @@ -113,6 +135,7 @@ void ClientTraitsHandler::sendChangedTraitsToMixer() { void ClientTraitsHandler::processTraitOverride(QSharedPointer message, SharedNodePointer sendingNode) { if (sendingNode->getType() == NodeType::AvatarMixer) { + Lock lock(_traitLock); while (message->getBytesLeftToRead()) { AvatarTraits::TraitType traitType; message->readPrimitive(&traitType); diff --git a/libraries/avatars/src/ClientTraitsHandler.h b/libraries/avatars/src/ClientTraitsHandler.h index 27ba58d46b..3900268101 100644 --- a/libraries/avatars/src/ClientTraitsHandler.h +++ b/libraries/avatars/src/ClientTraitsHandler.h @@ -26,14 +26,11 @@ public: void sendChangedTraitsToMixer(); - bool hasChangedTraits() { return _hasChangedTraits; } + bool hasChangedTraits() const { return _hasChangedTraits; } - void markTraitUpdated(AvatarTraits::TraitType updatedTrait) - { _traitStatuses[updatedTrait] = Updated; _hasChangedTraits = true; } - void markInstancedTraitUpdated(AvatarTraits::TraitType traitType, QUuid updatedInstanceID) - { _traitStatuses.instanceInsert(traitType, updatedInstanceID, Updated); _hasChangedTraits = true; } - void markInstancedTraitDeleted(AvatarTraits::TraitType traitType, QUuid deleteInstanceID) - { _traitStatuses.instanceInsert(traitType, deleteInstanceID, Deleted); _hasChangedTraits = true; } + void markTraitUpdated(AvatarTraits::TraitType updatedTrait); + void markInstancedTraitUpdated(AvatarTraits::TraitType traitType, QUuid updatedInstanceID); + void markInstancedTraitDeleted(AvatarTraits::TraitType traitType, QUuid deleteInstanceID); void resetForNewMixer(); @@ -41,17 +38,21 @@ public slots: void processTraitOverride(QSharedPointer message, SharedNodePointer sendingNode); private: + using Mutex = std::recursive_mutex; + using Lock = std::lock_guard; + enum ClientTraitStatus { Unchanged, Updated, Deleted }; - AvatarData* _owningAvatar; + AvatarData* const _owningAvatar; + Mutex _traitLock; AvatarTraits::AssociatedTraitValues _traitStatuses; - AvatarTraits::TraitVersion _currentTraitVersion { AvatarTraits::DEFAULT_TRAIT_VERSION }; + AvatarTraits::TraitVersion _currentTraitVersion { AvatarTraits::DEFAULT_TRAIT_VERSION }; AvatarTraits::TraitVersion _currentSkeletonVersion { AvatarTraits::NULL_TRAIT_VERSION }; bool _shouldPerformInitialSend { false }; From daeedc6ef1ee64472d66ef52decf4d6380c210f9 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 11 Oct 2018 17:10:14 -0700 Subject: [PATCH 089/362] Lots of progress today --- interface/src/Application.cpp | 11 ++- ...nterface.cpp => TTSScriptingInterface.cpp} | 58 ++++++++---- ...ingInterface.h => TTSScriptingInterface.h} | 19 ++-- libraries/audio-client/src/AudioClient.cpp | 88 ++++++++++--------- libraries/audio-client/src/AudioClient.h | 3 + 5 files changed, 111 insertions(+), 68 deletions(-) rename interface/src/scripting/{SpeechScriptingInterface.cpp => TTSScriptingInterface.cpp} (64%) rename interface/src/scripting/{SpeechScriptingInterface.h => TTSScriptingInterface.h} (79%) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 74532ef53a..728fea8c10 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -182,7 +182,7 @@ #include "scripting/RatesScriptingInterface.h" #include "scripting/SelectionScriptingInterface.h" #include "scripting/WalletScriptingInterface.h" -#include "scripting/SpeechScriptingInterface.h" +#include "scripting/TTSScriptingInterface.h" #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif @@ -944,7 +944,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); @@ -1179,6 +1179,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo recording::Frame::registerFrameHandler(AudioConstants::getAudioFrameName(), [&audioIO](recording::Frame::ConstPointer frame) { audioIO->handleRecordedAudioInput(frame->data); }); + + auto TTS = DependencyManager::get().data(); + connect(TTS, &TTSScriptingInterface::ttsSampleCreated, audioIO, &AudioClient::handleTTSAudioInput); connect(audioIO, &AudioClient::inputReceived, [](const QByteArray& audio) { static auto recorder = DependencyManager::get(); @@ -3129,7 +3132,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); surfaceContext->setContextProperty("Wallet", DependencyManager::get().data()); surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); - surfaceContext->setContextProperty("Speech", DependencyManager::get().data()); + surfaceContext->setContextProperty("TextToSpeech", DependencyManager::get().data()); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); @@ -6800,7 +6803,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("Wallet", DependencyManager::get().data()); scriptEngine->registerGlobalObject("AddressManager", DependencyManager::get().data()); scriptEngine->registerGlobalObject("HifiAbout", AboutUtil::getInstance()); - scriptEngine->registerGlobalObject("Speech", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("TextToSpeech", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine.data(), OverlayIDtoScriptValue, OverlayIDfromScriptValue); diff --git a/interface/src/scripting/SpeechScriptingInterface.cpp b/interface/src/scripting/TTSScriptingInterface.cpp similarity index 64% rename from interface/src/scripting/SpeechScriptingInterface.cpp rename to interface/src/scripting/TTSScriptingInterface.cpp index b8e0f5c3e8..fdbb37e586 100644 --- a/interface/src/scripting/SpeechScriptingInterface.cpp +++ b/interface/src/scripting/TTSScriptingInterface.cpp @@ -1,6 +1,6 @@ // -// SpeechScriptingInterface.cpp -// interface/src/scripting +// TTSScriptingInterface.cpp +// libraries/audio-client/src/scripting // // Created by Zach Fox on 2018-10-10. // Copyright 2018 High Fidelity, Inc. @@ -9,10 +9,10 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "SpeechScriptingInterface.h" +#include "TTSScriptingInterface.h" #include "avatar/AvatarManager.h" -SpeechScriptingInterface::SpeechScriptingInterface() { +TTSScriptingInterface::TTSScriptingInterface() { // // Create text to speech engine // @@ -38,8 +38,7 @@ SpeechScriptingInterface::SpeechScriptingInterface() { } } -SpeechScriptingInterface::~SpeechScriptingInterface() { - +TTSScriptingInterface::~TTSScriptingInterface() { } class ReleaseOnExit { @@ -55,7 +54,28 @@ private: IUnknown* m_p; }; -void SpeechScriptingInterface::speakText(const QString& textToSpeak) { +void TTSScriptingInterface::testTone(const bool& alsoInject) { + QByteArray byteArray(480000, 0); + _lastSoundByteArray.resize(0); + _lastSoundByteArray.resize(480000); + + int32_t a = 0; + int16_t* samples = reinterpret_cast(byteArray.data()); + for (a = 0; a < 240000; a++) { + int16_t temp = (glm::sin(glm::radians((float)a))) * 32768; + samples[a] = temp; + } + emit ttsSampleCreated(_lastSoundByteArray); + + if (alsoInject) { + AudioInjectorOptions options; + options.position = DependencyManager::get()->getMyAvatarPosition(); + + _lastSoundAudioInjector = AudioInjector::playSound(_lastSoundByteArray, options); + } +} + +void TTSScriptingInterface::speakText(const QString& textToSpeak, const bool& alsoInject) { WAVEFORMATEX fmt; fmt.wFormatTag = WAVE_FORMAT_PCM; fmt.nSamplesPerSec = 24000; @@ -92,9 +112,8 @@ void SpeechScriptingInterface::speakText(const QString& textToSpeak) { ReleaseOnExit rStream(pStream); ULONG streamNumber; - hr = m_tts->Speak(reinterpret_cast(textToSpeak.utf16()), - SPF_IS_XML | SPF_ASYNC | SPF_PURGEBEFORESPEAK, - &streamNumber); + hr = m_tts->Speak(reinterpret_cast(textToSpeak.utf16()), SPF_IS_XML | SPF_ASYNC | SPF_PURGEBEFORESPEAK, + &streamNumber); if (FAILED(hr)) { qDebug() << "Speak failed."; } @@ -124,14 +143,21 @@ void SpeechScriptingInterface::speakText(const QString& textToSpeak) { qDebug() << "Couldn't read from stream."; } - QByteArray byteArray = QByteArray::QByteArray(buf1, dwSize); + _lastSoundByteArray.resize(0); + _lastSoundByteArray.append(buf1, dwSize); - AudioInjectorOptions options; - options.position = DependencyManager::get()->getMyAvatarPosition(); + emit ttsSampleCreated(_lastSoundByteArray); - lastSound = AudioInjector::playSound(byteArray, options); + if (alsoInject) { + AudioInjectorOptions options; + options.position = DependencyManager::get()->getMyAvatarPosition(); + + _lastSoundAudioInjector = AudioInjector::playSound(_lastSoundByteArray, options); + } } -void SpeechScriptingInterface::stopLastSpeech() { - lastSound->stop(); +void TTSScriptingInterface::stopLastSpeech() { + if (_lastSoundAudioInjector) { + _lastSoundAudioInjector->stop(); + } } diff --git a/interface/src/scripting/SpeechScriptingInterface.h b/interface/src/scripting/TTSScriptingInterface.h similarity index 79% rename from interface/src/scripting/SpeechScriptingInterface.h rename to interface/src/scripting/TTSScriptingInterface.h index c683a1a3c6..cb9c6c8c3e 100644 --- a/interface/src/scripting/SpeechScriptingInterface.h +++ b/interface/src/scripting/TTSScriptingInterface.h @@ -1,5 +1,5 @@ -// SpeechScriptingInterface.h -// interface/src/scripting +// TTSScriptingInterface.h +// libraries/audio-client/src/scripting // // Created by Zach Fox on 2018-10-10. // Copyright 2018 High Fidelity, Inc. @@ -20,16 +20,20 @@ #include // SAPI Helper #include -class SpeechScriptingInterface : public QObject, public Dependency { +class TTSScriptingInterface : public QObject, public Dependency { Q_OBJECT public: - SpeechScriptingInterface(); - ~SpeechScriptingInterface(); + TTSScriptingInterface(); + ~TTSScriptingInterface(); - Q_INVOKABLE void speakText(const QString& textToSpeak); + Q_INVOKABLE void testTone(const bool& alsoInject = false); + Q_INVOKABLE void speakText(const QString& textToSpeak, const bool& alsoInject = false); Q_INVOKABLE void stopLastSpeech(); +signals: + void ttsSampleCreated(QByteArray outputArray); + private: class CComAutoInit { public: @@ -72,7 +76,8 @@ private: // Default voice token CComPtr m_voiceToken; - AudioInjectorPointer lastSound; + QByteArray _lastSoundByteArray; + AudioInjectorPointer _lastSoundByteArray; }; #endif // hifi_SpeechScriptingInterface_h diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index d00bc29054..96f1c97878 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1135,6 +1135,46 @@ void AudioClient::handleAudioInput(QByteArray& audioBuffer) { } } +void AudioClient::processAudioAndAddToRingBuffer(QByteArray& inputByteArray, const uchar& channelCount, const qint32& bytesForDuration) { + // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output + const int inputSamplesRequired = + (_inputToNetworkResampler ? _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) + : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * + channelCount; + + const auto inputAudioSamples = std::unique_ptr(new int16_t[inputSamplesRequired]); + + handleLocalEchoAndReverb(inputByteArray); + + _inputRingBuffer.writeData(inputByteArray.data(), inputByteArray.size()); + + float audioInputMsecsRead = inputByteArray.size() / (float)(bytesForDuration); + _stats.updateInputMsRead(audioInputMsecsRead); + + const int numNetworkBytes = + _isStereoInput ? AudioConstants::NETWORK_FRAME_BYTES_STEREO : AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; + const int numNetworkSamples = + _isStereoInput ? AudioConstants::NETWORK_FRAME_SAMPLES_STEREO : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; + + static int16_t networkAudioSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; + + while (_inputRingBuffer.samplesAvailable() >= inputSamplesRequired) { + if (_muted) { + _inputRingBuffer.shiftReadPosition(inputSamplesRequired); + } else { + _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); + possibleResampling(_inputToNetworkResampler, inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, + numNetworkSamples, channelCount, _desiredInputFormat.channelCount()); + } + int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE; + float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); + _stats.updateInputMsUnplayed(msecsInInputRingBuffer); + + QByteArray audioBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); + handleAudioInput(audioBuffer); + } +} + void AudioClient::handleMicAudioInput() { if (!_inputDevice || _isPlayingBackRecording) { return; @@ -1144,47 +1184,8 @@ void AudioClient::handleMicAudioInput() { _inputReadsSinceLastCheck++; #endif - // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output - const int inputSamplesRequired = (_inputToNetworkResampler ? - _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : - AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * _inputFormat.channelCount(); - - const auto inputAudioSamples = std::unique_ptr(new int16_t[inputSamplesRequired]); - QByteArray inputByteArray = _inputDevice->readAll(); - - handleLocalEchoAndReverb(inputByteArray); - - _inputRingBuffer.writeData(inputByteArray.data(), inputByteArray.size()); - - float audioInputMsecsRead = inputByteArray.size() / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); - _stats.updateInputMsRead(audioInputMsecsRead); - - const int numNetworkBytes = _isStereoInput - ? AudioConstants::NETWORK_FRAME_BYTES_STEREO - : AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; - const int numNetworkSamples = _isStereoInput - ? AudioConstants::NETWORK_FRAME_SAMPLES_STEREO - : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; - - static int16_t networkAudioSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; - - while (_inputRingBuffer.samplesAvailable() >= inputSamplesRequired) { - if (_muted) { - _inputRingBuffer.shiftReadPosition(inputSamplesRequired); - } else { - _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); - possibleResampling(_inputToNetworkResampler, - inputAudioSamples.get(), networkAudioSamples, - inputSamplesRequired, numNetworkSamples, - _inputFormat.channelCount(), _desiredInputFormat.channelCount()); - } - int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE; - float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); - _stats.updateInputMsUnplayed(msecsInInputRingBuffer); - - QByteArray audioBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); - handleAudioInput(audioBuffer); - } + processAudioAndAddToRingBuffer(_inputDevice->readAll(), _inputFormat.channelCount(), + _inputFormat.bytesForDuration(USECS_PER_MSEC)); } void AudioClient::handleDummyAudioInput() { @@ -1201,6 +1202,11 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { handleAudioInput(audioBuffer); } +void AudioClient::handleTTSAudioInput(const QByteArray& audio) { + QByteArray audioBuffer(audio); + processAudioAndAddToRingBuffer(audioBuffer, 1, 48); +} + void AudioClient::prepareLocalAudioInjectors(std::unique_ptr localAudioLock) { bool doSynchronously = localAudioLock.operator bool(); if (!localAudioLock) { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 5e7f1fb8a0..170a355abe 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -197,6 +197,7 @@ public slots: void checkInputTimeout(); void handleDummyAudioInput(); void handleRecordedAudioInput(const QByteArray& audio); + void handleTTSAudioInput(const QByteArray& audio); void reset(); void audioMixerKilled(); @@ -289,6 +290,8 @@ private: float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); + void processAudioAndAddToRingBuffer(QByteArray& inputByteArray, const uchar& channelCount, const qint32& bytesForDuration); + #ifdef Q_OS_ANDROID QTimer _checkInputTimer; long _inputReadsSinceLastCheck = 0l; From a6339bbe859a00a0406b83be62d5b09522f8a3fa Mon Sep 17 00:00:00 2001 From: Liv Erickson Date: Thu, 11 Oct 2018 17:26:05 -0700 Subject: [PATCH 090/362] bug fix and new menu content --- cmake/externals/serverless-content/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/externals/serverless-content/CMakeLists.txt b/cmake/externals/serverless-content/CMakeLists.txt index 12e2b7a4c6..8bb49f0973 100644 --- a/cmake/externals/serverless-content/CMakeLists.txt +++ b/cmake/externals/serverless-content/CMakeLists.txt @@ -4,8 +4,8 @@ set(EXTERNAL_NAME serverless-content) ExternalProject_Add( ${EXTERNAL_NAME} - URL http://cdn.highfidelity.com/content-sets/serverless-tutorial-RC73.zip - URL_MD5 0c5edfb63cafb042311d3cf25261fbf2 + URL http://cdn.highfidelity.com/content-sets/serverless-tutorial-RC75.zip + URL_MD5 b4225d058952e17976ac228330ce8d51 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" From 67afc862224dcb753acdf62b4a8d77f276806b8c Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 11 Oct 2018 18:39:44 -0700 Subject: [PATCH 091/362] cleanup --- interface/resources/qml/hifi/AvatarApp.qml | 1 + .../resources/qml/hifi/avatarapp/Settings.qml | 19 ++++++++++++ interface/src/avatar/MyAvatar.cpp | 29 +++++++++---------- interface/src/avatar/MyAvatar.h | 13 ++++++++- interface/src/avatar/MySkeletonModel.cpp | 3 +- scripts/system/avatarapp.js | 14 ++++++++- 6 files changed, 60 insertions(+), 19 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index b06a2ca67c..bf647b65bb 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -253,6 +253,7 @@ Rectangle { dominantHand : settings.dominantHandIsLeft ? 'left' : 'right', collisionsEnabled : settings.avatarCollisionsOn, sittingEnabled : settings.avatarSittingOn, + lockStateEnabled : settings.avatarLockSitStandStateOn, animGraphOverrideUrl : settings.avatarAnimationOverrideJSON, collisionSoundUrl : settings.avatarCollisionSoundUrl }; diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index c4289ca650..8749079940 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -21,6 +21,7 @@ Rectangle { property alias dominantHandIsLeft: leftHandRadioButton.checked property alias avatarCollisionsOn: collisionsEnabledRadiobutton.checked property alias avatarSittingOn: sitRadiobutton.checked + property alias avatarLockSitStandStateOn: lockSitStandStateCheckbox.checked property alias avatarAnimationOverrideJSON: avatarAnimationUrlInputText.text property alias avatarAnimationJSON: avatarAnimationUrlInputText.placeholderText property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text @@ -52,6 +53,12 @@ Rectangle { standRadioButton.checked = true; } + if (settings.lockStateEnabled) { + lockSitStandStateCheckbox.checked = true; + } else { + lockSitStandStateCheckbox.checked = false; + } + avatarAnimationJSON = settings.animGraphUrl; avatarAnimationOverrideJSON = settings.animGraphOverrideUrl; avatarCollisionSoundUrl = settings.collisionSoundUrl; @@ -343,6 +350,18 @@ Rectangle { text: "Stand" boxSize: 20 } + + // "Lock State" Checkbox + + HifiControlsUit.CheckBox { + id: lockSitStandStateCheckbox; + visible: activeTab == "nearbyTab"; + anchors.right: reloadNearbyContainer.left; + anchors.rightMargin: 20; + checked: settings.lockStateEnabled; + text: "lock"; + boxSize: 24; + } } ColumnLayout { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 6b4459e97c..745705f7b8 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -531,7 +531,7 @@ void MyAvatar::update(float deltaTime) { glm::vec3 sensorHips = transformPoint(glm::inverse(getSensorToWorldMatrix()), worldHips); // put update sit stand state counts here - if (!_lockSitStandState) { + if (getIsSitStandStateLocked()) { if (getIsInSittingState()) { if (newHeightReading.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { // if we recenter upwards then no longer in sitting state @@ -584,7 +584,6 @@ void MyAvatar::update(float deltaTime) { } } - if (_drawAverageFacingEnabled) { auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); glm::vec3 worldHeadPos = transformPoint(getSensorToWorldMatrix(), sensorHeadPose.getTranslation()); @@ -595,16 +594,9 @@ void MyAvatar::update(float deltaTime) { // draw hand azimuth vector glm::vec3 handAzimuthMidpoint = transformPoint(getTransform().getMatrix(), glm::vec3(_hipToHandController.x, 0.0f, _hipToHandController.y)); - DebugDraw::getInstance().drawRay(getWorldPosition(), handAzimuthMidpoint, glm::vec4(0.0f, 1.0f, 1.0f, 1.0f)); - - + DebugDraw::getInstance().drawRay(getWorldPosition(), handAzimuthMidpoint, glm::vec4(0.0f, 1.0f, 1.0f, 1.0f)); } - // temp: draw spine 2 position for hand azimuth purposes. - int spine2Index = getJointIndex("Spine2"); - glm::vec3 spine2WorldPosition = transformPoint(getTransform().getMatrix(), getAbsoluteJointTranslationInObjectFrame(spine2Index)); - DebugDraw::getInstance().addMarker("spine2 location", Quaternions::IDENTITY, spine2WorldPosition, glm::vec4(1)); - if (_goToPending) { setWorldPosition(_goToPosition); setWorldOrientation(_goToOrientation); @@ -949,8 +941,6 @@ glm::vec2 MyAvatar::computeHandAzimuth() const { if (!(spine2Index < 0)) { // use the spine for the azimuth origin. azimuthOrigin = getAbsoluteJointTranslationInObjectFrame(spine2Index); - } else { - // use the avatar root as the azimuth origin. } controller::Pose leftHandPoseAvatarSpace = getLeftHandPose(); @@ -3882,6 +3872,10 @@ bool MyAvatar::getIsInSittingState() const { return _isInSittingState.get(); } +bool MyAvatar::getIsSitStandStateLocked() const { + return _lockSitStandState.get(); +} + float MyAvatar::getWalkSpeed() const { return _walkSpeed.get() * _walkSpeedScalar; } @@ -3907,6 +3901,11 @@ void MyAvatar::setIsInSittingState(bool isSitting) { emit sittingEnabledChanged(isSitting); } +void MyAvatar::setIsSitStandStateLocked(bool isLocked) { + _lockSitStandState.set(isLocked); + emit sitStandStateLockEnabledChanged(isLocked); +} + void MyAvatar::setWalkSpeed(float value) { _walkSpeed.set(value); } @@ -4152,7 +4151,7 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); bool returnValue = false; - returnValue = (offset.y > CYLINDER_TOP);// || (offset.y < CYLINDER_BOTTOM); + returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); if (myAvatar.getSitStandStateChange()) { qCDebug(interfaceapp) << "sit state change"; @@ -4206,7 +4205,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } } } - + if (_velocityCount > 60) { if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Vertical); @@ -4216,7 +4215,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat _velocityCount++; } } - + } else { if (!isActive(Rotation) && getForceActivateRotation()) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index f026f39493..674d4b8b70 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -244,6 +244,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(float walkBackwardSpeed READ getWalkBackwardSpeed WRITE setWalkBackwardSpeed); Q_PROPERTY(float sprintSpeed READ getSprintSpeed WRITE setSprintSpeed); Q_PROPERTY(bool isInSittingState READ getIsInSittingState WRITE setIsInSittingState); + Q_PROPERTY(bool isSitStandStateLocked READ getIsSitStandStateLocked WRITE setIsSitStandStateLocked); const QString DOMINANT_LEFT_HAND = "left"; const QString DOMINANT_RIGHT_HAND = "right"; @@ -1105,6 +1106,8 @@ public: bool getIsInWalkingState() const; void setIsInSittingState(bool isSitting); bool getIsInSittingState() const; + void setIsSitStandStateLocked(bool isLocked); + bool getIsSitStandStateLocked() const; void setWalkSpeed(float value); float getWalkSpeed() const; void setWalkBackwardSpeed(float value); @@ -1526,6 +1529,14 @@ signals: */ void sittingEnabledChanged(bool enabled); + /**jsdoc + * Triggered when the sit state is enabled or disabled + * @function MyAvatar.sitStandStateLockEnabledChanged + * @param {boolean} enabled + * @returns {Signal} + */ + void sitStandStateLockEnabledChanged(bool enabled); + private slots: void leaveDomain(); void updateCollisionCapsuleCache(); @@ -1824,7 +1835,7 @@ private: float _sumUserHeightSensorSpace{ DEFAULT_AVATAR_HEIGHT }; int _averageUserHeightCount{ 1 }; bool _sitStandStateChange{ false }; - bool _lockSitStandState { true }; + ThreadSafeValueCache _lockSitStandState { true }; // max unscaled forward movement speed ThreadSafeValueCache _walkSpeed { DEFAULT_AVATAR_MAX_WALKING_SPEED }; diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index ce8fefa0c5..78c5c03cc9 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -239,7 +239,6 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { params.primaryControllerFlags[Rig::PrimaryControllerType_Hips] = (uint8_t)Rig::ControllerFlags::Enabled | (uint8_t)Rig::ControllerFlags::Estimated; // set spine2 if we have hand controllers - // myAvatar->getHMDLeanRecenterEnabled() && if (myAvatar->getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid() && myAvatar->getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && !(params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] & (uint8_t)Rig::ControllerFlags::Enabled)) { @@ -251,7 +250,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { bool headExists = _rig.getAbsoluteJointPoseInRigFrame(_rig.indexOfJoint("Head"), currentHeadPose); bool hipsExists = _rig.getAbsoluteJointPoseInRigFrame(_rig.indexOfJoint("Hips"), currentHipsPose); if (spine2Exists && headExists && hipsExists) { - // qCDebug(interfaceapp) << "hips forward direction "<< (currentHipsPose.rot() * glm::vec3(0.0f, 0.0f, 1.0f)); + AnimPose rigSpaceYaw(myAvatar->getSpine2RotationRigSpace()); glm::vec3 u, v, w; glm::vec3 fwd = rigSpaceYaw.rot() * glm::vec3(0.0f, 0.0f, 1.0f); diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index faf624392a..4a25ab9551 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -64,7 +64,8 @@ function getMyAvatarSettings() { return { dominantHand: MyAvatar.getDominantHand(), collisionsEnabled : MyAvatar.getCollisionsEnabled(), - sittingEnabled : MyAvatar.isInSittingState, + sittingEnabled: MyAvatar.isInSittingState, + lockStateEnabled: MyAvatar.isSitStandStateLocked, collisionSoundUrl : MyAvatar.collisionSoundURL, animGraphUrl: MyAvatar.getAnimGraphUrl(), animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(), @@ -145,6 +146,14 @@ function onSittingEnabledChanged(isSitting) { } } +function onSitStandStateLockedEnabledChanged(isLocked) { + if (currentAvatarSettings.lockStateEnabled !== isLocked) { + currentAvatarSettings.lockStateEnabled = isLocked; + print("emit lock sit stand state changed"); + sendToQml({ 'method': 'settingChanged', 'name': 'lockStateEnabled', 'value': isLocked }) + } +} + function onNewCollisionSoundUrl(url) { if(currentAvatarSettings.collisionSoundUrl !== url) { currentAvatarSettings.collisionSoundUrl = url; @@ -324,6 +333,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See MyAvatar.setDominantHand(message.settings.dominantHand); MyAvatar.setCollisionsEnabled(message.settings.collisionsEnabled); MyAvatar.isInSittingState = message.settings.sittingEnabled; + MyAvatar.isSitStandStateLocked = message.settings.lockStateEnabled; MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl); @@ -518,6 +528,7 @@ function off() { MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); MyAvatar.collisionsEnabledChanged.disconnect(onCollisionsEnabledChanged); MyAvatar.sittingEnabledChanged.disconnect(onSittingEnabledChanged); + MyAvatar.sitStandStateLockEnabledChanged.disconnect(onSitStandStateLockedEnabledChanged); MyAvatar.newCollisionSoundURL.disconnect(onNewCollisionSoundUrl); MyAvatar.animGraphUrlChanged.disconnect(onAnimGraphUrlChanged); MyAvatar.targetScaleChanged.disconnect(onTargetScaleChanged); @@ -533,6 +544,7 @@ function on() { MyAvatar.dominantHandChanged.connect(onDominantHandChanged); MyAvatar.collisionsEnabledChanged.connect(onCollisionsEnabledChanged); MyAvatar.sittingEnabledChanged.connect(onSittingEnabledChanged); + MyAvatar.sitStandStateLockEnabledChanged.connect(onSitStandStateLockedEnabledChanged); MyAvatar.newCollisionSoundURL.connect(onNewCollisionSoundUrl); MyAvatar.animGraphUrlChanged.connect(onAnimGraphUrlChanged); MyAvatar.targetScaleChanged.connect(onTargetScaleChanged); From 4883a60afc9326c016ef37f617d70c2bca27a13a Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 11 Oct 2018 18:55:14 -0700 Subject: [PATCH 092/362] entity list type filter WIP - still some style issues --- scripts/system/html/css/edit-style.css | 46 +++++++++-- scripts/system/html/entityList.html | 20 +++-- scripts/system/html/js/entityList.js | 109 +++++++++++++++++++------ 3 files changed, 136 insertions(+), 39 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 6c1931932a..fc055174fb 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1031,30 +1031,60 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { position: relative; /* New positioning context. */ } -#search-area { +#filter-area { padding-right: 168px; padding-bottom: 24px; } -#filter { - width: 98%; +#filter-type-multiselect { + position: relative; +} +#filter-type-selectBox { + position: absolute; +} +#filter-type-selectBox select { + font-weight: bold; + font-family: FiraSans-SemiBold; + color: #404040; + background-color: #afafaf; +} +#filter-type-checkboxes { + position: absolute; + top: 20px; + display: none; + border: 1px #dadada solid; +} +#filter-type-checkboxes label { + display: block; + font-family: FiraSans-SemiBold; + color: #404040; + background-color: #afafaf; +} +#filter-type-checkboxes label:hover { + background-color: #1e90ff; } -#in-view { +#filter-search-and-icon { + position: absolute; + left: 100px; + width: 60%; +} + +#filter-in-view { position: absolute; right: 126px; } -#radius-and-unit { +#filter-radius-and-unit { + position: relative; float: right; margin-right: -168px; - position: relative; top: -17px; } -#radius-and-unit label { +#filter-radius-and-unit label { margin-left: 2px; } -#radius-and-unit input { +#filter-radius-and-unit input { width: 120px; } diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 7eed78ecf3..8bcdc37d64 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -30,12 +30,22 @@
-
- Y - -
+
+
+
+ +
+
+ +
+
+
+ Y +
+ +
- +
diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index fed4dfb632..6b23d703ee 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -59,6 +59,18 @@ const COMPARE_DESCENDING = function(a, b) { return COMPARE_ASCENDING(b, a); } +const FILTER_TYPES = [ + "Shape", + "Model", + "Image", + "Light", + "Zone", + "Web", + "Material", + "ParticleEffect", + "Text", +]; + // List of all entities var entities = [] // List of all entities, indexed by Entity ID @@ -72,6 +84,7 @@ var entityList = null; // The ListView var currentSortColumn = 'type'; var currentSortOrder = ASCENDING_SORT; +var typeFilters = []; var isFilterInView = false; var showExtraInfo = false; @@ -105,9 +118,11 @@ function loaded() { elToggleLocked = document.getElementById("locked"); elToggleVisible = document.getElementById("visible"); elDelete = document.getElementById("delete"); - elFilter = document.getElementById("filter"); - elInView = document.getElementById("in-view") - elRadius = document.getElementById("radius"); + elFilterTypeSelectBox = document.getElementById("filter-type-selectBox"); + elFilterTypeCheckboxes = document.getElementById("filter-type-checkboxes"); + elFilterSearch = document.getElementById("filter-search"); + elFilterInView = document.getElementById("filter-in-view") + elFilterRadius = document.getElementById("filter-radius"); elExport = document.getElementById("export"); elPal = document.getElementById("pal"); elInfoToggle = document.getElementById("info-toggle"); @@ -171,13 +186,33 @@ function loaded() { elDelete.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); } - elFilter.onkeyup = refreshEntityList; - elFilter.onpaste = refreshEntityList; - elFilter.onchange = onFilterChange; - elFilter.onblur = refreshFooter; - elInView.onclick = toggleFilterInView; - elRadius.onchange = onRadiusChange; + elFilterSearch.onkeyup = refreshEntityList; + elFilterSearch.onpaste = refreshEntityList; + elFilterSearch.onchange = onFilterChange; + elFilterSearch.onblur = refreshFooter; + elFilterInView.onclick = toggleFilterInView; + elFilterRadius.onchange = onRadiusChange; elInfoToggle.onclick = toggleInfo; + + // create filter type dropdown checkboxes w/ label for each type + elFilterTypeSelectBox.onclick = toggleTypeDropdown; + for (let i = 0; i < FILTER_TYPES.length; ++i) { + let type = FILTER_TYPES[i]; + let typeFilterID = "filter-type-" + type; + let elDiv = document.createElement('div'); + let elLabel = document.createElement('label'); + elLabel.setAttribute("for", typeFilterID); + elLabel.innerText = type; + let elInput = document.createElement('input'); + elInput.setAttribute("type", "checkbox"); + elInput.setAttribute("id", typeFilterID); + elInput.checked = true; // all types are checked initially + toggleTypeFilter(type, false); // add all types to the initial type filter + elInput.onclick = onToggleTypeFilter(type); + elDiv.appendChild(elInput); + elDiv.appendChild(elLabel); + elFilterTypeCheckboxes.appendChild(elDiv); + } elNoEntitiesInView.style.display = "none"; @@ -301,17 +336,16 @@ function loaded() { function refreshEntityList() { PROFILE("refresh-entity-list", function() { PROFILE("filter", function() { - let searchTerm = elFilter.value.toLowerCase(); - if (searchTerm === '') { - visibleEntities = entities.slice(0); - } else { - visibleEntities = entities.filter(function(e) { - return e.name.toLowerCase().indexOf(searchTerm) > -1 - || e.type.toLowerCase().indexOf(searchTerm) > -1 - || e.fullUrl.toLowerCase().indexOf(searchTerm) > -1 - || e.id.toLowerCase().indexOf(searchTerm) > -1; - }); - } + let searchTerm = elFilterSearch.value.toLowerCase(); + visibleEntities = entities.filter(function(e) { + let type = e.type === "Box" || e.type === "Sphere" ? "Shape" : e.type; + let typeFilter = typeFilters.indexOf(type) > -1; + let searchFilter = searchTerm === '' || (e.name.toLowerCase().indexOf(searchTerm) > -1 || + e.type.toLowerCase().indexOf(searchTerm) > -1 || + e.fullUrl.toLowerCase().indexOf(searchTerm) > -1 || + e.id.toLowerCase().indexOf(searchTerm) > -1); + return typeFilter && searchFilter; + }); }); PROFILE("sort", function() { @@ -588,10 +622,10 @@ function loaded() { function toggleFilterInView() { isFilterInView = !isFilterInView; if (isFilterInView) { - elInView.setAttribute(FILTER_IN_VIEW_ATTRIBUTE, FILTER_IN_VIEW_ATTRIBUTE); + elFilterInView.setAttribute(FILTER_IN_VIEW_ATTRIBUTE, FILTER_IN_VIEW_ATTRIBUTE); elNoEntitiesInView.style.display = "inline"; } else { - elInView.removeAttribute(FILTER_IN_VIEW_ATTRIBUTE); + elFilterInView.removeAttribute(FILTER_IN_VIEW_ATTRIBUTE); elNoEntitiesInView.style.display = "none"; } EventBridge.emitWebEvent(JSON.stringify({ type: "filterInView", filterInView: isFilterInView })); @@ -604,12 +638,34 @@ function loaded() { } function onRadiusChange() { - elRadius.value = Math.max(elRadius.value, 0); - elNoEntitiesRadius.firstChild.nodeValue = elRadius.value; + elFilterRadius.value = Math.max(elFilterRadius.value, 0); + elNoEntitiesRadius.firstChild.nodeValue = elFilterRadius.value; elNoEntitiesMessage.style.display = "none"; - EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elRadius.value })); + EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elFilterRadius.value })); refreshEntities(); } + + function toggleTypeDropdown() { + elFilterTypeCheckboxes.style.display = elFilterTypeCheckboxes.style.display === "block" ? "none" : "block"; + } + + function toggleTypeFilter(type, refresh) { + let typeFilterIndex = typeFilters.indexOf(type); + if (typeFilterIndex > -1) { + typeFilters.splice(typeFilterIndex, 1); + } else { + typeFilters.push(type); + } + if (refresh) { + refreshEntityList(); + } + } + + function onToggleTypeFilter(type) { + return function() { + toggleTypeFilter(type, true); + }; + } function toggleInfo(event) { showExtraInfo = !showExtraInfo; @@ -623,7 +679,7 @@ function loaded() { entityList.resize(); event.stopPropagation(); } - + document.addEventListener("keydown", function (keyDownEvent) { if (keyDownEvent.target.nodeName === "INPUT") { return; @@ -675,6 +731,7 @@ function loaded() { refreshSortOrder(); refreshEntities(); }); + augmentSpinButtons(); From 97438a08d931da46505830deaf6529943983ac9d Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 11 Oct 2018 23:04:33 -0700 Subject: [PATCH 093/362] styling fixes, add icon, adjust Types text, click outside to close --- scripts/system/html/css/edit-style.css | 80 ++++++++---- scripts/system/html/entityList.html | 7 +- scripts/system/html/js/entityList.js | 168 +++++++++++++++---------- 3 files changed, 165 insertions(+), 90 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index fc055174fb..da0d806a41 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1036,38 +1036,74 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { padding-bottom: 24px; } -#filter-type-multiselect { - position: relative; +#filter-area .multiselect { + position: relative; } -#filter-type-selectBox { - position: absolute; +#filter-area .selectBox { + position: absolute; } -#filter-type-selectBox select { - font-weight: bold; - font-family: FiraSans-SemiBold; - color: #404040; - background-color: #afafaf; +#filter-area .selectBox select { + font-family: FiraSans-SemiBold; + font-size: 15px; + color: #afafaf; + background-color: #252525; + border: none; + height: 28px; + width: 107px; + text-align-last: center; +} +#filter-area .overSelect { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; } #filter-type-checkboxes { - position: absolute; - top: 20px; - display: none; - border: 1px #dadada solid; + position: absolute; + z-index: 2; + top: 15px; + display: none; + border: none; +} +#filter-type-checkboxes div { + height: 19px; +} +#filter-type-checkboxes span { + font-family: hifi-glyphs; + font-size: 16px; + color: #404040; + padding: 12px 12px; + vertical-align: middle; } #filter-type-checkboxes label { - display: block; - font-family: FiraSans-SemiBold; - color: #404040; - background-color: #afafaf; + display: block; + font-family: FiraSans-SemiBold; + color: #404040; + background-color: #afafaf; + padding: 0 20px; + vertical-align: middle; } #filter-type-checkboxes label:hover { - background-color: #1e90ff; + background-color: #1e90ff; +} +#filter-type-checkboxes input[type=checkbox] { + display: block; + position: relative; + top: 17px; + right: -10px; +} +#filter-type-checkboxes input[type=checkbox] + label { + background-image: none; +} +#filter-type-checkboxes input[type=checkbox]:checked + label { + background-image: none; } #filter-search-and-icon { - position: absolute; - left: 100px; - width: 60%; + position: absolute; + left: 120px; + width: calc(100% - 300px); } #filter-in-view { @@ -1076,7 +1112,7 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { } #filter-radius-and-unit { - position: relative; + position: relative; float: right; margin-right: -168px; top: -17px; diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 8bcdc37d64..e301f36945 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -31,9 +31,12 @@
-
+
- + +
diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 6b23d703ee..8812594408 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -60,17 +60,29 @@ const COMPARE_DESCENDING = function(a, b) { } const FILTER_TYPES = [ - "Shape", - "Model", - "Image", - "Light", - "Zone", - "Web", - "Material", - "ParticleEffect", - "Text", + "Shape", + "Model", + "Image", + "Light", + "Zone", + "Web", + "Material", + "ParticleEffect", + "Text", ]; +const ICON_FOR_TYPE = { + Shape: "n", + Model: "", + Image: "", + Light: "p", + Zone: "o", + Web: "q", + Material: "", + ParticleEffect: "", + Text: "l", +}; + // List of all entities var entities = [] // List of all entities, indexed by Entity ID @@ -118,8 +130,9 @@ function loaded() { elToggleLocked = document.getElementById("locked"); elToggleVisible = document.getElementById("visible"); elDelete = document.getElementById("delete"); - elFilterTypeSelectBox = document.getElementById("filter-type-selectBox"); - elFilterTypeCheckboxes = document.getElementById("filter-type-checkboxes"); + elFilterTypeSelectBox = document.getElementById("filter-type-selectBox"); + elFilterTypeText = document.getElementById("filter-type-text"); + elFilterTypeCheckboxes = document.getElementById("filter-type-checkboxes"); elFilterSearch = document.getElementById("filter-search"); elFilterInView = document.getElementById("filter-in-view") elFilterRadius = document.getElementById("filter-radius"); @@ -132,6 +145,8 @@ function loaded() { elNoEntitiesInView = document.getElementById("no-entities-in-view"); elNoEntitiesRadius = document.getElementById("no-entities-radius"); + document.body.onclick = onBodyClick; + document.getElementById("entity-name").onclick = function() { setSortColumn('name'); }; @@ -193,26 +208,30 @@ function loaded() { elFilterInView.onclick = toggleFilterInView; elFilterRadius.onchange = onRadiusChange; elInfoToggle.onclick = toggleInfo; - - // create filter type dropdown checkboxes w/ label for each type - elFilterTypeSelectBox.onclick = toggleTypeDropdown; - for (let i = 0; i < FILTER_TYPES.length; ++i) { - let type = FILTER_TYPES[i]; - let typeFilterID = "filter-type-" + type; - let elDiv = document.createElement('div'); - let elLabel = document.createElement('label'); - elLabel.setAttribute("for", typeFilterID); - elLabel.innerText = type; - let elInput = document.createElement('input'); - elInput.setAttribute("type", "checkbox"); - elInput.setAttribute("id", typeFilterID); - elInput.checked = true; // all types are checked initially - toggleTypeFilter(type, false); // add all types to the initial type filter - elInput.onclick = onToggleTypeFilter(type); - elDiv.appendChild(elInput); - elDiv.appendChild(elLabel); - elFilterTypeCheckboxes.appendChild(elDiv); - } + + // create filter type dropdown checkboxes w/ label for each type + elFilterTypeSelectBox.onclick = toggleTypeDropdown; + for (let i = 0; i < FILTER_TYPES.length; ++i) { + let type = FILTER_TYPES[i]; + let typeFilterID = "filter-type-" + type; + let elDiv = document.createElement('div'); + let elLabel = document.createElement('label'); + elLabel.setAttribute("for", typeFilterID); + elLabel.innerText = type; + let elSpan = document.createElement('span'); + elSpan.setAttribute("class", "typeIcon"); + elSpan.innerHTML = ICON_FOR_TYPE[type]; + let elInput = document.createElement('input'); + elInput.setAttribute("type", "checkbox"); + elInput.setAttribute("id", typeFilterID); + elInput.checked = true; // all types are checked initially + toggleTypeFilter(type, false); // add all types to the initial type filter + elInput.onclick = onToggleTypeFilter(type); + elDiv.appendChild(elInput); + elLabel.insertBefore(elSpan, elLabel.childNodes[0]); + elDiv.appendChild(elLabel); + elFilterTypeCheckboxes.appendChild(elDiv); + } elNoEntitiesInView.style.display = "none"; @@ -336,16 +355,16 @@ function loaded() { function refreshEntityList() { PROFILE("refresh-entity-list", function() { PROFILE("filter", function() { - let searchTerm = elFilterSearch.value.toLowerCase(); - visibleEntities = entities.filter(function(e) { - let type = e.type === "Box" || e.type === "Sphere" ? "Shape" : e.type; - let typeFilter = typeFilters.indexOf(type) > -1; - let searchFilter = searchTerm === '' || (e.name.toLowerCase().indexOf(searchTerm) > -1 || - e.type.toLowerCase().indexOf(searchTerm) > -1 || - e.fullUrl.toLowerCase().indexOf(searchTerm) > -1 || - e.id.toLowerCase().indexOf(searchTerm) > -1); - return typeFilter && searchFilter; - }); + let searchTerm = elFilterSearch.value.toLowerCase(); + visibleEntities = entities.filter(function(e) { + let type = e.type === "Box" || e.type === "Sphere" ? "Shape" : e.type; + let typeFilter = typeFilters.indexOf(type) > -1; + let searchFilter = searchTerm === '' || (e.name.toLowerCase().indexOf(searchTerm) > -1 || + e.type.toLowerCase().indexOf(searchTerm) > -1 || + e.fullUrl.toLowerCase().indexOf(searchTerm) > -1 || + e.id.toLowerCase().indexOf(searchTerm) > -1); + return typeFilter && searchFilter; + }); }); PROFILE("sort", function() { @@ -644,29 +663,38 @@ function loaded() { EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elFilterRadius.value })); refreshEntities(); } - - function toggleTypeDropdown() { - elFilterTypeCheckboxes.style.display = elFilterTypeCheckboxes.style.display === "block" ? "none" : "block"; - } - - function toggleTypeFilter(type, refresh) { - let typeFilterIndex = typeFilters.indexOf(type); - if (typeFilterIndex > -1) { - typeFilters.splice(typeFilterIndex, 1); - } else { - typeFilters.push(type); - } - if (refresh) { - refreshEntityList(); - } - } - - function onToggleTypeFilter(type) { - return function() { - toggleTypeFilter(type, true); - }; - } - + + function toggleTypeDropdown() { + elFilterTypeCheckboxes.style.display = elFilterTypeCheckboxes.style.display === "block" ? "none" : "block"; + } + + function toggleTypeFilter(type, refresh) { + let typeFilterIndex = typeFilters.indexOf(type); + if (typeFilterIndex > -1) { + typeFilters.splice(typeFilterIndex, 1); + } else { + typeFilters.push(type); + } + + if (typeFilters.length === 0) { + elFilterTypeText.innerText = "No Types"; + } else if (typeFilters.length === FILTER_TYPES.length) { + elFilterTypeText.innerText = "All Types"; + } else { + elFilterTypeText.innerText = "Types..."; + } + + if (refresh) { + refreshEntityList(); + } + } + + function onToggleTypeFilter(type) { + return function() { + toggleTypeFilter(type, true); + }; + } + function toggleInfo(event) { showExtraInfo = !showExtraInfo; if (showExtraInfo) { @@ -679,7 +707,15 @@ function loaded() { entityList.resize(); event.stopPropagation(); } - + + function onBodyClick(event) { + let targetNode = event.target; + if (!elFilterTypeSelectBox.contains(targetNode) && !elFilterTypeCheckboxes.contains(targetNode) && + elFilterTypeCheckboxes.style.display === "block") { + toggleTypeDropdown(); + } + } + document.addEventListener("keydown", function (keyDownEvent) { if (keyDownEvent.target.nodeName === "INPUT") { return; @@ -731,7 +767,7 @@ function loaded() { refreshSortOrder(); refreshEntities(); }); - + augmentSpinButtons(); From 53226e7924d109be6a1a763da0793d721bbe32be Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 12 Oct 2018 11:19:52 -0700 Subject: [PATCH 094/362] Prevent overflows; still not working --- interface/src/scripting/TTSScriptingInterface.h | 2 +- libraries/audio-client/src/AudioClient.cpp | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/interface/src/scripting/TTSScriptingInterface.h b/interface/src/scripting/TTSScriptingInterface.h index cb9c6c8c3e..c1fffe67d1 100644 --- a/interface/src/scripting/TTSScriptingInterface.h +++ b/interface/src/scripting/TTSScriptingInterface.h @@ -77,7 +77,7 @@ private: CComPtr m_voiceToken; QByteArray _lastSoundByteArray; - AudioInjectorPointer _lastSoundByteArray; + AudioInjectorPointer _lastSoundAudioInjector; }; #endif // hifi_SpeechScriptingInterface_h diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 96f1c97878..12da7ea3be 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1167,7 +1167,7 @@ void AudioClient::processAudioAndAddToRingBuffer(QByteArray& inputByteArray, con numNetworkSamples, channelCount, _desiredInputFormat.channelCount()); } int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE; - float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); + float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(bytesForDuration); _stats.updateInputMsUnplayed(msecsInInputRingBuffer); QByteArray audioBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); @@ -1204,7 +1204,12 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { void AudioClient::handleTTSAudioInput(const QByteArray& audio) { QByteArray audioBuffer(audio); - processAudioAndAddToRingBuffer(audioBuffer, 1, 48); + while (audioBuffer.size() > 0) { + QByteArray part; + part.append(audioBuffer.data(), AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + audioBuffer.remove(0, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + processAudioAndAddToRingBuffer(part, 1, 48); + } } void AudioClient::prepareLocalAudioInjectors(std::unique_ptr localAudioLock) { From 52ed6cb6e95299c578b2c3803ed42959c192d040 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 12 Oct 2018 11:35:51 -0700 Subject: [PATCH 095/362] Abort settle animation if already moving --- interface/src/avatar/AvatarManager.cpp | 6 +++++- .../avatars-renderer/src/avatars-renderer/Avatar.cpp | 8 +++++++- libraries/avatars-renderer/src/avatars-renderer/Avatar.h | 2 ++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 80e4e4ba66..5fb9e0e0af 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -168,6 +168,10 @@ void AvatarManager::playTransitAnimations(AvatarTransit::Status status) { qDebug() << "END_FRAME"; _myAvatar->restoreAnimation(); break; + case AvatarTransit::Status::PRE_TRANSIT_IDLE: + break; + case AvatarTransit::Status::POST_TRANSIT_IDLE: + break; case AvatarTransit::Status::IDLE: break; case AvatarTransit::Status::TRANSITING: @@ -180,7 +184,7 @@ void AvatarManager::updateMyAvatar(float deltaTime) { PerformanceWarning warn(showWarnings, "AvatarManager::updateMyAvatar()"); AvatarTransit::Status status = _myAvatar->updateTransit(deltaTime, _myAvatar->getNextPosition(), _transitConfig); - if (status != AvatarTransit::Status::IDLE) { + if (status != AvatarTransit::Status::IDLE && status != AvatarTransit::Status::PRE_TRANSIT_IDLE && status != AvatarTransit::Status::POST_TRANSIT_IDLE) { playTransitAnimations(status); } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 53c2019138..d8587b6086 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -127,6 +127,10 @@ AvatarTransit::Status AvatarTransit::update(float deltaTime, const glm::vec3& av } _lastPosition = avatarPosition; _status = updatePosition(deltaTime); + if (_isTransiting && oneFrameDistance > 0.1f && _status == Status::POST_TRANSIT_IDLE) { + reset(); + _status = Status::END_FRAME; + } return _status; } @@ -177,18 +181,20 @@ AvatarTransit::Status AvatarTransit::updatePosition(float deltaTime) { float nextTime = _currentTime + deltaTime; if (nextTime < _preTime) { _currentPosition = _startPosition; + status = Status::PRE_TRANSIT_IDLE; if (_currentTime == 0) { status = Status::START_FRAME; } } else if (nextTime < _totalTime - _postTime){ + status = Status::TRANSITING; if (_currentTime <= _preTime) { status = Status::START_TRANSIT; } else { float percentageIntoTransit = (nextTime - _preTime) / _transitTime; _currentPosition = _startPosition + getEaseValue(_easeType, percentageIntoTransit) * _transitLine; - status = Status::TRANSITING; } } else { + status = Status::POST_TRANSIT_IDLE; _currentPosition = _endPosition; if (nextTime >= _totalTime) { _isTransiting = false; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index ffe90ecaa2..fe163b9dae 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -55,9 +55,11 @@ public: enum Status { IDLE = 0, START_FRAME, + PRE_TRANSIT_IDLE, START_TRANSIT, TRANSITING, END_TRANSIT, + POST_TRANSIT_IDLE, END_FRAME, ABORT_TRANSIT }; From 6320f9bab7f6a34fde523e9544c75dbd83b0919e Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 12 Oct 2018 11:39:54 -0700 Subject: [PATCH 096/362] tweaks --- scripts/system/html/css/edit-style.css | 26 ++++++++++++++++-------- scripts/system/html/js/entityList.js | 28 +++++++++++++++----------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index da0d806a41..8179b95e35 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1036,13 +1036,13 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { padding-bottom: 24px; } -#filter-area .multiselect { +.multiselect { position: relative; } -#filter-area .selectBox { +.selectBox { position: absolute; } -#filter-area .selectBox select { +.selectBox select { font-family: FiraSans-SemiBold; font-size: 15px; color: #afafaf; @@ -1052,22 +1052,24 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { width: 107px; text-align-last: center; } -#filter-area .overSelect { +.overSelect { position: absolute; left: 0; right: 0; top: 0; bottom: 0; } + #filter-type-checkboxes { position: absolute; z-index: 2; - top: 15px; + top: 28px; display: none; border: none; } #filter-type-checkboxes div { - height: 19px; + position: relative; + height: 25px; } #filter-type-checkboxes span { font-family: hifi-glyphs; @@ -1077,21 +1079,26 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { vertical-align: middle; } #filter-type-checkboxes label { + position: relative; + top: -13px; + z-index: 3; display: block; font-family: FiraSans-SemiBold; color: #404040; background-color: #afafaf; padding: 0 20px; + height: 25px; vertical-align: middle; } #filter-type-checkboxes label:hover { background-color: #1e90ff; } #filter-type-checkboxes input[type=checkbox] { - display: block; position: relative; - top: 17px; + top: 4px; right: -10px; + z-index: 4; + display: block; } #filter-type-checkboxes input[type=checkbox] + label { background-image: none; @@ -1099,6 +1106,9 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { #filter-type-checkboxes input[type=checkbox]:checked + label { background-image: none; } +#filter-type-checkboxes input[type=checkbox]:hover + label { + background-color: #1e90ff; +} #filter-search-and-icon { position: absolute; diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 8812594408..f780fd0e2e 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -146,7 +146,6 @@ function loaded() { elNoEntitiesRadius = document.getElementById("no-entities-radius"); document.body.onclick = onBodyClick; - document.getElementById("entity-name").onclick = function() { setSortColumn('name'); }; @@ -209,7 +208,7 @@ function loaded() { elFilterRadius.onchange = onRadiusChange; elInfoToggle.onclick = toggleInfo; - // create filter type dropdown checkboxes w/ label for each type + // create filter type dropdown checkboxes with label and icon for each type elFilterTypeSelectBox.onclick = toggleTypeDropdown; for (let i = 0; i < FILTER_TYPES.length; ++i) { let type = FILTER_TYPES[i]; @@ -225,7 +224,7 @@ function loaded() { elInput.setAttribute("type", "checkbox"); elInput.setAttribute("id", typeFilterID); elInput.checked = true; // all types are checked initially - toggleTypeFilter(type, false); // add all types to the initial type filter + toggleTypeFilter(type, false); // add all types to the initial types filter elInput.onclick = onToggleTypeFilter(type); elDiv.appendChild(elInput); elLabel.insertBefore(elSpan, elLabel.childNodes[0]); @@ -664,8 +663,12 @@ function loaded() { refreshEntities(); } + function isTypeDropdownVisible() { + return elFilterTypeCheckboxes.style.display === "block"; + } + function toggleTypeDropdown() { - elFilterTypeCheckboxes.style.display = elFilterTypeCheckboxes.style.display === "block" ? "none" : "block"; + elFilterTypeCheckboxes.style.display = isTypeDropdownVisible() ? "none" : "block"; } function toggleTypeFilter(type, refresh) { @@ -695,6 +698,15 @@ function loaded() { }; } + function onBodyClick(event) { + // if clicking anywhere outside of the type filter dropdown and it's open then close it + let elTarget = event.target; + if (isTypeDropdownVisible() && !elFilterTypeSelectBox.contains(elTarget) && + !elFilterTypeCheckboxes.contains(elTarget)) { + toggleTypeDropdown(); + } + } + function toggleInfo(event) { showExtraInfo = !showExtraInfo; if (showExtraInfo) { @@ -707,14 +719,6 @@ function loaded() { entityList.resize(); event.stopPropagation(); } - - function onBodyClick(event) { - let targetNode = event.target; - if (!elFilterTypeSelectBox.contains(targetNode) && !elFilterTypeCheckboxes.contains(targetNode) && - elFilterTypeCheckboxes.style.display === "block") { - toggleTypeDropdown(); - } - } document.addEventListener("keydown", function (keyDownEvent) { if (keyDownEvent.target.nodeName === "INPUT") { From d7dc69129aed97cb6808101e504d92d5a478e6e7 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Fri, 12 Oct 2018 11:43:03 -0700 Subject: [PATCH 097/362] improve loading bar progress --- .../resources/sounds/crystals_and_voices.mp3 | Bin 0 -> 337547 bytes scripts/system/interstitialPage.js | 23 +++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 interface/resources/sounds/crystals_and_voices.mp3 diff --git a/interface/resources/sounds/crystals_and_voices.mp3 b/interface/resources/sounds/crystals_and_voices.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..b5bee31381639f66aaf6c2ba0724ea7fa9620268 GIT binary patch literal 337547 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%0D!?6J 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 =4U{m+sJ@}sfm+aFA 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

Qd%Ka2Apmb~P_L>@7Kk>gbHUQ;C^H}3_k<9&MH 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 (TOTAL_LOADING_PROGRESS * 0.4)) { + if (currentProgress >= ((TOTAL_LOADING_PROGRESS * 0.4) - TEXTURE_EPSILON)) { var textureResourceGPUMemSize = renderStats.textureResourceGPUMemSize; var texturePopulatedGPUMemSize = renderStats.textureResourcePopulatedGPUMemSize; @@ -472,10 +474,9 @@ textureMemSizeAtLastCheck = textureResourceGPUMemSize; - if (textureMemSizeStabilityCount >= 20) { + if (textureMemSizeStabilityCount >= 30) { if (textureResourceGPUMemSize > 0) { - // print((texturePopulatedGPUMemSize / textureResourceGPUMemSize)); var gpuPercantage = (TOTAL_LOADING_PROGRESS * 0.6) * (texturePopulatedGPUMemSize / textureResourceGPUMemSize); var totalProgress = progress + gpuPercantage; if (totalProgress >= target) { @@ -489,7 +490,11 @@ target = TOTAL_LOADING_PROGRESS; } - currentProgress = lerp(currentProgress, target, 0.2); + if (deltaTime > 1.0) { + deltaTimeMS = 0.02; + } + + currentProgress = lerp(currentProgress, target, (deltaTimeMS * 2.0)); var properties = { localPosition: { x: (1.85 - (currentProgress / 2) - (-0.029 * (currentProgress / TOTAL_LOADING_PROGRESS))), y: -0.935, z: 0.0 }, dimensions: { From fa6b3019f6768ec3901e1e74dd5ea2972a3ff2e8 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Fri, 12 Oct 2018 12:07:39 -0700 Subject: [PATCH 098/362] fix js memory leak --- .../controllers/controllerModules/webSurfaceLaserInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index 898164dc99..229f1c411f 100644 --- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js +++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -109,7 +109,7 @@ Script.include("/~/system/libraries/controllers.js"); Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)); } - this.ignoredOverlays = []; + this.ignoredObjects = []; }; this.isPointingAtTriggerable = function(controllerData, triggerPressed, checkEntitiesOnly) { From 34befd4a52e085bbf548a200e507cc34afe07b3c Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 12 Oct 2018 12:14:51 -0700 Subject: [PATCH 099/362] Just to make sure, writes data back to a WAV file --- libraries/audio-client/src/AudioClient.cpp | 378 ++++++++++----------- 1 file changed, 188 insertions(+), 190 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 12da7ea3be..858f6e738c 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -67,7 +68,7 @@ static const int CHECK_INPUT_READS_MSECS = 2000; static const int MIN_READS_TO_CONSIDER_INPUT_ALIVE = 10; #endif -static const auto DEFAULT_POSITION_GETTER = []{ return Vectors::ZERO; }; +static const auto DEFAULT_POSITION_GETTER = [] { return Vectors::ZERO; }; static const auto DEFAULT_ORIENTATION_GETTER = [] { return Quaternions::IDENTITY; }; static const int DEFAULT_BUFFER_FRAMES = 1; @@ -78,12 +79,11 @@ static const int OUTPUT_CHANNEL_COUNT = 2; static const bool DEFAULT_STARVE_DETECTION_ENABLED = true; static const int STARVE_DETECTION_THRESHOLD = 3; -static const int STARVE_DETECTION_PERIOD = 10 * 1000; // 10 Seconds +static const int STARVE_DETECTION_PERIOD = 10 * 1000; // 10 Seconds Setting::Handle dynamicJitterBufferEnabled("dynamicJitterBuffersEnabled", - InboundAudioStream::DEFAULT_DYNAMIC_JITTER_BUFFER_ENABLED); -Setting::Handle staticJitterBufferFrames("staticJitterBufferFrames", - InboundAudioStream::DEFAULT_STATIC_JITTER_FRAMES); + InboundAudioStream::DEFAULT_DYNAMIC_JITTER_BUFFER_ENABLED); +Setting::Handle staticJitterBufferFrames("staticJitterBufferFrames", InboundAudioStream::DEFAULT_STATIC_JITTER_FRAMES); // protect the Qt internal device list using Mutex = std::mutex; @@ -127,7 +127,7 @@ QAudioDeviceInfo AudioClient::getActiveAudioDevice(QAudio::Mode mode) const { if (mode == QAudio::AudioInput) { return _inputDeviceInfo; - } else { // if (mode == QAudio::AudioOutput) + } else { // if (mode == QAudio::AudioOutput) return _outputDeviceInfo; } } @@ -137,14 +137,13 @@ QList AudioClient::getAudioDevices(QAudio::Mode mode) const { if (mode == QAudio::AudioInput) { return _inputDevices; - } else { // if (mode == QAudio::AudioOutput) + } else { // if (mode == QAudio::AudioOutput) return _outputDevices; } } static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { - for (int i = 0; i < numSamples/2; i++) { - + for (int i = 0; i < numSamples / 2; i++) { // read 2 samples int16_t left = *source++; int16_t right = *source++; @@ -159,8 +158,7 @@ static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int num } static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { - for (int i = 0; i < numSamples/2; i++) { - + for (int i = 0; i < numSamples / 2; i++) { // read 2 samples int16_t left = *source++; int16_t right = *source++; @@ -175,48 +173,22 @@ static inline float convertToFloat(int16_t sample) { } AudioClient::AudioClient() : - AbstractAudioInterface(), - _gate(this), - _audioInput(NULL), - _dummyAudioInput(NULL), - _desiredInputFormat(), - _inputFormat(), - _numInputCallbackBytes(0), - _audioOutput(NULL), - _desiredOutputFormat(), - _outputFormat(), - _outputFrameSize(0), - _numOutputCallbackBytes(0), - _loopbackAudioOutput(NULL), - _loopbackOutputDevice(NULL), - _inputRingBuffer(0), - _localInjectorsStream(0, 1), - _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), - _isStereoInput(false), - _outputStarveDetectionStartTimeMsec(0), - _outputStarveDetectionCount(0), + AbstractAudioInterface(), _gate(this), _audioInput(NULL), _dummyAudioInput(NULL), _desiredInputFormat(), _inputFormat(), + _numInputCallbackBytes(0), _audioOutput(NULL), _desiredOutputFormat(), _outputFormat(), _outputFrameSize(0), + _numOutputCallbackBytes(0), _loopbackAudioOutput(NULL), _loopbackOutputDevice(NULL), _inputRingBuffer(0), + _localInjectorsStream(0, 1), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), _isStereoInput(false), + _outputStarveDetectionStartTimeMsec(0), _outputStarveDetectionCount(0), _outputBufferSizeFrames("audioOutputBufferFrames", DEFAULT_BUFFER_FRAMES), _sessionOutputBufferSizeFrames(_outputBufferSizeFrames.get()), _outputStarveDetectionEnabled("audioOutputStarveDetectionEnabled", DEFAULT_STARVE_DETECTION_ENABLED), - _lastInputLoudness(0.0f), - _timeSinceLastClip(-1.0f), - _muted(false), - _shouldEchoLocally(false), - _shouldEchoToServer(false), - _isNoiseGateEnabled(true), - _reverb(false), - _reverbOptions(&_scriptReverbOptions), - _inputToNetworkResampler(NULL), - _networkToOutputResampler(NULL), - _localToOutputResampler(NULL), - _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), - _outgoingAvatarAudioSequenceNumber(0), - _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), - _stats(&_receivedAudioStream), + _lastInputLoudness(0.0f), _timeSinceLastClip(-1.0f), _muted(false), _shouldEchoLocally(false), _shouldEchoToServer(false), + _isNoiseGateEnabled(true), _reverb(false), _reverbOptions(&_scriptReverbOptions), _inputToNetworkResampler(NULL), + _networkToOutputResampler(NULL), _localToOutputResampler(NULL), + _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), + _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), _stats(&_receivedAudioStream), _positionGetter(DEFAULT_POSITION_GETTER), #if defined(Q_OS_ANDROID) - _checkInputTimer(this), - _isHeadsetPluggedIn(false), + _checkInputTimer(this), _isHeadsetPluggedIn(false), #endif _orientationGetter(DEFAULT_ORIENTATION_GETTER) { // avoid putting a lock in the device callback @@ -226,16 +198,20 @@ AudioClient::AudioClient() : { Setting::Handle::Deprecated("maxFramesOverDesired", InboundAudioStream::MAX_FRAMES_OVER_DESIRED); Setting::Handle::Deprecated("windowStarveThreshold", InboundAudioStream::WINDOW_STARVE_THRESHOLD); - Setting::Handle::Deprecated("windowSecondsForDesiredCalcOnTooManyStarves", InboundAudioStream::WINDOW_SECONDS_FOR_DESIRED_CALC_ON_TOO_MANY_STARVES); - Setting::Handle::Deprecated("windowSecondsForDesiredReduction", InboundAudioStream::WINDOW_SECONDS_FOR_DESIRED_REDUCTION); + Setting::Handle::Deprecated("windowSecondsForDesiredCalcOnTooManyStarves", + InboundAudioStream::WINDOW_SECONDS_FOR_DESIRED_CALC_ON_TOO_MANY_STARVES); + Setting::Handle::Deprecated("windowSecondsForDesiredReduction", + InboundAudioStream::WINDOW_SECONDS_FOR_DESIRED_REDUCTION); Setting::Handle::Deprecated("useStDevForJitterCalc", InboundAudioStream::USE_STDEV_FOR_JITTER); Setting::Handle::Deprecated("repetitionWithFade", InboundAudioStream::REPETITION_WITH_FADE); } - connect(&_receivedAudioStream, &MixedProcessedAudioStream::processSamples, - this, &AudioClient::processReceivedSamples, Qt::DirectConnection); + connect(&_receivedAudioStream, &MixedProcessedAudioStream::processSamples, this, &AudioClient::processReceivedSamples, + Qt::DirectConnection); connect(this, &AudioClient::changeDevice, this, [=](const QAudioDeviceInfo& outputDeviceInfo) { - qCDebug(audioclient) << "got AudioClient::changeDevice signal, about to call switchOutputToAudioDevice() outputDeviceInfo: [" << outputDeviceInfo.deviceName() << "]"; + qCDebug(audioclient) + << "got AudioClient::changeDevice signal, about to call switchOutputToAudioDevice() outputDeviceInfo: [" + << outputDeviceInfo.deviceName() << "]"; switchOutputToAudioDevice(outputDeviceInfo); }); @@ -244,20 +220,18 @@ AudioClient::AudioClient() : // initialize wasapi; if getAvailableDevices is called from the CheckDevicesThread before this, it will crash getAvailableDevices(QAudio::AudioInput); getAvailableDevices(QAudio::AudioOutput); - + // start a thread to detect any device changes _checkDevicesTimer = new QTimer(this); - connect(_checkDevicesTimer, &QTimer::timeout, this, [this] { - QtConcurrent::run(QThreadPool::globalInstance(), [this] { checkDevices(); }); - }); + connect(_checkDevicesTimer, &QTimer::timeout, this, + [this] { QtConcurrent::run(QThreadPool::globalInstance(), [this] { checkDevices(); }); }); const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000; _checkDevicesTimer->start(DEVICE_CHECK_INTERVAL_MSECS); // start a thread to detect peak value changes _checkPeakValuesTimer = new QTimer(this); - connect(_checkPeakValuesTimer, &QTimer::timeout, this, [this] { - QtConcurrent::run(QThreadPool::globalInstance(), [this] { checkPeakValues(); }); - }); + connect(_checkPeakValuesTimer, &QTimer::timeout, this, + [this] { QtConcurrent::run(QThreadPool::globalInstance(), [this] { checkPeakValues(); }); }); const unsigned long PEAK_VALUES_CHECK_INTERVAL_MSECS = 50; _checkPeakValuesTimer->start(PEAK_VALUES_CHECK_INTERVAL_MSECS); @@ -289,11 +263,11 @@ void AudioClient::customDeleter() { } void AudioClient::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) { - qCDebug(audioclient) << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec; + qCDebug(audioclient) << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec + << "recievedCodec:" << recievedCodec; selectAudioFormat(recievedCodec); } - void AudioClient::reset() { _receivedAudioStream.reset(); _stats.reset(); @@ -321,7 +295,7 @@ void AudioClient::setAudioPaused(bool pause) { QAudioDeviceInfo getNamedAudioDeviceForMode(QAudio::Mode mode, const QString& deviceName) { QAudioDeviceInfo result; - foreach(QAudioDeviceInfo audioDevice, getAvailableDevices(mode)) { + foreach (QAudioDeviceInfo audioDevice, getAvailableDevices(mode)) { if (audioDevice.deviceName().trimmed() == deviceName.trimmed()) { result = audioDevice; break; @@ -356,7 +330,8 @@ QString AudioClient::getWinDeviceName(wchar_t* guid) { HRESULT hr = S_OK; CoInitialize(nullptr); IMMDeviceEnumerator* pMMDeviceEnumerator = nullptr; - CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pMMDeviceEnumerator); + CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), + (void**)&pMMDeviceEnumerator); IMMDevice* pEndpoint; hr = pMMDeviceEnumerator->GetDevice(guid, &pEndpoint); if (hr == E_NOTFOUND) { @@ -380,34 +355,26 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { if (getAvailableDevices(mode).size() > 1) { AudioDeviceID defaultDeviceID = 0; uint32_t propertySize = sizeof(AudioDeviceID); - AudioObjectPropertyAddress propertyAddress = { - kAudioHardwarePropertyDefaultInputDevice, - kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMaster - }; + AudioObjectPropertyAddress propertyAddress = { kAudioHardwarePropertyDefaultInputDevice, + kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster }; if (mode == QAudio::AudioOutput) { propertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; } - - OSStatus getPropertyError = AudioObjectGetPropertyData(kAudioObjectSystemObject, - &propertyAddress, - 0, - NULL, - &propertySize, - &defaultDeviceID); + OSStatus getPropertyError = + AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &propertySize, &defaultDeviceID); if (!getPropertyError && propertySize) { CFStringRef deviceName = NULL; propertySize = sizeof(deviceName); propertyAddress.mSelector = kAudioDevicePropertyDeviceNameCFString; - getPropertyError = AudioObjectGetPropertyData(defaultDeviceID, &propertyAddress, 0, - NULL, &propertySize, &deviceName); + getPropertyError = + AudioObjectGetPropertyData(defaultDeviceID, &propertyAddress, 0, NULL, &propertySize, &deviceName); if (!getPropertyError && propertySize) { // find a device in the list that matches the name we have and return it - foreach(QAudioDeviceInfo audioDevice, getAvailableDevices(mode)) { + foreach (QAudioDeviceInfo audioDevice, getAvailableDevices(mode)) { if (audioDevice.deviceName() == CFStringGetCStringPtr(deviceName, kCFStringEncodingMacRoman)) { return audioDevice; } @@ -419,7 +386,7 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { #ifdef WIN32 QString deviceName; //Check for Windows Vista or higher, IMMDeviceEnumerator doesn't work below that. - if (!IsWindowsVistaOrGreater()) { // lower then vista + if (!IsWindowsVistaOrGreater()) { // lower then vista if (mode == QAudio::AudioInput) { WAVEINCAPS wic; // first use WAVE_MAPPER to get the default devices manufacturer ID @@ -441,9 +408,11 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { HRESULT hr = S_OK; CoInitialize(NULL); IMMDeviceEnumerator* pMMDeviceEnumerator = NULL; - CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pMMDeviceEnumerator); + CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), + (void**)&pMMDeviceEnumerator); IMMDevice* pEndpoint; - hr = pMMDeviceEnumerator->GetDefaultAudioEndpoint(mode == QAudio::AudioOutput ? eRender : eCapture, eMultimedia, &pEndpoint); + hr = pMMDeviceEnumerator->GetDefaultAudioEndpoint(mode == QAudio::AudioOutput ? eRender : eCapture, eMultimedia, + &pEndpoint); if (hr == E_NOTFOUND) { printf("Audio Error: device not found\n"); deviceName = QString("NONE"); @@ -457,22 +426,22 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { CoUninitialize(); } - qCDebug(audioclient) << "defaultAudioDeviceForMode mode: " << (mode == QAudio::AudioOutput ? "Output" : "Input") - << " [" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]"; + qCDebug(audioclient) << "defaultAudioDeviceForMode mode: " << (mode == QAudio::AudioOutput ? "Output" : "Input") << " [" + << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]"; return getNamedAudioDeviceForMode(mode, deviceName); #endif -#if defined (Q_OS_ANDROID) +#if defined(Q_OS_ANDROID) if (mode == QAudio::AudioInput) { Setting::Handle enableAEC(SETTING_AEC_KEY, false); bool aecEnabled = enableAEC.get(); auto audioClient = DependencyManager::get(); - bool headsetOn = audioClient? audioClient->isHeadsetPluggedIn() : false; + bool headsetOn = audioClient ? audioClient->isHeadsetPluggedIn() : false; auto inputDevices = QAudioDeviceInfo::availableDevices(QAudio::AudioInput); for (auto inputDevice : inputDevices) { if (((headsetOn || !aecEnabled) && inputDevice.deviceName() == VOICE_RECOGNITION) || - ((!headsetOn && aecEnabled) && inputDevice.deviceName() == VOICE_COMMUNICATION)) { + ((!headsetOn && aecEnabled) && inputDevice.deviceName() == VOICE_COMMUNICATION)) { return inputDevice; } } @@ -486,11 +455,8 @@ bool AudioClient::getNamedAudioDeviceForModeExists(QAudio::Mode mode, const QStr return (getNamedAudioDeviceForMode(mode, deviceName).deviceName() == deviceName); } - // attempt to use the native sample rate and channel count -bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, - QAudioFormat& audioFormat) { - +bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, QAudioFormat& audioFormat) { audioFormat = audioDevice.preferredFormat(); audioFormat.setCodec("audio/pcm"); @@ -513,7 +479,6 @@ bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, const QAudioFormat& desiredAudioFormat, QAudioFormat& adjustedAudioFormat) { - qCDebug(audioclient) << "The desired format for audio I/O is" << desiredAudioFormat; #if defined(Q_OS_ANDROID) || defined(Q_OS_OSX) @@ -539,12 +504,11 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, // Attempt the device sample rate and channel count in decreasing order of preference. // const int sampleRates[] = { 48000, 44100, 32000, 24000, 16000, 96000, 192000, 88200, 176400 }; - const int inputChannels[] = { 1, 2, 4, 6, 8 }; // prefer mono - const int outputChannels[] = { 2, 4, 6, 8, 1 }; // prefer stereo, downmix as last resort + const int inputChannels[] = { 1, 2, 4, 6, 8 }; // prefer mono + const int outputChannels[] = { 2, 4, 6, 8, 1 }; // prefer stereo, downmix as last resort for (int channelCount : (desiredAudioFormat.channelCount() == 1 ? inputChannels : outputChannels)) { for (int sampleRate : sampleRates) { - adjustedAudioFormat.setChannelCount(channelCount); adjustedAudioFormat.setSampleRate(sampleRate); @@ -554,11 +518,14 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, } } - return false; // a supported format could not be found + return false; // a supported format could not be found } -bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationSamples, unsigned int numSourceSamples, - const int sourceChannelCount, const int destinationChannelCount) { +bool sampleChannelConversion(const int16_t* sourceSamples, + int16_t* destinationSamples, + unsigned int numSourceSamples, + const int sourceChannelCount, + const int destinationChannelCount) { if (sourceChannelCount == 2 && destinationChannelCount == 1) { // loop through the stereo input audio samples and average every two samples for (uint i = 0; i < numSourceSamples; i += 2) { @@ -567,7 +534,6 @@ bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationS return true; } else if (sourceChannelCount == 1 && destinationChannelCount == 2) { - // loop through the mono input audio and repeat each sample twice for (uint i = 0; i < numSourceSamples; ++i) { destinationSamples[i * 2] = destinationSamples[(i * 2) + 1] = sourceSamples[i]; @@ -580,32 +546,31 @@ bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationS } void possibleResampling(AudioSRC* resampler, - const int16_t* sourceSamples, int16_t* destinationSamples, - unsigned int numSourceSamples, unsigned int numDestinationSamples, - const int sourceChannelCount, const int destinationChannelCount) { - + const int16_t* sourceSamples, + int16_t* destinationSamples, + unsigned int numSourceSamples, + unsigned int numDestinationSamples, + const int sourceChannelCount, + const int destinationChannelCount) { if (numSourceSamples > 0) { if (!resampler) { - if (!sampleChannelConversion(sourceSamples, destinationSamples, numSourceSamples, - sourceChannelCount, destinationChannelCount)) { + if (!sampleChannelConversion(sourceSamples, destinationSamples, numSourceSamples, sourceChannelCount, + destinationChannelCount)) { // no conversion, we can copy the samples directly across memcpy(destinationSamples, sourceSamples, numSourceSamples * AudioConstants::SAMPLE_SIZE); } } else { - if (sourceChannelCount != destinationChannelCount) { - int numChannelCoversionSamples = (numSourceSamples * destinationChannelCount) / sourceChannelCount; int16_t* channelConversionSamples = new int16_t[numChannelCoversionSamples]; - sampleChannelConversion(sourceSamples, channelConversionSamples, numSourceSamples, - sourceChannelCount, destinationChannelCount); + sampleChannelConversion(sourceSamples, channelConversionSamples, numSourceSamples, sourceChannelCount, + destinationChannelCount); resampler->render(channelConversionSamples, destinationSamples, numChannelCoversionSamples); delete[] channelConversionSamples; } else { - unsigned int numAdjustedSourceSamples = numSourceSamples; unsigned int numAdjustedDestinationSamples = numDestinationSamples; @@ -621,7 +586,6 @@ void possibleResampling(AudioSRC* resampler, } void AudioClient::start() { - // set up the desired audio format _desiredInputFormat.setSampleRate(AudioConstants::SAMPLE_RATE); _desiredInputFormat.setSampleSize(16); @@ -710,7 +674,6 @@ void AudioClient::handleAudioDataPacket(QSharedPointer message) nodeList->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveFirstAudioPacket); if (_audioOutput) { - if (!_hasReceivedFirstPacket) { _hasReceivedFirstPacket = true; @@ -727,8 +690,8 @@ void AudioClient::handleAudioDataPacket(QSharedPointer message) } } -AudioClient::Gate::Gate(AudioClient* audioClient) : - _audioClient(audioClient) {} +AudioClient::Gate::Gate(AudioClient* audioClient) : _audioClient(audioClient) { +} void AudioClient::Gate::setIsSimulatingJitter(bool enable) { std::lock_guard lock(_mutex); @@ -781,7 +744,6 @@ void AudioClient::Gate::flush() { _index = 0; } - void AudioClient::handleNoisyMutePacket(QSharedPointer message) { if (!_muted) { setMuted(true); @@ -827,7 +789,6 @@ void AudioClient::handleSelectedAudioFormat(QSharedPointer mess } void AudioClient::selectAudioFormat(const QString& selectedCodecName) { - _selectedCodecName = selectedCodecName; qCDebug(audioclient) << "Selected Codec:" << _selectedCodecName << "isStereoInput:" << _isStereoInput; @@ -845,12 +806,12 @@ void AudioClient::selectAudioFormat(const QString& selectedCodecName) { if (_selectedCodecName == plugin->getName()) { _codec = plugin; _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); - _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, _isStereoInput ? AudioConstants::STEREO : AudioConstants::MONO); + _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, + _isStereoInput ? AudioConstants::STEREO : AudioConstants::MONO); qCDebug(audioclient) << "Selected Codec Plugin:" << _codec.get(); break; } } - } bool AudioClient::switchAudioDevice(QAudio::Mode mode, const QAudioDeviceInfo& deviceInfo) { @@ -862,7 +823,7 @@ bool AudioClient::switchAudioDevice(QAudio::Mode mode, const QAudioDeviceInfo& d if (mode == QAudio::AudioInput) { return switchInputToAudioDevice(device); - } else { // if (mode == QAudio::AudioOutput) + } else { // if (mode == QAudio::AudioOutput) return switchOutputToAudioDevice(device); } } @@ -904,8 +865,8 @@ void AudioClient::configureReverb() { p.sampleRate = _outputFormat.sampleRate(); p.wetDryMix = 100.0f; p.preDelay = 0.0f; - p.earlyGain = -96.0f; // disable ER - p.lateGain += _reverbOptions->getWetDryMix() * (24.0f/100.0f) - 24.0f; // -0dB to -24dB, based on wetDryMix + p.earlyGain = -96.0f; // disable ER + p.lateGain += _reverbOptions->getWetDryMix() * (24.0f / 100.0f) - 24.0f; // -0dB to -24dB, based on wetDryMix p.lateMixLeft = 0.0f; p.lateMixRight = 0.0f; @@ -915,7 +876,6 @@ void AudioClient::configureReverb() { void AudioClient::updateReverbOptions() { bool reverbChanged = false; if (_receivedAudioStream.hasReverb()) { - if (_zoneReverbOptions.getReverbTime() != _receivedAudioStream.getRevebTime()) { _zoneReverbOptions.setReverbTime(_receivedAudioStream.getRevebTime()); reverbChanged = true; @@ -1020,7 +980,8 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { int16_t* loopbackSamples = reinterpret_cast(loopBackByteArray.data()); // upmix mono to stereo - if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat.channelCount(), OUTPUT_CHANNEL_COUNT)) { + if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat.channelCount(), + OUTPUT_CHANNEL_COUNT)) { // no conversion, just copy the samples memcpy(loopbackSamples, inputSamples, numInputSamples * AudioConstants::SAMPLE_SIZE); } @@ -1028,17 +989,15 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // apply stereo reverb at the source, to the loopback audio if (!_shouldEchoLocally && hasReverb) { updateReverbOptions(); - _sourceReverb.render(loopbackSamples, loopbackSamples, numLoopbackSamples/2); + _sourceReverb.render(loopbackSamples, loopbackSamples, numLoopbackSamples / 2); } // if required, upmix or downmix to deviceChannelCount int deviceChannelCount = _outputFormat.channelCount(); if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { - _loopbackOutputDevice->write(loopBackByteArray); } else { - static QByteArray deviceByteArray; int numDeviceSamples = (numLoopbackSamples * deviceChannelCount) / OUTPUT_CHANNEL_COUNT; @@ -1074,7 +1033,7 @@ void AudioClient::handleAudioInput(QByteArray& audioBuffer) { } int32_t loudness = 0; - assert(numSamples < 65536); // int32_t loudness cannot overflow + assert(numSamples < 65536); // int32_t loudness cannot overflow bool didClip = false; for (int i = 0; i < numSamples; ++i) { const int32_t CLIPPING_THRESHOLD = (int32_t)(AudioConstants::MAX_SAMPLE_VALUE * 0.9f); @@ -1129,13 +1088,14 @@ void AudioClient::handleAudioInput(QByteArray& audioBuffer) { } emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, _isStereoInput, - audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, - packetType, _selectedCodecName); + audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, packetType, _selectedCodecName); _stats.sentPacket(); } } -void AudioClient::processAudioAndAddToRingBuffer(QByteArray& inputByteArray, const uchar& channelCount, const qint32& bytesForDuration) { +void AudioClient::processAudioAndAddToRingBuffer(QByteArray& inputByteArray, + const uchar& channelCount, + const qint32& bytesForDuration) { // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output const int inputSamplesRequired = (_inputToNetworkResampler ? _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) @@ -1189,11 +1149,10 @@ void AudioClient::handleMicAudioInput() { } void AudioClient::handleDummyAudioInput() { - const int numNetworkBytes = _isStereoInput - ? AudioConstants::NETWORK_FRAME_BYTES_STEREO - : AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; + const int numNetworkBytes = + _isStereoInput ? AudioConstants::NETWORK_FRAME_BYTES_STEREO : AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; - QByteArray audioBuffer(numNetworkBytes, 0); // silent + QByteArray audioBuffer(numNetworkBytes, 0); // silent handleAudioInput(audioBuffer); } @@ -1202,13 +1161,59 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { handleAudioInput(audioBuffer); } + int rawToWav(const char* rawData, const int& rawLength, const char* wavfn, long frequency) { + long chunksize = 0x10; + + struct { + unsigned short wFormatTag; + unsigned short wChannels; + unsigned long dwSamplesPerSec; + unsigned long dwAvgBytesPerSec; + unsigned short wBlockAlign; + unsigned short wBitsPerSample; + } fmt; + + long samplecount = rawLength / 2; + long riffsize = samplecount * 2 + 0x24; + long datasize = samplecount * 2; + + FILE* wav = fopen(wavfn, "wb"); + if (!wav) { + return -3; + } + + fwrite("RIFF", 1, 4, wav); + fwrite(&riffsize, 4, 1, wav); + fwrite("WAVEfmt ", 1, 8, wav); + fwrite(&chunksize, 4, 1, wav); + + fmt.wFormatTag = 1; // PCM + fmt.wChannels = 1; // MONO + fmt.dwSamplesPerSec = frequency * 1; + fmt.dwAvgBytesPerSec = frequency * 1 * 2; // 16 bit + fmt.wBlockAlign = 2; + fmt.wBitsPerSample = 16; + + fwrite(&fmt, sizeof(fmt), 1, wav); + fwrite("data", 1, 4, wav); + fwrite(&datasize, 4, 1, wav); + fwrite(rawData, 1, rawLength, wav); + fclose(wav); +} + void AudioClient::handleTTSAudioInput(const QByteArray& audio) { QByteArray audioBuffer(audio); + QVector audioBufferReal; + + QString filename = QString::number(usecTimestampNow()); + QString path = PathUtils::getAppDataPath() + "Audio/" + filename + ".wav"; + rawToWav(audioBuffer.data(), audioBuffer.size(), path.toLocal8Bit(), 24000); + while (audioBuffer.size() > 0) { QByteArray part; part.append(audioBuffer.data(), AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); audioBuffer.remove(0, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - processAudioAndAddToRingBuffer(part, 1, 48); + processAudioAndAddToRingBuffer(part, 1, 48); } } @@ -1234,9 +1239,8 @@ void AudioClient::prepareLocalAudioInjectors(std::unique_ptr localAudioLoc int bufferCapacity = _localInjectorsStream.getSampleCapacity(); int maxOutputSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * AudioConstants::STEREO; if (_localToOutputResampler) { - maxOutputSamples = - _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * - AudioConstants::STEREO; + maxOutputSamples = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * + AudioConstants::STEREO; } samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed); @@ -1259,7 +1263,7 @@ void AudioClient::prepareLocalAudioInjectors(std::unique_ptr localAudioLoc if (_localToOutputResampler) { // resample to output sample rate int frames = _localToOutputResampler->render(_localMixBuffer, _localOutputMixBuffer, - AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); // write to local injectors' ring buffer samples = frames * AudioConstants::STEREO; @@ -1268,8 +1272,7 @@ void AudioClient::prepareLocalAudioInjectors(std::unique_ptr localAudioLoc } else { // write to local injectors' ring buffer samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; - _localInjectorsStream.writeSamples(_localMixBuffer, - AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + _localInjectorsStream.writeSamples(_localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); } _localSamplesAvailable.fetch_add(samples, std::memory_order_release); @@ -1294,18 +1297,16 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // the lock guarantees that injectorBuffer, if found, is invariant AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { - static const int HRTF_DATASET_INDEX = 1; - int numChannels = injector->isAmbisonic() ? AudioConstants::AMBISONIC : (injector->isStereo() ? AudioConstants::STEREO : AudioConstants::MONO); + int numChannels = injector->isAmbisonic() ? AudioConstants::AMBISONIC + : (injector->isStereo() ? AudioConstants::STEREO : AudioConstants::MONO); size_t bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; // get one frame from the injector memset(_localScratchBuffer, 0, bytesToRead); if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - if (injector->isAmbisonic()) { - // no distance attenuation float gain = injector->getVolume(); @@ -1322,11 +1323,10 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { float qz = relativeOrientation.y; // Ambisonic gets spatialized into mixBuffer - injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, - qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, qw, qx, qy, qz, gain, + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } else if (injector->isStereo()) { - // stereo gets directly mixed into mixBuffer float gain = injector->getVolume(); for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { @@ -1334,7 +1334,6 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } } else { - // calculate distance, gain and azimuth for hrtf glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); @@ -1342,19 +1341,17 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { float azimuth = azimuthForSource(relativePosition); // mono gets spatialized into mixBuffer - injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, - azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, azimuth, distance, gain, + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } } else { - qCDebug(audioclient) << "injector has no more data, marking finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } else { - qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); @@ -1373,7 +1370,6 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteArray& outputBuffer) { - const int16_t* decodedSamples = reinterpret_cast(decodedBuffer.data()); assert(decodedBuffer.size() == AudioConstants::NETWORK_FRAME_BYTES_STEREO); @@ -1442,7 +1438,6 @@ void AudioClient::setNoiseReduction(bool enable, bool emitSignal) { } } - bool AudioClient::setIsStereoInput(bool isStereoInput) { bool stereoInputChanged = false; if (isStereoInput != _isStereoInput && _inputDeviceInfo.supportedChannelCounts().contains(2)) { @@ -1460,7 +1455,8 @@ bool AudioClient::setIsStereoInput(bool isStereoInput) { if (_encoder) { _codec->releaseEncoder(_encoder); } - _encoder = _codec->createEncoder(AudioConstants::SAMPLE_RATE, _isStereoInput ? AudioConstants::STEREO : AudioConstants::MONO); + _encoder = _codec->createEncoder(AudioConstants::SAMPLE_RATE, + _isStereoInput ? AudioConstants::STEREO : AudioConstants::MONO); } qCDebug(audioclient) << "Reset Codec:" << _selectedCodecName << "isStereoInput:" << _isStereoInput; @@ -1500,7 +1496,7 @@ bool AudioClient::outputLocalInjector(const AudioInjectorPointer& injector) { void AudioClient::outputFormatChanged() { _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * OUTPUT_CHANNEL_COUNT * _outputFormat.sampleRate()) / - _desiredOutputFormat.sampleRate(); + _desiredOutputFormat.sampleRate(); _receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } @@ -1514,7 +1510,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo inputDeviceInf Lock lock(_deviceMutex); #if defined(Q_OS_ANDROID) - _shouldRestartInputSetup = false; // avoid a double call to _audioInput->start() from audioInputStateChanged + _shouldRestartInputSetup = false; // avoid a double call to _audioInput->start() from audioInputStateChanged #endif // cleanup any previously initialized device @@ -1565,15 +1561,15 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo inputDeviceInf // we've got the best we can get for input // if required, setup a resampler for this input to our desired network format - if (_inputFormat != _desiredInputFormat - && _inputFormat.sampleRate() != _desiredInputFormat.sampleRate()) { + if (_inputFormat != _desiredInputFormat && _inputFormat.sampleRate() != _desiredInputFormat.sampleRate()) { qCDebug(audioclient) << "Attemping to create a resampler for input format to network format."; assert(_inputFormat.sampleSize() == 16); assert(_desiredInputFormat.sampleSize() == 16); int channelCount = (_inputFormat.channelCount() == 2 && _desiredInputFormat.channelCount() == 2) ? 2 : 1; - _inputToNetworkResampler = new AudioSRC(_inputFormat.sampleRate(), _desiredInputFormat.sampleRate(), channelCount); + _inputToNetworkResampler = + new AudioSRC(_inputFormat.sampleRate(), _desiredInputFormat.sampleRate(), channelCount); } else { qCDebug(audioclient) << "No resampling required for audio input to match desired network format."; @@ -1607,7 +1603,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo inputDeviceInf connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput())); supportedFormat = true; } else { - qCDebug(audioclient) << "Error starting audio input -" << _audioInput->error(); + qCDebug(audioclient) << "Error starting audio input -" << _audioInput->error(); _audioInput->deleteLater(); _audioInput = NULL; } @@ -1677,7 +1673,7 @@ void AudioClient::checkInputTimeout() { void AudioClient::setHeadsetPluggedIn(bool pluggedIn) { #if defined(Q_OS_ANDROID) if (pluggedIn == !_isHeadsetPluggedIn && !_inputDeviceInfo.isNull()) { - QAndroidJniObject brand = QAndroidJniObject::getStaticObjectField("android/os/Build", "BRAND"); + QAndroidJniObject brand = QAndroidJniObject::getStaticObjectField("android/os/Build", "BRAND"); // some samsung phones needs more time to shutdown the previous input device if (brand.toString().contains("samsung", Qt::CaseInsensitive)) { switchInputToAudioDevice(QAudioDeviceInfo(), true); @@ -1715,8 +1711,8 @@ void AudioClient::outputNotify() { int newOutputBufferSizeFrames = setOutputBufferSize(oldOutputBufferSizeFrames + 1, false); if (newOutputBufferSizeFrames > oldOutputBufferSizeFrames) { - qCDebug(audioclient, - "Starve threshold surpassed (%d starves in %d ms)", _outputStarveDetectionCount, dt); + qCDebug(audioclient, "Starve threshold surpassed (%d starves in %d ms)", _outputStarveDetectionCount, + dt); } _outputStarveDetectionStartTimeMsec = now; @@ -1730,7 +1726,8 @@ void AudioClient::outputNotify() { bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceInfo, bool isShutdownRequest) { Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); - qCDebug(audioclient) << "AudioClient::switchOutputToAudioDevice() outputDeviceInfo: [" << outputDeviceInfo.deviceName() << "]"; + qCDebug(audioclient) << "AudioClient::switchOutputToAudioDevice() outputDeviceInfo: [" << outputDeviceInfo.deviceName() + << "]"; bool supportedFormat = false; // NOTE: device start() uses the Qt internal device list @@ -1789,15 +1786,16 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceI // we've got the best we can get for input // if required, setup a resampler for this input to our desired network format - if (_desiredOutputFormat != _outputFormat - && _desiredOutputFormat.sampleRate() != _outputFormat.sampleRate()) { + if (_desiredOutputFormat != _outputFormat && _desiredOutputFormat.sampleRate() != _outputFormat.sampleRate()) { qCDebug(audioclient) << "Attemping to create a resampler for network format to output format."; assert(_desiredOutputFormat.sampleSize() == 16); assert(_outputFormat.sampleSize() == 16); - _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); - _localToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); + _networkToOutputResampler = + new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); + _localToOutputResampler = + new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } else { qCDebug(audioclient) << "No resampling required for network output to match actual output format."; @@ -1809,7 +1807,9 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceI _audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); int deviceChannelCount = _outputFormat.channelCount(); - int frameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); + int frameSize = + (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / + _desiredOutputFormat.sampleRate(); int requestedSize = _sessionOutputBufferSizeFrames * frameSize * AudioConstants::SAMPLE_SIZE; _audioOutput->setBufferSize(requestedSize); @@ -1825,7 +1825,10 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceI _outputScratchBuffer = new int16_t[_outputPeriod]; // size local output mix buffer based on resampled network frame size - int networkPeriod = _localToOutputResampler ? _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO) : AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; + int networkPeriod = + _localToOutputResampler + ? _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO) + : AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; _localOutputMixBuffer = new float[networkPeriod]; // local period should be at least twice the output period, @@ -1875,7 +1878,8 @@ int AudioClient::setOutputBufferSize(int numFrames, bool persist) { qCDebug(audioclient) << __FUNCTION__ << "numFrames:" << numFrames << "persist:" << persist; numFrames = std::min(std::max(numFrames, MIN_BUFFER_FRAMES), MAX_BUFFER_FRAMES); - qCDebug(audioclient) << __FUNCTION__ << "clamped numFrames:" << numFrames << "_sessionOutputBufferSizeFrames:" << _sessionOutputBufferSizeFrames; + qCDebug(audioclient) << __FUNCTION__ << "clamped numFrames:" << numFrames + << "_sessionOutputBufferSizeFrames:" << _sessionOutputBufferSizeFrames; if (numFrames != _sessionOutputBufferSizeFrames) { qCInfo(audioclient, "Audio output buffer set to %d frames", numFrames); @@ -1906,10 +1910,10 @@ const float AudioClient::CALLBACK_ACCELERATOR_RATIO = 2.0f; #endif int AudioClient::calculateNumberOfInputCallbackBytes(const QAudioFormat& format) const { - int numInputCallbackBytes = (int)(((AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL - * format.channelCount() - * ((float) format.sampleRate() / AudioConstants::SAMPLE_RATE)) - / CALLBACK_ACCELERATOR_RATIO) + 0.5f); + int numInputCallbackBytes = (int)(((AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL * format.channelCount() * + ((float)format.sampleRate() / AudioConstants::SAMPLE_RATE)) / + CALLBACK_ACCELERATOR_RATIO) + + 0.5f); return numInputCallbackBytes; } @@ -1931,10 +1935,9 @@ float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { float rotatedSourcePositionLength2 = glm::length2(rotatedSourcePosition); if (rotatedSourcePositionLength2 > SOURCE_DISTANCE_THRESHOLD) { - // produce an oriented angle about the y-axis glm::vec3 direction = rotatedSourcePosition * (1.0f / fastSqrtf(rotatedSourcePositionLength2)); - float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward" + float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward" return (direction.x < 0.0f) ? -angle : angle; } else { @@ -1944,7 +1947,6 @@ float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { } float AudioClient::gainForSource(float distance, float volume) { - // attenuation = -6dB * log2(distance) // reference attenuation of 0dB at distance = 1.0m float gain = volume / std::max(distance, HRTF_NEARFIELD_MIN); @@ -1952,8 +1954,7 @@ float AudioClient::gainForSource(float distance, float volume) { return gain; } -qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { - +qint64 AudioClient::AudioOutputIODevice::readData(char* data, qint64 maxSize) { // samples requested from OUTPUT_CHANNEL_COUNT int deviceChannelCount = _audio->_outputFormat.channelCount(); int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; @@ -1965,7 +1966,8 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { int networkSamplesPopped; if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { - qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable(), samplesRequested); + qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, + _receivedAudioStream.getSamplesAvailable(), samplesRequested); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); for (int i = 0; i < networkSamplesPopped; i++) { @@ -1997,14 +1999,13 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { samplesRequested = std::min(samplesRequested, samplesAvailable); if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { _audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release); - qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); + qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, + _localInjectorsStream.samplesAvailable(), samplesRequested); } } // prepare injectors for the next callback - QtConcurrent::run(QThreadPool::globalInstance(), [this] { - _audio->prepareLocalAudioInjectors(); - }); + QtConcurrent::run(QThreadPool::globalInstance(), [this] { _audio->prepareLocalAudioInjectors(); }); int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped); int framesPopped = samplesPopped / AudioConstants::STEREO; @@ -2038,7 +2039,6 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { _audio->_audioFileWav.addRawAudioChunk(reinterpret_cast(scratchBuffer), bytesWritten); } - int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree(); float msecsAudioOutputUnplayed = bytesAudioOutputUnplayed / (float)_audio->_outputFormat.bytesForDuration(USECS_PER_MSEC); _audio->_stats.updateOutputMsUnplayed(msecsAudioOutputUnplayed); @@ -2075,7 +2075,6 @@ void AudioClient::loadSettings() { for (auto& plugin : codecPlugins) { qCDebug(audioclient) << "Codec available:" << plugin->getName(); } - } void AudioClient::saveSettings() { @@ -2088,7 +2087,6 @@ void AudioClient::setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 sca avatarBoundingBoxScale = scale; } - void AudioClient::startThread() { moveToNewNamedThread(this, "Audio Thread", [this] { start(); }, QThread::TimeCriticalPriority); } From 7dd156ccdd790bbc750d05465ad4ad3de6698067 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 12 Oct 2018 15:00:15 -0700 Subject: [PATCH 100/362] made the threshold for the spine angle larger to make auto transitions more robust --- interface/src/avatar/MyAvatar.cpp | 57 +++++++++++++++--------- interface/src/avatar/MyAvatar.h | 2 +- interface/src/avatar/MySkeletonModel.cpp | 2 +- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 745705f7b8..e130f78dc4 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -471,6 +471,7 @@ void MyAvatar::update(float deltaTime) { const float STANDING_HEIGHT_MULTIPLE = 1.2f; const float SITTING_HEIGHT_MULTIPLE = 0.833f; const float COSINE_TEN_DEGREES = 0.98f; + const float COSINE_THIRTY_DEGREES = 0.866f; const int SITTING_COUNT_THRESHOLD = 300; const int SQUATTY_COUNT_THRESHOLD = 600; @@ -518,7 +519,7 @@ void MyAvatar::update(float deltaTime) { upSpine2 = glm::normalize(upSpine2); } float angleSpine2 = glm::dot(upSpine2, glm::vec3(0.0f, 1.0f, 0.0f)); - if (getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPositionAvatarSpace.y - SQUAT_THRESHOLD) && (angleSpine2 > COSINE_TEN_DEGREES)) { + if (getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPositionAvatarSpace.y - SQUAT_THRESHOLD) && (angleSpine2 > COSINE_THIRTY_DEGREES)) { _squatCount++; } else { _squatCount = 0; @@ -531,7 +532,7 @@ void MyAvatar::update(float deltaTime) { glm::vec3 sensorHips = transformPoint(glm::inverse(getSensorToWorldMatrix()), worldHips); // put update sit stand state counts here - if (getIsSitStandStateLocked()) { + if (!getIsSitStandStateLocked() && (_follow._velocityCount > 60)) { if (getIsInSittingState()) { if (newHeightReading.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { // if we recenter upwards then no longer in sitting state @@ -544,10 +545,10 @@ void MyAvatar::update(float deltaTime) { _tippingPoint = newHeightReading.getTranslation().y; } _averageUserHeightCount = 1; - setResetMode(true); + // setResetMode(true); setIsInSittingState(false); - setCenterOfGravityModelEnabled(true); - setSitStandStateChange(true); + // setCenterOfGravityModelEnabled(true); + // setSitStandStateChange(true); } qCDebug(interfaceapp) << "going to stand state"; } else { @@ -557,7 +558,7 @@ void MyAvatar::update(float deltaTime) { } } else { // in the standing state - if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint))) {// && (angleSpine2 > COSINE_TEN_DEGREES) && !(sensorHips.y > (0.4f * averageSensorSpaceHeight))) { + if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) && (angleSpine2 > COSINE_THIRTY_DEGREES)) { _sitStandStateCount++; if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { _sitStandStateCount = 0; @@ -567,11 +568,10 @@ void MyAvatar::update(float deltaTime) { _tippingPoint = newHeightReading.getTranslation().y; } _averageUserHeightCount = 1; - setResetMode(true); + // setResetMode(true); setIsInSittingState(true); - setCenterOfGravityModelEnabled(false); - - setSitStandStateChange(true); + // setCenterOfGravityModelEnabled(false); + // setSitStandStateChange(true); } qCDebug(interfaceapp) << "going to sit state"; } else { @@ -3898,6 +3898,13 @@ void MyAvatar::setIsInWalkingState(bool isWalking) { void MyAvatar::setIsInSittingState(bool isSitting) { _isInSittingState.set(isSitting); + setResetMode(true); + if (isSitting) { + setCenterOfGravityModelEnabled(false); + } else { + setCenterOfGravityModelEnabled(true); + } + setSitStandStateChange(true); emit sittingEnabledChanged(isSitting); } @@ -4146,32 +4153,34 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl const float CYLINDER_TOP = 0.1f; const float CYLINDER_BOTTOM = -1.5f; const float SITTING_BOTTOM = -0.02f; - const int SQUATTY_COUNT_THRESHOLD = 600; + const int SQUATTY_COUNT_THRESHOLD = 1800; glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); bool returnValue = false; - returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); if (myAvatar.getSitStandStateChange()) { - qCDebug(interfaceapp) << "sit state change"; + // qCDebug(interfaceapp) << "sit state change"; returnValue = true; + } if (myAvatar.getIsInSittingState()) { + if (myAvatar.getIsSitStandStateLocked()) { + returnValue = (offset.y > CYLINDER_TOP); + } if (offset.y < SITTING_BOTTOM) { // we recenter when sitting. - qCDebug(interfaceapp) << "lean back sitting "; + // qCDebug(interfaceapp) << "lean back sitting "; returnValue = true; } } else { // in the standing state - if (myAvatar._squatCount > SQUATTY_COUNT_THRESHOLD) { + returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + // finally check for squats in standing + if ((myAvatar._squatCount > SQUATTY_COUNT_THRESHOLD) && !myAvatar.getIsSitStandStateLocked()) { myAvatar._squatCount = 0; - qCDebug(interfaceapp) << "squatting "; - // returnValue = true; - } - if (returnValue == true) { - qCDebug(interfaceapp) << "above or below capsule in standing"; + // qCDebug(interfaceapp) << "squatting "; + returnValue = true; } } return returnValue; @@ -4185,6 +4194,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); + qCDebug(interfaceapp) << "should activate rotation true "; myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); } if (myAvatar.getCenterOfGravityModelEnabled()) { @@ -4199,7 +4209,8 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat // center of gravity model is not enabled if (!isActive(Horizontal) && (shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Horizontal); - if (myAvatar.getEnableStepResetRotation()) { + if (myAvatar.getEnableStepResetRotation() && !myAvatar.getIsInSittingState()) { + qCDebug(interfaceapp) << "step recenter rotation true "; activate(Rotation); myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); } @@ -4214,6 +4225,10 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat if ((glm::length(myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD).getVelocity()) > 0.1f)) { _velocityCount++; } + if (_velocityCount > 60) { + qCDebug(interfaceapp) << "reached velocity count "; + } + } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 674d4b8b70..10b5510e4d 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1835,7 +1835,7 @@ private: float _sumUserHeightSensorSpace{ DEFAULT_AVATAR_HEIGHT }; int _averageUserHeightCount{ 1 }; bool _sitStandStateChange{ false }; - ThreadSafeValueCache _lockSitStandState { true }; + ThreadSafeValueCache _lockSitStandState { false }; // max unscaled forward movement speed ThreadSafeValueCache _walkSpeed { DEFAULT_AVATAR_MAX_WALKING_SPEED }; diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 78c5c03cc9..e5b9df7dd1 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -46,7 +46,7 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { } glm::mat4 hipsMat; - if (myAvatar->getCenterOfGravityModelEnabled() && !isFlying && !(myAvatar->getIsInWalkingState()) && !(myAvatar->getIsInSittingState())) { + if (myAvatar->getCenterOfGravityModelEnabled() && !isFlying && !(myAvatar->getIsInWalkingState()) && !(myAvatar->getIsInSittingState()) && myAvatar->getHMDLeanRecenterEnabled()) { // then we use center of gravity model hipsMat = myAvatar->deriveBodyUsingCgModel(); } else { From ee830cdcb0ef19f7f731d6032eccefe957a7fec3 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 12 Oct 2018 16:05:31 -0700 Subject: [PATCH 101/362] cleaned white space --- interface/src/avatar/MyAvatar.cpp | 40 +++++++++---------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index e130f78dc4..5e4db283a3 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -470,7 +470,6 @@ void MyAvatar::update(float deltaTime) { const float PERCENTAGE_WEIGHT_HEAD_VS_SHOULDERS_AZIMUTH = 0.0f; // 100 percent shoulders const float STANDING_HEIGHT_MULTIPLE = 1.2f; const float SITTING_HEIGHT_MULTIPLE = 0.833f; - const float COSINE_TEN_DEGREES = 0.98f; const float COSINE_THIRTY_DEGREES = 0.866f; const int SITTING_COUNT_THRESHOLD = 300; const int SQUATTY_COUNT_THRESHOLD = 600; @@ -545,12 +544,8 @@ void MyAvatar::update(float deltaTime) { _tippingPoint = newHeightReading.getTranslation().y; } _averageUserHeightCount = 1; - // setResetMode(true); setIsInSittingState(false); - // setCenterOfGravityModelEnabled(true); - // setSitStandStateChange(true); } - qCDebug(interfaceapp) << "going to stand state"; } else { _sitStandStateCount = 0; // tipping point is average height when sitting. @@ -568,19 +563,13 @@ void MyAvatar::update(float deltaTime) { _tippingPoint = newHeightReading.getTranslation().y; } _averageUserHeightCount = 1; - // setResetMode(true); setIsInSittingState(true); - // setCenterOfGravityModelEnabled(false); - // setSitStandStateChange(true); } - qCDebug(interfaceapp) << "going to sit state"; } else { - // returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); // use the mode height for the tipping point when we are standing. _tippingPoint = getCurrentStandingHeight(); _sitStandStateCount = 0; } - } } @@ -594,7 +583,7 @@ void MyAvatar::update(float deltaTime) { // draw hand azimuth vector glm::vec3 handAzimuthMidpoint = transformPoint(getTransform().getMatrix(), glm::vec3(_hipToHandController.x, 0.0f, _hipToHandController.y)); - DebugDraw::getInstance().drawRay(getWorldPosition(), handAzimuthMidpoint, glm::vec4(0.0f, 1.0f, 1.0f, 1.0f)); + DebugDraw::getInstance().drawRay(getWorldPosition(), handAzimuthMidpoint, glm::vec4(0.0f, 1.0f, 1.0f, 1.0f)); } if (_goToPending) { @@ -3909,7 +3898,12 @@ void MyAvatar::setIsInSittingState(bool isSitting) { } void MyAvatar::setIsSitStandStateLocked(bool isLocked) { + const float DEFAULT_FLOOR_HEIGHT = 0.0f; _lockSitStandState.set(isLocked); + _sitStandStateCount = 0; + _sumUserHeightSensorSpace = DEFAULT_AVATAR_HEIGHT; + _tippingPoint = DEFAULT_FLOOR_HEIGHT; + _averageUserHeightCount = 1; emit sitStandStateLockEnabledChanged(isLocked); } @@ -4156,21 +4150,17 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl const int SQUATTY_COUNT_THRESHOLD = 1800; glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); - bool returnValue = false; if (myAvatar.getSitStandStateChange()) { - // qCDebug(interfaceapp) << "sit state change"; returnValue = true; - } if (myAvatar.getIsInSittingState()) { if (myAvatar.getIsSitStandStateLocked()) { returnValue = (offset.y > CYLINDER_TOP); } if (offset.y < SITTING_BOTTOM) { - // we recenter when sitting. - // qCDebug(interfaceapp) << "lean back sitting "; + // we recenter more easily when in sitting state. returnValue = true; } } else { @@ -4179,7 +4169,6 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl // finally check for squats in standing if ((myAvatar._squatCount > SQUATTY_COUNT_THRESHOLD) && !myAvatar.getIsSitStandStateLocked()) { myAvatar._squatCount = 0; - // qCDebug(interfaceapp) << "squatting "; returnValue = true; } } @@ -4194,7 +4183,6 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); - qCDebug(interfaceapp) << "should activate rotation true "; myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); } if (myAvatar.getCenterOfGravityModelEnabled()) { @@ -4210,28 +4198,22 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat if (!isActive(Horizontal) && (shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Horizontal); if (myAvatar.getEnableStepResetRotation() && !myAvatar.getIsInSittingState()) { - qCDebug(interfaceapp) << "step recenter rotation true "; activate(Rotation); myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); } } } - - if (_velocityCount > 60) { + const int VELOCITY_COUNT_THRESHOLD = 60; + const float MINIMUM_HMD_VELOCITY = 0.1f; + if (_velocityCount > VELOCITY_COUNT_THRESHOLD) { if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Vertical); } } else { - if ((glm::length(myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD).getVelocity()) > 0.1f)) { + if ((glm::length(myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD).getVelocity()) > MINIMUM_HMD_VELOCITY)) { _velocityCount++; } - if (_velocityCount > 60) { - qCDebug(interfaceapp) << "reached velocity count "; - } - } - - } else { if (!isActive(Rotation) && getForceActivateRotation()) { activate(Rotation); From 90c9e578c845df4661adeda1f8d3809d2fb9fe53 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 12 Oct 2018 16:47:53 -0700 Subject: [PATCH 102/362] when you unlock the transitions you now start in stand state --- interface/src/avatar/MyAvatar.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5e4db283a3..172fdac96c 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3904,6 +3904,10 @@ void MyAvatar::setIsSitStandStateLocked(bool isLocked) { _sumUserHeightSensorSpace = DEFAULT_AVATAR_HEIGHT; _tippingPoint = DEFAULT_FLOOR_HEIGHT; _averageUserHeightCount = 1; + if (!isLocked) { + // always start the auto transition mode in standing state. + setIsInSittingState(false); + } emit sitStandStateLockEnabledChanged(isLocked); } From a85336044f0201afa66fd287e5d9cdca13a99b1c Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 12 Oct 2018 18:03:47 -0700 Subject: [PATCH 103/362] add X to clear button to filter search --- scripts/system/html/css/edit-style.css | 14 +++++++++++++- scripts/system/html/entityList.html | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 8179b95e35..69fed02099 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -198,7 +198,7 @@ td.url { } -input[type="text"], input[type="number"], textarea { +input[type="text"], input[type="search"], input[type="number"], textarea { margin: 0; padding: 0 12px; color: #afafaf; @@ -257,6 +257,18 @@ input[type="text"] { width: 100%; } +input[type="search"] { + height: 28px; + width: 100%; +} +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; + height: 20px; + width: 20px; + border-radius:10px; + background-image: url('data:image/png;base64,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') +} + input[type="number"] { position: relative; height: 28px; diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index e301f36945..626402362d 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -43,7 +43,7 @@

- Y + Y
From 978cd4bb7764cb4bbc017da1164043d0f509d75c Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 12 Oct 2018 18:57:26 -0700 Subject: [PATCH 104/362] styling tweaks, fix X clear search not refreshing --- scripts/system/html/css/edit-style.css | 15 ++++++++------- scripts/system/html/entityList.html | 2 +- scripts/system/html/js/entityList.js | 9 +-------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 69fed02099..699c27448a 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -265,7 +265,6 @@ input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; height: 20px; width: 20px; - border-radius:10px; background-image: url('data:image/png;base64,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') } @@ -1084,11 +1083,13 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { height: 25px; } #filter-type-checkboxes span { + position: relative; + top: 3px; font-family: hifi-glyphs; font-size: 16px; color: #404040; - padding: 12px 12px; - vertical-align: middle; + padding-left: 12px; + padding-right: 12px; } #filter-type-checkboxes label { position: relative; @@ -1098,16 +1099,16 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { font-family: FiraSans-SemiBold; color: #404040; background-color: #afafaf; - padding: 0 20px; - height: 25px; - vertical-align: middle; + padding-top: 1px; + padding-right: 12px; + height: 24px; } #filter-type-checkboxes label:hover { background-color: #1e90ff; } #filter-type-checkboxes input[type=checkbox] { position: relative; - top: 4px; + top: 6px; right: -10px; z-index: 4; display: block; diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 626402362d..06c2be8e73 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -39,7 +39,7 @@
- +
diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index f780fd0e2e..0f3f27a547 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -201,9 +201,7 @@ function loaded() { EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); } elFilterSearch.onkeyup = refreshEntityList; - elFilterSearch.onpaste = refreshEntityList; - elFilterSearch.onchange = onFilterChange; - elFilterSearch.onblur = refreshFooter; + elFilterSearch.onsearch = refreshEntityList; elFilterInView.onclick = toggleFilterInView; elFilterRadius.onchange = onRadiusChange; elInfoToggle.onclick = toggleInfo; @@ -650,11 +648,6 @@ function loaded() { refreshEntities(); } - function onFilterChange() { - refreshEntityList(); - entityList.resize(); - } - function onRadiusChange() { elFilterRadius.value = Math.max(elFilterRadius.value, 0); elNoEntitiesRadius.firstChild.nodeValue = elFilterRadius.value; From ad09007bd8f20f62618871b105bd288551534d26 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Sat, 13 Oct 2018 16:32:51 -0700 Subject: [PATCH 105/362] Fix flickering holded entity, renames and refactors --- interface/src/avatar/AvatarActionHold.cpp | 4 +- interface/src/avatar/AvatarManager.cpp | 26 ++++--- interface/src/avatar/AvatarManager.h | 2 +- .../src/avatars-renderer/Avatar.cpp | 73 +++++++++---------- .../src/avatars-renderer/Avatar.h | 20 +++-- 5 files changed, 64 insertions(+), 61 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 230f8aa64b..53074ac4ba 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -135,7 +135,7 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm:: glm::vec3 palmPosition; glm::quat palmRotation; - bool isTransitingWithAvatar = holdingAvatar->getTransit()->isTransiting(); + bool isTransitingWithAvatar = holdingAvatar->getTransit()->isActive(); if (isTransitingWithAvatar != _isTransitingWithAvatar) { _isTransitingWithAvatar = isTransitingWithAvatar; auto ownerEntity = _ownerEntity.lock(); @@ -424,7 +424,7 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) { if (ownerEntity) { ownerEntity->setDynamicDataDirty(true); ownerEntity->setDynamicDataNeedsTransmit(true); - ownerEntity->setTransitingWithAvatar(myAvatar->getTransit()->isTransiting()); + ownerEntity->setTransitingWithAvatar(myAvatar->getTransit()->isActive()); } }); } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 5fb9e0e0af..c1fddaa680 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -142,14 +142,14 @@ void AvatarManager::setSpace(workload::SpacePointer& space ) { _space = space; } -void AvatarManager::playTransitAnimations(AvatarTransit::Status status) { +void AvatarManager::handleTransitAnimations(AvatarTransit::Status status) { auto startAnimation = _transitConfig._startTransitAnimation; auto middleAnimation = _transitConfig._middleTransitAnimation; auto endAnimation = _transitConfig._endTransitAnimation; const float REFERENCE_FPS = 30.0f; switch (status) { - case AvatarTransit::Status::START_FRAME: + case AvatarTransit::Status::STARTED: qDebug() << "START_FRAME"; _myAvatar->overrideAnimation(startAnimation._animationUrl, REFERENCE_FPS, false, startAnimation._firstFrame, startAnimation._firstFrame + startAnimation._frameCount); @@ -164,18 +164,20 @@ void AvatarManager::playTransitAnimations(AvatarTransit::Status status) { _myAvatar->overrideAnimation(endAnimation._animationUrl, REFERENCE_FPS, false, endAnimation._firstFrame, endAnimation._firstFrame + endAnimation._frameCount); break; - case AvatarTransit::Status::END_FRAME: + case AvatarTransit::Status::ENDED: qDebug() << "END_FRAME"; _myAvatar->restoreAnimation(); break; - case AvatarTransit::Status::PRE_TRANSIT_IDLE: + case AvatarTransit::Status::PRE_TRANSIT: break; - case AvatarTransit::Status::POST_TRANSIT_IDLE: + case AvatarTransit::Status::POST_TRANSIT: break; case AvatarTransit::Status::IDLE: break; case AvatarTransit::Status::TRANSITING: break; + case AvatarTransit::Status::ABORT_TRANSIT: + break; } } @@ -184,9 +186,10 @@ void AvatarManager::updateMyAvatar(float deltaTime) { PerformanceWarning warn(showWarnings, "AvatarManager::updateMyAvatar()"); AvatarTransit::Status status = _myAvatar->updateTransit(deltaTime, _myAvatar->getNextPosition(), _transitConfig); - if (status != AvatarTransit::Status::IDLE && status != AvatarTransit::Status::PRE_TRANSIT_IDLE && status != AvatarTransit::Status::POST_TRANSIT_IDLE) { - playTransitAnimations(status); - } + handleTransitAnimations(status); + + bool sendFirstTransitPacket = (status == AvatarTransit::Status::STARTED); + bool sendCurrentTransitPacket = (status != AvatarTransit::Status::IDLE && (status != AvatarTransit::Status::POST_TRANSIT || status != AvatarTransit::Status::ENDED)); _myAvatar->update(deltaTime); render::Transaction transaction; @@ -196,9 +199,14 @@ void AvatarManager::updateMyAvatar(float deltaTime) { quint64 now = usecTimestampNow(); quint64 dt = now - _lastSendAvatarDataTime; - if (dt > MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS && !_myAvatarDataPacketsPaused) { + if (sendFirstTransitPacket || (dt > MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS && !_myAvatarDataPacketsPaused)) { // send head/hand data to the avatar mixer and voxel server PerformanceTimer perfTimer("send"); + if (sendFirstTransitPacket) { + _myAvatar->overrideNextPacketPositionData(_myAvatar->getTransit()->getEndPosition()); + } else if (sendCurrentTransitPacket) { + _myAvatar->overrideNextPacketPositionData(_myAvatar->getTransit()->getCurrentPosition()); + } _myAvatar->sendAvatarDataPacket(); _lastSendAvatarDataTime = now; _myAvatarSendRate.increment(); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 0dc39e991b..0f82aa7329 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -213,7 +213,7 @@ private: // frequently grabs a read lock on the hash to get a given avatar by ID void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override; - void playTransitAnimations(AvatarTransit::Status status); + void handleTransitAnimations(AvatarTransit::Status status); QVector _avatarsToFade; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index d8587b6086..b2747277c9 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -114,10 +114,12 @@ void Avatar::setShowNamesAboveHeads(bool show) { } AvatarTransit::Status AvatarTransit::update(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config) { - float oneFrameDistance = _isTransiting ? glm::length(avatarPosition - _endPosition) : glm::length(avatarPosition - _lastPosition); - const float MAX_TRANSIT_DISTANCE = 30.0f; - float scaledMaxTransitDistance = MAX_TRANSIT_DISTANCE * _scale; - if (oneFrameDistance > config._triggerDistance) { + bool checkDistance = (!_isActive || (_status == Status::POST_TRANSIT || _status == Status::ENDED)); + float oneFrameDistance = _isActive ? glm::length(avatarPosition - _endPosition) : glm::length(avatarPosition - _lastPosition); + + const float TRANSIT_TRIGGER_MAX_DISTANCE = 30.0f; + float scaledMaxTransitDistance = TRANSIT_TRIGGER_MAX_DISTANCE * _scale; + if (oneFrameDistance > config._triggerDistance && checkDistance) { if (oneFrameDistance < scaledMaxTransitDistance) { start(deltaTime, _lastPosition, avatarPosition, config); } else { @@ -127,9 +129,12 @@ AvatarTransit::Status AvatarTransit::update(float deltaTime, const glm::vec3& av } _lastPosition = avatarPosition; _status = updatePosition(deltaTime); - if (_isTransiting && oneFrameDistance > 0.1f && _status == Status::POST_TRANSIT_IDLE) { + + const float SETTLE_ABORT_DISTANCE = 0.1f; + float scaledSettleAbortDistance = SETTLE_ABORT_DISTANCE * _scale; + if (_isActive && oneFrameDistance > scaledSettleAbortDistance && _status == Status::POST_TRANSIT) { reset(); - _status = Status::END_FRAME; + _status = Status::ENDED; } return _status; } @@ -137,7 +142,7 @@ AvatarTransit::Status AvatarTransit::update(float deltaTime, const glm::vec3& av void AvatarTransit::reset() { _lastPosition = _endPosition; _currentPosition = _endPosition; - _isTransiting = false; + _isActive = false; } void AvatarTransit::start(float deltaTime, const glm::vec3& startPosition, const glm::vec3& endPosition, const AvatarTransit::TransitConfig& config) { _startPosition = startPosition; @@ -147,14 +152,14 @@ void AvatarTransit::start(float deltaTime, const glm::vec3& startPosition, const _totalDistance = glm::length(_transitLine); _easeType = config._easeType; const float REFERENCE_FRAMES_PER_SECOND = 30.0f; - _preTime = (float)config._startTransitAnimation._frameCount / REFERENCE_FRAMES_PER_SECOND; - _postTime = (float)config._endTransitAnimation._frameCount / REFERENCE_FRAMES_PER_SECOND; + _preTransitTime = (float)config._startTransitAnimation._frameCount / REFERENCE_FRAMES_PER_SECOND; + _postTransitTime = (float)config._endTransitAnimation._frameCount / REFERENCE_FRAMES_PER_SECOND; int transitFrames = (!config._isDistanceBased) ? config._totalFrames : config._framesPerMeter * _totalDistance; _transitTime = (float)transitFrames / REFERENCE_FRAMES_PER_SECOND; - _totalTime = _transitTime + _preTime + _postTime; - _currentTime = _isTransiting ? _preTime : 0.0f; - _isTransiting = true; + _totalTime = _transitTime + _preTransitTime + _postTransitTime; + _currentTime = _isActive ? _preTransitTime : 0.0f; + _isActive = true; } float AvatarTransit::getEaseValue(AvatarTransit::EaseType type, float value) { @@ -177,29 +182,29 @@ float AvatarTransit::getEaseValue(AvatarTransit::EaseType type, float value) { AvatarTransit::Status AvatarTransit::updatePosition(float deltaTime) { Status status = Status::IDLE; - if (_isTransiting) { + if (_isActive) { float nextTime = _currentTime + deltaTime; - if (nextTime < _preTime) { + if (nextTime < _preTransitTime) { _currentPosition = _startPosition; - status = Status::PRE_TRANSIT_IDLE; + status = Status::PRE_TRANSIT; if (_currentTime == 0) { - status = Status::START_FRAME; + status = Status::STARTED; } - } else if (nextTime < _totalTime - _postTime){ + } else if (nextTime < _totalTime - _postTransitTime){ status = Status::TRANSITING; - if (_currentTime <= _preTime) { + if (_currentTime <= _preTransitTime) { status = Status::START_TRANSIT; } else { - float percentageIntoTransit = (nextTime - _preTime) / _transitTime; + float percentageIntoTransit = (nextTime - _preTransitTime) / _transitTime; _currentPosition = _startPosition + getEaseValue(_easeType, percentageIntoTransit) * _transitLine; } } else { - status = Status::POST_TRANSIT_IDLE; + status = Status::POST_TRANSIT; _currentPosition = _endPosition; if (nextTime >= _totalTime) { - _isTransiting = false; - status = Status::END_FRAME; - } else if (_currentTime < _totalTime - _postTime) { + _isActive = false; + status = Status::ENDED; + } else if (_currentTime < _totalTime - _postTransitTime) { status = Status::END_TRANSIT; } } @@ -208,11 +213,6 @@ AvatarTransit::Status AvatarTransit::updatePosition(float deltaTime) { return status; } -bool AvatarTransit::getNextPosition(glm::vec3& nextPosition) { - nextPosition = _currentPosition; - return _isTransiting; -} - Avatar::Avatar(QThread* thread) : _voiceSphereID(GeometryCache::UNKNOWN_ID) { @@ -554,14 +554,11 @@ void Avatar::relayJointDataToChildren() { void Avatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "simulate"); - if (_transit.isTransiting()) { - glm::vec3 nextPosition; - if (_transit.getNextPosition(nextPosition)) { - _globalPosition = nextPosition; - _globalPositionChanged = usecTimestampNow(); - if (!hasParent()) { - setLocalPosition(nextPosition); - } + if (_transit.isActive()) { + _globalPosition = _transit.getCurrentPosition(); + _globalPositionChanged = usecTimestampNow(); + if (!hasParent()) { + setLocalPosition(_transit.getCurrentPosition()); } } @@ -575,7 +572,7 @@ void Avatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "updateJoints"); if (inView) { Head* head = getHead(); - if (_hasNewJointData || _transit.isTransiting()) { + if (_hasNewJointData || _transit.isActive()) { _skeletonModel->getRig().copyJointsFromJointData(_jointData); glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset()); _skeletonModel->getRig().computeExternalPoses(rootTransform); @@ -2006,7 +2003,7 @@ void Avatar::setTransitScale(float scale) { return _transit.setScale(scale); } -void Avatar::overrideNextPackagePositionData(const glm::vec3& position) { +void Avatar::overrideNextPacketPositionData(const glm::vec3& position) { std::lock_guard lock(_transitLock); _overrideGlobalPosition = true; _globalPositionOverride = position; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index fe163b9dae..e4fd667c1f 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -54,13 +54,13 @@ class AvatarTransit { public: enum Status { IDLE = 0, - START_FRAME, - PRE_TRANSIT_IDLE, + STARTED, + PRE_TRANSIT, START_TRANSIT, TRANSITING, END_TRANSIT, - POST_TRANSIT_IDLE, - END_FRAME, + POST_TRANSIT, + ENDED, ABORT_TRANSIT }; @@ -95,11 +95,9 @@ public: AvatarTransit() {}; Status update(float deltaTime, const glm::vec3& avatarPosition, const TransitConfig& config); Status getStatus() { return _status; } - bool isTransiting() { return _isTransiting; } + bool isActive() { return _isActive; } glm::vec3 getCurrentPosition() { return _currentPosition; } - bool getNextPosition(glm::vec3& nextPosition); glm::vec3 getEndPosition() { return _endPosition; } - float getTransitTime() { return _totalTime; } void setScale(float scale) { _scale = scale; } void reset(); @@ -107,7 +105,7 @@ private: Status updatePosition(float deltaTime); void start(float deltaTime, const glm::vec3& startPosition, const glm::vec3& endPosition, const TransitConfig& config); float getEaseValue(AvatarTransit::EaseType type, float value); - bool _isTransiting { false }; + bool _isActive { false }; glm::vec3 _startPosition; glm::vec3 _endPosition; @@ -117,10 +115,10 @@ private: glm::vec3 _transitLine; float _totalDistance { 0.0f }; - float _preTime { 0.0f }; + float _preTransitTime { 0.0f }; float _totalTime { 0.0f }; float _transitTime { 0.0f }; - float _postTime { 0.0f }; + float _postTransitTime { 0.0f }; float _currentTime { 0.0f }; EaseType _easeType { EaseType::EASE_OUT }; Status _status { Status::IDLE }; @@ -452,7 +450,7 @@ public: AvatarTransit::Status updateTransit(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config); void setTransitScale(float scale); - void overrideNextPackagePositionData(const glm::vec3& position); + void overrideNextPacketPositionData(const glm::vec3& position); signals: void targetScaleChanged(float targetScale); From 8e272d6dd27c95a9a2a73dc14c6e7755a97d9887 Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 15 Oct 2018 08:43:25 -0700 Subject: [PATCH 106/362] added the correction code for when you are misslabeled as sitting while you are standing. It will now handle a sit down correctly. Also made the first approximation of state based on the average user height. This should use the user input of their height associated with their account --- interface/src/avatar/MyAvatar.cpp | 12 ++++++++++++ interface/src/avatar/MyAvatar.h | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 172fdac96c..458c3ee422 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -546,6 +546,18 @@ void MyAvatar::update(float deltaTime) { _averageUserHeightCount = 1; setIsInSittingState(false); } + } else if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) && (angleSpine2 > COSINE_THIRTY_DEGREES)) { + _sitStandStateCount++; + if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sitStandStateCount = 0; + _squatCount = 0; + if (newHeightReading.isValid()) { + _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; + _tippingPoint = newHeightReading.getTranslation().y; + } + _averageUserHeightCount = 1; + setIsInSittingState(true); + } } else { _sitStandStateCount = 0; // tipping point is average height when sitting. diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 10b5510e4d..61eed672ef 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1846,7 +1846,7 @@ private: ThreadSafeValueCache _isInSittingState { false }; int _sitStandStateCount { 0 }; int _squatCount { 0 }; - float _tippingPoint { DEFAULT_FLOOR_HEIGHT }; + float _tippingPoint { DEFAULT_AVATAR_HEIGHT }; // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; From ae3ae9ce9a081e7b41d544f038655adda975b367 Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 15 Oct 2018 09:52:34 -0700 Subject: [PATCH 107/362] put in condition for away mode so that when you take the hmd off and put it back on the initial guess to your state is made again. --- interface/src/avatar/MyAvatar.cpp | 102 +++++++++++++++++------------- 1 file changed, 57 insertions(+), 45 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 458c3ee422..4e347eebbb 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -530,58 +530,70 @@ void MyAvatar::update(float deltaTime) { glm::vec3 worldHips = transformPoint(getTransform().getMatrix(), avatarHips); glm::vec3 sensorHips = transformPoint(glm::inverse(getSensorToWorldMatrix()), worldHips); + const int VELOCITY_COUNT_THRESHOLD = 60; // put update sit stand state counts here - if (!getIsSitStandStateLocked() && (_follow._velocityCount > 60)) { - if (getIsInSittingState()) { - if (newHeightReading.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { - // if we recenter upwards then no longer in sitting state - _sitStandStateCount++; - if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { - _sitStandStateCount = 0; - _squatCount = 0; - if (newHeightReading.isValid()) { - _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; - _tippingPoint = newHeightReading.getTranslation().y; + if (!getIsSitStandStateLocked() && (_follow._velocityCount > VELOCITY_COUNT_THRESHOLD)) { + if (!getIsAway()) { + if (getIsInSittingState()) { + if (newHeightReading.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { + // if we recenter upwards then no longer in sitting state + _sitStandStateCount++; + if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sitStandStateCount = 0; + _squatCount = 0; + if (newHeightReading.isValid()) { + _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; + _tippingPoint = newHeightReading.getTranslation().y; + } + _averageUserHeightCount = 1; + setIsInSittingState(false); } - _averageUserHeightCount = 1; - setIsInSittingState(false); - } - } else if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) && (angleSpine2 > COSINE_THIRTY_DEGREES)) { - _sitStandStateCount++; - if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { - _sitStandStateCount = 0; - _squatCount = 0; - if (newHeightReading.isValid()) { - _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; - _tippingPoint = newHeightReading.getTranslation().y; + } else if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) && (angleSpine2 > COSINE_THIRTY_DEGREES)) { + _sitStandStateCount++; + if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sitStandStateCount = 0; + _squatCount = 0; + if (newHeightReading.isValid()) { + _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; + _tippingPoint = newHeightReading.getTranslation().y; + } + _averageUserHeightCount = 1; + setIsInSittingState(true); } - _averageUserHeightCount = 1; - setIsInSittingState(true); + } else { + _sitStandStateCount = 0; + // tipping point is average height when sitting. + _tippingPoint = averageSensorSpaceHeight; } } else { - _sitStandStateCount = 0; - // tipping point is average height when sitting. - _tippingPoint = averageSensorSpaceHeight; + // in the standing state + if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) && (angleSpine2 > COSINE_THIRTY_DEGREES)) { + _sitStandStateCount++; + if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sitStandStateCount = 0; + _squatCount = 0; + if (newHeightReading.isValid()) { + _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; + _tippingPoint = newHeightReading.getTranslation().y; + } + _averageUserHeightCount = 1; + setIsInSittingState(true); + } + } else { + // use the mode height for the tipping point when we are standing. + _tippingPoint = getCurrentStandingHeight(); + _sitStandStateCount = 0; + } } } else { - // in the standing state - if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) && (angleSpine2 > COSINE_THIRTY_DEGREES)) { - _sitStandStateCount++; - if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { - _sitStandStateCount = 0; - _squatCount = 0; - if (newHeightReading.isValid()) { - _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; - _tippingPoint = newHeightReading.getTranslation().y; - } - _averageUserHeightCount = 1; - setIsInSittingState(true); - } - } else { - // use the mode height for the tipping point when we are standing. - _tippingPoint = getCurrentStandingHeight(); - _sitStandStateCount = 0; - } + // if you are away then reset the average and set state to standing. + _squatCount = 0; + _sitStandStateCount = 0; + _follow._velocityCount = 0; + _averageUserHeightCount = 1; + _sumUserHeightSensorSpace = DEFAULT_AVATAR_HEIGHT; + _tippingPoint = DEFAULT_AVATAR_HEIGHT; + setIsInSittingState(false); } } From d9873d363322e5b15cb0e2c8e976e8595f3505ea Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 15 Oct 2018 11:33:42 -0700 Subject: [PATCH 108/362] Adding some debug stuff... --- libraries/audio-client/src/AudioClient.cpp | 48 ++++++++++++++-------- libraries/audio-client/src/AudioClient.h | 5 ++- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 858f6e738c..c2b066b716 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1095,7 +1095,8 @@ void AudioClient::handleAudioInput(QByteArray& audioBuffer) { void AudioClient::processAudioAndAddToRingBuffer(QByteArray& inputByteArray, const uchar& channelCount, - const qint32& bytesForDuration) { + const qint32& bytesForDuration, + QByteArray& rollingBuffer) { // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output const int inputSamplesRequired = (_inputToNetworkResampler ? _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) @@ -1131,6 +1132,7 @@ void AudioClient::processAudioAndAddToRingBuffer(QByteArray& inputByteArray, _stats.updateInputMsUnplayed(msecsInInputRingBuffer); QByteArray audioBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); + rollingBuffer.append(audioBuffer); handleAudioInput(audioBuffer); } } @@ -1144,8 +1146,10 @@ void AudioClient::handleMicAudioInput() { _inputReadsSinceLastCheck++; #endif + QByteArray temp; + processAudioAndAddToRingBuffer(_inputDevice->readAll(), _inputFormat.channelCount(), - _inputFormat.bytesForDuration(USECS_PER_MSEC)); + _inputFormat.bytesForDuration(USECS_PER_MSEC), temp); } void AudioClient::handleDummyAudioInput() { @@ -1161,9 +1165,7 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { handleAudioInput(audioBuffer); } - int rawToWav(const char* rawData, const int& rawLength, const char* wavfn, long frequency) { - long chunksize = 0x10; - +int rawToWav(const char* rawData, const int& rawLength, const char* wavfn, long frequency, unsigned short channels) { struct { unsigned short wFormatTag; unsigned short wChannels; @@ -1174,47 +1176,59 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { } fmt; long samplecount = rawLength / 2; - long riffsize = samplecount * 2 + 0x24; - long datasize = samplecount * 2; FILE* wav = fopen(wavfn, "wb"); if (!wav) { - return -3; + return -1; } fwrite("RIFF", 1, 4, wav); + + long riffsize = samplecount * 2 + 0x24; fwrite(&riffsize, 4, 1, wav); + fwrite("WAVEfmt ", 1, 8, wav); + + long chunksize = 0x10; fwrite(&chunksize, 4, 1, wav); - fmt.wFormatTag = 1; // PCM - fmt.wChannels = 1; // MONO + fmt.wFormatTag = 1; // WAVE_FORMAT_PCM + fmt.wChannels = channels; fmt.dwSamplesPerSec = frequency * 1; - fmt.dwAvgBytesPerSec = frequency * 1 * 2; // 16 bit - fmt.wBlockAlign = 2; fmt.wBitsPerSample = 16; - + fmt.wBlockAlign = fmt.wChannels * fmt.wBitsPerSample / 8; + fmt.dwAvgBytesPerSec = fmt.dwSamplesPerSec * fmt.wBlockAlign; fwrite(&fmt, sizeof(fmt), 1, wav); + fwrite("data", 1, 4, wav); + long datasize = samplecount * 2; fwrite(&datasize, 4, 1, wav); fwrite(rawData, 1, rawLength, wav); + fclose(wav); + + return 0; } void AudioClient::handleTTSAudioInput(const QByteArray& audio) { QByteArray audioBuffer(audio); - QVector audioBufferReal; QString filename = QString::number(usecTimestampNow()); - QString path = PathUtils::getAppDataPath() + "Audio/" + filename + ".wav"; - rawToWav(audioBuffer.data(), audioBuffer.size(), path.toLocal8Bit(), 24000); + QString path = PathUtils::getAppDataPath() + "Audio/" + filename + "-before.wav"; + rawToWav(audioBuffer.data(), audioBuffer.size(), path.toLocal8Bit(), 24000, 1); + + QByteArray temp; while (audioBuffer.size() > 0) { QByteArray part; part.append(audioBuffer.data(), AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); audioBuffer.remove(0, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - processAudioAndAddToRingBuffer(part, 1, 48); + processAudioAndAddToRingBuffer(part, 1, 48, temp); } + + filename = QString::number(usecTimestampNow()); + path = PathUtils::getAppDataPath() + "Audio/" + filename + "-after.wav"; + rawToWav(temp.data(), temp.size(), path.toLocal8Bit(), 12000, 1); } void AudioClient::prepareLocalAudioInjectors(std::unique_ptr localAudioLock) { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 170a355abe..1ca7cac6ca 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -290,7 +290,10 @@ private: float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); - void processAudioAndAddToRingBuffer(QByteArray& inputByteArray, const uchar& channelCount, const qint32& bytesForDuration); + void processAudioAndAddToRingBuffer(QByteArray& inputByteArray, + const uchar& channelCount, + const qint32& bytesForDuration, + QByteArray& rollingBuffer); #ifdef Q_OS_ANDROID QTimer _checkInputTimer; From fede22499c137785b50cfbb26023847b76b55b3b Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Mon, 15 Oct 2018 11:58:13 -0700 Subject: [PATCH 109/362] Added app to control exponential filters on vive trackers, in real time --- scripts/developer/exponentialFilterApp.js | 240 ++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 scripts/developer/exponentialFilterApp.js diff --git a/scripts/developer/exponentialFilterApp.js b/scripts/developer/exponentialFilterApp.js new file mode 100644 index 0000000000..774ea95533 --- /dev/null +++ b/scripts/developer/exponentialFilterApp.js @@ -0,0 +1,240 @@ +var LEFT_HAND_INDEX = 0; +var RIGHT_HAND_INDEX = 1; +var LEFT_FOOT_INDEX = 2; +var RIGHT_FOOT_INDEX = 3; +var HIPS_INDEX = 4; +var SPINE2_INDEX = 5; + +var HAND_SMOOTHING_TRANSLATION = 0.3; +var HAND_SMOOTHING_ROTATION = 0.15; +var FOOT_SMOOTHING_TRANSLATION = 0.3; +var FOOT_SMOOTHING_ROTATION = 0.15; +var TORSO_SMOOTHING_TRANSLATION = 0.3; +var TORSO_SMOOTHING_ROTATION = 0.16; + +var mappingJson = { + name: "com.highfidelity.testing.exponentialFilterApp", + channels: [ + { + from: "Standard.LeftHand", + to: "Actions.LeftHand", + filters: [ + { + type: "exponentialSmoothing", + translation: HAND_SMOOTHING_TRANSLATION, + rotation: HAND_SMOOTHING_ROTATION + } + ] + }, + { + from: "Standard.RightHand", + to: "Actions.RightHand", + filters: [ + { + type: "exponentialSmoothing", + translation: HAND_SMOOTHING_TRANSLATION, + rotation: HAND_SMOOTHING_ROTATION + } + ] + }, + { + from: "Standard.LeftFoot", + to: "Actions.LeftFoot", + filters: [ + { + type: "exponentialSmoothing", + translation: FOOT_SMOOTHING_TRANSLATION, + rotation: FOOT_SMOOTHING_ROTATION + } + ] + }, + { + from: "Standard.RightFoot", + to: "Actions.RightFoot", + filters: [ + { + type: "exponentialSmoothing", + translation: FOOT_SMOOTHING_TRANSLATION, + rotation: FOOT_SMOOTHING_ROTATION + } + ] + }, + { + from: "Standard.Hips", + to: "Actions.Hips", + filters: [ + { + type: "exponentialSmoothing", + translation: TORSO_SMOOTHING_TRANSLATION, + rotation: TORSO_SMOOTHING_ROTATION + } + ] + }, + { + from: "Standard.Spine2", + to: "Actions.Spine2", + filters: [ + { + type: "exponentialSmoothing", + translation: TORSO_SMOOTHING_TRANSLATION, + rotation: TORSO_SMOOTHING_ROTATION + } + ] + } + ] +}; + +// +// tablet app boiler plate +// + +var TABLET_BUTTON_NAME = "EXPFILT"; +var HTML_URL = "https://s3.amazonaws.com/hifi-public/tony/html/exponentialFilterApp.html?7"; + +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); +var tabletButton = tablet.addButton({ + text: TABLET_BUTTON_NAME, + icon: "https://s3.amazonaws.com/hifi-public/tony/icons/tpose-i.svg", + activeIcon: "https://s3.amazonaws.com/hifi-public/tony/icons/tpose-a.svg" +}); + +tabletButton.clicked.connect(function () { + if (shown) { + tablet.gotoHomeScreen(); + } else { + tablet.gotoWebScreen(HTML_URL); + } +}); + +var shown = false; + +function onScreenChanged(type, url) { + if (type === "Web" && url === HTML_URL) { + tabletButton.editProperties({isActive: true}); + if (!shown) { + // hook up to event bridge + tablet.webEventReceived.connect(onWebEventReceived); + shownChanged(true); + } + shown = true; + } else { + tabletButton.editProperties({isActive: false}); + if (shown) { + // disconnect from event bridge + tablet.webEventReceived.disconnect(onWebEventReceived); + shownChanged(false); + } + shown = false; + } +} + +function getTranslation(i) { + return mappingJson.channels[i].filters[0].translation; +} +function setTranslation(i, value) { + mappingJson.channels[i].filters[0].translation = value; + mappingChanged(); +} +function getRotation(i) { + return mappingJson.channels[i].filters[0].rotation; +} +function setRotation(i, value) { + mappingJson.channels[i].filters[0].rotation = value; mappingChanged(); +} + +function onWebEventReceived(msg) { + if (msg.name === "init-complete") { + var values = [ + {name: "enable-filtering", val: filterEnabled ? "on" : "off", checked: false}, + {name: "left-hand-translation", val: getTranslation(LEFT_HAND_INDEX), checked: false}, + {name: "left-hand-rotation", val: getRotation(LEFT_HAND_INDEX), checked: false}, + {name: "right-hand-translation", val: getTranslation(RIGHT_HAND_INDEX), checked: false}, + {name: "right-hand-rotation", val: getRotation(RIGHT_HAND_INDEX), checked: false}, + {name: "left-foot-translation", val: getTranslation(LEFT_FOOT_INDEX), checked: false}, + {name: "left-foot-rotation", val: getRotation(LEFT_FOOT_INDEX), checked: false}, + {name: "right-foot-translation", val: getTranslation(RIGHT_FOOT_INDEX), checked: false}, + {name: "right-foot-rotation", val: getRotation(RIGHT_FOOT_INDEX), checked: false}, + {name: "hips-translation", val: getTranslation(HIPS_INDEX), checked: false}, + {name: "hips-rotation", val: getRotation(HIPS_INDEX), checked: false}, + {name: "spine2-translation", val: getTranslation(SPINE2_INDEX), checked: false}, + {name: "spine2-rotation", val: getRotation(SPINE2_INDEX), checked: false} + ]; + tablet.emitScriptEvent(JSON.stringify(values)); + } else if (msg.name === "enable-filtering") { + if (msg.val === "on") { + filterEnabled = true; + } else if (msg.val === "off") { + filterEnabled = false; + } + mappingChanged(); + } else if (msg.name === "left-hand-translation") { + setTranslation(LEFT_HAND_INDEX, Number(msg.val)); + } else if (msg.name === "left-hand-rotation") { + setRotation(LEFT_HAND_INDEX, Number(msg.val)); + } else if (msg.name === "right-hand-translation") { + setTranslation(RIGHT_HAND_INDEX, Number(msg.val)); + } else if (msg.name === "right-hand-rotation") { + setRotation(RIGHT_HAND_INDEX, Number(msg.val)); + } else if (msg.name === "left-foot-translation") { + setTranslation(LEFT_FOOT_INDEX, Number(msg.val)); + } else if (msg.name === "left-foot-rotation") { + setRotation(LEFT_FOOT_INDEX, Number(msg.val)); + } else if (msg.name === "right-foot-translation") { + setTranslation(RIGHT_FOOT_INDEX, Number(msg.val)); + } else if (msg.name === "right-foot-rotation") { + setRotation(RIGHT_FOOT_INDEX, Number(msg.val)); + } else if (msg.name === "hips-translation") { + setTranslation(HIPS_INDEX, Number(msg.val)); + } else if (msg.name === "hips-rotation") { + setRotation(HIPS_INDEX, Number(msg.val)); + } else if (msg.name === "spine2-translation") { + setTranslation(SPINE2_INDEX, Number(msg.val)); + } else if (msg.name === "spine2-rotation") { + setRotation(SPINE2_INDEX, Number(msg.val)); + } +} + +tablet.screenChanged.connect(onScreenChanged); + +function shutdownTabletApp() { + tablet.removeButton(tabletButton); + if (shown) { + tablet.webEventReceived.disconnect(onWebEventReceived); + tablet.gotoHomeScreen(); + } + tablet.screenChanged.disconnect(onScreenChanged); +} + +// +// end tablet app boiler plate +// + +var filterEnabled = true; +var mapping; +function mappingChanged() { + if (mapping) { + mapping.disable(); + } + if (filterEnabled) { + mapping = Controller.parseMapping(JSON.stringify(mappingJson)); + mapping.enable(); + } +} + +function shownChanged(newShown) { + if (newShown) { + mappingChanged(); + } else { + mapping.disable(); + } +} + +mappingChanged(); + +Script.scriptEnding.connect(function() { + if (mapping) { + mapping.disable(); + } + tablet.removeButton(tabletButton); +}); + From 640ed6a32a23b4f9b0ebe980d7952ad47359f2bb Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Mon, 15 Oct 2018 12:42:07 -0700 Subject: [PATCH 110/362] Added "Developer > Avatar > Show Tracked Objects" menu --- interface/src/Application.cpp | 46 ++++++++++--------- interface/src/Application.h | 5 ++ interface/src/Menu.cpp | 2 + interface/src/Menu.h | 1 + .../animation/src/AnimInverseKinematics.cpp | 2 +- libraries/animation/src/Rig.cpp | 2 +- 6 files changed, 34 insertions(+), 24 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7457afe091..b05de598a0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5802,43 +5802,41 @@ void Application::update(float deltaTime) { myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix)); } - // AJT: TODO put a nice menu around this. - // Make sure to remove all markers when menu is turned off. - { + static const std::vector trackedObjectStringLiterals = { + QStringLiteral("_TrackedObject00"), QStringLiteral("_TrackedObject01"), QStringLiteral("_TrackedObject02"), QStringLiteral("_TrackedObject03"), + QStringLiteral("_TrackedObject04"), QStringLiteral("_TrackedObject05"), QStringLiteral("_TrackedObject06"), QStringLiteral("_TrackedObject07"), + QStringLiteral("_TrackedObject08"), QStringLiteral("_TrackedObject09"), QStringLiteral("_TrackedObject10"), QStringLiteral("_TrackedObject11"), + QStringLiteral("_TrackedObject12"), QStringLiteral("_TrackedObject13"), QStringLiteral("_TrackedObject14"), QStringLiteral("_TrackedObject15") + }; + + // Controlled by the Developer > Avatar > Show Tracked Objects menu. + if (_showTrackedObjects) { static const std::vector trackedObjectActions = { - controller::Action::TRACKED_OBJECT_00, - controller::Action::TRACKED_OBJECT_01, - controller::Action::TRACKED_OBJECT_02, - controller::Action::TRACKED_OBJECT_03, - controller::Action::TRACKED_OBJECT_04, - controller::Action::TRACKED_OBJECT_05, - controller::Action::TRACKED_OBJECT_06, - controller::Action::TRACKED_OBJECT_07, - controller::Action::TRACKED_OBJECT_08, - controller::Action::TRACKED_OBJECT_09, - controller::Action::TRACKED_OBJECT_10, - controller::Action::TRACKED_OBJECT_11, - controller::Action::TRACKED_OBJECT_12, - controller::Action::TRACKED_OBJECT_13, - controller::Action::TRACKED_OBJECT_14, - controller::Action::TRACKED_OBJECT_15 + controller::Action::TRACKED_OBJECT_00, controller::Action::TRACKED_OBJECT_01, controller::Action::TRACKED_OBJECT_02, controller::Action::TRACKED_OBJECT_03, + controller::Action::TRACKED_OBJECT_04, controller::Action::TRACKED_OBJECT_05, controller::Action::TRACKED_OBJECT_06, controller::Action::TRACKED_OBJECT_07, + controller::Action::TRACKED_OBJECT_08, controller::Action::TRACKED_OBJECT_09, controller::Action::TRACKED_OBJECT_10, controller::Action::TRACKED_OBJECT_11, + controller::Action::TRACKED_OBJECT_12, controller::Action::TRACKED_OBJECT_13, controller::Action::TRACKED_OBJECT_14, controller::Action::TRACKED_OBJECT_15 }; int i = 0; glm::vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f); for (auto& action : trackedObjectActions) { - QString key = QString("_TrackedObject%1").arg(i); controller::Pose pose = userInputMapper->getPoseState(action); if (pose.valid) { glm::vec3 pos = transformPoint(myAvatarMatrix, pose.translation); glm::quat rot = glmExtractRotation(myAvatarMatrix) * pose.rotation; - DebugDraw::getInstance().addMarker(key, rot, pos, BLUE); + DebugDraw::getInstance().addMarker(trackedObjectStringLiterals[i], rot, pos, BLUE); } else { - DebugDraw::getInstance().removeMarker(key); + DebugDraw::getInstance().removeMarker(trackedObjectStringLiterals[i]); } i++; } + } else if (_prevShowTrackedObjects) { + for (auto& key : trackedObjectStringLiterals) { + DebugDraw::getInstance().removeMarker(key); + } } + _prevShowTrackedObjects = _showTrackedObjects; } updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... @@ -8330,6 +8328,10 @@ void Application::setShowBulletConstraintLimits(bool value) { _physicsEngine->setShowBulletConstraintLimits(value); } +void Application::setShowTrackedObjects(bool value) { + _showTrackedObjects = value; +} + void Application::startHMDStandBySession() { _autoSwitchDisplayModeSupportedHMDPlugin->startStandBySession(); } diff --git a/interface/src/Application.h b/interface/src/Application.h index 75260b910f..739738e7e8 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -496,6 +496,8 @@ private slots: void setShowBulletConstraints(bool value); void setShowBulletConstraintLimits(bool value); + void setShowTrackedObjects(bool value); + private: void init(); bool handleKeyEventForFocusedEntityOrOverlay(QEvent* event); @@ -777,5 +779,8 @@ private: std::atomic _pendingRenderEvent { true }; bool quitWhenFinished { false }; + + bool _showTrackedObjects { false }; + bool _prevShowTrackedObjects { false }; }; #endif // hifi_Application_h diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index eef14c873e..8340cb8d20 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -570,6 +570,8 @@ Menu::Menu() { avatar.get(), SLOT(updateMotionBehaviorFromMenu()), UNSPECIFIED_POSITION, "Developer"); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowTrackedObjects, 0, false, qApp, SLOT(setShowTrackedObjects(bool))); + // Developer > Hands >>> MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands"); addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 031ee2561c..1e9955a760 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -183,6 +183,7 @@ namespace MenuOption { const QString RunClientScriptTests = "Run Client Script Tests"; const QString RunTimingTests = "Run Timing Tests"; const QString ScriptedMotorControl = "Enable Scripted Motor Control"; + const QString ShowTrackedObjects = "Show Tracked Objects"; const QString SendWrongDSConnectVersion = "Send wrong DS connect version"; const QString SendWrongProtocolVersion = "Send wrong protocol version"; const QString SetHomeLocation = "Set Home Location"; diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 399eaf3fab..a1809f3438 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -24,7 +24,7 @@ #include "AnimUtil.h" static const int MAX_TARGET_MARKERS = 30; -static const float JOINT_CHAIN_INTERP_TIME = 0.25f; +static const float JOINT_CHAIN_INTERP_TIME = 0.5f; static void lookupJointInfo(const AnimInverseKinematics::JointChainInfo& jointChainInfo, int indexA, int indexB, diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index d076ce5029..55cfbd6901 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1666,7 +1666,7 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo // This should help smooth out problems with the vive tracker when the sensor is occluded. if (prevHipsEnabled && hipsEstimated != prevHipsEstimated) { // blend from a snapshot of the previous hips. - const float HIPS_BLEND_DURATION = 0.3f; + const float HIPS_BLEND_DURATION = 0.5f; _hipsBlendHelper.setBlendDuration(HIPS_BLEND_DURATION); _hipsBlendHelper.setSnapshot(_previousControllerParameters.primaryControllerPoses[PrimaryControllerType_Hips]); } else if (!prevHipsEnabled) { From 738ac30f1dc880f1dd3c903554282b543176be12 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 15 Oct 2018 11:16:30 -0700 Subject: [PATCH 111/362] Update default entity properties in Create --- scripts/system/edit.js | 215 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 203 insertions(+), 12 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 27858722ec..96d3d763b0 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -42,6 +42,9 @@ var TITLE_OFFSET = 60; var CREATE_TOOLS_WIDTH = 490; var MAX_DEFAULT_ENTITY_LIST_HEIGHT = 942; +var IMAGE_MODEL = "https://hifi-content.s3.amazonaws.com/DomainContent/production/default-image-model.fbx"; +var DEFAULT_IMAGE = "https://hifi-content.s3.amazonaws.com/DomainContent/production/no-image.jpg"; + var createToolsWindow = new CreateWindow( Script.resourcesPath() + "qml/hifi/tablet/EditTools.qml", 'Create Tools', @@ -294,6 +297,187 @@ function checkEditPermissionsAndUpdate() { } } +const DEFAULT_ENTITY_PROPERTIES = { + All: { + collisionless: true, + description: "", + rotation: { x: 0, y: 0, z: 0, w: 1 }, + collidesWith: "static,dynamic,kinematic,otherAvatar", + collisionSoundURL: "", + cloneable: false, + ignoreIK: true, + canCastShadow: true, + href: "", + script: "", + serverScripts:"", + velocity: { + x: 0, + y: 0, + z: 0 + }, + damping: 0, + angularVelocity: { + x: 0, + y: 0, + z: 0 + }, + angularDamping: 0, + restitution: 0.5, + friction: 0.5, + density: 1000, + gravity: { + x: 0, + y: 0, + z: 0 + }, + acceleration: { + x: 0, + y: 0, + z: 0 + }, + dynamic: false, + }, + Shape: { + shape: "Box", + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + color: { red: 0, green: 180, blue: 239 }, + }, + Text: { + text: "Text", + textColor: { red: 255, green: 255, blue: 255 }, + backgroundColor: { red: 0, green: 0, blue: 0 }, + lineHeight: 0.06, + faceCamera: false, + }, + Zone: { + flyingAllowed: true, + ghostingAllowed: true, + filter: "", + keyLightMode: "inherit", + keyLightColor: { red: 255, green: 255, blue: 255 }, + keyLight: { + intensity: 1.0, + direction: { + x: 0.0, + y: Math.PI / 4, + }, + castShadows: true + }, + ambientLightMode: "inherit", + ambientLight: { + ambientIntensity: 0.5, + ambientURL: "" + }, + hazeMode: "inherit", + haze: { + hazeRange: 1000, + hazeAltitudeEffect: false, + hazeBaseRef: 0, + hazeColor: { + red: 128, + green: 154, + blue: 129 + }, + hazeBackgroundBlend: 0, + hazeEnableGlare: false, + hazeGlareColor: { + red: 255, + green: 229, + blue: 179 + }, + }, + bloomMode: "inherit" + }, + Model: { + modelURL: "", + collisionShape: "none", + compoundShapeURL: "", + animation: { + url: "", + running: false, + allowTranslation: false, + loop: true, + hold: false, + currentFrame: 0, + firstFrame: 0, + lastFrame: 100000, + fps: 30.0, + } + }, + Image: { + dimensions: { + x: 0.5385, + y: 0.2819, + z: 0.0092 + }, + shapeType: "box", + collisionless: true, + modelURL: IMAGE_MODEL, + textures: JSON.stringify({ "tex.picture": DEFAULT_IMAGE }) + }, + Web: { + sourceUrl: "https://highfidelity.com/", + dpi: 30, + }, + Particles: { + lifespan: 1.5, + maxParticles: 10, + textures: "https://content.highfidelity.com/DomainContent/production/Particles/wispy-smoke.png", + emitRate: 5.5, + emitSpeed: 0, + speedSpread: 0, + emitDimensions: { x: 0, y: 0, z: 0 }, + emitOrientation: { x: 0, y: 0, z: 0, w: 1 }, + emitterShouldTrail: true, + particleRadius: 0.25, + radiusStart: 0, + radiusFinish: 0.1, + radiusSpread: 0, + particleColor: { + red: 255, + green: 255, + blue: 255 + }, + colorSpread: { + red: 0, + green: 0, + blue: 0 + }, + alpha: 0, + alphaStart: 1, + alphaFinish: 0, + alphaSpread: 0, + emitAcceleration: { + x: 0, + y: 2.5, + z: 0 + }, + accelerationSpread: { + x: 0, + y: 0, + z: 0 + }, + particleSpin: 0, + spinStart: 0, + spinFinish: 0, + spinSpread: 0, + rotateWithEntity: false, + azimuthStart: 0, + azimuthFinish: 0, + polarStart: -Math.PI, + polarFinish: Math.PI + }, + Light: { + color: { red: 255, green: 255, blue: 255 }, + intensity: 5.0, + falloffRadius: 1.0, + isSpotlight: false, + exponent: 1.0, + cutoff: 75.0, + dimensions: { x: 20, y: 20, z: 20 }, + }, +}; + var toolBar = (function () { var EDIT_SETTING = "io.highfidelity.isEditing"; // for communication with other scripts var that = {}, @@ -303,11 +487,29 @@ var toolBar = (function () { dialogWindow = null, tablet = null; + function applyProperties(originalProperties, newProperties) { + for (var key in newProperties) { + originalProperties[key] = newProperties[key]; + } + } function createNewEntity(properties) { var dimensions = properties.dimensions ? properties.dimensions : DEFAULT_DIMENSIONS; var position = getPositionToCreateEntity(); var entityID = null; + applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.All); + + var type = properties.type; + if (type == "Box" || type == "Sphere") { + applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.Shape); + } else if (type == "Image") { + properties.type = "Model"; + applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.Image); + } else { + applyProperties(properties, DEFAULT_ENTITY_PROPERTIES[type]); + } + + if (position !== null && position !== undefined) { var direction; if (Camera.mode === "entity" || Camera.mode === "independent") { @@ -712,19 +914,8 @@ var toolBar = (function () { }); addButton("newImageButton", function () { - var IMAGE_MODEL = "https://hifi-content.s3.amazonaws.com/DomainContent/production/default-image-model.fbx"; - var DEFAULT_IMAGE = "https://hifi-content.s3.amazonaws.com/DomainContent/production/no-image.jpg"; createNewEntity({ - type: "Model", - dimensions: { - x: 0.5385, - y: 0.2819, - z: 0.0092 - }, - shapeType: "box", - collisionless: true, - modelURL: IMAGE_MODEL, - textures: JSON.stringify({ "tex.picture": DEFAULT_IMAGE }) + type: "Image", }); }); From 9ec999e15e84b7eda15462efbe5ffb109955f9c4 Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 15 Oct 2018 16:43:03 -0700 Subject: [PATCH 112/362] added condition for start up in oculus that gets the correct starting height --- interface/src/avatar/MyAvatar.cpp | 104 +++++++++++++++++------------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 4e347eebbb..c809a2d614 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -471,7 +471,9 @@ void MyAvatar::update(float deltaTime) { const float STANDING_HEIGHT_MULTIPLE = 1.2f; const float SITTING_HEIGHT_MULTIPLE = 0.833f; const float COSINE_THIRTY_DEGREES = 0.866f; - const int SITTING_COUNT_THRESHOLD = 300; + const float COSINE_TEN_DEGREES = 0.9848f; + const int SITTING_COUNT_THRESHOLD = 100; + const int STANDING_COUNT_THRESHOLD = 10; const int SQUATTY_COUNT_THRESHOLD = 600; float tau = deltaTime / HMD_FACING_TIMESCALE; @@ -530,59 +532,70 @@ void MyAvatar::update(float deltaTime) { glm::vec3 worldHips = transformPoint(getTransform().getMatrix(), avatarHips); glm::vec3 sensorHips = transformPoint(glm::inverse(getSensorToWorldMatrix()), worldHips); + glm::vec3 headUp = newHeightReading.getRotation() * glm::vec3(0.0f, 1.0f, 0.0f); + if (glm::length(headUp) > 0.0f) { + headUp = glm::normalize(headUp); + } + float angleHeadUp = glm::dot(headUp, glm::vec3(0.0f, 1.0f, 0.0f)); + const int VELOCITY_COUNT_THRESHOLD = 60; // put update sit stand state counts here - if (!getIsSitStandStateLocked() && (_follow._velocityCount > VELOCITY_COUNT_THRESHOLD)) { + if (!getIsSitStandStateLocked()) { if (!getIsAway()) { - if (getIsInSittingState()) { - if (newHeightReading.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { - // if we recenter upwards then no longer in sitting state - _sitStandStateCount++; - if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { - _sitStandStateCount = 0; - _squatCount = 0; - if (newHeightReading.isValid()) { - _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; - _tippingPoint = newHeightReading.getTranslation().y; + if ((_follow._velocityCount > VELOCITY_COUNT_THRESHOLD) || (qApp->isHMDMode() && (qApp->getActiveDisplayPlugin()->getName() == "Oculus Rift"))) { + if (getIsInSittingState()) { + if (newHeightReading.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { + // if we recenter upwards then no longer in sitting state + _sitStandStateCount++; + if (_sitStandStateCount > STANDING_COUNT_THRESHOLD) { + _sitStandStateCount = 0; + _squatCount = 0; + if (newHeightReading.isValid()) { + _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; + _tippingPoint = newHeightReading.getTranslation().y; + } + _averageUserHeightCount = 1; + setIsInSittingState(false); } - _averageUserHeightCount = 1; - setIsInSittingState(false); - } - } else if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) && (angleSpine2 > COSINE_THIRTY_DEGREES)) { - _sitStandStateCount++; - if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { - _sitStandStateCount = 0; - _squatCount = 0; - if (newHeightReading.isValid()) { - _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; - _tippingPoint = newHeightReading.getTranslation().y; + } else if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) && (angleHeadUp > COSINE_THIRTY_DEGREES)) { + // if we are mis labelled as sitting but we are standing in the real world this will + // make sure that a real sit is still recognized so we won't be stuck in sitting unable to change state + _sitStandStateCount++; + if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sitStandStateCount = 0; + _squatCount = 0; + if (newHeightReading.isValid()) { + _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; + _tippingPoint = newHeightReading.getTranslation().y; + } + _averageUserHeightCount = 1; + // here we stay in sit state but reset the average height + setIsInSittingState(true); } - _averageUserHeightCount = 1; - setIsInSittingState(true); + } else { + _sitStandStateCount = 0; + // tipping point is average height when sitting. + _tippingPoint = averageSensorSpaceHeight; } } else { - _sitStandStateCount = 0; - // tipping point is average height when sitting. - _tippingPoint = averageSensorSpaceHeight; - } - } else { - // in the standing state - if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) && (angleSpine2 > COSINE_THIRTY_DEGREES)) { - _sitStandStateCount++; - if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { - _sitStandStateCount = 0; - _squatCount = 0; - if (newHeightReading.isValid()) { - _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; - _tippingPoint = newHeightReading.getTranslation().y; + // in the standing state + if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) && (angleHeadUp > COSINE_THIRTY_DEGREES)) { + _sitStandStateCount++; + if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sitStandStateCount = 0; + _squatCount = 0; + if (newHeightReading.isValid()) { + _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; + _tippingPoint = newHeightReading.getTranslation().y; + } + _averageUserHeightCount = 1; + setIsInSittingState(true); } - _averageUserHeightCount = 1; - setIsInSittingState(true); + } else { + // use the mode height for the tipping point when we are standing. + _tippingPoint = getCurrentStandingHeight(); + _sitStandStateCount = 0; } - } else { - // use the mode height for the tipping point when we are standing. - _tippingPoint = getCurrentStandingHeight(); - _sitStandStateCount = 0; } } } else { @@ -4240,6 +4253,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } else { if ((glm::length(myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD).getVelocity()) > MINIMUM_HMD_VELOCITY)) { _velocityCount++; + qCDebug(interfaceapp) << "velocity count is " << _velocityCount << " is away " << myAvatar.getIsAway() << " hmd mode "<< qApp->isHMDMode() << " " << qApp->getActiveDisplayPlugin()->getName(); } } } else { From ae3d5c148ae3882d74628994cbf3cd68c945f52c Mon Sep 17 00:00:00 2001 From: sam gateau Date: Mon, 15 Oct 2018 17:48:16 -0700 Subject: [PATCH 113/362] Debugging the tablet disappearing in hmd --- interface/src/ui/overlays/ModelOverlay.cpp | 20 ++++++++++++++++++-- libraries/render-utils/src/Model.cpp | 20 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 2379685252..5040842b3b 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -571,8 +571,24 @@ void ModelOverlay::locationChanged(bool tellPhysics) { // FIXME Start using the _renderTransform instead of calling for Transform and Dimensions from here, do the custom things needed in evalRenderTransform() if (_model && _model->isActive()) { - _model->setRotation(getWorldOrientation()); - _model->setTranslation(getWorldPosition()); + if (!_isLODEnabled) { + auto rot = _model->getRotation(); + auto tra = _model->getTranslation(); + + auto nrot = getWorldOrientation(); + auto ntra = getWorldPosition(); + if (glm::any(glm::notEqual(rot, nrot))) { + rot = nrot; + _model->setRotation(rot); + } + if (glm::any(glm::notEqual(tra, ntra))) { + tra = ntra; + _model->setTranslation(tra); + } + } else { + _model->setRotation(getWorldOrientation()); + _model->setTranslation(getWorldPosition()); + } } } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 8b50f2e420..2bc9d25aff 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -228,6 +228,10 @@ void Model::updateRenderItems() { bool isWireframe = self->isWireframe(); auto renderItemKeyGlobalFlags = self->getRenderItemKeyGlobalFlags(); + if (renderItemKeyGlobalFlags.isLODDisabled()) { + modelTransform.setScale(glm::vec3(1.0f)); + } + render::Transaction transaction; for (int i = 0; i < (int) self->_modelMeshRenderItemIDs.size(); i++) { @@ -247,6 +251,8 @@ void Model::updateRenderItems() { data.updateClusterBuffer(meshState.clusterMatrices); } + auto bound = data.getBound(); + Transform renderTransform = modelTransform; if (useDualQuaternionSkinning) { @@ -264,6 +270,15 @@ void Model::updateRenderItems() { } data.updateTransformForSkinnedMesh(renderTransform, modelTransform); + if (renderItemKeyGlobalFlags.isLODDisabled()) { + auto newBound = data.getBound(); + if (bound != newBound) { + data.updateTransformForSkinnedMesh(renderTransform, modelTransform); + } else { + data.updateTransformForSkinnedMesh(renderTransform, modelTransform); + } + } + data.updateKey(renderItemKeyGlobalFlags); data.setShapeKey(invalidatePayloadShapeKey, isWireframe, useDualQuaternionSkinning); }); @@ -893,7 +908,12 @@ void Model::updateRenderItemsKey(const render::ScenePointer& scene) { _needsFixupInScene = true; return; } + auto prevVal = _needsFixupInScene; auto renderItemsKey = _renderItemKeyGlobalFlags; + if (renderItemsKey.isLODDisabled()) { + _needsFixupInScene = true; + _needsFixupInScene = prevVal; + } render::Transaction transaction; foreach(auto item, _modelMeshRenderItemsMap.keys()) { transaction.updateItem(item, [renderItemsKey](ModelMeshPartPayload& data) { From 49b869c5e31c7d0b45ff51eaa54af28850cd26bf Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 15 Oct 2018 18:02:50 -0700 Subject: [PATCH 114/362] got rid of velocity count, now use 'away' to trigger when to start computing the sit stand state --- interface/src/avatar/MyAvatar.cpp | 15 +-------------- interface/src/avatar/MyAvatar.h | 1 - 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c809a2d614..66e8570743 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -542,7 +542,7 @@ void MyAvatar::update(float deltaTime) { // put update sit stand state counts here if (!getIsSitStandStateLocked()) { if (!getIsAway()) { - if ((_follow._velocityCount > VELOCITY_COUNT_THRESHOLD) || (qApp->isHMDMode() && (qApp->getActiveDisplayPlugin()->getName() == "Oculus Rift"))) { + if (qApp->isHMDMode()) { if (getIsInSittingState()) { if (newHeightReading.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { // if we recenter upwards then no longer in sitting state @@ -602,7 +602,6 @@ void MyAvatar::update(float deltaTime) { // if you are away then reset the average and set state to standing. _squatCount = 0; _sitStandStateCount = 0; - _follow._velocityCount = 0; _averageUserHeightCount = 1; _sumUserHeightSensorSpace = DEFAULT_AVATAR_HEIGHT; _tippingPoint = DEFAULT_AVATAR_HEIGHT; @@ -4244,18 +4243,6 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } } } - const int VELOCITY_COUNT_THRESHOLD = 60; - const float MINIMUM_HMD_VELOCITY = 0.1f; - if (_velocityCount > VELOCITY_COUNT_THRESHOLD) { - if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { - activate(Vertical); - } - } else { - if ((glm::length(myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD).getVelocity()) > MINIMUM_HMD_VELOCITY)) { - _velocityCount++; - qCDebug(interfaceapp) << "velocity count is " << _velocityCount << " is away " << myAvatar.getIsAway() << " hmd mode "<< qApp->isHMDMode() << " " << qApp->getActiveDisplayPlugin()->getName(); - } - } } else { if (!isActive(Rotation) && getForceActivateRotation()) { activate(Rotation); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 61eed672ef..e8d9090e03 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1760,7 +1760,6 @@ private: std::atomic _forceActivateVertical { false }; std::atomic _forceActivateHorizontal { false }; std::atomic _toggleHipsFollowing { true }; - int _velocityCount { 0 }; }; FollowHelper _follow; From 7d7fe8c08913de6fc852881df268984934c2c80a Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 15 Oct 2018 18:12:27 -0700 Subject: [PATCH 115/362] removed some cruft --- interface/src/avatar/MyAvatar.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 66e8570743..00a8e0f30e 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -510,6 +510,7 @@ void MyAvatar::update(float deltaTime) { setCurrentStandingHeight(computeStandingHeightMode(newHeightReading)); setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); } + float averageSensorSpaceHeight = _sumUserHeightSensorSpace / _averageUserHeightCount; // if the spine is straight and the head is below the default position by 5 cm then increment squatty count. const float SQUAT_THRESHOLD = 0.05f; @@ -526,12 +527,6 @@ void MyAvatar::update(float deltaTime) { _squatCount = 0; } - float averageSensorSpaceHeight = _sumUserHeightSensorSpace / _averageUserHeightCount; - - glm::vec3 avatarHips = getAbsoluteJointTranslationInObjectFrame(getJointIndex("Hips")); - glm::vec3 worldHips = transformPoint(getTransform().getMatrix(), avatarHips); - glm::vec3 sensorHips = transformPoint(glm::inverse(getSensorToWorldMatrix()), worldHips); - glm::vec3 headUp = newHeightReading.getRotation() * glm::vec3(0.0f, 1.0f, 0.0f); if (glm::length(headUp) > 0.0f) { headUp = glm::normalize(headUp); From 7da5fa9ea7fcba68b9c4ac46af3d558c5a0b3cba Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Tue, 16 Oct 2018 11:33:58 -0700 Subject: [PATCH 116/362] Transit animations won't play, they just get sent over the network --- interface/src/avatar/AvatarManager.cpp | 8 +- interface/src/avatar/MyAvatar.cpp | 17 ++++ interface/src/avatar/MyAvatar.h | 2 + libraries/animation/src/Rig.cpp | 120 +++++++++++++++++++++++-- libraries/animation/src/Rig.h | 8 ++ 5 files changed, 146 insertions(+), 9 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index c1fddaa680..373ae9980a 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -151,22 +151,22 @@ void AvatarManager::handleTransitAnimations(AvatarTransit::Status status) { switch (status) { case AvatarTransit::Status::STARTED: qDebug() << "START_FRAME"; - _myAvatar->overrideAnimation(startAnimation._animationUrl, REFERENCE_FPS, false, startAnimation._firstFrame, + _myAvatar->overrideNetworkAnimation(startAnimation._animationUrl, REFERENCE_FPS, false, startAnimation._firstFrame, startAnimation._firstFrame + startAnimation._frameCount); break; case AvatarTransit::Status::START_TRANSIT: qDebug() << "START_TRANSIT"; - _myAvatar->overrideAnimation(middleAnimation._animationUrl, REFERENCE_FPS, false, middleAnimation._firstFrame, + _myAvatar->overrideNetworkAnimation(middleAnimation._animationUrl, REFERENCE_FPS, false, middleAnimation._firstFrame, middleAnimation._firstFrame + middleAnimation._frameCount); break; case AvatarTransit::Status::END_TRANSIT: qDebug() << "END_TRANSIT"; - _myAvatar->overrideAnimation(endAnimation._animationUrl, REFERENCE_FPS, false, endAnimation._firstFrame, + _myAvatar->overrideNetworkAnimation(endAnimation._animationUrl, REFERENCE_FPS, false, endAnimation._firstFrame, endAnimation._firstFrame + endAnimation._frameCount); break; case AvatarTransit::Status::ENDED: qDebug() << "END_FRAME"; - _myAvatar->restoreAnimation(); + _myAvatar->restoreNetworkAnimation(); break; case AvatarTransit::Status::PRE_TRANSIT: break; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b347963cf1..152215e717 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1124,6 +1124,15 @@ void MyAvatar::overrideAnimation(const QString& url, float fps, bool loop, float _skeletonModel->getRig().overrideAnimation(url, fps, loop, firstFrame, lastFrame); } +void MyAvatar::overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "overrideNetworkAnimation", Q_ARG(const QString&, url), Q_ARG(float, fps), + Q_ARG(bool, loop), Q_ARG(float, firstFrame), Q_ARG(float, lastFrame)); + return; + } + _skeletonModel->getRig().overrideNetworkAnimation(url, fps, loop, firstFrame, lastFrame); +} + void MyAvatar::restoreAnimation() { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "restoreAnimation"); @@ -1132,6 +1141,14 @@ void MyAvatar::restoreAnimation() { _skeletonModel->getRig().restoreAnimation(); } +void MyAvatar::restoreNetworkAnimation() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "restoreNetworkAnimation"); + return; + } + _skeletonModel->getRig().restoreNetworkAnimation(); +} + QStringList MyAvatar::getAnimationRoles() { if (QThread::currentThread() != thread()) { QStringList result; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 16b765711a..9770a5bb1a 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -376,6 +376,7 @@ public: * }, 3000); */ Q_INVOKABLE void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); + Q_INVOKABLE void overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); /**jsdoc * The avatar animation system includes a set of default animations along with rules for how those animations are blended together with @@ -392,6 +393,7 @@ public: * }, 3000); */ Q_INVOKABLE void restoreAnimation(); + Q_INVOKABLE void restoreNetworkAnimation(); /**jsdoc * Each avatar has an avatar-animation.json file that defines which animations are used and how they are blended together with procedural data diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 91d4e0f9d3..b9e654964a 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -133,6 +133,43 @@ void Rig::overrideAnimation(const QString& url, float fps, bool loop, float firs _animVars.set("userAnimB", clipNodeEnum == UserAnimState::B); } +void Rig::overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { + UserAnimState::ClipNodeEnum clipNodeEnum; + if (_networkAnimState.clipNodeEnum == UserAnimState::None || _networkAnimState.clipNodeEnum == UserAnimState::B) { + clipNodeEnum = UserAnimState::A; + } + else { + clipNodeEnum = UserAnimState::B; + } + if (_networkNode) { + // find an unused AnimClip clipNode + _sendNetworkNode = true; + std::shared_ptr clip; + if (clipNodeEnum == UserAnimState::A) { + clip = std::dynamic_pointer_cast(_networkNode->findByName("userAnimA")); + } + else { + clip = std::dynamic_pointer_cast(_networkNode->findByName("userAnimB")); + } + if (clip) { + // set parameters + clip->setLoopFlag(loop); + clip->setStartFrame(firstFrame); + clip->setEndFrame(lastFrame); + const float REFERENCE_FRAMES_PER_SECOND = 30.0f; + float timeScale = fps / REFERENCE_FRAMES_PER_SECOND; + clip->setTimeScale(timeScale); + clip->loadURL(url); + } + } + // store current user anim state. + _networkAnimState = { clipNodeEnum, url, fps, loop, firstFrame, lastFrame }; + // notify the userAnimStateMachine the desired state. + _networkVars.set("userAnimNone", false); + _networkVars.set("userAnimA", clipNodeEnum == UserAnimState::A); + _networkVars.set("userAnimB", clipNodeEnum == UserAnimState::B); +} + void Rig::restoreAnimation() { if (_userAnimState.clipNodeEnum != UserAnimState::None) { _userAnimState.clipNodeEnum = UserAnimState::None; @@ -144,6 +181,17 @@ void Rig::restoreAnimation() { } } +void Rig::restoreNetworkAnimation() { + _sendNetworkNode = false; + if (_networkAnimState.clipNodeEnum != UserAnimState::None) { + _networkAnimState.clipNodeEnum = UserAnimState::None; + // notify the userAnimStateMachine the desired state. + _networkVars.set("userAnimNone", true); + _networkVars.set("userAnimA", false); + _networkVars.set("userAnimB", false); + } +} + QStringList Rig::getAnimationRoles() const { if (_animNode) { QStringList list; @@ -208,11 +256,17 @@ void Rig::restoreRoleAnimation(const QString& role) { void Rig::destroyAnimGraph() { _animSkeleton.reset(); _animLoader.reset(); + _networkLoader.reset(); _animNode.reset(); _internalPoseSet._relativePoses.clear(); _internalPoseSet._absolutePoses.clear(); _internalPoseSet._overridePoses.clear(); _internalPoseSet._overrideFlags.clear(); + _networkNode.reset(); + _networkPoseSet._relativePoses.clear(); + _networkPoseSet._absolutePoses.clear(); + _networkPoseSet._overridePoses.clear(); + _networkPoseSet._overrideFlags.clear(); _numOverrides = 0; _leftEyeJointChildren.clear(); _rightEyeJointChildren.clear(); @@ -229,14 +283,24 @@ void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOff _internalPoseSet._relativePoses.clear(); _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); + _networkPoseSet._relativePoses.clear(); + _networkPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); + buildAbsoluteRigPoses(_networkPoseSet._relativePoses, _networkPoseSet._absolutePoses); _internalPoseSet._overridePoses.clear(); _internalPoseSet._overridePoses = _animSkeleton->getRelativeDefaultPoses(); _internalPoseSet._overrideFlags.clear(); _internalPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false); + + _networkPoseSet._overridePoses.clear(); + _networkPoseSet._overridePoses = _animSkeleton->getRelativeDefaultPoses(); + + _networkPoseSet._overrideFlags.clear(); + _networkPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false); + _numOverrides = 0; buildAbsoluteRigPoses(_animSkeleton->getRelativeDefaultPoses(), _absoluteDefaultPoses); @@ -270,6 +334,18 @@ void Rig::reset(const FBXGeometry& geometry) { _internalPoseSet._overrideFlags.clear(); _internalPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false); + + _networkPoseSet._relativePoses.clear(); + _networkPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); + + buildAbsoluteRigPoses(_networkPoseSet._relativePoses, _networkPoseSet._absolutePoses); + + _networkPoseSet._overridePoses.clear(); + _networkPoseSet._overridePoses = _animSkeleton->getRelativeDefaultPoses(); + + _networkPoseSet._overrideFlags.clear(); + _networkPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false); + _numOverrides = 0; buildAbsoluteRigPoses(_animSkeleton->getRelativeDefaultPoses(), _absoluteDefaultPoses); @@ -1049,26 +1125,37 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons updateAnimationStateHandlers(); _animVars.setRigToGeometryTransform(_rigToGeometryTransform); - + if (_networkNode) { + _networkVars.setRigToGeometryTransform(_rigToGeometryTransform); + } AnimContext context(_enableDebugDrawIKTargets, _enableDebugDrawIKConstraints, _enableDebugDrawIKChains, getGeometryToRigTransform(), rigToWorldTransform); // evaluate the animation AnimVariantMap triggersOut; - + AnimVariantMap networkTriggersOut; _internalPoseSet._relativePoses = _animNode->evaluate(_animVars, context, deltaTime, triggersOut); + if (_networkNode) { + _networkPoseSet._relativePoses = _networkNode->evaluate(_networkVars, context, deltaTime, networkTriggersOut); + } if ((int)_internalPoseSet._relativePoses.size() != _animSkeleton->getNumJoints()) { // animations haven't fully loaded yet. _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); } + if ((int)_networkPoseSet._relativePoses.size() != _animSkeleton->getNumJoints()) { + // animations haven't fully loaded yet. + _networkPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); + } _lastAnimVars = _animVars; _animVars.clearTriggers(); _animVars = triggersOut; + _networkVars.clearTriggers(); + _networkVars = networkTriggersOut; _lastContext = context; } applyOverridePoses(); buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); - + buildAbsoluteRigPoses(_networkPoseSet._relativePoses, _networkPoseSet._absolutePoses); // copy internal poses to external poses { QWriteLocker writeLock(&_externalPoseSetLock); @@ -1707,9 +1794,12 @@ void Rig::initAnimGraph(const QUrl& url) { _animGraphURL = url; _animNode.reset(); + _networkNode.reset(); // load the anim graph _animLoader.reset(new AnimNodeLoader(url)); + _networkLoader.reset(new AnimNodeLoader(url)); + std::weak_ptr weakSkeletonPtr = _animSkeleton; connect(_animLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr](AnimNode::Pointer nodeIn) { _animNode = nodeIn; @@ -1740,6 +1830,26 @@ void Rig::initAnimGraph(const QUrl& url) { connect(_animLoader.get(), &AnimNodeLoader::error, [url](int error, QString str) { qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; }); + + connect(_networkLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr](AnimNode::Pointer nodeIn) { + _networkNode = nodeIn; + // abort load if the previous skeleton was deleted. + auto sharedSkeletonPtr = weakSkeletonPtr.lock(); + if (!sharedSkeletonPtr) { + return; + } + _networkNode->setSkeleton(sharedSkeletonPtr); + if (_networkAnimState.clipNodeEnum != UserAnimState::None) { + // restore the user animation we had before reset. + UserAnimState origState = _networkAnimState; + _networkAnimState = { UserAnimState::None, "", 30.0f, false, 0.0f, 0.0f }; + overrideNetworkAnimation(origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame); + } + // emit onLoadComplete(); + }); + connect(_networkLoader.get(), &AnimNodeLoader::error, [url](int error, QString str) { + qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; + }); } } @@ -1817,13 +1927,13 @@ void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { if (isIndexValid(i)) { // rotations are in absolute rig frame. glm::quat defaultAbsRot = geometryToRigPose.rot() * _animSkeleton->getAbsoluteDefaultPose(i).rot(); - data.rotation = _internalPoseSet._absolutePoses[i].rot(); + data.rotation = !_sendNetworkNode ? _internalPoseSet._absolutePoses[i].rot() : _networkPoseSet._absolutePoses[i].rot(); data.rotationIsDefaultPose = isEqual(data.rotation, defaultAbsRot); // translations are in relative frame but scaled so that they are in meters, // instead of geometry units. glm::vec3 defaultRelTrans = _geometryOffset.scale() * _animSkeleton->getRelativeDefaultPose(i).trans(); - data.translation = _geometryOffset.scale() * _internalPoseSet._relativePoses[i].trans(); + data.translation = _geometryOffset.scale() * (!_sendNetworkNode ? _internalPoseSet._relativePoses[i].trans() : _networkPoseSet._relativePoses[i].trans()); data.translationIsDefaultPose = isEqual(data.translation, defaultRelTrans); } else { data.translationIsDefaultPose = true; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 48f00d4e5d..e1012df029 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -113,7 +113,9 @@ public: void destroyAnimGraph(); void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); + void overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); void restoreAnimation(); + void restoreNetworkAnimation(); QStringList getAnimationRoles() const; void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); void restoreRoleAnimation(const QString& role); @@ -270,6 +272,7 @@ protected: // Only accessed by the main thread PoseSet _internalPoseSet; + PoseSet _networkPoseSet; // Copy of the _poseSet for external threads. PoseSet _externalPoseSet; @@ -301,9 +304,12 @@ protected: QUrl _animGraphURL; std::shared_ptr _animNode; + std::shared_ptr _networkNode; std::shared_ptr _animSkeleton; std::unique_ptr _animLoader; + std::unique_ptr _networkLoader; AnimVariantMap _animVars; + AnimVariantMap _networkVars; enum class RigRole { Idle = 0, @@ -350,6 +356,7 @@ protected: }; UserAnimState _userAnimState; + UserAnimState _networkAnimState; std::map _roleAnimStates; float _leftHandOverlayAlpha { 0.0f }; @@ -391,6 +398,7 @@ protected: int _rigId; bool _headEnabled { false }; + bool _sendNetworkNode { false }; AnimContext _lastContext; AnimVariantMap _lastAnimVars; From 50becb5c378e80da26740a2b4702923d71481c37 Mon Sep 17 00:00:00 2001 From: David Back Date: Tue, 16 Oct 2018 12:39:34 -0700 Subject: [PATCH 117/362] merge particle explorer into properties, resurrect sliders, other tidying/fixes --- .../resources/qml/hifi/tablet/EditTabView.qml | 20 +- .../qml/hifi/tablet/EditToolsTabView.qml | 23 +- scripts/system/edit.js | 77 +- scripts/system/html/css/edit-style.css | 68 +- scripts/system/html/entityProperties.html | 1 + scripts/system/html/js/entityProperties.js | 1074 ++++++++++++----- scripts/system/html/js/underscore-min.js | 6 + 7 files changed, 863 insertions(+), 406 deletions(-) create mode 100644 scripts/system/html/js/underscore-min.js diff --git a/interface/resources/qml/hifi/tablet/EditTabView.qml b/interface/resources/qml/hifi/tablet/EditTabView.qml index 4ac8755570..bf7dd3e66b 100644 --- a/interface/resources/qml/hifi/tablet/EditTabView.qml +++ b/interface/resources/qml/hifi/tablet/EditTabView.qml @@ -175,7 +175,7 @@ TabBar { method: "newEntityButtonClicked", params: { buttonName: "newParticleButton" } }); - editTabView.currentIndex = 4 + editTabView.currentIndex = 2 } } @@ -279,21 +279,6 @@ TabBar { } } - EditTabButton { - title: "P" - active: true - enabled: true - property string originalUrl: "" - - property Component visualItem: Component { - WebView { - id: particleExplorerWebView - url: Paths.defaultScripts + "/system/particle_explorer/particleExplorer.html" - enabled: true - } - } - } - function fromScript(message) { switch (message.method) { case 'selectTab': @@ -326,9 +311,6 @@ TabBar { case 'grid': editTabView.currentIndex = 3; break; - case 'particle': - editTabView.currentIndex = 4; - break; default: console.warn('Attempt to switch to invalid tab:', id); } diff --git a/interface/resources/qml/hifi/tablet/EditToolsTabView.qml b/interface/resources/qml/hifi/tablet/EditToolsTabView.qml index 00084b8ca9..13b1caf8fb 100644 --- a/interface/resources/qml/hifi/tablet/EditToolsTabView.qml +++ b/interface/resources/qml/hifi/tablet/EditToolsTabView.qml @@ -18,7 +18,6 @@ TabBar { readonly property int create: 0 readonly property int properties: 1 readonly property int grid: 2 - readonly property int particle: 3 } readonly property HifiConstants hifi: HifiConstants {} @@ -182,7 +181,7 @@ TabBar { method: "newEntityButtonClicked", params: { buttonName: "newParticleButton" } }); - editTabView.currentIndex = tabIndex.particle + editTabView.currentIndex = tabIndex.properties } } @@ -271,21 +270,6 @@ TabBar { } } - EditTabButton { - title: "P" - active: true - enabled: true - property string originalUrl: "" - - property Component visualItem: Component { - WebView { - id: particleExplorerWebView - url: Paths.defaultScripts + "/system/particle_explorer/particleExplorer.html" - enabled: true - } - } - } - function fromScript(message) { switch (message.method) { case 'selectTab': @@ -299,7 +283,7 @@ TabBar { // Changes the current tab based on tab index or title as input function selectTab(id) { if (typeof id === 'number') { - if (id >= tabIndex.create && id <= tabIndex.particle) { + if (id >= tabIndex.create && id <= tabIndex.grid) { editTabView.currentIndex = id; } else { console.warn('Attempt to switch to invalid tab:', id); @@ -315,9 +299,6 @@ TabBar { case 'grid': editTabView.currentIndex = tabIndex.grid; break; - case 'particle': - editTabView.currentIndex = tabIndex.particle; - break; default: console.warn('Attempt to switch to invalid tab:', id); } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 27858722ec..bcfc831d57 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -12,7 +12,7 @@ /* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager, Overlays, OverlayWebWindow, UserActivityLogger, Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, - progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool, OverlaySystemWindow */ + progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, OverlaySystemWindow */ (function() { // BEGIN LOCAL_SCOPE @@ -32,7 +32,6 @@ Script.include([ "libraries/gridTool.js", "libraries/entityList.js", "libraries/utils.js", - "particle_explorer/particleExplorerTool.js", "libraries/entityIconOverlayManager.js" ]); @@ -109,28 +108,6 @@ var entityListTool = new EntityListTool(shouldUseEditTabletApp); selectionManager.addEventListener(function () { selectionDisplay.updateHandles(); entityIconOverlayManager.updatePositions(); - - // Update particle explorer - var needToDestroyParticleExplorer = false; - if (selectionManager.selections.length === 1) { - var selectedEntityID = selectionManager.selections[0]; - if (selectedEntityID === selectedParticleEntityID) { - return; - } - var type = Entities.getEntityProperties(selectedEntityID, "type").type; - if (type === "ParticleEffect") { - selectParticleEntity(selectedEntityID); - } else { - needToDestroyParticleExplorer = true; - } - } else { - needToDestroyParticleExplorer = true; - } - - if (needToDestroyParticleExplorer && selectedParticleEntityID !== null) { - selectedParticleEntityID = null; - particleExplorerTool.destroyWebView(); - } }); var KEY_P = 80; //Key code for letter p used for Parenting hotkey. @@ -359,10 +336,6 @@ var toolBar = (function () { properties: properties }], [], true); - if (properties.type === "ParticleEffect") { - selectParticleEntity(entityID); - } - var POST_ADJUST_ENTITY_TYPES = ["Model"]; if (POST_ADJUST_ENTITY_TYPES.indexOf(properties.type) !== -1) { // Adjust position of entity per bounding box after it has been created and auto-resized. @@ -1178,13 +1151,6 @@ function mouseClickEvent(event) { orientation = MyAvatar.orientation; intersection = rayPlaneIntersection(pickRay, P, Quat.getForward(orientation)); - if (event.isShifted) { - particleExplorerTool.destroyWebView(); - } - if (properties.type !== "ParticleEffect") { - particleExplorerTool.destroyWebView(); - } - if (!event.isShifted) { selectionManager.setSelections([foundEntity]); } else { @@ -1604,8 +1570,6 @@ function deleteSelectedEntities() { if (SelectionManager.hasSelection()) { var deletedIDs = []; - selectedParticleEntityID = null; - particleExplorerTool.destroyWebView(); SelectionManager.saveProperties(); var savedProperties = []; var newSortedSelection = sortSelectedEntities(selectionManager.selections); @@ -2572,31 +2536,6 @@ propertyMenu.onSelectMenuItem = function (name) { var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace"); var propertiesTool = new PropertiesTool(); -var particleExplorerTool = new ParticleExplorerTool(createToolsWindow); -var selectedParticleEntityID = null; - -function selectParticleEntity(entityID) { - selectedParticleEntityID = entityID; - - var properties = Entities.getEntityProperties(entityID); - if (properties.emitOrientation) { - properties.emitOrientation = Quat.safeEulerAngles(properties.emitOrientation); - } - - particleExplorerTool.destroyWebView(); - particleExplorerTool.createWebView(); - - particleExplorerTool.setActiveParticleEntity(entityID); - - // Switch to particle explorer - var selectTabMethod = { method: 'selectTab', params: { id: 'particle' } }; - if (shouldUseEditTabletApp()) { - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - tablet.sendToQml(selectTabMethod); - } else { - createToolsWindow.sendToQml(selectTabMethod); - } -} entityListTool.webView.webEventReceived.connect(function(data) { try { @@ -2610,20 +2549,6 @@ entityListTool.webView.webEventReceived.connect(function(data) { parentSelectedEntities(); } else if (data.type === 'unparent') { unparentSelectedEntities(); - } else if (data.type === "selectionUpdate") { - var ids = data.entityIds; - if (ids.length === 1) { - if (Entities.getEntityProperties(ids[0], "type").type === "ParticleEffect") { - if (JSON.stringify(selectedParticleEntityID) === JSON.stringify(ids[0])) { - // This particle entity is already selected, so return - return; - } - // Destroy the old particles web view first - } else { - selectedParticleEntityID = 0; - particleExplorerTool.destroyWebView(); - } - } } }); diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index f69ace2401..8d334609a6 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -300,6 +300,28 @@ input[type=number].hover-down::-webkit-inner-spin-button:after { color: #ffffff; } +input[type=range] { + -webkit-appearance: none; + background: #2e2e2e; + height: 1.8rem; + border-radius: 1rem; +} +input[type=range]::-webkit-slider-thumb { + -webkit-appearance:none; + width: 0.6rem; + height: 1.8rem; + padding:0; + margin: 0; + background-color: #696969; + border-radius: 1rem; +} +input[type=range]::-webkit-slider-thumb:hover { + background-color: white; +} +input[type=range]:focus { /*#252525*/ + outline: none; +} + input.no-spin::-webkit-outer-spin-button, input.no-spin::-webkit-inner-spin-button { display: none; @@ -623,6 +645,15 @@ hr { margin-left: 10px; } +.property.range label{ + display: block; +} +.property.range input[type=number]{ + margin-left: 0.8rem; + width: 5.4rem; + height: 1.8rem; +} + .text label, .url label, .number label, .textarea label, .xy label, .wh label, .rgb label, .xyz label,.pyr label, .dropdown label, .gen label { float: left; margin-left: 1px; @@ -853,13 +884,13 @@ div.refresh input[type="button"] { color: #1080b8; } -.tuple .red:focus, .tuple .x:focus, .tuple .pitch:focus { +.tuple .red:focus, .tuple .x:focus, .tuple .pitch:focus, .tuple .width:focus { outline-color: #e2334d; } -.tuple .green:focus, .tuple .y:focus, .tuple .yaw:focus { +.tuple .green:focus, .tuple .y:focus, .tuple .yaw:focus, .tuple .height:focus { outline-color: #1ac567; } -tuple, .blue:focus, .tuple .z:focus, .tuple .roll:focus { +.tuple .blue:focus, .tuple .z:focus, .tuple .roll:focus { outline-color: #1080b8; } @@ -884,6 +915,37 @@ tuple, .blue:focus, .tuple .z:focus, .tuple .roll:focus { float: left; } +.property.texture { + display: block; +} +.property.texture input{ + margin: 0.4rem 0; +} +.texture-image img{ + padding: 0; + margin: 0; + width: 100%; + height: 100%; + display: none; +} +.texture-image { + display: block; + position: relative; + background-repeat: no-repeat; + background-position: center; + background-size: 100% 100%; + margin-top: 0.4rem; + height:128px; + width: 128px; + background-image: url('data:image/png;base64,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'); +} +.texture-image.no-texture { + background-image: url('data:image/png;base64,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'); +} +.texture-image.no-preview { + background-image: url('data:image/png;base64,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'); +} + .two-column { display: table; width: 100%; diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 93f80e19b3..370681339e 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -20,6 +20,7 @@ + diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 1d8a397f07..328a998dda 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -58,6 +58,11 @@ const GROUPS = [ type: "string", propertyID: "parentID", }, + { + label: "Parent Joint Index", + type: "number", + propertyID: "parentJointIndex", + }, { label: "Locked", glyph: "", @@ -116,7 +121,7 @@ const GROUPS = [ type: "number", min: 0, step: 0.005, - fixedDecimals: 4, + decimals: 4, unit: "m", propertyID: "lineHeight" }, @@ -165,14 +170,14 @@ const GROUPS = [ min: 0, max: 10, step: 0.1, - fixedDecimals: 2, + decimals: 2, propertyID: "keyLight.intensity", showPropertyRule: { "keyLightMode": "enabled" }, }, { label: "Light Altitude", type: "number", - fixedDecimals: 2, + decimals: 2, unit: "deg", propertyID: "keyLight.direction.y", showPropertyRule: { "keyLightMode": "enabled" }, @@ -180,7 +185,7 @@ const GROUPS = [ { label: "Light Azimuth", type: "number", - fixedDecimals: 2, + decimals: 2, unit: "deg", propertyID: "keyLight.direction.x", showPropertyRule: { "keyLightMode": "enabled" }, @@ -228,7 +233,7 @@ const GROUPS = [ min: 0, max: 10, step: 0.1, - fixedDecimals: 2, + decimals: 2, propertyID: "ambientLight.ambientIntensity", showPropertyRule: { "ambientLightMode": "enabled" }, }, @@ -250,7 +255,7 @@ const GROUPS = [ min: 5, max: 10000, step: 5, - fixedDecimals: 0, + decimals: 0, unit: "m", propertyID: "haze.hazeRange", showPropertyRule: { "hazeMode": "enabled" }, @@ -267,7 +272,7 @@ const GROUPS = [ min: -1000, max: 1000, step: 10, - fixedDecimals: 0, + decimals: 0, unit: "m", propertyID: "haze.hazeBaseRef", showPropertyRule: { "hazeMode": "enabled" }, @@ -278,7 +283,7 @@ const GROUPS = [ min: -1000, max: 5000, step: 10, - fixedDecimals: 0, + decimals: 0, unit: "m", propertyID: "haze.hazeCeiling", showPropertyRule: { "hazeMode": "enabled" }, @@ -291,11 +296,11 @@ const GROUPS = [ }, { label: "Background Blend", - type: "number", + type: "slider", min: 0.0, max: 1.0, step: 0.01, - fixedDecimals: 2, + decimals: 2, propertyID: "haze.hazeBackgroundBlend", showPropertyRule: { "hazeMode": "enabled" }, }, @@ -313,11 +318,11 @@ const GROUPS = [ }, { label: "Glare Angle", - type: "number", + type: "slider", min: 0, max: 180, step: 1, - fixedDecimals: 0, + decimals: 0, propertyID: "haze.hazeGlareAngle", showPropertyRule: { "hazeMode": "enabled" }, }, @@ -329,31 +334,31 @@ const GROUPS = [ }, { label: "Bloom Intensity", - type: "number", + type: "slider", min: 0, max: 1, step: 0.01, - fixedDecimals: 2, + decimals: 2, propertyID: "bloom.bloomIntensity", showPropertyRule: { "bloomMode": "enabled" }, }, { label: "Bloom Threshold", - type: "number", + type: "slider", min: 0, min: 1, step: 0.01, - fixedDecimals: 2, + decimals: 2, propertyID: "bloom.bloomThreshold", showPropertyRule: { "bloomMode": "enabled" }, }, { label: "Bloom Size", - type: "number", + type: "slider", min: 0, min: 2, step: 0.01, - fixedDecimals: 2, + decimals: 2, propertyID: "bloom.bloomSize", showPropertyRule: { "bloomMode": "enabled" }, }, @@ -469,7 +474,6 @@ const GROUPS = [ { id: "light", addToGroup: "base", - addToGroup: "base", properties: [ { label: "Light Color", @@ -482,7 +486,7 @@ const GROUPS = [ type: "number", min: 0, step: 0.1, - fixedDecimals: 1, + decimals: 1, propertyID: "intensity", }, { @@ -490,7 +494,7 @@ const GROUPS = [ type: "number", min: 0, step: 0.1, - fixedDecimals: 1, + decimals: 1, unit: "m", propertyID: "falloffRadius", }, @@ -503,14 +507,14 @@ const GROUPS = [ label: "Spotlight Exponent", type: "number", step: 0.01, - fixedDecimals: 2, + decimals: 2, propertyID: "exponent", }, { label: "Spotlight Cut-Off", type: "number", step: 0.01, - fixedDecimals: 2, + decimals: 2, propertyID: "cutoff", }, ] @@ -558,19 +562,21 @@ const GROUPS = [ { label: "Material Position", type: "vec2", + vec2Type: "xy", min: 0, min: 1, step: 0.1, - vec2Type: "xy", + decimals: 4, subLabels: [ "x", "y" ], propertyID: "materialMappingPos", }, { label: "Material Scale", type: "vec2", + vec2Type: "wh", min: 0, step: 0.1, - vec2Type: "wh", + decimals: 4, subLabels: [ "width", "height" ], propertyID: "materialMappingScale", }, @@ -578,12 +584,326 @@ const GROUPS = [ label: "Material Rotation", type: "number", step: 0.1, - fixedDecimals: 2, + decimals: 2, unit: "deg", propertyID: "materialMappingRot", }, ] }, + { + id: "particles", + addToGroup: "base", + properties: [ + { + label: "Emit", + type: "bool", + propertyID: "isEmitting", + }, + { + label: "Lifespan", + type: "slider", + unit: "s", + min: 0.01, + max: 10, + step: 0.01, + propertyID: "lifespan", + }, + { + label: "Max Particles", + type: "slider", + min: 1, + max: 10000, + step: 1, + propertyID: "maxParticles", + }, + { + label: "Texture", + type: "texture", + propertyID: "particleTextures", + propertyName: "textures", // actual entity property name + }, + ] + }, + { + id: "particles_emit", + label: "EMIT", + properties: [ + { + label: "Emit Rate", + type: "slider", + min: 1, + max: 1000, + step: 1, + propertyID: "emitRate", + }, + { + label: "Emit Speed", + type: "slider", + min: 0, + max: 5, + step: 0.01, + propertyID: "emitSpeed", + }, + { + label: "Speed Spread", + type: "slider", + min: 0, + max: 5, + step: 0.01, + propertyID: "speedSpread", + }, + { + label: "Emit Dimension", + type: "vec3", + vec3Type: "xyz", + min: 0, + step: 0.01, + subLabels: [ "x", "y", "z" ], + propertyID: "emitDimensions", + }, + { + label: "Emit Orientation", + type: "vec3", + vec3Type: "pyr", + min: 0, + step: 0.01, + subLabels: [ "pitch", "yaw", "roll" ], + unit: "deg", + propertyID: "emitOrientation", + }, + { + label: "Trails", + type: "bool", + propertyID: "emitterShouldTrail", + }, + ] + }, + { + id: "particles_size", + label: "SIZE", + properties: [ + { + label: "Size", + type: "slider", + max: 4, + step: 0.01, + propertyID: "particleRadius", + }, + { + label: "Size Spread", + type: "slider", + max: 4, + step: 0.01, + propertyID: "radiusSpread", + }, + { + label: "Size Start", + type: "slider", + max: 4, + step: 0.01, + propertyID: "radiusStart", + fallbackProperty: "particleRadius", + }, + { + label: "Size Finish", + type: "slider", + max: 4, + step: 0.01, + propertyID: "radiusFinish", + fallbackProperty: "particleRadius", + }, + ] + }, + { + id: "particles_color", + label: "COLOR", + properties: [ + { + label: "Color", + type: "color", + propertyID: "particleColor", + propertyName: "color", // actual entity property name + }, + { + label: "Color Start", + type: "color", + propertyID: "colorStart", + fallbackProperty: "color", + }, + { + label: "Color Finish", + type: "color", + propertyID: "colorFinish", + fallbackProperty: "color", + }, + { + label: "Color Spread", + type: "color", + propertyID: "colorSpread", + }, + ] + }, + { + id: "particles_alpha", + label: "ALPHA", + properties: [ + { + label: "Alpha", + type: "slider", + min: 0, + max: 1, + step: 0.01, + propertyID: "alpha", + }, + { + label: "Alpha Spread", + type: "slider", + min: 0, + max: 1, + step: 0.01, + propertyID: "alphaSpread", + }, + { + label: "Alpha Start", + type: "slider", + min: 0, + max: 1, + step: 0.01, + propertyID: "alphaStart", + fallbackProperty: "alpha", + }, + { + label: "Alpha Finish", + type: "slider", + min: 0, + max: 1, + step: 0.01, + propertyID: "alphaFinish", + fallbackProperty: "alpha", + }, + ] + }, + { + id: "particles_acceleration", + label: "ACCELERATION", + properties: [ + { + label: "Emit Acceleration", + type: "vec3", + vec3Type: "xyz", + step: 0.01, + subLabels: [ "x", "y", "z" ], + propertyID: "emitAcceleration", + }, + { + label: "Acceleration Spread", + type: "vec3", + vec3Type: "xyz", + step: 0.01, + subLabels: [ "x", "y", "z" ], + propertyID: "accelerationSpread", + }, + ] + }, + { + id: "particles_spin", + label: "SPIN", + properties: [ + { + label: "Spin", + type: "slider", + min: -360, + max: 360, + step: 1, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "particleSpin", + }, + { + label: "Spin Spread", + type: "slider", + min: 0, + max: 360, + step: 1, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "spinSpread", + }, + { + label: "Spin Start", + type: "slider", + min: -360, + max: 360, + step: 1, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "spinStart", + fallbackProperty: "particleSpin", + }, + { + label: "Spin Finish", + type: "slider", + min: -360, + max: 360, + step: 1, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "spinFinish", + fallbackProperty: "particleSpin", + }, + { + label: "Rotate with Entity", + type: "bool", + propertyID: "rotateWithEntity", + }, + ] + }, + { + id: "particles_constraints", + label: "CONSTRAINTS", + properties: [ + { + label: "Horizontal Angle Start", + type: "slider", + min: 0, + max: 180, + step: 1, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "azimuthStart", + }, + { + label: "Horizontal Angle Finish", + type: "slider", + min: -180, + max: 0, + step: 1, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "azimuthFinish", + }, + { + label: "Verical Angle Start", + type: "slider", + min: 0, + max: 180, + step: 1, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "polarStart", + }, + { + label: "Verical Angle Finish", + type: "slider", + min: 0, + max: 180, + step: 1, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "polarFinish", + }, + ] + }, { id: "spatial", label: "SPATIAL", @@ -592,6 +912,7 @@ const GROUPS = [ label: "Position", type: "vec3", vec3Type: "xyz", + decimals: 4, subLabels: [ "x", "y", "z" ], unit: "m", propertyID: "position", @@ -599,8 +920,9 @@ const GROUPS = [ { label: "Rotation", type: "vec3", - step: 0.1, vec3Type: "pyr", + step: 0.1, + decimals: 4, subLabels: [ "pitch", "yaw", "roll" ], unit: "deg", propertyID: "rotation", @@ -608,8 +930,9 @@ const GROUPS = [ { label: "Dimension", type: "vec3", - step: 0.1, vec3Type: "xyz", + step: 0.1, + decimals: 4, subLabels: [ "x", "y", "z" ], unit: "m", propertyID: "dimensions", @@ -626,8 +949,9 @@ const GROUPS = [ { label: "Pivot", type: "vec3", - step: 0.1, vec3Type: "xyz", + step: 0.1, + decimals: 4, subLabels: [ "x", "y", "z" ], unit: "(ratio of dimension)", propertyID: "registrationPoint", @@ -821,6 +1145,7 @@ const GROUPS = [ label: "Linear Velocity", type: "vec3", vec3Type: "xyz", + decimals: 4, subLabels: [ "x", "y", "z" ], unit: "m/s", propertyID: "velocity", @@ -828,14 +1153,15 @@ const GROUPS = [ { label: "Linear Damping", type: "number", - fixedDecimals: 2, + decimals: 2, propertyID: "damping", }, { label: "Angular Velocity", type: "vec3", - multiplier: DEGREES_TO_RADIANS, vec3Type: "pyr", + multiplier: DEGREES_TO_RADIANS, + decimals: 4, subLabels: [ "pitch", "yaw", "roll" ], unit: "deg/s", propertyID: "angularVelocity", @@ -843,25 +1169,25 @@ const GROUPS = [ { label: "Angular Damping", type: "number", - fixedDecimals: 4, + decimals: 4, propertyID: "angularDamping", }, { label: "Bounciness", type: "number", - fixedDecimals: 4, + decimals: 4, propertyID: "restitution", }, { label: "Friction", type: "number", - fixedDecimals: 4, + decimals: 4, propertyID: "friction", }, { label: "Density", type: "number", - fixedDecimals: 4, + decimals: 4, propertyID: "density", }, { @@ -877,6 +1203,7 @@ const GROUPS = [ type: "vec3", vec3Type: "xyz", subLabels: [ "x", "y", "z" ], + decimals: 4, unit: "m/s2", propertyID: "acceleration", }, @@ -894,11 +1221,13 @@ const GROUPS_PER_TYPE = { Web: [ 'base', 'web', 'spatial', 'collision', 'behavior', 'physics' ], Light: [ 'base', 'light', 'spatial', 'collision', 'behavior', 'physics' ], Material: [ 'base', 'material', 'spatial', 'behavior' ], - ParticleEffect: [ 'base', 'spatial', 'behavior', 'physics' ], + ParticleEffect: [ 'base', 'particles', 'particles_emit', 'particles_size', 'particles_color', 'particles_alpha', + 'particles_acceleration', 'particles_spin', 'particles_constraints', 'spatial', 'behavior', 'physics' ], Multiple: [ 'base', 'spatial', 'collision', 'behavior', 'physics' ], }; const EDITOR_TIMEOUT_DURATION = 1500; +const DEBOUNCE_TIMEOUT = 125; const KEY_P = 80; // Key code for letter p used for Parenting hotkey. const MATERIAL_PREFIX_STRING = "mat::"; @@ -921,10 +1250,31 @@ function debugPrint(message) { } -// GENERAL PROPERTY/GROUP FUNCTIONS +/** + * GENERAL PROPERTY/GROUP FUNCTIONS + */ -function getPropertyElement(propertyID) { - return properties[propertyID].el; +function getPropertyInputElement(propertyID) { + let property = properties[propertyID]; + switch (property.data.type) { + case 'string': + case 'bool': + case 'number': + case 'slider': + case 'dropdown': + case 'textarea': + case 'texture': + return property.elInput; + case 'vec3': + case 'vec2': + return { x: property.elInputX, y: property.elInputY, z: property.elInputZ }; + case 'color': + return { red: property.elInputR, green: property.elInputG, blue: property.elInputB }; + case 'icon': + return property.elLabel; + default: + return undefined; + } } function enableChildren(el, selector) { @@ -944,7 +1294,7 @@ function disableChildren(el, selector) { function enableProperties() { enableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); enableChildren(document, ".colpick"); - var elLocked = getPropertyElement("locked"); + var elLocked = getPropertyInputElement("locked"); if (elLocked.checked === false) { removeStaticUserData(); @@ -958,7 +1308,7 @@ function disableProperties() { for (var pickKey in colorPickers) { colorPickers[pickKey].colpickHide(); } - var elLocked = getPropertyElement("locked"); + var elLocked = getPropertyInputElement("locked"); if (elLocked.checked === true) { if ($('#property-userData-editor').css('display') === "block") { @@ -971,74 +1321,70 @@ function disableProperties() { } function showPropertyElement(propertyID, show) { - let elProperty = properties[propertyID].el; - let elNode = elProperty; - if (elNode.nodeName !== "DIV") { - let elParent = elProperty.parentNode; - if (elParent === undefined && elProperty instanceof Array) { - elParent = elProperty[0].parentNode; - } - if (elParent !== undefined) { - elNode = elParent; - } - } - elNode.style.display = show ? "table" : "none"; + let elProperty = properties[propertyID].elProperty; + elProperty.style.display = show ? "table" : "none"; } function resetProperties() { for (let propertyID in properties) { - let elProperty = properties[propertyID].el; - let propertyData = properties[propertyID].data; + let property = properties[propertyID]; + let propertyData = property.data; switch (propertyData.type) { case 'string': { - elProperty.value = ""; + property.elInput.value = ""; break; } case 'bool': { - elProperty.checked = false; + property.elInput.checked = false; break; } - case 'number': { + case 'number': + case 'slider': { if (propertyData.defaultValue !== undefined) { - elProperty.value = propertyData.defaultValue; + property.elInput.value = propertyData.defaultValue; } else { - elProperty.value = ""; + property.elInput.value = ""; + } + if (property.elSlider !== undefined) { + property.elSlider.value = property.elInput.value; } break; } case 'vec3': case 'vec2': { - // vec2/vec3 are array of 2/3 elInput numbers - elProperty[0].value = ""; - elProperty[1].value = ""; - if (elProperty[2] !== undefined) { - elProperty[2].value = ""; + property.elInputX.value = ""; + property.elInputY.value = ""; + if (property.elInputZ !== undefined) { + property.elInputZ.value = ""; } break; } case 'color': { - // color is array of color picker and 3 elInput numbers - elProperty[0].style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elProperty[1].value = ""; - elProperty[2].value = ""; - elProperty[3].value = ""; + property.elColorPicker.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; + property.elInputR.value = ""; + property.elInputG.value = ""; + property.elInputB.value = ""; break; } case 'dropdown': { - elProperty.value = ""; - setDropdownText(elProperty); + property.elInput.value = ""; + setDropdownText(property.elInput); break; } case 'textarea': { - elProperty.value = ""; - setTextareaScrolling(elProperty); + property.elInput.value = ""; + setTextareaScrolling(property.elInput); break; } case 'icon': { - // icon is array of elSpan (icon glyph) and elLabel - elProperty[0].style.display = "none"; - elProperty[1].innerHTML = propertyData.label; + property.elSpan.style.display = "none"; + property.elLabel.innerHTML = propertyData.label; + break; + } + case 'texture': { + property.elInput.value = ""; + property.elInput.imageLoad(property.elInput.value); break; } } @@ -1069,8 +1415,34 @@ function showGroupsForType(type) { } } +function getPropertyValue(propertyName) { + // if this is a compound property name (i.e. animation.running) + // then split it by . up to 3 times to find property value + let propertyValue; + let splitPropertyName = propertyName.split('.'); + if (splitPropertyName.length > 1) { + let propertyGroupName = splitPropertyName[0]; + let subPropertyName = splitPropertyName[1]; + let groupProperties = selectedEntityProperties[propertyGroupName]; + if (groupProperties === undefined || groupProperties[subPropertyName] === undefined) { + return undefined; + } + if (splitPropertyName.length === 3) { + let subSubPropertyName = splitPropertyName[2]; + propertyValue = groupProperties[subPropertyName][subSubPropertyName]; + } else { + propertyValue = groupProperties[subPropertyName]; + } + } else { + propertyValue = selectedEntityProperties[propertyName]; + } + return propertyValue; +} -// PROPERTY UPDATE FUNCTIONS + +/** + * PROPERTY UPDATE FUNCTIONS + */ function updateProperty(propertyName, propertyValue) { let propertyUpdate = {}; @@ -1114,26 +1486,24 @@ function createEmitCheckedPropertyUpdateFunction(propertyName, inverse) { }; } -function createEmitNumberPropertyUpdateFunction(propertyName, decimals) { - decimals = ((decimals === undefined) ? 4 : decimals); +function createEmitNumberPropertyUpdateFunction(propertyName, multiplier, decimals) { return function() { - let value = parseFloat(this.value).toFixed(decimals); + let value = parseFloat(this.value); + if (multiplier !== undefined) { + value *= multiplier; + } + if (decimals !== undefined) { + value = value.toFixed(decimals); + } updateProperty(propertyName, value); }; } -function createEmitVec2PropertyUpdateFunction(propertyName, elX, elY) { - return function () { - let newValue = { - x: elX.value, - y: elY.value - }; - updateProperty(propertyName, newValue); - }; -} - -function createEmitVec2PropertyUpdateFunctionWithMultiplier(propertyName, elX, elY, multiplier) { +function createEmitVec2PropertyUpdateFunction(propertyName, elX, elY, multiplier) { return function () { + if (multiplier === undefined) { + multiplier = 1; + } let newValue = { x: elX.value * multiplier, y: elY.value * multiplier @@ -1142,19 +1512,11 @@ function createEmitVec2PropertyUpdateFunctionWithMultiplier(propertyName, elX, e }; } -function createEmitVec3PropertyUpdateFunction(propertyName, elX, elY, elZ) { - return function() { - let newValue = { - x: elX.value, - y: elY.value, - z: elZ ? elZ.value : 0 - }; - updateProperty(propertyName, newValue); - }; -} - -function createEmitVec3PropertyUpdateFunctionWithMultiplier(propertyName, elX, elY, elZ, multiplier) { +function createEmitVec3PropertyUpdateFunction(propertyName, elX, elY, elZ, multiplier) { return function() { + if (multiplier === undefined) { + multiplier = 1; + } let newValue = { x: elX.value * multiplier, y: elY.value * multiplier, @@ -1199,14 +1561,16 @@ function createImageURLUpdateFunction(propertyName) { } -// PROPERTY ELEMENT CREATION FUNCTIONS +/** + * PROPERTY ELEMENT CREATION FUNCTIONS + */ function createStringProperty(property, elProperty, elLabel) { let propertyName = property.name; let elementID = property.elementID; let propertyData = property.data; - elProperty.setAttribute("class", "property text"); + elProperty.className = "property text"; let elInput = document.createElement('input'); elInput.setAttribute("id", elementID); @@ -1232,7 +1596,7 @@ function createBoolProperty(property, elProperty, elLabel) { let elementID = property.elementID; let propertyData = property.data; - elProperty.setAttribute("class", "property checkbox"); + elProperty.className = "property checkbox"; if (propertyData.glyph !== undefined) { elLabel.innerText = " " + propertyData.label; @@ -1260,15 +1624,108 @@ function createBoolProperty(property, elProperty, elLabel) { return elInput; } -function createVec3Property(property, elProperty, elLabel) { +function createNumberProperty(property, elProperty, elLabel) { let propertyName = property.name; let elementID = property.elementID; let propertyData = property.data; - elProperty.setAttribute("class", "property " + propertyData.vec3Type + " fstuple"); + elProperty.className = "property number"; + + addUnit(propertyData.unit, elLabel); + + let elInput = document.createElement('input'); + elInput.setAttribute("id", elementID); + elInput.setAttribute("type", "number"); + if (propertyData.min !== undefined) { + elInput.setAttribute("min", propertyData.min); + } + if (propertyData.max !== undefined) { + elInput.setAttribute("max", propertyData.max); + } + if (propertyData.step !== undefined) { + elInput.setAttribute("step", propertyData.step); + } + + let defaultValue = propertyData.defaultValue; + if (defaultValue !== undefined) { + elInput.value = defaultValue; + } + + elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(propertyName, propertyData.muliplier, propertyData.decimals)); + + elProperty.appendChild(elLabel); + elProperty.appendChild(elInput); + + if (propertyData.buttons !== undefined) { + addButtons(elProperty, elementID, propertyData.buttons, true); + } + + return elInput; +} + +function createSliderProperty(property, elProperty, elLabel) { + let propertyData = property.data; + + elProperty.className = "property range"; + + let elDiv = document.createElement("div"); + elDiv.className = "slider-wrapper"; + + let elSlider = document.createElement("input"); + elSlider.setAttribute("type", "range"); + + let elInput = document.createElement("input"); + elInput.setAttribute("type", "number"); + + if (propertyData.min !== undefined) { + elInput.setAttribute("min", propertyData.min); + elSlider.setAttribute("min", propertyData.min); + } + if (propertyData.max !== undefined) { + elInput.setAttribute("max", propertyData.max); + elSlider.setAttribute("max", propertyData.max); + elSlider.setAttribute("data-max", propertyData.max); + } + if (propertyData.step !== undefined) { + elInput.setAttribute("step", propertyData.step); + elSlider.setAttribute("step", propertyData.step); + } + + elInput.oninput = function (event) { + let value = event.target.value; + elSlider.value = value; + if (propertyData.multiplier !== undefined) { + value *= propertyData.multiplier; + } + updateProperty(property.name, value); + }; + elInput.onchange = elInput.oninput; + elSlider.oninput = function (event) { + let value = event.target.value; + elInput.value = value; + if (propertyData.multiplier !== undefined) { + value *= propertyData.multiplier; + } + updateProperty(property.name, value); + }; + + elDiv.appendChild(elLabel); + elDiv.appendChild(elSlider); + elDiv.appendChild(elInput); + elProperty.appendChild(elDiv); + + return [ elSlider, elInput ]; +} + +function createVec3Property(property, elProperty, elLabel) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; + + elProperty.className = "property " + propertyData.vec3Type + " fstuple"; let elTuple = document.createElement('div'); - elTuple.setAttribute("class", "tuple"); + elTuple.className = "tuple"; addUnit(propertyData.unit, elLabel); @@ -1282,13 +1739,8 @@ function createVec3Property(property, elProperty, elLabel) { let elInputZ = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[2], propertyData.min, propertyData.max, propertyData.step); - let inputChangeFunction; - if (propertyData.multiplier !== undefined) { - inputChangeFunction = createEmitVec3PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY, - elInputZ, propertyData.multiplier); - } else { - inputChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elInputX, elInputY, elInputZ); - } + let inputChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elInputX, elInputY, + elInputZ, propertyData.multiplier); elInputX.addEventListener('change', inputChangeFunction); elInputY.addEventListener('change', inputChangeFunction); elInputZ.addEventListener('change', inputChangeFunction); @@ -1301,10 +1753,10 @@ function createVec2Property(property, elProperty, elLabel) { let elementID = property.elementID; let propertyData = property.data; - elProperty.setAttribute("class", "property " + propertyData.vec2Type + " fstuple"); + elProperty.className = "property " + propertyData.vec2Type + " fstuple"; let elTuple = document.createElement('div'); - elTuple.setAttribute("class", "tuple"); + elTuple.className = "tuple"; addUnit(propertyData.unit, elLabel); @@ -1316,13 +1768,8 @@ function createVec2Property(property, elProperty, elLabel) { let elInputY = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[1], propertyData.min, propertyData.max, propertyData.step); - let inputChangeFunction; - if (propertyData.multiplier !== undefined) { - inputChangeFunction = createEmitVec2PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY, - propertyData.multiplier); - } else { - inputChangeFunction = createEmitVec2PropertyUpdateFunctionWithMultiplier(propertyName, elInputX, elInputY); - } + let inputChangeFunction = createEmitVec2PropertyUpdateFunction(propertyName, elInputX, + elInputY, propertyData.multiplier); elInputX.addEventListener('change', inputChangeFunction); elInputY.addEventListener('change', inputChangeFunction); @@ -1333,14 +1780,14 @@ function createColorProperty(property, elProperty, elLabel) { let propertyName = property.name; let elementID = property.elementID; - elProperty.setAttribute("class", "property rgb fstuple"); + elProperty.className = "property rgb fstuple"; let elColorPicker = document.createElement('div'); - elColorPicker.setAttribute("class", "color-picker"); + elColorPicker.className = "color-picker"; elColorPicker.setAttribute("id", elementID); let elTuple = document.createElement('div'); - elTuple.setAttribute("class", "tuple"); + elTuple.className = "tuple"; elProperty.appendChild(elColorPicker); elProperty.appendChild(elLabel); @@ -1388,7 +1835,7 @@ function createDropdownProperty(property, propertyID, elProperty, elLabel) { let elementID = property.elementID; let propertyData = property.data; - elProperty.setAttribute("class", "property dropdown"); + elProperty.className = "property dropdown"; let elInput = document.createElement('select'); elInput.setAttribute("id", elementID); @@ -1409,43 +1856,12 @@ function createDropdownProperty(property, propertyID, elProperty, elLabel) { return elInput; } -function createNumberProperty(property, elProperty, elLabel) { - let propertyName = property.name; - let elementID = property.elementID; - let propertyData = property.data; - - elProperty.setAttribute("class", "property number"); - - addUnit(propertyData.unit, elLabel); - - let elInput = document.createElement('input'); - elInput.setAttribute("id", elementID); - elInput.setAttribute("type", "number"); - - let defaultValue = propertyData.defaultValue; - if (defaultValue !== undefined) { - elInput.value = defaultValue; - } - - let fixedDecimals = propertyData.fixedDecimals !== undefined ? propertyData.fixedDecimals : 0; - elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(propertyName, fixedDecimals)); - - elProperty.appendChild(elLabel); - elProperty.appendChild(elInput); - - if (propertyData.buttons !== undefined) { - addButtons(elProperty, elementID, propertyData.buttons, true); - } - - return elInput; -} - function createTextareaProperty(property, elProperty, elLabel) { let propertyName = property.name; let elementID = property.elementID; let propertyData = property.data; - elProperty.setAttribute("class", "property textarea"); + elProperty.className = "property textarea"; elProperty.appendChild(elLabel); @@ -1470,7 +1886,7 @@ function createIconProperty(property, elProperty, elLabel) { let elementID = property.elementID; let propertyData = property.data; - elProperty.setAttribute("class", "property value"); + elProperty.className = "property value"; elLabel.setAttribute("id", elementID); elLabel.innerHTML = " " + propertyData.label; @@ -1484,11 +1900,62 @@ function createIconProperty(property, elProperty, elLabel) { return [ elSpan, elLabel ]; } +function createTextureProperty(property, elProperty, elLabel) { + let elementID = property.elementID; + + elProperty.className = "property texture"; + + let elDiv = document.createElement("div"); + let elImage = document.createElement("img"); + elDiv.className = "texture-image no-texture"; + elDiv.appendChild(elImage); + + let elInput = document.createElement('input'); + elInput.setAttribute("id", elementID); + elInput.setAttribute("type", "text"); + + let imageLoad = _.debounce(function (url) { + if (url.slice(0, 5).toLowerCase() === "atp:/") { + elImage.src = ""; + elImage.style.display = "none"; + elDiv.classList.remove("with-texture"); + elDiv.classList.remove("no-texture"); + elDiv.classList.add("no-preview"); + } else if (url.length > 0) { + elDiv.classList.remove("no-texture"); + elDiv.classList.remove("no-preview"); + elDiv.classList.add("with-texture"); + elImage.src = url; + elImage.style.display = "block"; + } else { + elImage.src = ""; + elImage.style.display = "none"; + elDiv.classList.remove("with-texture"); + elDiv.classList.remove("no-preview"); + elDiv.classList.add("no-texture"); + } + }, DEBOUNCE_TIMEOUT * 2); + elInput.imageLoad = imageLoad; + elInput.oninput = function (event) { + // Add throttle + var url = event.target.value; + imageLoad(url); + updateProperty(property.name, url) + }; + elInput.onchange = elInput.oninput; + + elProperty.appendChild(elLabel); + elProperty.appendChild(elDiv); + elProperty.appendChild(elInput); + + return [ elImage, elInput ]; +} + function createButtonsProperty(property, elProperty, elLabel) { let elementID = property.elementID; let propertyData = property.data; - elProperty.setAttribute("class", "property text"); + elProperty.className = "property text"; let hasLabel = propertyData.label !== undefined; if (hasLabel) { @@ -1511,9 +1978,12 @@ function createTupleNumberInput(elTuple, propertyElementID, subLabel, min, max, elLabel.setAttribute("for", elementID); let elInput = document.createElement('input'); + elInput.className = subLabel; elInput.setAttribute("id", elementID); elInput.setAttribute("type", "number"); - elInput.setAttribute("class", subLabel); + elInput.setAttribute("min", min); + elInput.setAttribute("max", max); + elInput.setAttribute("step", step); elDiv.appendChild(elInput); elDiv.appendChild(elLabel); @@ -1525,7 +1995,7 @@ function createTupleNumberInput(elTuple, propertyElementID, subLabel, min, max, function addUnit(unit, elLabel) { if (unit !== undefined) { let elSpan = document.createElement('span'); - elSpan.setAttribute("class", "unit"); + elSpan.className = "unit"; elSpan.innerHTML = unit; elLabel.appendChild(elSpan); } @@ -1533,12 +2003,12 @@ function addUnit(unit, elLabel) { function addButtons(elProperty, propertyID, buttons, newRow) { let elDiv = document.createElement('div'); - elDiv.setAttribute("class", "row"); + elDiv.className = "row"; buttons.forEach(function(button) { let elButton = document.createElement('input'); + elButton.className = button.className; elButton.setAttribute("type", "button"); - elButton.setAttribute("class", button.className); elButton.setAttribute("id", propertyID + "-button-" + button.id); elButton.setAttribute("value", button.label); elButton.addEventListener("click", button.onClick); @@ -1556,7 +2026,9 @@ function addButtons(elProperty, propertyID, buttons, newRow) { } -// BUTTON CALLBACKS +/** + * BUTTON CALLBACKS + */ function rescaleDimensions() { EventBridge.emitWebEvent(JSON.stringify({ @@ -1604,16 +2076,18 @@ function reloadServerScripts() { } function copySkyboxURLToAmbientURL() { - let skyboxURL = getPropertyElement("skybox.url").value; - getPropertyElement("ambientLight.ambientURL").value = skyboxURL; + let skyboxURL = getPropertyInputElement("skybox.url").value; + getPropertyInputElement("ambientLight.ambientURL").value = skyboxURL; updateProperty("ambientLight.ambientURL", skyboxURL); } -// USER DATA FUNCTIONS +/** + * USER DATA FUNCTIONS + */ function clearUserData() { - let elUserData = getPropertyElement("userData"); + let elUserData = getPropertyInputElement("userData"); deleteJSONEditor(); elUserData.value = ""; showUserDataTextArea(); @@ -1831,10 +2305,12 @@ function saveJSONUserData(noUpdate) { } -// MATERIAL DATA FUNCTIONS +/** + * MATERIAL DATA FUNCTIONS + */ function clearMaterialData() { - let elMaterialData = getPropertyElement("materialData"); + let elMaterialData = getPropertyInputElement("materialData"); deleteJSONMaterialEditor(); elMaterialData.value = ""; showMaterialDataTextArea(); @@ -2015,7 +2491,9 @@ function bindAllNonJSONEditorElements() { } -// DROPDOWN FUNCTIONS +/** + * DROPDOWN FUNCTIONS + */ function setDropdownText(dropdown) { let lis = dropdown.parentNode.getElementsByTagName("li"); @@ -2051,7 +2529,9 @@ function setDropdownValue(event) { } -// TEXTAREA / PARENT MATERIAL NAME FUNCTIONS +/** + * TEXTAREA / PARENT MATERIAL NAME FUNCTIONS + */ function setTextareaScrolling(element) { var isScrolling = element.scrollHeight > element.offsetHeight; @@ -2084,17 +2564,17 @@ function loaded() { fieldset.appendChild(elGroup); } else { elGroup = document.createElement('fieldset'); - elGroup.setAttribute("class", "major"); + elGroup.className = "major"; elGroup.setAttribute("id", "properties-" + group.id); elPropertiesList.appendChild(elGroup); } if (group.label !== undefined) { let elLegend = document.createElement('legend'); + elLegend.className = "section-header"; elLegend.innerText = group.label; - elLegend.setAttribute("class", "section-header"); let elSpan = document.createElement('span'); - elSpan.setAttribute("class", ".collapse-icon"); + elSpan.className = ".collapse-icon"; elSpan.innerText = "M"; elLegend.appendChild(elSpan); elGroup.appendChild(elLegend); @@ -2109,8 +2589,8 @@ function loaded() { let elProperty; if (propertyType === "sub-header") { elProperty = document.createElement('legend'); + elProperty.className = "sub-section-header"; elProperty.innerText = propertyData.label; - elProperty.setAttribute("class", "sub-section-header"); } else { elProperty = document.createElement('div'); elProperty.setAttribute("id", "div-" + propertyElementID); @@ -2124,12 +2604,12 @@ function loaded() { let elColumnDiv = document.getElementById(columnDivName); if (!elColumnDiv) { elColumnDiv = document.createElement('div'); - elColumnDiv.setAttribute("class", "two-column"); + elColumnDiv.className = "two-column"; elColumnDiv.setAttribute("id", group.id + "columnDiv"); elGroup.appendChild(elColumnDiv); } elColumn = document.createElement('fieldset'); - elColumn.setAttribute("class", "column"); + elColumn.className = "column"; elColumn.setAttribute("id", columnName); elColumnDiv.appendChild(elColumn); } @@ -2142,53 +2622,79 @@ function loaded() { elLabel.innerText = propertyData.label; elLabel.setAttribute("for", propertyElementID); - properties[propertyID] = { data: propertyData, elementID: propertyElementID, name: propertyName }; - - let property = properties[propertyID]; + let property = { + data: propertyData, + elementID: propertyElementID, + name: propertyName, + elProperty: elProperty + }; + properties[propertyID] = property; switch (propertyType) { case 'string': { - properties[propertyID].el = createStringProperty(property, elProperty, elLabel); + properties[propertyID].elInput = createStringProperty(property, elProperty, elLabel); break; } case 'bool': { - properties[propertyID].el = createBoolProperty(property, elProperty, elLabel); + properties[propertyID].elInput = createBoolProperty(property, elProperty, elLabel); break; } case 'number': { - properties[propertyID].el = createNumberProperty(property, elProperty, elLabel); + properties[propertyID].elInput = createNumberProperty(property, elProperty, elLabel); + break; + } + case 'slider': { + let elSlider = createSliderProperty(property, elProperty, elLabel); + properties[propertyID].elSlider = elSlider[0]; + properties[propertyID].elInput = elSlider[1]; break; } case 'vec3': { - properties[propertyID].el = createVec3Property(property, elProperty, elLabel); + let elVec3 = createVec3Property(property, elProperty, elLabel); + properties[propertyID].elInputX = elVec3[0]; + properties[propertyID].elInputY = elVec3[1]; + properties[propertyID].elInputZ = elVec3[2]; break; } case 'vec2': { - properties[propertyID].el = createVec2Property(property, elProperty, elLabel); + let elVec2 = createVec2Property(property, elProperty, elLabel); + properties[propertyID].elInputX = elVec2[0]; + properties[propertyID].elInputY = elVec2[1]; break; } case 'color': { - properties[propertyID].el = createColorProperty(property, elProperty, elLabel); + let elColor = createColorProperty(property, elProperty, elLabel); + properties[propertyID].elColorPicker = elColor[0]; + properties[propertyID].elInputR = elColor[1]; + properties[propertyID].elInputG = elColor[2]; + properties[propertyID].elInputB = elColor[3]; break; } case 'dropdown': { - properties[propertyID].el = createDropdownProperty(property, propertyID, elProperty, elLabel); + properties[propertyID].elInput = createDropdownProperty(property, propertyID, elProperty, elLabel); break; } case 'textarea': { - properties[propertyID].el = createTextareaProperty(property, elProperty, elLabel); + properties[propertyID].elInput = createTextareaProperty(property, elProperty, elLabel); break; } case 'icon': { - properties[propertyID].el = createIconProperty(property, elProperty, elLabel); + let elIcon = createIconProperty(property, elProperty, elLabel); + properties[propertyID].elSpan = elIcon[0]; + properties[propertyID].elLabel = elIcon[1]; + break; + } + case 'texture': { + let elTexture = createTextureProperty(property, elProperty, elLabel); + properties[propertyID].elImage = elTexture[0]; + properties[propertyID].elInput = elTexture[1]; break; } case 'buttons': { - properties[propertyID].el = createButtonsProperty(property, elProperty, elLabel); + properties[propertyID].elProperty = createButtonsProperty(property, elProperty, elLabel); break; } case 'sub-header': { - properties[propertyID].el = elProperty; break; } default: { @@ -2258,13 +2764,13 @@ function loaded() { showGroupsForType("None"); deleteJSONEditor(); - getPropertyElement("userData").value = ""; + getPropertyInputElement("userData").value = ""; showUserDataTextArea(); showSaveUserDataButton(); showNewJSONEditorButton(); deleteJSONMaterialEditor(); - getPropertyElement("materialData").value = ""; + getPropertyInputElement("materialData").value = ""; showMaterialDataTextArea(); showSaveMaterialDataButton(); showNewJSONMaterialEditorButton(); @@ -2299,16 +2805,13 @@ function loaded() { showGroupsForType(type); let typeProperty = properties["type"]; - let elTypeProperty = typeProperty.el; - elTypeProperty[0].innerHTML = typeProperty.data.icons[type]; - elTypeProperty[0].style.display = "inline-block"; - elTypeProperty[1].innerHTML = type + " (" + data.selections.length + ")"; + typeProperty.elSpan.innerHTML = typeProperty.data.icons[type]; + typeProperty.elSpan.style.display = "inline-block"; + typeProperty.elLabel.innerHTML = type + " (" + data.selections.length + ")"; disableProperties(); } else { - selectedEntityProperties = data.selections[0].properties; - - showGroupsForType(selectedEntityProperties.type); + selectedEntityProperties = data.selections[0].properties; if (lastEntityID !== '"' + selectedEntityProperties.id + '"' && lastEntityID !== null) { if (editor !== null) { @@ -2335,41 +2838,26 @@ function loaded() { } } + showGroupsForType(selectedEntityProperties.type); + for (let propertyID in properties) { let property = properties[propertyID]; - let elProperty = property.el; let propertyData = property.data; let propertyName = property.name; - - // if this is a compound property name (i.e. animation.running) - // then split it by . up to 3 times to find property value - let propertyValue; - let splitPropertyName = propertyName.split('.'); - if (splitPropertyName.length > 1) { - let propertyGroupName = splitPropertyName[0]; - let subPropertyName = splitPropertyName[1]; - let groupProperties = selectedEntityProperties[propertyGroupName]; - if (groupProperties === undefined || groupProperties[subPropertyName] === undefined) { - continue; - } - if (splitPropertyName.length === 3) { - let subSubPropertyName = splitPropertyName[2]; - propertyValue = groupProperties[subPropertyName][subSubPropertyName]; - } else { - propertyValue = groupProperties[subPropertyName]; - } - } else { - propertyValue = selectedEntityProperties[propertyName]; - } + let propertyValue = getPropertyValue(propertyName); let isSubProperty = propertyData.subPropertyOf !== undefined; - if (elProperty === undefined || (propertyValue === undefined && !isSubProperty)) { + if (propertyValue === undefined && !isSubProperty) { continue; } + if (!propertyValue && propertyData.fallbackProperty !== undefined) { + propertyValue = getPropertyValue(propertyData.fallbackProperty); + } + switch (propertyData.type) { case 'string': { - elProperty.value = propertyValue; + property.elInput.value = propertyValue; break; } case 'bool': { @@ -2378,53 +2866,64 @@ function loaded() { let propertyValue = selectedEntityProperties[propertyData.subPropertyOf]; let subProperties = propertyValue.split(","); let subPropertyValue = subProperties.indexOf(propertyID) > -1; - elProperty.checked = inverse ? !subPropertyValue : subPropertyValue; + property.elInput.checked = inverse ? !subPropertyValue : subPropertyValue; } else { - elProperty.checked = inverse ? !propertyValue : propertyValue; + property.elInput.checked = inverse ? !propertyValue : propertyValue; } break; } - case 'number': { - let fixedDecimals = propertyData.fixedDecimals !== undefined ? propertyData.fixedDecimals : 0; - elProperty.value = propertyValue.toFixed(fixedDecimals); + case 'number': + case 'slider': { + let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; + let decimals = propertyData.decimals !== undefined ? propertyData.decimals : 0; + property.elInput.value = (propertyValue / multiplier).toFixed(decimals); + if (property.elSlider !== undefined) { + property.elSlider.value = property.elInput.value; + } break; } case 'vec3': case 'vec2': { - // vec2/vec3 are array of 2/3 elInput numbers let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; - elProperty[0].value = (propertyValue.x / multiplier).toFixed(4); - elProperty[1].value = (propertyValue.y / multiplier).toFixed(4); - if (elProperty[2] !== undefined) { - elProperty[2].value = (propertyValue.z / multiplier).toFixed(4); + let decimals = propertyData.decimals !== undefined ? propertyData.decimals : 0; + property.elInputX.value = (propertyValue.x / multiplier).toFixed(decimals); + property.elInputY.value = (propertyValue.y / multiplier).toFixed(decimals); + if (property.elInputZ !== undefined) { + property.elInputZ.value = (propertyValue.z / multiplier).toFixed(decimals); } break; } case 'color': { - // color is array of color picker and 3 elInput numbers - elProperty[0].style.backgroundColor = "rgb(" + propertyValue.red + "," + - propertyValue.green + "," + - propertyValue.blue + ")"; - elProperty[1].value = propertyValue.red; - elProperty[2].value = propertyValue.green; - elProperty[3].value = propertyValue.blue; + if (!propertyValue.red && propertyData.fallbackProperty !== undefined) { + propertyValue = getPropertyValue(propertyData.fallbackProperty); + } + property.elColorPicker.style.backgroundColor = "rgb(" + propertyValue.red + "," + + propertyValue.green + "," + + propertyValue.blue + ")"; + property.elInputR.value = propertyValue.red; + property.elInputG.value = propertyValue.green; + property.elInputB.value = propertyValue.blue; break; } case 'dropdown': { - elProperty.value = propertyValue; - setDropdownText(elProperty); + property.elInput.value = propertyValue; + setDropdownText(property.elInput); break; } case 'textarea': { - elProperty.value = propertyValue; - setTextareaScrolling(elProperty); + property.elInput.value = propertyValue; + setTextareaScrolling(property.elInput); break; } case 'icon': { - // icon is array of elSpan (icon glyph) and elLabel - elProperty[0].innerHTML = propertyData.icons[propertyValue]; - elProperty[0].style.display = "inline-block"; - elProperty[1].innerHTML = propertyValue; + property.elSpan.innerHTML = propertyData.icons[propertyValue]; + property.elSpan.style.display = "inline-block"; + property.elLabel.innerHTML = propertyValue; + break; + } + case 'texture': { + property.elInput.value = propertyValue; + property.elInput.imageLoad(property.elInput.value); break; } } @@ -2439,10 +2938,10 @@ function loaded() { } } - let elGrabbable = getPropertyElement("grabbable"); - let elTriggerable = getPropertyElement("triggerable"); - let elIgnoreIK = getPropertyElement("ignoreIK"); - elGrabbable.checked = getPropertyElement("dynamic").checked; + let elGrabbable = getPropertyInputElement("grabbable"); + let elTriggerable = getPropertyInputElement("triggerable"); + let elIgnoreIK = getPropertyInputElement("ignoreIK"); + elGrabbable.checked = getPropertyInputElement("dynamic").checked; elTriggerable.checked = false; elIgnoreIK.checked = true; let grabbablesSet = false; @@ -2481,11 +2980,11 @@ function loaded() { if (selectedEntityProperties.type === "Image") { let imageLink = JSON.parse(selectedEntityProperties.textures)["tex.picture"]; - getPropertyElement("image").value = imageLink; + getPropertyInputElement("image").value = imageLink; } else if (selectedEntityProperties.type === "Material") { - let elParentMaterialNameString = getPropertyElement("materialNameToReplace"); - let elParentMaterialNameNumber = getPropertyElement("submeshToReplace"); - let elParentMaterialNameCheckbox = getPropertyElement("selectSubmesh"); + let elParentMaterialNameString = getPropertyInputElement("materialNameToReplace"); + let elParentMaterialNameNumber = getPropertyInputElement("submeshToReplace"); + let elParentMaterialNameCheckbox = getPropertyInputElement("selectSubmesh"); let parentMaterialName = selectedEntityProperties.parentMaterialName; if (parentMaterialName.startsWith(MATERIAL_PREFIX_STRING)) { elParentMaterialNameString.value = parentMaterialName.replace(MATERIAL_PREFIX_STRING, ""); @@ -2504,7 +3003,7 @@ function loaded() { } catch (e) { // normal text deleteJSONEditor(); - getPropertyElement("userData").value = selectedEntityProperties.userData; + getPropertyInputElement("userData").value = selectedEntityProperties.userData; showUserDataTextArea(); showNewJSONEditorButton(); hideSaveUserDataButton(); @@ -2527,7 +3026,7 @@ function loaded() { } catch (e) { // normal text deleteJSONMaterialEditor(); - getPropertyElement("materialData").value = selectedEntityProperties.materialData; + getPropertyInputElement("materialData").value = selectedEntityProperties.materialData; showMaterialDataTextArea(); showNewJSONMaterialEditorButton(); hideSaveMaterialDataButton(); @@ -2546,7 +3045,7 @@ function loaded() { if (selectedEntityProperties.locked) { disableProperties(); - getPropertyElement("locked").removeAttribute('disabled'); + getPropertyInputElement("locked").removeAttribute('disabled'); } else { enableProperties(); disableSaveUserDataButton(); @@ -2564,10 +3063,10 @@ function loaded() { // Server Script Status let serverScriptProperty = properties["serverScripts"]; - let elServerScript = serverScriptProperty.el; + let elServerScript = serverScriptProperty.elInput; let serverScriptElementID = serverScriptProperty.elementID; let elDiv = document.createElement('div'); - elDiv.setAttribute("class", "property"); + elDiv.className = "property"; let elLabel = document.createElement('label'); elLabel.setAttribute("for", serverScriptElementID + "-status"); elLabel.innerText = "Server Script Status"; @@ -2579,19 +3078,19 @@ function loaded() { // Server Script Error elDiv = document.createElement('div'); - elDiv.setAttribute("class", "property"); + elDiv.className = "property"; let elServerScriptError = document.createElement('textarea'); elServerScriptError.setAttribute("id", serverScriptElementID + "-error"); elDiv.appendChild(elServerScriptError); elServerScript.parentNode.appendChild(elDiv); - let elScript = getPropertyElement("script"); - elScript.parentNode.setAttribute("class", "property url refresh"); - elServerScript.parentNode.setAttribute("class", "property url refresh"); + let elScript = getPropertyInputElement("script"); + elScript.parentNode.className = "property url refresh"; + elServerScript.parentNode.className = "property url refresh"; // User Data let userDataProperty = properties["userData"]; - let elUserData = userDataProperty.el; + let elUserData = userDataProperty.elInput; let userDataElementID = userDataProperty.elementID; elDiv = elUserData.parentNode; let elStaticUserData = document.createElement('div'); @@ -2607,7 +3106,7 @@ function loaded() { // Material Data let materialDataProperty = properties["materialData"]; - let elMaterialData = materialDataProperty.el; + let elMaterialData = materialDataProperty.elInput; let materialDataElementID = materialDataProperty.elementID; elDiv = elMaterialData.parentNode; let elStaticMaterialData = document.createElement('div'); @@ -2622,9 +3121,9 @@ function loaded() { elDiv.insertBefore(elMaterialDataEditor, elMaterialData); // User Data Fields - let elGrabbable = getPropertyElement("grabbable"); - let elTriggerable = getPropertyElement("triggerable"); - let elIgnoreIK = getPropertyElement("ignoreIK"); + let elGrabbable = getPropertyInputElement("grabbable"); + let elTriggerable = getPropertyInputElement("triggerable"); + let elIgnoreIK = getPropertyInputElement("ignoreIK"); elGrabbable.addEventListener('change', function() { userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, true); }); @@ -2636,9 +3135,9 @@ function loaded() { }); // Special Property Callbacks - let elParentMaterialNameString = getPropertyElement("materialNameToReplace"); - let elParentMaterialNameNumber = getPropertyElement("submeshToReplace"); - let elParentMaterialNameCheckbox = getPropertyElement("selectSubmesh"); + let elParentMaterialNameString = getPropertyInputElement("materialNameToReplace"); + let elParentMaterialNameNumber = getPropertyInputElement("submeshToReplace"); + let elParentMaterialNameCheckbox = getPropertyInputElement("selectSubmesh"); elParentMaterialNameString.addEventListener('change', function () { updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + this.value); }); @@ -2655,7 +3154,7 @@ function loaded() { } }); - getPropertyElement("image").addEventListener('change', createImageURLUpdateFunction('textures')); + getPropertyInputElement("image").addEventListener('change', createImageURLUpdateFunction('textures')); // Collapsible sections let elCollapsible = document.getElementsByClassName("section-header"); @@ -2704,7 +3203,8 @@ function loaded() { // let elDropdowns = document.getElementsByTagName("select"); for (let dropDownIndex = 0; dropDownIndex < elDropdowns.length; ++dropDownIndex) { - let options = elDropdowns[dropDownIndex].getElementsByTagName("option"); + let elDropdown = elDropdowns[dropDownIndex]; + let options = elDropdown.getElementsByTagName("option"); let selectedOption = 0; for (let optionIndex = 0; optionIndex < options.length; ++optionIndex) { if (options[optionIndex].getAttribute("selected") === "selected") { @@ -2712,14 +3212,14 @@ function loaded() { // TODO: Shouldn't there be a break here? } } - let div = elDropdowns[dropDownIndex].parentNode; + let div = elDropdown.parentNode; let dl = document.createElement("dl"); div.appendChild(dl); let dt = document.createElement("dt"); - dt.name = elDropdowns[dropDownIndex].name; - dt.id = elDropdowns[dropDownIndex].id; + dt.name = elDropdown.name; + dt.id = elDropdown.id; dt.addEventListener("click", toggleDropdown, true); dl.appendChild(dt); @@ -2746,9 +3246,9 @@ function loaded() { ul.appendChild(li); } - let propertyID = elDropdowns[dropDownIndex].getAttribute("propertyID"); + let propertyID = elDropdown.getAttribute("propertyID"); let property = properties[propertyID]; - property.el = dt; + property.elInput = dt; dt.addEventListener('change', createEmitTextPropertyUpdateFunction(property.name)); } diff --git a/scripts/system/html/js/underscore-min.js b/scripts/system/html/js/underscore-min.js new file mode 100644 index 0000000000..f01025b7bc --- /dev/null +++ b/scripts/system/html/js/underscore-min.js @@ -0,0 +1,6 @@ +// Underscore.js 1.8.3 +// http://underscorejs.org +// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. +(function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); +//# sourceMappingURL=underscore-min.map \ No newline at end of file From 078baa86e4fbe56cd095e2ea46c59abe0dc34e05 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Tue, 16 Oct 2018 13:16:01 -0700 Subject: [PATCH 118/362] Sitting abort post transit animation --- interface/src/avatar/AvatarManager.cpp | 2 +- libraries/animation/src/Rig.h | 1 + .../src/avatars-renderer/Avatar.cpp | 13 +++++++++++-- .../avatars-renderer/src/avatars-renderer/Avatar.h | 9 +++++++++ .../controllers/controllerModules/teleport.js | 2 ++ 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 373ae9980a..c3f6579e90 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -162,7 +162,7 @@ void AvatarManager::handleTransitAnimations(AvatarTransit::Status status) { case AvatarTransit::Status::END_TRANSIT: qDebug() << "END_TRANSIT"; _myAvatar->overrideNetworkAnimation(endAnimation._animationUrl, REFERENCE_FPS, false, endAnimation._firstFrame, - endAnimation._firstFrame + endAnimation._frameCount); + endAnimation._firstFrame + endAnimation._frameCount); break; case AvatarTransit::Status::ENDED: qDebug() << "END_FRAME"; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index e1012df029..37d1ec1dd3 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -116,6 +116,7 @@ public: void overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); void restoreAnimation(); void restoreNetworkAnimation(); + QStringList getAnimationRoles() const; void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); void restoreRoleAnimation(const QString& role); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index b2747277c9..b43e1c23f6 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -132,9 +132,12 @@ AvatarTransit::Status AvatarTransit::update(float deltaTime, const glm::vec3& av const float SETTLE_ABORT_DISTANCE = 0.1f; float scaledSettleAbortDistance = SETTLE_ABORT_DISTANCE * _scale; - if (_isActive && oneFrameDistance > scaledSettleAbortDistance && _status == Status::POST_TRANSIT) { + bool abortPostTransit = (_status == Status::POST_TRANSIT && _purpose == Purpose::SIT) || + (_isActive && oneFrameDistance > scaledSettleAbortDistance && _status == Status::POST_TRANSIT); + if (abortPostTransit) { reset(); _status = Status::ENDED; + _purpose = Purpose::UNDEFINED; } return _status; } @@ -203,6 +206,7 @@ AvatarTransit::Status AvatarTransit::updatePosition(float deltaTime) { _currentPosition = _endPosition; if (nextTime >= _totalTime) { _isActive = false; + _purpose = Purpose::UNDEFINED; status = Status::ENDED; } else if (_currentTime < _totalTime - _postTransitTime) { status = Status::END_TRANSIT; @@ -2000,7 +2004,12 @@ AvatarTransit::Status Avatar::updateTransit(float deltaTime, const glm::vec3& av void Avatar::setTransitScale(float scale) { std::lock_guard lock(_transitLock); - return _transit.setScale(scale); + _transit.setScale(scale); +} + +void Avatar::setTransitPurpose(int purpose) { + std::lock_guard lock(_transitLock); + _transit.setPurpose(static_cast(purpose)); } void Avatar::overrideNextPacketPositionData(const glm::vec3& position) { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index e4fd667c1f..d375909609 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -71,6 +71,12 @@ public: EASE_IN_OUT }; + enum Purpose { + UNDEFINED = 0, + TELEPORT, + SIT + }; + struct TransitAnimation { TransitAnimation(){}; TransitAnimation(const QString& animationUrl, int firstFrame, int frameCount) : @@ -99,6 +105,7 @@ public: glm::vec3 getCurrentPosition() { return _currentPosition; } glm::vec3 getEndPosition() { return _endPosition; } void setScale(float scale) { _scale = scale; } + void setPurpose(const Purpose& purpose) { _purpose = purpose; } void reset(); private: @@ -123,6 +130,7 @@ private: EaseType _easeType { EaseType::EASE_OUT }; Status _status { Status::IDLE }; float _scale { 1.0f }; + Purpose _purpose { Purpose::UNDEFINED }; }; class Avatar : public AvatarData, public scriptable::ModelProvider { @@ -449,6 +457,7 @@ public: AvatarTransit::Status updateTransit(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config); void setTransitScale(float scale); + Q_INVOKABLE void setTransitPurpose(int purpose); void overrideNextPacketPositionData(const glm::vec3& position); diff --git a/scripts/system/controllers/controllerModules/teleport.js b/scripts/system/controllers/controllerModules/teleport.js index bf5022cdaf..d31f207b19 100644 --- a/scripts/system/controllers/controllerModules/teleport.js +++ b/scripts/system/controllers/controllerModules/teleport.js @@ -781,11 +781,13 @@ Script.include("/~/system/libraries/controllers.js"); if (target === TARGET.NONE || target === TARGET.INVALID) { // Do nothing } else if (target === TARGET.SEAT) { + MyAvatar.setTransitPurpose(2); 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.setTransitPurpose(1); MyAvatar.goToLocation(result.intersection, true, HMD.orientation, false, shouldLandSafe); HMD.centerUI(); MyAvatar.centerBody(); From 0b77eb5e9ce7e5ebbab42f0db90dece9764a0e82 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 16 Oct 2018 14:25:38 -0700 Subject: [PATCH 119/362] Control UUID inclusion in avatar data with new param Increase estimate of avatars to be sent; remove start/end segment in avatar identity writes. --- .../src/avatars/AvatarMixerSlave.cpp | 5 ++-- libraries/avatars/src/AvatarData.cpp | 25 +++++++++++-------- libraries/avatars/src/AvatarData.h | 1 + 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 0e0802bf38..75d36783a5 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -72,9 +72,7 @@ int AvatarMixerSlave::sendIdentityPacket(NLPacketList& packetList, const AvatarM if (destinationNode.getType() == NodeType::Agent && !destinationNode.isUpstream()) { QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray(); individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious - packetList.startSegment(); packetList.write(individualData); - packetList.endSegment(); _stats.numIdentityPackets++; return individualData.size(); } else { @@ -248,7 +246,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) distribution.reset(); // Estimate number to sort on number sent last frame (with min. of 20). - const int numToSendEst = std::max(nodeData->getNumAvatarsSentLastFrame() * 2, 20); + const int numToSendEst = std::max(int(nodeData->getNumAvatarsSentLastFrame() * 2.5f), 20); // reset the number of sent avatars nodeData->resetNumAvatarsSentLastFrame(); @@ -455,6 +453,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) const bool distanceAdjust = true; const bool dropFaceTracking = false; AvatarDataPacket::SendStatus sendStatus; + sendStatus.sendUUID = true; do { auto startSerialize = chrono::high_resolution_clock::now(); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 422db2f2ed..3ac8b32d48 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -229,8 +229,7 @@ QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail, bool dro AvatarDataPacket::SendStatus sendStatus; auto avatarByteArray = AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(), sendStatus, dropFaceTracking, false, glm::vec3(0), nullptr, 0, &_outboundDataRate); - // Strip UUID - return avatarByteArray.right(avatarByteArray.size() - NUM_BYTES_RFC4122_UUID); + return avatarByteArray; } QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, @@ -255,7 +254,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent if (dataDetail == NoData) { sendStatus.itemFlags = wantedFlags; - QByteArray avatarDataByteArray(getSessionUUID().toRfc4122().data(), NUM_BYTES_RFC4122_UUID + sizeof wantedFlags); + QByteArray avatarDataByteArray; + if (sendStatus.sendUUID) { + avatarDataByteArray.append(getSessionUUID().toRfc4122().data(), NUM_BYTES_RFC4122_UUID); + } + avatarDataByteArray.append((char*) &wantedFlags, sizeof wantedFlags); return avatarDataByteArray; } @@ -392,13 +395,6 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent const unsigned char* const startPosition = destinationBuffer; const unsigned char* const packetEnd = destinationBuffer + maxDataSize; - // Packets always have UUID. - memcpy(destinationBuffer, getSessionUUID().toRfc4122(), NUM_BYTES_RFC4122_UUID); - destinationBuffer += NUM_BYTES_RFC4122_UUID; - - unsigned char * packetFlagsLocation = destinationBuffer; - destinationBuffer += sizeof(wantedFlags); - #define AVATAR_MEMCPY(src) \ memcpy(destinationBuffer, &(src), sizeof(src)); \ destinationBuffer += sizeof(src); @@ -409,6 +405,14 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent && (packetEnd - destinationBuffer) >= (ptrdiff_t)(space) \ && (includedFlags |= AvatarDataPacket::flag)) + if (sendStatus.sendUUID) { + memcpy(destinationBuffer, getSessionUUID().toRfc4122(), NUM_BYTES_RFC4122_UUID); + destinationBuffer += NUM_BYTES_RFC4122_UUID; + } + + unsigned char * packetFlagsLocation = destinationBuffer; + destinationBuffer += sizeof(wantedFlags); + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_GLOBAL_POSITION, sizeof _globalPosition) { auto startSection = destinationBuffer; if (_overrideGlobalPosition) { @@ -417,7 +421,6 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent AVATAR_MEMCPY(_globalPosition); } - int numBytes = destinationBuffer - startSection; if (outboundDataRateOut) { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index aa3d3d328c..1f5b7093b9 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -302,6 +302,7 @@ namespace AvatarDataPacket { struct SendStatus { HasFlags itemFlags { 0 }; + bool sendUUID { false }; int rotationsSent { 0 }; // ie: index of next unsent joint int translationsSent { 0 }; operator bool() { return itemFlags == 0; } From a2aa63e9ed8ee169cef523c22de24f40abd52a9f Mon Sep 17 00:00:00 2001 From: David Back Date: Tue, 16 Oct 2018 14:28:52 -0700 Subject: [PATCH 120/362] adjust no entities message --- scripts/system/html/entityList.html | 2 +- scripts/system/html/js/entityList.js | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 06c2be8e73..f9b6fbd59b 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -108,7 +108,7 @@
- No entities found in view within a 100 meter radius. Try moving to a different location and refreshing. + There are no entities to display. Please check your filters or create an entity to begin.
diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 0f3f27a547..2b720f614d 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -142,8 +142,6 @@ function loaded() { elInfoToggleGlyph = elInfoToggle.firstChild; elFooter = document.getElementById("footer-text"); elNoEntitiesMessage = document.getElementById("no-entities"); - elNoEntitiesInView = document.getElementById("no-entities-in-view"); - elNoEntitiesRadius = document.getElementById("no-entities-radius"); document.body.onclick = onBodyClick; document.getElementById("entity-name").onclick = function() { @@ -230,8 +228,6 @@ function loaded() { elFilterTypeCheckboxes.appendChild(elDiv); } - elNoEntitiesInView.style.display = "none"; - entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow, createRow, updateRow, clearRow, WINDOW_NONVARIABLE_HEIGHT); @@ -639,10 +635,8 @@ function loaded() { isFilterInView = !isFilterInView; if (isFilterInView) { elFilterInView.setAttribute(FILTER_IN_VIEW_ATTRIBUTE, FILTER_IN_VIEW_ATTRIBUTE); - elNoEntitiesInView.style.display = "inline"; } else { elFilterInView.removeAttribute(FILTER_IN_VIEW_ATTRIBUTE); - elNoEntitiesInView.style.display = "none"; } EventBridge.emitWebEvent(JSON.stringify({ type: "filterInView", filterInView: isFilterInView })); refreshEntities(); @@ -650,8 +644,6 @@ function loaded() { function onRadiusChange() { elFilterRadius.value = Math.max(elFilterRadius.value, 0); - elNoEntitiesRadius.firstChild.nodeValue = elFilterRadius.value; - elNoEntitiesMessage.style.display = "none"; EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elFilterRadius.value })); refreshEntities(); } @@ -765,7 +757,6 @@ function loaded() { refreshEntities(); }); - augmentSpinButtons(); // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked From 952b1122713192f70bb5b1df4fb8ae987da16522 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 16 Oct 2018 15:10:26 -0700 Subject: [PATCH 121/362] changed the transition times to make them shorter sit == 3sec stand == 1sec, also added failsafe for when the average height is above 5ft. this can recover from a missed transition to standing. --- interface/src/avatar/MyAvatar.cpp | 153 ++++++++++++++---------------- interface/src/avatar/MyAvatar.h | 10 +- 2 files changed, 76 insertions(+), 87 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 00a8e0f30e..b59ff675e1 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -464,17 +464,72 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { } } +void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { + const float STANDING_HEIGHT_MULTIPLE = 1.2f; + const float SITTING_HEIGHT_MULTIPLE = 0.833f; + const int SITTING_COUNT_THRESHOLD = 180; + const int STANDING_COUNT_THRESHOLD = 60; + const int SQUATTY_COUNT_THRESHOLD = 600; + + // qCDebug(interfaceapp) << "locked " << getIsSitStandStateLocked() << " away " << getIsAway() << " hmd " << qApp->isHMDMode() << " user height " << _userHeight.get(); + if (!getIsSitStandStateLocked() && !getIsAway() && qApp->isHMDMode()) { + if (getIsInSittingState()) { + if (newHeightReading > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { + // if we recenter upwards then no longer in sitting state + _sitStandStateCount++; + if (_sitStandStateCount > STANDING_COUNT_THRESHOLD) { + _sumUserHeightSensorSpace = newHeightReading; + _tippingPoint = newHeightReading; + setIsInSittingState(false); + } + } else if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) { + // if we are mis labelled as sitting but we are standing in the real world this will + // make sure that a real sit is still recognized so we won't be stuck in sitting unable to change state + _sitStandStateCount++; + if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sumUserHeightSensorSpace = newHeightReading; + _tippingPoint = newHeightReading; + // here we stay in sit state but reset the average height + setIsInSittingState(true); + } + } else { + // sanity check if average height greater than 5ft they are not sitting(or get off your dangerous barstool please) + if (_sumUserHeightSensorSpace > 1.52f) { + setIsInSittingState(true); + } else { + // tipping point is average height when sitting. + _tippingPoint = _sumUserHeightSensorSpace; + _sitStandStateCount = 0; + } + } + } else { + // in the standing state + if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) { + _sitStandStateCount++; + if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sumUserHeightSensorSpace = newHeightReading; + _tippingPoint = newHeightReading; + setIsInSittingState(true); + } + } else { + // use the mode height for the tipping point when we are standing. + _tippingPoint = getCurrentStandingHeight(); + _sitStandStateCount = 0; + } + } + } else { + // if you are away then reset the average and set state to standing. + _sumUserHeightSensorSpace = _userHeight.get(); + _tippingPoint = _userHeight.get(); + setIsInSittingState(false); + } +} + void MyAvatar::update(float deltaTime) { // update moving average of HMD facing in xz plane. const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); const float PERCENTAGE_WEIGHT_HEAD_VS_SHOULDERS_AZIMUTH = 0.0f; // 100 percent shoulders - const float STANDING_HEIGHT_MULTIPLE = 1.2f; - const float SITTING_HEIGHT_MULTIPLE = 0.833f; const float COSINE_THIRTY_DEGREES = 0.866f; - const float COSINE_TEN_DEGREES = 0.9848f; - const int SITTING_COUNT_THRESHOLD = 100; - const int STANDING_COUNT_THRESHOLD = 10; - const int SQUATTY_COUNT_THRESHOLD = 600; float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); @@ -504,13 +559,11 @@ void MyAvatar::update(float deltaTime) { controller::Pose newHeightReading = getControllerPoseInSensorFrame(controller::Action::HEAD); if (newHeightReading.isValid()) { int newHeightReadingInCentimeters = glm::floor(newHeightReading.getTranslation().y * CENTIMETERS_PER_METER); - _sumUserHeightSensorSpace += newHeightReading.getTranslation().y; - _averageUserHeightCount++; + _sumUserHeightSensorSpace = lerp(_sumUserHeightSensorSpace, newHeightReading.getTranslation().y, 0.01f); _recentModeReadings.insert(newHeightReadingInCentimeters); setCurrentStandingHeight(computeStandingHeightMode(newHeightReading)); setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); } - float averageSensorSpaceHeight = _sumUserHeightSensorSpace / _averageUserHeightCount; // if the spine is straight and the head is below the default position by 5 cm then increment squatty count. const float SQUAT_THRESHOLD = 0.05f; @@ -533,76 +586,8 @@ void MyAvatar::update(float deltaTime) { } float angleHeadUp = glm::dot(headUp, glm::vec3(0.0f, 1.0f, 0.0f)); - const int VELOCITY_COUNT_THRESHOLD = 60; // put update sit stand state counts here - if (!getIsSitStandStateLocked()) { - if (!getIsAway()) { - if (qApp->isHMDMode()) { - if (getIsInSittingState()) { - if (newHeightReading.getTranslation().y > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { - // if we recenter upwards then no longer in sitting state - _sitStandStateCount++; - if (_sitStandStateCount > STANDING_COUNT_THRESHOLD) { - _sitStandStateCount = 0; - _squatCount = 0; - if (newHeightReading.isValid()) { - _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; - _tippingPoint = newHeightReading.getTranslation().y; - } - _averageUserHeightCount = 1; - setIsInSittingState(false); - } - } else if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) && (angleHeadUp > COSINE_THIRTY_DEGREES)) { - // if we are mis labelled as sitting but we are standing in the real world this will - // make sure that a real sit is still recognized so we won't be stuck in sitting unable to change state - _sitStandStateCount++; - if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { - _sitStandStateCount = 0; - _squatCount = 0; - if (newHeightReading.isValid()) { - _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; - _tippingPoint = newHeightReading.getTranslation().y; - } - _averageUserHeightCount = 1; - // here we stay in sit state but reset the average height - setIsInSittingState(true); - } - } else { - _sitStandStateCount = 0; - // tipping point is average height when sitting. - _tippingPoint = averageSensorSpaceHeight; - } - } else { - // in the standing state - if ((newHeightReading.getTranslation().y < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) && (angleHeadUp > COSINE_THIRTY_DEGREES)) { - _sitStandStateCount++; - if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { - _sitStandStateCount = 0; - _squatCount = 0; - if (newHeightReading.isValid()) { - _sumUserHeightSensorSpace = newHeightReading.getTranslation().y; - _tippingPoint = newHeightReading.getTranslation().y; - } - _averageUserHeightCount = 1; - setIsInSittingState(true); - } - } else { - // use the mode height for the tipping point when we are standing. - _tippingPoint = getCurrentStandingHeight(); - _sitStandStateCount = 0; - } - } - } - } else { - // if you are away then reset the average and set state to standing. - _squatCount = 0; - _sitStandStateCount = 0; - _averageUserHeightCount = 1; - _sumUserHeightSensorSpace = DEFAULT_AVATAR_HEIGHT; - _tippingPoint = DEFAULT_AVATAR_HEIGHT; - setIsInSittingState(false); - } - } + updateSitStandState(newHeightReading.getTranslation().y, angleHeadUp); if (_drawAverageFacingEnabled) { auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); @@ -3917,6 +3902,9 @@ void MyAvatar::setIsInWalkingState(bool isWalking) { } void MyAvatar::setIsInSittingState(bool isSitting) { + _sitStandStateCount = 0; + _squatCount = 0; + // on reset height we need the count to be more than one in case the user sits and stands up quickly. _isInSittingState.set(isSitting); setResetMode(true); if (isSitting) { @@ -3929,12 +3917,10 @@ void MyAvatar::setIsInSittingState(bool isSitting) { } void MyAvatar::setIsSitStandStateLocked(bool isLocked) { - const float DEFAULT_FLOOR_HEIGHT = 0.0f; _lockSitStandState.set(isLocked); _sitStandStateCount = 0; - _sumUserHeightSensorSpace = DEFAULT_AVATAR_HEIGHT; - _tippingPoint = DEFAULT_FLOOR_HEIGHT; - _averageUserHeightCount = 1; + _sumUserHeightSensorSpace = _userHeight.get(); + _tippingPoint = _userHeight.get(); if (!isLocked) { // always start the auto transition mode in standing state. setIsInSittingState(false); @@ -4238,6 +4224,9 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } } } + if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { + activate(Vertical); + } } else { if (!isActive(Rotation) && getForceActivateRotation()) { activate(Rotation); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index e8d9090e03..59f9145404 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1116,6 +1116,7 @@ public: float getSprintSpeed() const; void setSitStandStateChange(bool stateChanged); float getSitStandStateChange() const; + void updateSitStandState(float newHeightReading, float angleHeadUp); QVector getScriptUrls(); @@ -1830,10 +1831,9 @@ private: const float DEFAULT_FLOOR_HEIGHT = 0.0f; // height of user in sensor space, when standing erect. - ThreadSafeValueCache _userHeight{ DEFAULT_AVATAR_HEIGHT }; - float _sumUserHeightSensorSpace{ DEFAULT_AVATAR_HEIGHT }; - int _averageUserHeightCount{ 1 }; - bool _sitStandStateChange{ false }; + ThreadSafeValueCache _userHeight { DEFAULT_AVATAR_HEIGHT }; + float _sumUserHeightSensorSpace { _userHeight.get() }; + bool _sitStandStateChange { false }; ThreadSafeValueCache _lockSitStandState { false }; // max unscaled forward movement speed @@ -1845,7 +1845,7 @@ private: ThreadSafeValueCache _isInSittingState { false }; int _sitStandStateCount { 0 }; int _squatCount { 0 }; - float _tippingPoint { DEFAULT_AVATAR_HEIGHT }; + float _tippingPoint { _userHeight.get() }; // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; From cf7dc49499814f286b0e19c7ad75482b9557e367 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 16 Oct 2018 16:04:31 -0700 Subject: [PATCH 122/362] removed a commment and changed the sanity check to be a const instead of a magic number --- interface/src/avatar/MyAvatar.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b59ff675e1..742dda72b2 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -470,8 +470,8 @@ void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { const int SITTING_COUNT_THRESHOLD = 180; const int STANDING_COUNT_THRESHOLD = 60; const int SQUATTY_COUNT_THRESHOLD = 600; + const float SITTING_UPPER_BOUND = 1.52f; - // qCDebug(interfaceapp) << "locked " << getIsSitStandStateLocked() << " away " << getIsAway() << " hmd " << qApp->isHMDMode() << " user height " << _userHeight.get(); if (!getIsSitStandStateLocked() && !getIsAway() && qApp->isHMDMode()) { if (getIsInSittingState()) { if (newHeightReading > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { @@ -494,7 +494,7 @@ void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { } } else { // sanity check if average height greater than 5ft they are not sitting(or get off your dangerous barstool please) - if (_sumUserHeightSensorSpace > 1.52f) { + if (_sumUserHeightSensorSpace > SITTING_UPPER_BOUND) { setIsInSittingState(true); } else { // tipping point is average height when sitting. From 1764531822f86b160a2e509c6e46820d9a088f1c Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 16 Oct 2018 16:31:55 -0700 Subject: [PATCH 123/362] removed hand azimuth changes, that fixed azimuth when hands go behind origin of MyAvatar, these changes will be in a separate pr --- interface/src/avatar/MyAvatar.cpp | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 742dda72b2..7a35b0ee56 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -941,13 +941,6 @@ void MyAvatar::updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix) { // Find the vector halfway between the hip to hand azimuth vectors // This midpoint hand azimuth is in Avatar space glm::vec2 MyAvatar::computeHandAzimuth() const { - int spine2Index = _skeletonModel->getRig().indexOfJoint("Spine2"); - glm::vec3 azimuthOrigin(0.0f,0.0f,0.0f); - if (!(spine2Index < 0)) { - // use the spine for the azimuth origin. - azimuthOrigin = getAbsoluteJointTranslationInObjectFrame(spine2Index); - } - controller::Pose leftHandPoseAvatarSpace = getLeftHandPose(); controller::Pose rightHandPoseAvatarSpace = getRightHandPose(); controller::Pose headPoseAvatarSpace = getControllerPoseInAvatarFrame(controller::Action::HEAD); @@ -955,13 +948,11 @@ glm::vec2 MyAvatar::computeHandAzimuth() const { glm::vec2 latestHipToHandController = _hipToHandController; if (leftHandPoseAvatarSpace.isValid() && rightHandPoseAvatarSpace.isValid() && headPoseAvatarSpace.isValid()) { - glm::vec3 rightHandOffset = rightHandPoseAvatarSpace.translation - azimuthOrigin; - glm::vec3 leftHandOffset = leftHandPoseAvatarSpace.translation - azimuthOrigin; // we need the old azimuth reading to prevent flipping the facing direction 180 // in the case where the hands go from being slightly less than 180 apart to slightly more than 180 apart. glm::vec2 oldAzimuthReading = _hipToHandController; - if ((glm::length(glm::vec2(rightHandOffset.x, rightHandOffset.z)) > 0.0f) && (glm::length(glm::vec2(leftHandOffset.x, leftHandOffset.z)) > 0.0f)) { - latestHipToHandController = lerp(glm::normalize(glm::vec2(rightHandOffset.x, rightHandOffset.z)), glm::normalize(glm::vec2(leftHandOffset.x, leftHandOffset.z)), HALFWAY); + if ((glm::length(glm::vec2(rightHandPoseAvatarSpace.translation.x, rightHandPoseAvatarSpace.translation.z)) > 0.0f) && (glm::length(glm::vec2(leftHandPoseAvatarSpace.translation.x, leftHandPoseAvatarSpace.translation.z)) > 0.0f)) { + latestHipToHandController = lerp(glm::normalize(glm::vec2(rightHandPoseAvatarSpace.translation.x, rightHandPoseAvatarSpace.translation.z)), glm::normalize(glm::vec2(leftHandPoseAvatarSpace.translation.x, leftHandPoseAvatarSpace.translation.z)), HALFWAY); } else { latestHipToHandController = glm::vec2(0.0f, -1.0f); } From 4837fd86de6e72a59436280364d241fc4babe4f9 Mon Sep 17 00:00:00 2001 From: sam gateau Date: Tue, 16 Oct 2018 17:29:46 -0700 Subject: [PATCH 124/362] Desperataly trying to be efficient in js --- .../utilities/render/deferredLighting.qml | 26 +++- scripts/developer/utilities/render/luci.js | 128 +++++++++++++++--- 2 files changed, 129 insertions(+), 25 deletions(-) diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index a9479b2935..135c3fac97 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -279,11 +279,27 @@ Rectangle { } } Separator {} - HifiControls.Button { - text: "Engine" - // activeFocusOnPress: false - onClicked: { - sendToScript({method: "openEngineView"}); + Row { + HifiControls.Button { + text: "Engine" + // activeFocusOnPress: false + onClicked: { + sendToScript({method: "openEngineView"}); + } + } + HifiControls.Button { + text: "LOD" + // activeFocusOnPress: false + onClicked: { + sendToScript({method: "openEngineLODView"}); + } + } + HifiControls.Button { + text: "Cull" + // activeFocusOnPress: false + onClicked: { + sendToScript({method: "openCullInspectorView"}); + } } } } diff --git a/scripts/developer/utilities/render/luci.js b/scripts/developer/utilities/render/luci.js index cb5b01f9b2..40fb6e01d7 100644 --- a/scripts/developer/utilities/render/luci.js +++ b/scripts/developer/utilities/render/luci.js @@ -88,35 +88,123 @@ } - function fromQml(message) { - switch (message.method) { - case "openEngineView": - openEngineTaskView(); - break; - } - } + + var Page = function(title, qmlurl, width, height) { + + this.title = title; + this.qml = Script.resolvePath(qmlurl); + this.width = width; + this.height = height; + this.window = null; + }; + + Page.prototype.createView = function() { + if (this.window == null) { + var window = Desktop.createWindow(this.qml, { + title: this.title, + presentationMode: Desktop.PresentationMode.NATIVE, + size: {x: this.width, y: this.height} + }); + this.window = window + this.window.closed.connect(this.killView); + } + }; + + Page.prototype.killView = function() { + if (this.window !== undefined) { + this.window.closed.disconnect(this.killView); + this.window.close() + this.window = undefined + } + }; + + var pages = [] + pages.push_back(new Page('Render Engine', 'engineInspector.qml', 300, 400)) + + var engineInspectorView = null - function openEngineTaskView() { - if (engineInspectorView == null) { + function openEngineInspectorView() { + + /* if (engineInspectorView == null) { var qml = Script.resolvePath('engineInspector.qml'); - var window = new OverlayWindow({ + var window = Desktop.createWindow(qml, { title: 'Render Engine', - source: qml, - width: 300, - height: 400 + presentationMode: Desktop.PresentationMode.NATIVE, + size: {x: 300, y: 400} }); - window.setPosition(200, 50); engineInspectorView = window - window.closed.connect(function() { engineInspectorView = null; }); - } else { - engineInspectorView.setPosition(200, 50); + window.closed.connect(killEngineInspectorView); } } + function killEngineInspectorView() { + if (engineInspectorView !== undefined) { + engineInspectorView.closed.disconnect(killEngineInspectorView); + engineInspectorView.close() + engineInspectorView = undefined + } + } +*/ + var cullInspectorView = null + function openCullInspectorView() { + if (cullInspectorView == null) { + var qml = Script.resolvePath('culling.qml'); + var window = Desktop.createWindow(qml, { + title: 'Cull Inspector', + presentationMode: Desktop.PresentationMode.NATIVE, + size: {x: 400, y: 600} + }); + cullInspectorView = window + window.closed.connect(killCullInspectorView); + } + } + function killCullInspectorView() { + if (cullInspectorView !== undefined) { + cullInspectorView.closed.disconnect(killCullInspectorView); + cullInspectorView.close() + cullInspectorView = undefined + } + } + + var engineLODView = null + function openEngineLODView() { + if (engineLODView == null) { + engineLODView = Desktop.createWindow(Script.resolvePath('lod.qml'), { + title: 'Render LOD', + flags: Desktop.ALWAYS_ON_TOP, + presentationMode: Desktop.PresentationMode.NATIVE, + size: {x: 300, y: 500}, + }); + engineLODView.closed.connect(killEngineLODView); + } + } + function killEngineLODView() { + if (engineLODView !== undefined) { + engineLODView.closed.disconnect(killEngineLODView); + engineLODView.close() + engineLODView = undefined + } + } + + + function fromQml(message) { + switch (message.method) { + case "openEngineView": + openEngineInspectorView(); + break; + case "openCullInspectorView": + openCullInspectorView(); + break; + case "openEngineLODView": + openEngineLODView(); + break; + } + } + Script.scriptEnding.connect(function () { if (onLuciScreen) { tablet.gotoHomeScreen(); @@ -125,8 +213,8 @@ tablet.screenChanged.disconnect(onScreenChanged); tablet.removeButton(button); - if (engineInspectorView !== null) { - engineInspectorView.close() - } + killEngineInspectorView(); + killCullInspectorView(); + killEngineLODWindow(); }); }()); \ No newline at end of file From 26e388b139bb040ae2260042e7c8ff327ca18e1f Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 16 Oct 2018 17:34:48 -0700 Subject: [PATCH 125/362] Some experimentation yields promising results... --- interface/src/Application.cpp | 1 + .../src/scripting/TTSScriptingInterface.cpp | 17 +- .../src/scripting/TTSScriptingInterface.h | 11 +- libraries/audio-client/src/AudioClient.cpp | 1607 +++++++++-------- libraries/audio-client/src/AudioClient.h | 19 +- 5 files changed, 844 insertions(+), 811 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 728fea8c10..2991fab5f7 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1182,6 +1182,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo auto TTS = DependencyManager::get().data(); connect(TTS, &TTSScriptingInterface::ttsSampleCreated, audioIO, &AudioClient::handleTTSAudioInput); + connect(TTS, &TTSScriptingInterface::clearTTSBuffer, audioIO, &AudioClient::clearTTSBuffer); connect(audioIO, &AudioClient::inputReceived, [](const QByteArray& audio) { static auto recorder = DependencyManager::get(); diff --git a/interface/src/scripting/TTSScriptingInterface.cpp b/interface/src/scripting/TTSScriptingInterface.cpp index fdbb37e586..5fb47a73c3 100644 --- a/interface/src/scripting/TTSScriptingInterface.cpp +++ b/interface/src/scripting/TTSScriptingInterface.cpp @@ -65,7 +65,7 @@ void TTSScriptingInterface::testTone(const bool& alsoInject) { int16_t temp = (glm::sin(glm::radians((float)a))) * 32768; samples[a] = temp; } - emit ttsSampleCreated(_lastSoundByteArray); + emit ttsSampleCreated(_lastSoundByteArray, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * 50, 96); if (alsoInject) { AudioInjectorOptions options; @@ -75,11 +75,16 @@ void TTSScriptingInterface::testTone(const bool& alsoInject) { } } -void TTSScriptingInterface::speakText(const QString& textToSpeak, const bool& alsoInject) { +void TTSScriptingInterface::speakText(const QString& textToSpeak, + const int& newChunkSize, + const int& timerInterval, + const int& sampleRate, + const int& bitsPerSample, + const bool& alsoInject) { WAVEFORMATEX fmt; fmt.wFormatTag = WAVE_FORMAT_PCM; - fmt.nSamplesPerSec = 24000; - fmt.wBitsPerSample = 16; + fmt.nSamplesPerSec = sampleRate; + fmt.wBitsPerSample = bitsPerSample; fmt.nChannels = 1; fmt.nBlockAlign = fmt.nChannels * fmt.wBitsPerSample / 8; fmt.nAvgBytesPerSec = fmt.nSamplesPerSec * fmt.nBlockAlign; @@ -146,7 +151,7 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak, const bool& al _lastSoundByteArray.resize(0); _lastSoundByteArray.append(buf1, dwSize); - emit ttsSampleCreated(_lastSoundByteArray); + emit ttsSampleCreated(_lastSoundByteArray, newChunkSize, timerInterval); if (alsoInject) { AudioInjectorOptions options; @@ -160,4 +165,6 @@ void TTSScriptingInterface::stopLastSpeech() { if (_lastSoundAudioInjector) { _lastSoundAudioInjector->stop(); } + + emit clearTTSBuffer(); } diff --git a/interface/src/scripting/TTSScriptingInterface.h b/interface/src/scripting/TTSScriptingInterface.h index c1fffe67d1..f6eca081ab 100644 --- a/interface/src/scripting/TTSScriptingInterface.h +++ b/interface/src/scripting/TTSScriptingInterface.h @@ -19,6 +19,7 @@ #include // SAPI #include // SAPI Helper #include +#include class TTSScriptingInterface : public QObject, public Dependency { Q_OBJECT @@ -28,11 +29,17 @@ public: ~TTSScriptingInterface(); Q_INVOKABLE void testTone(const bool& alsoInject = false); - Q_INVOKABLE void speakText(const QString& textToSpeak, const bool& alsoInject = false); + Q_INVOKABLE void speakText(const QString& textToSpeak, + const int& newChunkSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * 50), + const int& timerInterval = 96, + const int& sampleRate = 24000, + const int& bitsPerSample = 16, + const bool& alsoInject = false); Q_INVOKABLE void stopLastSpeech(); signals: - void ttsSampleCreated(QByteArray outputArray); + void ttsSampleCreated(QByteArray outputArray, const int& newChunkSize, const int& timerInterval); + void clearTTSBuffer(); private: class CComAutoInit { diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index c2b066b716..606763e4ab 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -186,7 +186,7 @@ AudioClient::AudioClient() : _networkToOutputResampler(NULL), _localToOutputResampler(NULL), _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), _stats(&_receivedAudioStream), - _positionGetter(DEFAULT_POSITION_GETTER), + _positionGetter(DEFAULT_POSITION_GETTER), _TTSTimer(this), #if defined(Q_OS_ANDROID) _checkInputTimer(this), _isHeadsetPluggedIn(false), #endif @@ -245,6 +245,8 @@ AudioClient::AudioClient() : packetReceiver.registerListener(PacketType::NoisyMute, this, "handleNoisyMutePacket"); packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket"); packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat"); + + connect(&_TTSTimer, &QTimer::timeout, this, &AudioClient::processTTSBuffer); } AudioClient::~AudioClient() { @@ -939,7 +941,7 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) { } } -void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { +void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray, const int& sampleRate, const int& channelCount) { // If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here. bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); if (_muted || !_audioOutput || (!_shouldEchoLocally && !hasReverb)) { @@ -949,7 +951,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // NOTE: we assume the inputFormat and the outputFormat are the same, since on any modern // multimedia OS they should be. If there is a device that this is not true for, we can // add back support to do resampling. - if (_inputFormat.sampleRate() != _outputFormat.sampleRate()) { + if (sampleRate != _outputFormat.sampleRate()) { return; } @@ -972,7 +974,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { static QByteArray loopBackByteArray; int numInputSamples = inputByteArray.size() / AudioConstants::SAMPLE_SIZE; - int numLoopbackSamples = (numInputSamples * OUTPUT_CHANNEL_COUNT) / _inputFormat.channelCount(); + int numLoopbackSamples = (numInputSamples * OUTPUT_CHANNEL_COUNT) / channelCount; loopBackByteArray.resize(numLoopbackSamples * AudioConstants::SAMPLE_SIZE); @@ -980,7 +982,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { int16_t* loopbackSamples = reinterpret_cast(loopBackByteArray.data()); // upmix mono to stereo - if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat.channelCount(), + if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, channelCount, OUTPUT_CHANNEL_COUNT)) { // no conversion, just copy the samples memcpy(loopbackSamples, inputSamples, numInputSamples * AudioConstants::SAMPLE_SIZE); @@ -1093,23 +1095,29 @@ void AudioClient::handleAudioInput(QByteArray& audioBuffer) { } } -void AudioClient::processAudioAndAddToRingBuffer(QByteArray& inputByteArray, - const uchar& channelCount, - const qint32& bytesForDuration, - QByteArray& rollingBuffer) { +void AudioClient::handleMicAudioInput() { + if (!_inputDevice || _isPlayingBackRecording) { + return; + } + +#if defined(Q_OS_ANDROID) + _inputReadsSinceLastCheck++; +#endif + // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output const int inputSamplesRequired = (_inputToNetworkResampler ? _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * - channelCount; + _inputFormat.channelCount(); const auto inputAudioSamples = std::unique_ptr(new int16_t[inputSamplesRequired]); + QByteArray inputByteArray = _inputDevice->readAll(); - handleLocalEchoAndReverb(inputByteArray); + handleLocalEchoAndReverb(inputByteArray, _inputFormat.sampleRate(), _inputFormat.channelCount()); _inputRingBuffer.writeData(inputByteArray.data(), inputByteArray.size()); - float audioInputMsecsRead = inputByteArray.size() / (float)(bytesForDuration); + float audioInputMsecsRead = inputByteArray.size() / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); _stats.updateInputMsRead(audioInputMsecsRead); const int numNetworkBytes = @@ -1125,33 +1133,17 @@ void AudioClient::processAudioAndAddToRingBuffer(QByteArray& inputByteArray, } else { _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); possibleResampling(_inputToNetworkResampler, inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, - numNetworkSamples, channelCount, _desiredInputFormat.channelCount()); + numNetworkSamples, _inputFormat.channelCount(), _desiredInputFormat.channelCount()); } int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE; - float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(bytesForDuration); + float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); _stats.updateInputMsUnplayed(msecsInInputRingBuffer); QByteArray audioBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); - rollingBuffer.append(audioBuffer); handleAudioInput(audioBuffer); } } -void AudioClient::handleMicAudioInput() { - if (!_inputDevice || _isPlayingBackRecording) { - return; - } - -#if defined(Q_OS_ANDROID) - _inputReadsSinceLastCheck++; -#endif - - QByteArray temp; - - processAudioAndAddToRingBuffer(_inputDevice->readAll(), _inputFormat.channelCount(), - _inputFormat.bytesForDuration(USECS_PER_MSEC), temp); -} - void AudioClient::handleDummyAudioInput() { const int numNetworkBytes = _isStereoInput ? AudioConstants::NETWORK_FRAME_BYTES_STEREO : AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; @@ -1192,7 +1184,7 @@ int rawToWav(const char* rawData, const int& rawLength, const char* wavfn, long long chunksize = 0x10; fwrite(&chunksize, 4, 1, wav); - fmt.wFormatTag = 1; // WAVE_FORMAT_PCM + fmt.wFormatTag = 1; // WAVE_FORMAT_PCM fmt.wChannels = channels; fmt.dwSamplesPerSec = frequency * 1; fmt.wBitsPerSample = 16; @@ -1210,906 +1202,927 @@ int rawToWav(const char* rawData, const int& rawLength, const char* wavfn, long return 0; } -void AudioClient::handleTTSAudioInput(const QByteArray& audio) { - QByteArray audioBuffer(audio); - - QString filename = QString::number(usecTimestampNow()); - QString path = PathUtils::getAppDataPath() + "Audio/" + filename + "-before.wav"; - rawToWav(audioBuffer.data(), audioBuffer.size(), path.toLocal8Bit(), 24000, 1); - - QByteArray temp; - - while (audioBuffer.size() > 0) { +void AudioClient::processTTSBuffer() { + Lock lock(_TTSMutex); + if (_TTSAudioBuffer.size() > 0) { QByteArray part; - part.append(audioBuffer.data(), AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - audioBuffer.remove(0, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - processAudioAndAddToRingBuffer(part, 1, 48, temp); + part.append(_TTSAudioBuffer.data(), _TTSChunkSize); + _TTSAudioBuffer.remove(0, _TTSChunkSize); + handleAudioInput(part); + } else { + _isProcessingTTS = false; + _TTSTimer.stop(); } +} - filename = QString::number(usecTimestampNow()); - path = PathUtils::getAppDataPath() + "Audio/" + filename + "-after.wav"; - rawToWav(temp.data(), temp.size(), path.toLocal8Bit(), 12000, 1); +void AudioClient::handleTTSAudioInput(const QByteArray& audio, const int& newChunkSize, const int& timerInterval) { + _TTSChunkSize = newChunkSize; + _TTSAudioBuffer.append(audio); + + handleLocalEchoAndReverb(_TTSAudioBuffer, 48000, 1); + + //QString filename = QString::number(usecTimestampNow()); + //QString path = PathUtils::getAppDataPath() + "Audio/" + filename + "-before.wav"; + //rawToWav(_TTSAudioBuffer.data(), _TTSAudioBuffer.size(), path.toLocal8Bit(), 24000, 1); + + //QByteArray temp; + + _isProcessingTTS = true; + _TTSTimer.start(timerInterval); + + //filename = QString::number(usecTimestampNow()); + //path = PathUtils::getAppDataPath() + "Audio/" + filename + "-after.wav"; + //rawToWav(temp.data(), temp.size(), path.toLocal8Bit(), 12000, 1); +} + +void AudioClient::clearTTSBuffer() { + _TTSAudioBuffer.resize(0); + _isProcessingTTS = false; + _TTSTimer.stop(); } void AudioClient::prepareLocalAudioInjectors(std::unique_ptr localAudioLock) { - bool doSynchronously = localAudioLock.operator bool(); - if (!localAudioLock) { - localAudioLock.reset(new Lock(_localAudioMutex)); + bool doSynchronously = localAudioLock.operator bool(); + if (!localAudioLock) { + localAudioLock.reset(new Lock(_localAudioMutex)); + } + + int samplesNeeded = std::numeric_limits::max(); + while (samplesNeeded > 0) { + if (!doSynchronously) { + // unlock between every write to allow device switching + localAudioLock->unlock(); + localAudioLock->lock(); + } + + // in case of a device switch, consider bufferCapacity volatile across iterations + if (_outputPeriod == 0) { + return; + } + + int bufferCapacity = _localInjectorsStream.getSampleCapacity(); + int maxOutputSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * AudioConstants::STEREO; + if (_localToOutputResampler) { + maxOutputSamples = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * + AudioConstants::STEREO; + } + + samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed); + if (samplesNeeded < maxOutputSamples) { + // avoid overwriting the buffer to prevent losing frames + break; + } + + // get a network frame of local injectors' audio + if (!mixLocalAudioInjectors(_localMixBuffer)) { + break; + } + + // reverb + if (_reverb) { + _localReverb.render(_localMixBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } + + int samples; + if (_localToOutputResampler) { + // resample to output sample rate + int frames = _localToOutputResampler->render(_localMixBuffer, _localOutputMixBuffer, + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + + // write to local injectors' ring buffer + samples = frames * AudioConstants::STEREO; + _localInjectorsStream.writeSamples(_localOutputMixBuffer, samples); + + } else { + // write to local injectors' ring buffer + samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; + _localInjectorsStream.writeSamples(_localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + } + + _localSamplesAvailable.fetch_add(samples, std::memory_order_release); + samplesNeeded -= samples; + } } - int samplesNeeded = std::numeric_limits::max(); - while (samplesNeeded > 0) { - if (!doSynchronously) { - // unlock between every write to allow device switching - localAudioLock->unlock(); - localAudioLock->lock(); + bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { + // check the flag for injectors before attempting to lock + if (!_localInjectorsAvailable.load(std::memory_order_acquire)) { + return false; } - // in case of a device switch, consider bufferCapacity volatile across iterations - if (_outputPeriod == 0) { - return; - } + // lock the injectors + Lock lock(_injectorsMutex); - int bufferCapacity = _localInjectorsStream.getSampleCapacity(); - int maxOutputSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * AudioConstants::STEREO; - if (_localToOutputResampler) { - maxOutputSamples = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * - AudioConstants::STEREO; - } + QVector injectorsToRemove; - samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed); - if (samplesNeeded < maxOutputSamples) { - // avoid overwriting the buffer to prevent losing frames - break; - } + memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float)); - // get a network frame of local injectors' audio - if (!mixLocalAudioInjectors(_localMixBuffer)) { - break; - } + for (const AudioInjectorPointer& injector : _activeLocalAudioInjectors) { + // the lock guarantees that injectorBuffer, if found, is invariant + AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); + if (injectorBuffer) { + static const int HRTF_DATASET_INDEX = 1; - // reverb - if (_reverb) { - _localReverb.render(_localMixBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - } + int numChannels = injector->isAmbisonic() + ? AudioConstants::AMBISONIC + : (injector->isStereo() ? AudioConstants::STEREO : AudioConstants::MONO); + size_t bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; - int samples; - if (_localToOutputResampler) { - // resample to output sample rate - int frames = _localToOutputResampler->render(_localMixBuffer, _localOutputMixBuffer, - AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + // get one frame from the injector + memset(_localScratchBuffer, 0, bytesToRead); + if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { + if (injector->isAmbisonic()) { + // no distance attenuation + float gain = injector->getVolume(); - // write to local injectors' ring buffer - samples = frames * AudioConstants::STEREO; - _localInjectorsStream.writeSamples(_localOutputMixBuffer, samples); + // + // Calculate the soundfield orientation relative to the listener. + // Injector orientation can be used to align a recording to our world coordinates. + // + glm::quat relativeOrientation = injector->getOrientation() * glm::inverse(_orientationGetter()); - } else { - // write to local injectors' ring buffer - samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; - _localInjectorsStream.writeSamples(_localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); - } + // convert from Y-up (OpenGL) to Z-up (Ambisonic) coordinate system + float qw = relativeOrientation.w; + float qx = -relativeOrientation.z; + float qy = -relativeOrientation.x; + float qz = relativeOrientation.y; - _localSamplesAvailable.fetch_add(samples, std::memory_order_release); - samplesNeeded -= samples; - } -} + // Ambisonic gets spatialized into mixBuffer + injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, qw, qx, qy, qz, gain, + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); -bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { - // check the flag for injectors before attempting to lock - if (!_localInjectorsAvailable.load(std::memory_order_acquire)) { - return false; - } + } else if (injector->isStereo()) { + // stereo gets directly mixed into mixBuffer + float gain = injector->getVolume(); + for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { + mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain; + } - // lock the injectors - Lock lock(_injectorsMutex); + } else { + // calculate distance, gain and azimuth for hrtf + glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); + float distance = glm::max(glm::length(relativePosition), EPSILON); + float gain = gainForSource(distance, injector->getVolume()); + float azimuth = azimuthForSource(relativePosition); - QVector injectorsToRemove; - - memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float)); - - for (const AudioInjectorPointer& injector : _activeLocalAudioInjectors) { - // the lock guarantees that injectorBuffer, if found, is invariant - AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); - if (injectorBuffer) { - static const int HRTF_DATASET_INDEX = 1; - - int numChannels = injector->isAmbisonic() ? AudioConstants::AMBISONIC - : (injector->isStereo() ? AudioConstants::STEREO : AudioConstants::MONO); - size_t bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; - - // get one frame from the injector - memset(_localScratchBuffer, 0, bytesToRead); - if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - if (injector->isAmbisonic()) { - // no distance attenuation - float gain = injector->getVolume(); - - // - // Calculate the soundfield orientation relative to the listener. - // Injector orientation can be used to align a recording to our world coordinates. - // - glm::quat relativeOrientation = injector->getOrientation() * glm::inverse(_orientationGetter()); - - // convert from Y-up (OpenGL) to Z-up (Ambisonic) coordinate system - float qw = relativeOrientation.w; - float qx = -relativeOrientation.z; - float qy = -relativeOrientation.x; - float qz = relativeOrientation.y; - - // Ambisonic gets spatialized into mixBuffer - injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, qw, qx, qy, qz, gain, - AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - - } else if (injector->isStereo()) { - // stereo gets directly mixed into mixBuffer - float gain = injector->getVolume(); - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain; + // mono gets spatialized into mixBuffer + injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, azimuth, distance, + gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } } else { - // calculate distance, gain and azimuth for hrtf - glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); - float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = gainForSource(distance, injector->getVolume()); - float azimuth = azimuthForSource(relativePosition); - - // mono gets spatialized into mixBuffer - injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, azimuth, distance, gain, - AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + qCDebug(audioclient) << "injector has no more data, marking finished for removal"; + injector->finishLocalInjection(); + injectorsToRemove.append(injector); } } else { - qCDebug(audioclient) << "injector has no more data, marking finished for removal"; + qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } - - } else { - qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; - injector->finishLocalInjection(); - injectorsToRemove.append(injector); - } - } - - for (const AudioInjectorPointer& injector : injectorsToRemove) { - qCDebug(audioclient) << "removing injector"; - _activeLocalAudioInjectors.removeOne(injector); - } - - // update the flag - _localInjectorsAvailable.exchange(!_activeLocalAudioInjectors.empty(), std::memory_order_release); - - return true; -} - -void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteArray& outputBuffer) { - const int16_t* decodedSamples = reinterpret_cast(decodedBuffer.data()); - assert(decodedBuffer.size() == AudioConstants::NETWORK_FRAME_BYTES_STEREO); - - outputBuffer.resize(_outputFrameSize * AudioConstants::SAMPLE_SIZE); - int16_t* outputSamples = reinterpret_cast(outputBuffer.data()); - - bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); - - // apply stereo reverb - if (hasReverb) { - updateReverbOptions(); - int16_t* reverbSamples = _networkToOutputResampler ? _networkScratchBuffer : outputSamples; - _listenerReverb.render(decodedSamples, reverbSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - } - - // resample to output sample rate - if (_networkToOutputResampler) { - const int16_t* inputSamples = hasReverb ? _networkScratchBuffer : decodedSamples; - _networkToOutputResampler->render(inputSamples, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - } - - // if no transformations were applied, we still need to copy the buffer - if (!hasReverb && !_networkToOutputResampler) { - memcpy(outputSamples, decodedSamples, decodedBuffer.size()); - } -} - -void AudioClient::sendMuteEnvironmentPacket() { - auto nodeList = DependencyManager::get(); - - int dataSize = sizeof(glm::vec3) + sizeof(float); - - auto mutePacket = NLPacket::create(PacketType::MuteEnvironment, dataSize); - - const float MUTE_RADIUS = 50; - - glm::vec3 currentSourcePosition = _positionGetter(); - - mutePacket->writePrimitive(currentSourcePosition); - mutePacket->writePrimitive(MUTE_RADIUS); - - // grab our audio mixer from the NodeList, if it exists - SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer); - - if (audioMixer) { - // send off this mute packet - nodeList->sendPacket(std::move(mutePacket), *audioMixer); - } -} - -void AudioClient::setMuted(bool muted, bool emitSignal) { - if (_muted != muted) { - _muted = muted; - if (emitSignal) { - emit muteToggled(_muted); - } - } -} - -void AudioClient::setNoiseReduction(bool enable, bool emitSignal) { - if (_isNoiseGateEnabled != enable) { - _isNoiseGateEnabled = enable; - if (emitSignal) { - emit noiseReductionChanged(_isNoiseGateEnabled); - } - } -} - -bool AudioClient::setIsStereoInput(bool isStereoInput) { - bool stereoInputChanged = false; - if (isStereoInput != _isStereoInput && _inputDeviceInfo.supportedChannelCounts().contains(2)) { - _isStereoInput = isStereoInput; - stereoInputChanged = true; - - if (_isStereoInput) { - _desiredInputFormat.setChannelCount(2); - } else { - _desiredInputFormat.setChannelCount(1); } - // restart the codec - if (_codec) { - if (_encoder) { - _codec->releaseEncoder(_encoder); - } - _encoder = _codec->createEncoder(AudioConstants::SAMPLE_RATE, - _isStereoInput ? AudioConstants::STEREO : AudioConstants::MONO); + for (const AudioInjectorPointer& injector : injectorsToRemove) { + qCDebug(audioclient) << "removing injector"; + _activeLocalAudioInjectors.removeOne(injector); } - qCDebug(audioclient) << "Reset Codec:" << _selectedCodecName << "isStereoInput:" << _isStereoInput; - // restart the input device - switchInputToAudioDevice(_inputDeviceInfo); - - emit isStereoInputChanged(_isStereoInput); - } - - return stereoInputChanged; -} - -bool AudioClient::outputLocalInjector(const AudioInjectorPointer& injector) { - AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); - if (injectorBuffer) { - // local injectors are on the AudioInjectorsThread, so we must guard access - Lock lock(_injectorsMutex); - if (!_activeLocalAudioInjectors.contains(injector)) { - qCDebug(audioclient) << "adding new injector"; - _activeLocalAudioInjectors.append(injector); - // move local buffer to the LocalAudioThread to avoid dataraces with AudioInjector (like stop()) - injectorBuffer->setParent(nullptr); - - // update the flag - _localInjectorsAvailable.exchange(true, std::memory_order_release); - } else { - qCDebug(audioclient) << "injector exists in active list already"; - } + // update the flag + _localInjectorsAvailable.exchange(!_activeLocalAudioInjectors.empty(), std::memory_order_release); return true; - - } else { - // no local buffer - return false; } -} -void AudioClient::outputFormatChanged() { - _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * OUTPUT_CHANNEL_COUNT * _outputFormat.sampleRate()) / - _desiredOutputFormat.sampleRate(); - _receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); -} + void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteArray& outputBuffer) { + const int16_t* decodedSamples = reinterpret_cast(decodedBuffer.data()); + assert(decodedBuffer.size() == AudioConstants::NETWORK_FRAME_BYTES_STEREO); -bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo inputDeviceInfo, bool isShutdownRequest) { - Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); + outputBuffer.resize(_outputFrameSize * AudioConstants::SAMPLE_SIZE); + int16_t* outputSamples = reinterpret_cast(outputBuffer.data()); - qCDebug(audioclient) << __FUNCTION__ << "inputDeviceInfo: [" << inputDeviceInfo.deviceName() << "]"; - bool supportedFormat = false; + bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); - // NOTE: device start() uses the Qt internal device list - Lock lock(_deviceMutex); + // apply stereo reverb + if (hasReverb) { + updateReverbOptions(); + int16_t* reverbSamples = _networkToOutputResampler ? _networkScratchBuffer : outputSamples; + _listenerReverb.render(decodedSamples, reverbSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } + + // resample to output sample rate + if (_networkToOutputResampler) { + const int16_t* inputSamples = hasReverb ? _networkScratchBuffer : decodedSamples; + _networkToOutputResampler->render(inputSamples, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } + + // if no transformations were applied, we still need to copy the buffer + if (!hasReverb && !_networkToOutputResampler) { + memcpy(outputSamples, decodedSamples, decodedBuffer.size()); + } + } + + void AudioClient::sendMuteEnvironmentPacket() { + auto nodeList = DependencyManager::get(); + + int dataSize = sizeof(glm::vec3) + sizeof(float); + + auto mutePacket = NLPacket::create(PacketType::MuteEnvironment, dataSize); + + const float MUTE_RADIUS = 50; + + glm::vec3 currentSourcePosition = _positionGetter(); + + mutePacket->writePrimitive(currentSourcePosition); + mutePacket->writePrimitive(MUTE_RADIUS); + + // grab our audio mixer from the NodeList, if it exists + SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer); + + if (audioMixer) { + // send off this mute packet + nodeList->sendPacket(std::move(mutePacket), *audioMixer); + } + } + + void AudioClient::setMuted(bool muted, bool emitSignal) { + if (_muted != muted) { + _muted = muted; + if (emitSignal) { + emit muteToggled(_muted); + } + } + } + + void AudioClient::setNoiseReduction(bool enable, bool emitSignal) { + if (_isNoiseGateEnabled != enable) { + _isNoiseGateEnabled = enable; + if (emitSignal) { + emit noiseReductionChanged(_isNoiseGateEnabled); + } + } + } + + bool AudioClient::setIsStereoInput(bool isStereoInput) { + bool stereoInputChanged = false; + if (isStereoInput != _isStereoInput && _inputDeviceInfo.supportedChannelCounts().contains(2)) { + _isStereoInput = isStereoInput; + stereoInputChanged = true; + + if (_isStereoInput) { + _desiredInputFormat.setChannelCount(2); + } else { + _desiredInputFormat.setChannelCount(1); + } + + // restart the codec + if (_codec) { + if (_encoder) { + _codec->releaseEncoder(_encoder); + } + _encoder = _codec->createEncoder(AudioConstants::SAMPLE_RATE, + _isStereoInput ? AudioConstants::STEREO : AudioConstants::MONO); + } + qCDebug(audioclient) << "Reset Codec:" << _selectedCodecName << "isStereoInput:" << _isStereoInput; + + // restart the input device + switchInputToAudioDevice(_inputDeviceInfo); + + emit isStereoInputChanged(_isStereoInput); + } + + return stereoInputChanged; + } + + bool AudioClient::outputLocalInjector(const AudioInjectorPointer& injector) { + AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); + if (injectorBuffer) { + // local injectors are on the AudioInjectorsThread, so we must guard access + Lock lock(_injectorsMutex); + if (!_activeLocalAudioInjectors.contains(injector)) { + qCDebug(audioclient) << "adding new injector"; + _activeLocalAudioInjectors.append(injector); + // move local buffer to the LocalAudioThread to avoid dataraces with AudioInjector (like stop()) + injectorBuffer->setParent(nullptr); + + // update the flag + _localInjectorsAvailable.exchange(true, std::memory_order_release); + } else { + qCDebug(audioclient) << "injector exists in active list already"; + } + + return true; + + } else { + // no local buffer + return false; + } + } + + void AudioClient::outputFormatChanged() { + _outputFrameSize = + (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * OUTPUT_CHANNEL_COUNT * _outputFormat.sampleRate()) / + _desiredOutputFormat.sampleRate(); + _receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); + } + + bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo inputDeviceInfo, bool isShutdownRequest) { + Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); + + qCDebug(audioclient) << __FUNCTION__ << "inputDeviceInfo: [" << inputDeviceInfo.deviceName() << "]"; + bool supportedFormat = false; + + // NOTE: device start() uses the Qt internal device list + Lock lock(_deviceMutex); #if defined(Q_OS_ANDROID) - _shouldRestartInputSetup = false; // avoid a double call to _audioInput->start() from audioInputStateChanged + _shouldRestartInputSetup = false; // avoid a double call to _audioInput->start() from audioInputStateChanged #endif - // cleanup any previously initialized device - if (_audioInput) { - // The call to stop() causes _inputDevice to be destructed. - // That in turn causes it to be disconnected (see for example - // http://stackoverflow.com/questions/9264750/qt-signals-and-slots-object-disconnect). - _audioInput->stop(); - _inputDevice = NULL; + // cleanup any previously initialized device + if (_audioInput) { + // The call to stop() causes _inputDevice to be destructed. + // That in turn causes it to be disconnected (see for example + // http://stackoverflow.com/questions/9264750/qt-signals-and-slots-object-disconnect). + _audioInput->stop(); + _inputDevice = NULL; - _audioInput->deleteLater(); - _audioInput = NULL; - _numInputCallbackBytes = 0; + _audioInput->deleteLater(); + _audioInput = NULL; + _numInputCallbackBytes = 0; - _inputDeviceInfo = QAudioDeviceInfo(); - } + _inputDeviceInfo = QAudioDeviceInfo(); + } - if (_dummyAudioInput) { - _dummyAudioInput->stop(); + if (_dummyAudioInput) { + _dummyAudioInput->stop(); - _dummyAudioInput->deleteLater(); - _dummyAudioInput = NULL; - } + _dummyAudioInput->deleteLater(); + _dummyAudioInput = NULL; + } - if (_inputToNetworkResampler) { - // if we were using an input to network resampler, delete it here - delete _inputToNetworkResampler; - _inputToNetworkResampler = NULL; - } + if (_inputToNetworkResampler) { + // if we were using an input to network resampler, delete it here + delete _inputToNetworkResampler; + _inputToNetworkResampler = NULL; + } - if (_audioGate) { - delete _audioGate; - _audioGate = nullptr; - } + if (_audioGate) { + delete _audioGate; + _audioGate = nullptr; + } - if (isShutdownRequest) { - qCDebug(audioclient) << "The audio input device has shut down."; - return true; - } + if (isShutdownRequest) { + qCDebug(audioclient) << "The audio input device has shut down."; + return true; + } - if (!inputDeviceInfo.isNull()) { - qCDebug(audioclient) << "The audio input device " << inputDeviceInfo.deviceName() << "is available."; - _inputDeviceInfo = inputDeviceInfo; - emit deviceChanged(QAudio::AudioInput, inputDeviceInfo); + if (!inputDeviceInfo.isNull()) { + qCDebug(audioclient) << "The audio input device " << inputDeviceInfo.deviceName() << "is available."; + _inputDeviceInfo = inputDeviceInfo; + emit deviceChanged(QAudio::AudioInput, inputDeviceInfo); - if (adjustedFormatForAudioDevice(inputDeviceInfo, _desiredInputFormat, _inputFormat)) { - qCDebug(audioclient) << "The format to be used for audio input is" << _inputFormat; + if (adjustedFormatForAudioDevice(inputDeviceInfo, _desiredInputFormat, _inputFormat)) { + qCDebug(audioclient) << "The format to be used for audio input is" << _inputFormat; - // we've got the best we can get for input - // if required, setup a resampler for this input to our desired network format - if (_inputFormat != _desiredInputFormat && _inputFormat.sampleRate() != _desiredInputFormat.sampleRate()) { - qCDebug(audioclient) << "Attemping to create a resampler for input format to network format."; + // we've got the best we can get for input + // if required, setup a resampler for this input to our desired network format + if (_inputFormat != _desiredInputFormat && _inputFormat.sampleRate() != _desiredInputFormat.sampleRate()) { + qCDebug(audioclient) << "Attemping to create a resampler for input format to network format."; - assert(_inputFormat.sampleSize() == 16); - assert(_desiredInputFormat.sampleSize() == 16); - int channelCount = (_inputFormat.channelCount() == 2 && _desiredInputFormat.channelCount() == 2) ? 2 : 1; + assert(_inputFormat.sampleSize() == 16); + assert(_desiredInputFormat.sampleSize() == 16); + int channelCount = (_inputFormat.channelCount() == 2 && _desiredInputFormat.channelCount() == 2) ? 2 : 1; - _inputToNetworkResampler = - new AudioSRC(_inputFormat.sampleRate(), _desiredInputFormat.sampleRate(), channelCount); + _inputToNetworkResampler = + new AudioSRC(_inputFormat.sampleRate(), _desiredInputFormat.sampleRate(), channelCount); - } else { - qCDebug(audioclient) << "No resampling required for audio input to match desired network format."; + } else { + qCDebug(audioclient) << "No resampling required for audio input to match desired network format."; + } + + // the audio gate runs after the resampler + _audioGate = new AudioGate(_desiredInputFormat.sampleRate(), _desiredInputFormat.channelCount()); + qCDebug(audioclient) << "Noise gate created with" << _desiredInputFormat.channelCount() << "channels."; + + // if the user wants stereo but this device can't provide then bail + if (!_isStereoInput || _inputFormat.channelCount() == 2) { + _audioInput = new QAudioInput(inputDeviceInfo, _inputFormat, this); + _numInputCallbackBytes = calculateNumberOfInputCallbackBytes(_inputFormat); + _audioInput->setBufferSize(_numInputCallbackBytes); + // different audio input devices may have different volumes + emit inputVolumeChanged(_audioInput->volume()); + + // how do we want to handle input working, but output not working? + int numFrameSamples = calculateNumberOfFrameSamples(_numInputCallbackBytes); + _inputRingBuffer.resizeForFrameSize(numFrameSamples); + +#if defined(Q_OS_ANDROID) + if (_audioInput) { + _shouldRestartInputSetup = true; + connect(_audioInput, &QAudioInput::stateChanged, this, &AudioClient::audioInputStateChanged); + } +#endif + _inputDevice = _audioInput->start(); + + if (_inputDevice) { + connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput())); + supportedFormat = true; + } else { + qCDebug(audioclient) << "Error starting audio input -" << _audioInput->error(); + _audioInput->deleteLater(); + _audioInput = NULL; + } + } } + } + + // If there is no working input device, use the dummy input device. + // It generates audio callbacks on a timer to simulate a mic stream of silent packets. + // This enables clients without a mic to still receive an audio stream from the mixer. + if (!_audioInput) { + qCDebug(audioclient) << "Audio input device is not available, using dummy input."; + _inputDeviceInfo = QAudioDeviceInfo(); + emit deviceChanged(QAudio::AudioInput, _inputDeviceInfo); + + _inputFormat = _desiredInputFormat; + qCDebug(audioclient) << "The format to be used for audio input is" << _inputFormat; + qCDebug(audioclient) << "No resampling required for audio input to match desired network format."; - // the audio gate runs after the resampler _audioGate = new AudioGate(_desiredInputFormat.sampleRate(), _desiredInputFormat.channelCount()); qCDebug(audioclient) << "Noise gate created with" << _desiredInputFormat.channelCount() << "channels."; - // if the user wants stereo but this device can't provide then bail - if (!_isStereoInput || _inputFormat.channelCount() == 2) { - _audioInput = new QAudioInput(inputDeviceInfo, _inputFormat, this); - _numInputCallbackBytes = calculateNumberOfInputCallbackBytes(_inputFormat); - _audioInput->setBufferSize(_numInputCallbackBytes); - // different audio input devices may have different volumes - emit inputVolumeChanged(_audioInput->volume()); - - // how do we want to handle input working, but output not working? - int numFrameSamples = calculateNumberOfFrameSamples(_numInputCallbackBytes); - _inputRingBuffer.resizeForFrameSize(numFrameSamples); - -#if defined(Q_OS_ANDROID) - if (_audioInput) { - _shouldRestartInputSetup = true; - connect(_audioInput, &QAudioInput::stateChanged, this, &AudioClient::audioInputStateChanged); - } -#endif - _inputDevice = _audioInput->start(); - - if (_inputDevice) { - connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput())); - supportedFormat = true; - } else { - qCDebug(audioclient) << "Error starting audio input -" << _audioInput->error(); - _audioInput->deleteLater(); - _audioInput = NULL; - } - } - } - } - - // If there is no working input device, use the dummy input device. - // It generates audio callbacks on a timer to simulate a mic stream of silent packets. - // This enables clients without a mic to still receive an audio stream from the mixer. - if (!_audioInput) { - qCDebug(audioclient) << "Audio input device is not available, using dummy input."; - _inputDeviceInfo = QAudioDeviceInfo(); - emit deviceChanged(QAudio::AudioInput, _inputDeviceInfo); - - _inputFormat = _desiredInputFormat; - qCDebug(audioclient) << "The format to be used for audio input is" << _inputFormat; - qCDebug(audioclient) << "No resampling required for audio input to match desired network format."; - - _audioGate = new AudioGate(_desiredInputFormat.sampleRate(), _desiredInputFormat.channelCount()); - qCDebug(audioclient) << "Noise gate created with" << _desiredInputFormat.channelCount() << "channels."; - - // generate audio callbacks at the network sample rate - _dummyAudioInput = new QTimer(this); - connect(_dummyAudioInput, SIGNAL(timeout()), this, SLOT(handleDummyAudioInput())); - _dummyAudioInput->start((int)(AudioConstants::NETWORK_FRAME_MSECS + 0.5f)); - } - - return supportedFormat; -} - -void AudioClient::audioInputStateChanged(QAudio::State state) { -#if defined(Q_OS_ANDROID) - switch (state) { - case QAudio::StoppedState: - if (!_audioInput) { - break; - } - // Stopped on purpose - if (_shouldRestartInputSetup) { - Lock lock(_deviceMutex); - _inputDevice = _audioInput->start(); - lock.unlock(); - if (_inputDevice) { - connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput())); - } - } - break; - case QAudio::ActiveState: - break; - default: - break; - } -#endif -} - -void AudioClient::checkInputTimeout() { -#if defined(Q_OS_ANDROID) - if (_audioInput && _inputReadsSinceLastCheck < MIN_READS_TO_CONSIDER_INPUT_ALIVE) { - _audioInput->stop(); - } else { - _inputReadsSinceLastCheck = 0; - } -#endif -} - -void AudioClient::setHeadsetPluggedIn(bool pluggedIn) { -#if defined(Q_OS_ANDROID) - if (pluggedIn == !_isHeadsetPluggedIn && !_inputDeviceInfo.isNull()) { - QAndroidJniObject brand = QAndroidJniObject::getStaticObjectField("android/os/Build", "BRAND"); - // some samsung phones needs more time to shutdown the previous input device - if (brand.toString().contains("samsung", Qt::CaseInsensitive)) { - switchInputToAudioDevice(QAudioDeviceInfo(), true); - QThread::msleep(200); + // generate audio callbacks at the network sample rate + _dummyAudioInput = new QTimer(this); + connect(_dummyAudioInput, SIGNAL(timeout()), this, SLOT(handleDummyAudioInput())); + _dummyAudioInput->start((int)(AudioConstants::NETWORK_FRAME_MSECS + 0.5f)); } - Setting::Handle enableAEC(SETTING_AEC_KEY, false); - bool aecEnabled = enableAEC.get(); - - if ((pluggedIn || !aecEnabled) && _inputDeviceInfo.deviceName() != VOICE_RECOGNITION) { - switchAudioDevice(QAudio::AudioInput, VOICE_RECOGNITION); - } else if (!pluggedIn && aecEnabled && _inputDeviceInfo.deviceName() != VOICE_COMMUNICATION) { - switchAudioDevice(QAudio::AudioInput, VOICE_COMMUNICATION); - } + return supportedFormat; } - _isHeadsetPluggedIn = pluggedIn; -#endif -} -void AudioClient::outputNotify() { - int recentUnfulfilled = _audioOutputIODevice.getRecentUnfulfilledReads(); - if (recentUnfulfilled > 0) { - qCDebug(audioclient, "Starve detected, %d new unfulfilled reads", recentUnfulfilled); - - if (_outputStarveDetectionEnabled.get()) { - quint64 now = usecTimestampNow() / 1000; - int dt = (int)(now - _outputStarveDetectionStartTimeMsec); - if (dt > STARVE_DETECTION_PERIOD) { - _outputStarveDetectionStartTimeMsec = now; - _outputStarveDetectionCount = 0; - } else { - _outputStarveDetectionCount += recentUnfulfilled; - if (_outputStarveDetectionCount > STARVE_DETECTION_THRESHOLD) { - int oldOutputBufferSizeFrames = _sessionOutputBufferSizeFrames; - int newOutputBufferSizeFrames = setOutputBufferSize(oldOutputBufferSizeFrames + 1, false); - - if (newOutputBufferSizeFrames > oldOutputBufferSizeFrames) { - qCDebug(audioclient, "Starve threshold surpassed (%d starves in %d ms)", _outputStarveDetectionCount, - dt); + void AudioClient::audioInputStateChanged(QAudio::State state) { +#if defined(Q_OS_ANDROID) + switch (state) { + case QAudio::StoppedState: + if (!_audioInput) { + break; + } + // Stopped on purpose + if (_shouldRestartInputSetup) { + Lock lock(_deviceMutex); + _inputDevice = _audioInput->start(); + lock.unlock(); + if (_inputDevice) { + connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput())); } + } + break; + case QAudio::ActiveState: + break; + default: + break; + } +#endif + } + void AudioClient::checkInputTimeout() { +#if defined(Q_OS_ANDROID) + if (_audioInput && _inputReadsSinceLastCheck < MIN_READS_TO_CONSIDER_INPUT_ALIVE) { + _audioInput->stop(); + } else { + _inputReadsSinceLastCheck = 0; + } +#endif + } + + void AudioClient::setHeadsetPluggedIn(bool pluggedIn) { +#if defined(Q_OS_ANDROID) + if (pluggedIn == !_isHeadsetPluggedIn && !_inputDeviceInfo.isNull()) { + QAndroidJniObject brand = QAndroidJniObject::getStaticObjectField("android/os/Build", "BRAND"); + // some samsung phones needs more time to shutdown the previous input device + if (brand.toString().contains("samsung", Qt::CaseInsensitive)) { + switchInputToAudioDevice(QAudioDeviceInfo(), true); + QThread::msleep(200); + } + + Setting::Handle enableAEC(SETTING_AEC_KEY, false); + bool aecEnabled = enableAEC.get(); + + if ((pluggedIn || !aecEnabled) && _inputDeviceInfo.deviceName() != VOICE_RECOGNITION) { + switchAudioDevice(QAudio::AudioInput, VOICE_RECOGNITION); + } else if (!pluggedIn && aecEnabled && _inputDeviceInfo.deviceName() != VOICE_COMMUNICATION) { + switchAudioDevice(QAudio::AudioInput, VOICE_COMMUNICATION); + } + } + _isHeadsetPluggedIn = pluggedIn; +#endif + } + + void AudioClient::outputNotify() { + int recentUnfulfilled = _audioOutputIODevice.getRecentUnfulfilledReads(); + if (recentUnfulfilled > 0) { + qCDebug(audioclient, "Starve detected, %d new unfulfilled reads", recentUnfulfilled); + + if (_outputStarveDetectionEnabled.get()) { + quint64 now = usecTimestampNow() / 1000; + int dt = (int)(now - _outputStarveDetectionStartTimeMsec); + if (dt > STARVE_DETECTION_PERIOD) { _outputStarveDetectionStartTimeMsec = now; _outputStarveDetectionCount = 0; + } else { + _outputStarveDetectionCount += recentUnfulfilled; + if (_outputStarveDetectionCount > STARVE_DETECTION_THRESHOLD) { + int oldOutputBufferSizeFrames = _sessionOutputBufferSizeFrames; + int newOutputBufferSizeFrames = setOutputBufferSize(oldOutputBufferSizeFrames + 1, false); + + if (newOutputBufferSizeFrames > oldOutputBufferSizeFrames) { + qCDebug(audioclient, "Starve threshold surpassed (%d starves in %d ms)", + _outputStarveDetectionCount, dt); + } + + _outputStarveDetectionStartTimeMsec = now; + _outputStarveDetectionCount = 0; + } } } } } -} -bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceInfo, bool isShutdownRequest) { - Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); + bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceInfo, bool isShutdownRequest) { + Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); - qCDebug(audioclient) << "AudioClient::switchOutputToAudioDevice() outputDeviceInfo: [" << outputDeviceInfo.deviceName() - << "]"; - bool supportedFormat = false; + qCDebug(audioclient) << "AudioClient::switchOutputToAudioDevice() outputDeviceInfo: [" << outputDeviceInfo.deviceName() + << "]"; + bool supportedFormat = false; - // NOTE: device start() uses the Qt internal device list - Lock lock(_deviceMutex); + // NOTE: device start() uses the Qt internal device list + Lock lock(_deviceMutex); - Lock localAudioLock(_localAudioMutex); - _localSamplesAvailable.exchange(0, std::memory_order_release); + Lock localAudioLock(_localAudioMutex); + _localSamplesAvailable.exchange(0, std::memory_order_release); - // cleanup any previously initialized device - if (_audioOutput) { - _audioOutputIODevice.close(); - _audioOutput->stop(); + // cleanup any previously initialized device + if (_audioOutput) { + _audioOutputIODevice.close(); + _audioOutput->stop(); - //must be deleted in next eventloop cycle when its called from notify() - _audioOutput->deleteLater(); - _audioOutput = NULL; + //must be deleted in next eventloop cycle when its called from notify() + _audioOutput->deleteLater(); + _audioOutput = NULL; - _loopbackOutputDevice = NULL; - //must be deleted in next eventloop cycle when its called from notify() - _loopbackAudioOutput->deleteLater(); - _loopbackAudioOutput = NULL; + _loopbackOutputDevice = NULL; + //must be deleted in next eventloop cycle when its called from notify() + _loopbackAudioOutput->deleteLater(); + _loopbackAudioOutput = NULL; - delete[] _outputMixBuffer; - _outputMixBuffer = NULL; + delete[] _outputMixBuffer; + _outputMixBuffer = NULL; - delete[] _outputScratchBuffer; - _outputScratchBuffer = NULL; + delete[] _outputScratchBuffer; + _outputScratchBuffer = NULL; - delete[] _localOutputMixBuffer; - _localOutputMixBuffer = NULL; + delete[] _localOutputMixBuffer; + _localOutputMixBuffer = NULL; - _outputDeviceInfo = QAudioDeviceInfo(); - } + _outputDeviceInfo = QAudioDeviceInfo(); + } - if (_networkToOutputResampler) { - // if we were using an input to network resampler, delete it here - delete _networkToOutputResampler; - _networkToOutputResampler = NULL; + if (_networkToOutputResampler) { + // if we were using an input to network resampler, delete it here + delete _networkToOutputResampler; + _networkToOutputResampler = NULL; - delete _localToOutputResampler; - _localToOutputResampler = NULL; - } + delete _localToOutputResampler; + _localToOutputResampler = NULL; + } - if (isShutdownRequest) { - qCDebug(audioclient) << "The audio output device has shut down."; - return true; - } + if (isShutdownRequest) { + qCDebug(audioclient) << "The audio output device has shut down."; + return true; + } - if (!outputDeviceInfo.isNull()) { - qCDebug(audioclient) << "The audio output device " << outputDeviceInfo.deviceName() << "is available."; - _outputDeviceInfo = outputDeviceInfo; - emit deviceChanged(QAudio::AudioOutput, outputDeviceInfo); + if (!outputDeviceInfo.isNull()) { + qCDebug(audioclient) << "The audio output device " << outputDeviceInfo.deviceName() << "is available."; + _outputDeviceInfo = outputDeviceInfo; + emit deviceChanged(QAudio::AudioOutput, outputDeviceInfo); - if (adjustedFormatForAudioDevice(outputDeviceInfo, _desiredOutputFormat, _outputFormat)) { - qCDebug(audioclient) << "The format to be used for audio output is" << _outputFormat; + if (adjustedFormatForAudioDevice(outputDeviceInfo, _desiredOutputFormat, _outputFormat)) { + qCDebug(audioclient) << "The format to be used for audio output is" << _outputFormat; - // we've got the best we can get for input - // if required, setup a resampler for this input to our desired network format - if (_desiredOutputFormat != _outputFormat && _desiredOutputFormat.sampleRate() != _outputFormat.sampleRate()) { - qCDebug(audioclient) << "Attemping to create a resampler for network format to output format."; + // we've got the best we can get for input + // if required, setup a resampler for this input to our desired network format + if (_desiredOutputFormat != _outputFormat && _desiredOutputFormat.sampleRate() != _outputFormat.sampleRate()) { + qCDebug(audioclient) << "Attemping to create a resampler for network format to output format."; - assert(_desiredOutputFormat.sampleSize() == 16); - assert(_outputFormat.sampleSize() == 16); + assert(_desiredOutputFormat.sampleSize() == 16); + assert(_outputFormat.sampleSize() == 16); - _networkToOutputResampler = - new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); - _localToOutputResampler = - new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); + _networkToOutputResampler = + new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); + _localToOutputResampler = + new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); - } else { - qCDebug(audioclient) << "No resampling required for network output to match actual output format."; - } - - outputFormatChanged(); - - // setup our general output device for audio-mixer audio - _audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); - - int deviceChannelCount = _outputFormat.channelCount(); - int frameSize = - (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / - _desiredOutputFormat.sampleRate(); - int requestedSize = _sessionOutputBufferSizeFrames * frameSize * AudioConstants::SAMPLE_SIZE; - _audioOutput->setBufferSize(requestedSize); - - // initialize mix buffers on the _audioOutput thread to avoid races - connect(_audioOutput, &QAudioOutput::stateChanged, [&, frameSize, requestedSize](QAudio::State state) { - if (state == QAudio::ActiveState) { - // restrict device callback to _outputPeriod samples - _outputPeriod = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; - // device callback may exceed reported period, so double it to avoid stutter - _outputPeriod *= 2; - - _outputMixBuffer = new float[_outputPeriod]; - _outputScratchBuffer = new int16_t[_outputPeriod]; - - // size local output mix buffer based on resampled network frame size - int networkPeriod = - _localToOutputResampler - ? _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO) - : AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; - _localOutputMixBuffer = new float[networkPeriod]; - - // local period should be at least twice the output period, - // in case two device reads happen before more data can be read (worst case) - int localPeriod = _outputPeriod * 2; - // round up to an exact multiple of networkPeriod - localPeriod = ((localPeriod + networkPeriod - 1) / networkPeriod) * networkPeriod; - // this ensures lowest latency without stutter from underrun - _localInjectorsStream.resizeForFrameSize(localPeriod); - - int bufferSize = _audioOutput->bufferSize(); - int bufferSamples = bufferSize / AudioConstants::SAMPLE_SIZE; - int bufferFrames = bufferSamples / (float)frameSize; - qCDebug(audioclient) << "frame (samples):" << frameSize; - qCDebug(audioclient) << "buffer (frames):" << bufferFrames; - qCDebug(audioclient) << "buffer (samples):" << bufferSamples; - qCDebug(audioclient) << "buffer (bytes):" << bufferSize; - qCDebug(audioclient) << "requested (bytes):" << requestedSize; - qCDebug(audioclient) << "period (samples):" << _outputPeriod; - qCDebug(audioclient) << "local buffer (samples):" << localPeriod; - - disconnect(_audioOutput, &QAudioOutput::stateChanged, 0, 0); - - // unlock to avoid a deadlock with the device callback (which always succeeds this initialization) - localAudioLock.unlock(); + } else { + qCDebug(audioclient) << "No resampling required for network output to match actual output format."; } - }); - connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); - _audioOutputIODevice.start(); + outputFormatChanged(); - _audioOutput->start(&_audioOutputIODevice); + // setup our general output device for audio-mixer audio + _audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); - // setup a loopback audio output device - _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); + int deviceChannelCount = _outputFormat.channelCount(); + int frameSize = + (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / + _desiredOutputFormat.sampleRate(); + int requestedSize = _sessionOutputBufferSizeFrames * frameSize * AudioConstants::SAMPLE_SIZE; + _audioOutput->setBufferSize(requestedSize); - _timeSinceLastReceived.start(); + // initialize mix buffers on the _audioOutput thread to avoid races + connect(_audioOutput, &QAudioOutput::stateChanged, [&, frameSize, requestedSize](QAudio::State state) { + if (state == QAudio::ActiveState) { + // restrict device callback to _outputPeriod samples + _outputPeriod = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; + // device callback may exceed reported period, so double it to avoid stutter + _outputPeriod *= 2; - supportedFormat = true; + _outputMixBuffer = new float[_outputPeriod]; + _outputScratchBuffer = new int16_t[_outputPeriod]; + + // size local output mix buffer based on resampled network frame size + int networkPeriod = + _localToOutputResampler + ? _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO) + : AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; + _localOutputMixBuffer = new float[networkPeriod]; + + // local period should be at least twice the output period, + // in case two device reads happen before more data can be read (worst case) + int localPeriod = _outputPeriod * 2; + // round up to an exact multiple of networkPeriod + localPeriod = ((localPeriod + networkPeriod - 1) / networkPeriod) * networkPeriod; + // this ensures lowest latency without stutter from underrun + _localInjectorsStream.resizeForFrameSize(localPeriod); + + int bufferSize = _audioOutput->bufferSize(); + int bufferSamples = bufferSize / AudioConstants::SAMPLE_SIZE; + int bufferFrames = bufferSamples / (float)frameSize; + qCDebug(audioclient) << "frame (samples):" << frameSize; + qCDebug(audioclient) << "buffer (frames):" << bufferFrames; + qCDebug(audioclient) << "buffer (samples):" << bufferSamples; + qCDebug(audioclient) << "buffer (bytes):" << bufferSize; + qCDebug(audioclient) << "requested (bytes):" << requestedSize; + qCDebug(audioclient) << "period (samples):" << _outputPeriod; + qCDebug(audioclient) << "local buffer (samples):" << localPeriod; + + disconnect(_audioOutput, &QAudioOutput::stateChanged, 0, 0); + + // unlock to avoid a deadlock with the device callback (which always succeeds this initialization) + localAudioLock.unlock(); + } + }); + connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); + + _audioOutputIODevice.start(); + + _audioOutput->start(&_audioOutputIODevice); + + // setup a loopback audio output device + _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); + + _timeSinceLastReceived.start(); + + supportedFormat = true; + } } + + return supportedFormat; } - return supportedFormat; -} + int AudioClient::setOutputBufferSize(int numFrames, bool persist) { + qCDebug(audioclient) << __FUNCTION__ << "numFrames:" << numFrames << "persist:" << persist; -int AudioClient::setOutputBufferSize(int numFrames, bool persist) { - qCDebug(audioclient) << __FUNCTION__ << "numFrames:" << numFrames << "persist:" << persist; + numFrames = std::min(std::max(numFrames, MIN_BUFFER_FRAMES), MAX_BUFFER_FRAMES); + qCDebug(audioclient) << __FUNCTION__ << "clamped numFrames:" << numFrames + << "_sessionOutputBufferSizeFrames:" << _sessionOutputBufferSizeFrames; - numFrames = std::min(std::max(numFrames, MIN_BUFFER_FRAMES), MAX_BUFFER_FRAMES); - qCDebug(audioclient) << __FUNCTION__ << "clamped numFrames:" << numFrames - << "_sessionOutputBufferSizeFrames:" << _sessionOutputBufferSizeFrames; - - if (numFrames != _sessionOutputBufferSizeFrames) { - qCInfo(audioclient, "Audio output buffer set to %d frames", numFrames); - _sessionOutputBufferSizeFrames = numFrames; - if (persist) { - _outputBufferSizeFrames.set(numFrames); + if (numFrames != _sessionOutputBufferSizeFrames) { + qCInfo(audioclient, "Audio output buffer set to %d frames", numFrames); + _sessionOutputBufferSizeFrames = numFrames; + if (persist) { + _outputBufferSizeFrames.set(numFrames); + } } + return numFrames; } - return numFrames; -} -// The following constant is operating system dependent due to differences in -// the way input audio is handled. The audio input buffer size is inversely -// proportional to the accelerator ratio. + // The following constant is operating system dependent due to differences in + // the way input audio is handled. The audio input buffer size is inversely + // proportional to the accelerator ratio. #ifdef Q_OS_WIN -const float AudioClient::CALLBACK_ACCELERATOR_RATIO = IsWindows8OrGreater() ? 1.0f : 0.25f; + const float AudioClient::CALLBACK_ACCELERATOR_RATIO = IsWindows8OrGreater() ? 1.0f : 0.25f; #endif #ifdef Q_OS_MAC -const float AudioClient::CALLBACK_ACCELERATOR_RATIO = 2.0f; + const float AudioClient::CALLBACK_ACCELERATOR_RATIO = 2.0f; #endif #ifdef Q_OS_ANDROID -const float AudioClient::CALLBACK_ACCELERATOR_RATIO = 0.5f; + const float AudioClient::CALLBACK_ACCELERATOR_RATIO = 0.5f; #elif defined(Q_OS_LINUX) -const float AudioClient::CALLBACK_ACCELERATOR_RATIO = 2.0f; + const float AudioClient::CALLBACK_ACCELERATOR_RATIO = 2.0f; #endif -int AudioClient::calculateNumberOfInputCallbackBytes(const QAudioFormat& format) const { - int numInputCallbackBytes = (int)(((AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL * format.channelCount() * - ((float)format.sampleRate() / AudioConstants::SAMPLE_RATE)) / - CALLBACK_ACCELERATOR_RATIO) + - 0.5f); + int AudioClient::calculateNumberOfInputCallbackBytes(const QAudioFormat& format) const { + int numInputCallbackBytes = (int)(((AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL * format.channelCount() * + ((float)format.sampleRate() / AudioConstants::SAMPLE_RATE)) / + CALLBACK_ACCELERATOR_RATIO) + + 0.5f); - return numInputCallbackBytes; -} - -int AudioClient::calculateNumberOfFrameSamples(int numBytes) const { - int frameSamples = (int)(numBytes * CALLBACK_ACCELERATOR_RATIO + 0.5f) / AudioConstants::SAMPLE_SIZE; - return frameSamples; -} - -float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { - glm::quat inverseOrientation = glm::inverse(_orientationGetter()); - - glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; - - // project the rotated source position vector onto the XZ plane - rotatedSourcePosition.y = 0.0f; - - static const float SOURCE_DISTANCE_THRESHOLD = 1e-30f; - - float rotatedSourcePositionLength2 = glm::length2(rotatedSourcePosition); - if (rotatedSourcePositionLength2 > SOURCE_DISTANCE_THRESHOLD) { - // produce an oriented angle about the y-axis - glm::vec3 direction = rotatedSourcePosition * (1.0f / fastSqrtf(rotatedSourcePositionLength2)); - float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward" - return (direction.x < 0.0f) ? -angle : angle; - - } else { - // no azimuth if they are in same spot - return 0.0f; - } -} - -float AudioClient::gainForSource(float distance, float volume) { - // attenuation = -6dB * log2(distance) - // reference attenuation of 0dB at distance = 1.0m - float gain = volume / std::max(distance, HRTF_NEARFIELD_MIN); - - return gain; -} - -qint64 AudioClient::AudioOutputIODevice::readData(char* data, qint64 maxSize) { - // samples requested from OUTPUT_CHANNEL_COUNT - int deviceChannelCount = _audio->_outputFormat.channelCount(); - int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; - // restrict samplesRequested to the size of our mix/scratch buffers - samplesRequested = std::min(samplesRequested, _audio->_outputPeriod); - - int16_t* scratchBuffer = _audio->_outputScratchBuffer; - float* mixBuffer = _audio->_outputMixBuffer; - - int networkSamplesPopped; - if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { - qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, - _receivedAudioStream.getSamplesAvailable(), samplesRequested); - AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); - lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); - for (int i = 0; i < networkSamplesPopped; i++) { - mixBuffer[i] = convertToFloat(scratchBuffer[i]); - } - samplesRequested = networkSamplesPopped; + return numInputCallbackBytes; } - int injectorSamplesPopped = 0; - { - bool append = networkSamplesPopped > 0; - // check the samples we have available locklessly; this is possible because only two functions add to the count: - // - prepareLocalAudioInjectors will only increase samples count - // - switchOutputToAudioDevice will zero samples count, - // stop the device - so that readData will exhaust the existing buffer or see a zeroed samples count, - // and start the device - which can then only see a zeroed samples count - int samplesAvailable = _audio->_localSamplesAvailable.load(std::memory_order_acquire); - - // if we do not have enough samples buffered despite having injectors, buffer them synchronously - if (samplesAvailable < samplesRequested && _audio->_localInjectorsAvailable.load(std::memory_order_acquire)) { - // try_to_lock, in case the device is being shut down already - std::unique_ptr localAudioLock(new Lock(_audio->_localAudioMutex, std::try_to_lock)); - if (localAudioLock->owns_lock()) { - _audio->prepareLocalAudioInjectors(std::move(localAudioLock)); - samplesAvailable = _audio->_localSamplesAvailable.load(std::memory_order_acquire); - } - } - - samplesRequested = std::min(samplesRequested, samplesAvailable); - if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { - _audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release); - qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, - _localInjectorsStream.samplesAvailable(), samplesRequested); - } + int AudioClient::calculateNumberOfFrameSamples(int numBytes) const { + int frameSamples = (int)(numBytes * CALLBACK_ACCELERATOR_RATIO + 0.5f) / AudioConstants::SAMPLE_SIZE; + return frameSamples; } - // prepare injectors for the next callback - QtConcurrent::run(QThreadPool::globalInstance(), [this] { _audio->prepareLocalAudioInjectors(); }); + float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { + glm::quat inverseOrientation = glm::inverse(_orientationGetter()); + + glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; + + // project the rotated source position vector onto the XZ plane + rotatedSourcePosition.y = 0.0f; + + static const float SOURCE_DISTANCE_THRESHOLD = 1e-30f; + + float rotatedSourcePositionLength2 = glm::length2(rotatedSourcePosition); + if (rotatedSourcePositionLength2 > SOURCE_DISTANCE_THRESHOLD) { + // produce an oriented angle about the y-axis + glm::vec3 direction = rotatedSourcePosition * (1.0f / fastSqrtf(rotatedSourcePositionLength2)); + float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward" + return (direction.x < 0.0f) ? -angle : angle; - int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped); - int framesPopped = samplesPopped / AudioConstants::STEREO; - int bytesWritten; - if (samplesPopped > 0) { - if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { - // limit the audio - _audio->_audioLimiter.render(mixBuffer, (int16_t*)data, framesPopped); } else { - _audio->_audioLimiter.render(mixBuffer, scratchBuffer, framesPopped); + // no azimuth if they are in same spot + return 0.0f; + } + } - // upmix or downmix to deviceChannelCount - if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { - int extraChannels = deviceChannelCount - OUTPUT_CHANNEL_COUNT; - channelUpmix(scratchBuffer, (int16_t*)data, samplesPopped, extraChannels); - } else { - channelDownmix(scratchBuffer, (int16_t*)data, samplesPopped); + float AudioClient::gainForSource(float distance, float volume) { + // attenuation = -6dB * log2(distance) + // reference attenuation of 0dB at distance = 1.0m + float gain = volume / std::max(distance, HRTF_NEARFIELD_MIN); + + return gain; + } + + qint64 AudioClient::AudioOutputIODevice::readData(char* data, qint64 maxSize) { + // samples requested from OUTPUT_CHANNEL_COUNT + int deviceChannelCount = _audio->_outputFormat.channelCount(); + int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; + // restrict samplesRequested to the size of our mix/scratch buffers + samplesRequested = std::min(samplesRequested, _audio->_outputPeriod); + + int16_t* scratchBuffer = _audio->_outputScratchBuffer; + float* mixBuffer = _audio->_outputMixBuffer; + + int networkSamplesPopped; + if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { + qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, + _receivedAudioStream.getSamplesAvailable(), samplesRequested); + AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); + lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); + for (int i = 0; i < networkSamplesPopped; i++) { + mixBuffer[i] = convertToFloat(scratchBuffer[i]); + } + samplesRequested = networkSamplesPopped; + } + + int injectorSamplesPopped = 0; + { + bool append = networkSamplesPopped > 0; + // check the samples we have available locklessly; this is possible because only two functions add to the count: + // - prepareLocalAudioInjectors will only increase samples count + // - switchOutputToAudioDevice will zero samples count, + // stop the device - so that readData will exhaust the existing buffer or see a zeroed samples count, + // and start the device - which can then only see a zeroed samples count + int samplesAvailable = _audio->_localSamplesAvailable.load(std::memory_order_acquire); + + // if we do not have enough samples buffered despite having injectors, buffer them synchronously + if (samplesAvailable < samplesRequested && _audio->_localInjectorsAvailable.load(std::memory_order_acquire)) { + // try_to_lock, in case the device is being shut down already + std::unique_ptr localAudioLock(new Lock(_audio->_localAudioMutex, std::try_to_lock)); + if (localAudioLock->owns_lock()) { + _audio->prepareLocalAudioInjectors(std::move(localAudioLock)); + samplesAvailable = _audio->_localSamplesAvailable.load(std::memory_order_acquire); + } + } + + samplesRequested = std::min(samplesRequested, samplesAvailable); + if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { + _audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release); + qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, + _localInjectorsStream.samplesAvailable(), samplesRequested); } } - bytesWritten = framesPopped * AudioConstants::SAMPLE_SIZE * deviceChannelCount; - } else { - // nothing on network, don't grab anything from injectors, and just return 0s - memset(data, 0, maxSize); - bytesWritten = maxSize; + // prepare injectors for the next callback + QtConcurrent::run(QThreadPool::globalInstance(), [this] { _audio->prepareLocalAudioInjectors(); }); + + int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped); + int framesPopped = samplesPopped / AudioConstants::STEREO; + int bytesWritten; + if (samplesPopped > 0) { + if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { + // limit the audio + _audio->_audioLimiter.render(mixBuffer, (int16_t*)data, framesPopped); + } else { + _audio->_audioLimiter.render(mixBuffer, scratchBuffer, framesPopped); + + // upmix or downmix to deviceChannelCount + if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { + int extraChannels = deviceChannelCount - OUTPUT_CHANNEL_COUNT; + channelUpmix(scratchBuffer, (int16_t*)data, samplesPopped, extraChannels); + } else { + channelDownmix(scratchBuffer, (int16_t*)data, samplesPopped); + } + } + + bytesWritten = framesPopped * AudioConstants::SAMPLE_SIZE * deviceChannelCount; + } else { + // nothing on network, don't grab anything from injectors, and just return 0s + memset(data, 0, maxSize); + bytesWritten = maxSize; + } + + // send output buffer for recording + if (_audio->_isRecording) { + Lock lock(_recordMutex); + _audio->_audioFileWav.addRawAudioChunk(reinterpret_cast(scratchBuffer), bytesWritten); + } + + int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree(); + float msecsAudioOutputUnplayed = + bytesAudioOutputUnplayed / (float)_audio->_outputFormat.bytesForDuration(USECS_PER_MSEC); + _audio->_stats.updateOutputMsUnplayed(msecsAudioOutputUnplayed); + + if (bytesAudioOutputUnplayed == 0) { + _unfulfilledReads++; + } + + return bytesWritten; } - // send output buffer for recording - if (_audio->_isRecording) { - Lock lock(_recordMutex); - _audio->_audioFileWav.addRawAudioChunk(reinterpret_cast(scratchBuffer), bytesWritten); + bool AudioClient::startRecording(const QString& filepath) { + if (!_audioFileWav.create(_outputFormat, filepath)) { + qDebug() << "Error creating audio file: " + filepath; + return false; + } + _isRecording = true; + return true; } - int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree(); - float msecsAudioOutputUnplayed = bytesAudioOutputUnplayed / (float)_audio->_outputFormat.bytesForDuration(USECS_PER_MSEC); - _audio->_stats.updateOutputMsUnplayed(msecsAudioOutputUnplayed); - - if (bytesAudioOutputUnplayed == 0) { - _unfulfilledReads++; - } - - return bytesWritten; -} - -bool AudioClient::startRecording(const QString& filepath) { - if (!_audioFileWav.create(_outputFormat, filepath)) { - qDebug() << "Error creating audio file: " + filepath; - return false; - } - _isRecording = true; - return true; -} - -void AudioClient::stopRecording() { - if (_isRecording) { - _isRecording = false; - _audioFileWav.close(); - } -} - -void AudioClient::loadSettings() { - _receivedAudioStream.setDynamicJitterBufferEnabled(dynamicJitterBufferEnabled.get()); - _receivedAudioStream.setStaticJitterBufferFrames(staticJitterBufferFrames.get()); - - qCDebug(audioclient) << "---- Initializing Audio Client ----"; - auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); - for (auto& plugin : codecPlugins) { - qCDebug(audioclient) << "Codec available:" << plugin->getName(); - } -} - -void AudioClient::saveSettings() { - dynamicJitterBufferEnabled.set(_receivedAudioStream.dynamicJitterBufferEnabled()); - staticJitterBufferFrames.set(_receivedAudioStream.getStaticJitterBufferFrames()); -} - -void AudioClient::setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale) { - avatarBoundingBoxCorner = corner; - avatarBoundingBoxScale = scale; -} - -void AudioClient::startThread() { - moveToNewNamedThread(this, "Audio Thread", [this] { start(); }, QThread::TimeCriticalPriority); -} - -void AudioClient::setInputVolume(float volume, bool emitSignal) { - if (_audioInput && volume != (float)_audioInput->volume()) { - _audioInput->setVolume(volume); - if (emitSignal) { - emit inputVolumeChanged(_audioInput->volume()); + void AudioClient::stopRecording() { + if (_isRecording) { + _isRecording = false; + _audioFileWav.close(); + } + } + + void AudioClient::loadSettings() { + _receivedAudioStream.setDynamicJitterBufferEnabled(dynamicJitterBufferEnabled.get()); + _receivedAudioStream.setStaticJitterBufferFrames(staticJitterBufferFrames.get()); + + qCDebug(audioclient) << "---- Initializing Audio Client ----"; + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + for (auto& plugin : codecPlugins) { + qCDebug(audioclient) << "Codec available:" << plugin->getName(); + } + } + + void AudioClient::saveSettings() { + dynamicJitterBufferEnabled.set(_receivedAudioStream.dynamicJitterBufferEnabled()); + staticJitterBufferFrames.set(_receivedAudioStream.getStaticJitterBufferFrames()); + } + + void AudioClient::setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale) { + avatarBoundingBoxCorner = corner; + avatarBoundingBoxScale = scale; + } + + void AudioClient::startThread() { + moveToNewNamedThread(this, "Audio Thread", [this] { start(); }, QThread::TimeCriticalPriority); + } + + void AudioClient::setInputVolume(float volume, bool emitSignal) { + if (_audioInput && volume != (float)_audioInput->volume()) { + _audioInput->setVolume(volume); + if (emitSignal) { + emit inputVolumeChanged(_audioInput->volume()); + } } } -} diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 1ca7cac6ca..2e5ef65473 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -197,7 +197,11 @@ public slots: void checkInputTimeout(); void handleDummyAudioInput(); void handleRecordedAudioInput(const QByteArray& audio); - void handleTTSAudioInput(const QByteArray& audio); + void handleTTSAudioInput(const QByteArray& audio, + const int& newChunkSize, + const int& timerInterval); + void clearTTSBuffer(); + void processTTSBuffer(); void reset(); void audioMixerKilled(); @@ -289,11 +293,12 @@ private: bool mixLocalAudioInjectors(float* mixBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); - - void processAudioAndAddToRingBuffer(QByteArray& inputByteArray, - const uchar& channelCount, - const qint32& bytesForDuration, - QByteArray& rollingBuffer); + + Mutex _TTSMutex; + QTimer _TTSTimer; + bool _isProcessingTTS {false}; + QByteArray _TTSAudioBuffer; + int _TTSChunkSize = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * 50; #ifdef Q_OS_ANDROID QTimer _checkInputTimer; @@ -401,7 +406,7 @@ private: void configureReverb(); void updateReverbOptions(); - void handleLocalEchoAndReverb(QByteArray& inputByteArray); + void handleLocalEchoAndReverb(QByteArray& inputByteArray, const int& sampleRate, const int& channelCount); bool switchInputToAudioDevice(const QAudioDeviceInfo inputDeviceInfo, bool isShutdownRequest = false); bool switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceInfo, bool isShutdownRequest = false); From baeccebfb9e3349f238adc7d4175dbcf7e1c0adb Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 16 Oct 2018 17:45:55 -0700 Subject: [PATCH 126/362] changed the transition times to make the sit longer and the stand shorter --- interface/src/avatar/MyAvatar.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 7a35b0ee56..b85f19a06d 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -467,8 +467,8 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { const float STANDING_HEIGHT_MULTIPLE = 1.2f; const float SITTING_HEIGHT_MULTIPLE = 0.833f; - const int SITTING_COUNT_THRESHOLD = 180; - const int STANDING_COUNT_THRESHOLD = 60; + const int SITTING_COUNT_THRESHOLD = 240; + const int STANDING_COUNT_THRESHOLD = 20; const int SQUATTY_COUNT_THRESHOLD = 600; const float SITTING_UPPER_BOUND = 1.52f; @@ -495,7 +495,7 @@ void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { } else { // sanity check if average height greater than 5ft they are not sitting(or get off your dangerous barstool please) if (_sumUserHeightSensorSpace > SITTING_UPPER_BOUND) { - setIsInSittingState(true); + setIsInSittingState(false); } else { // tipping point is average height when sitting. _tippingPoint = _sumUserHeightSensorSpace; @@ -4192,7 +4192,6 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat if (myAvatar.getHMDLeanRecenterEnabled() && qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { - if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); From cd7af8b6052c887276e552b99be6a341530a459b Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 17 Oct 2018 08:51:40 -0700 Subject: [PATCH 127/362] changed the name of sumuserheightsensorspace to averageuserheightSensorSpace --- interface/src/avatar/MyAvatar.cpp | 16 ++++++++-------- interface/src/avatar/MyAvatar.h | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b85f19a06d..9fe91e44b9 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -478,7 +478,7 @@ void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { // if we recenter upwards then no longer in sitting state _sitStandStateCount++; if (_sitStandStateCount > STANDING_COUNT_THRESHOLD) { - _sumUserHeightSensorSpace = newHeightReading; + _averageUserHeightSensorSpace = newHeightReading; _tippingPoint = newHeightReading; setIsInSittingState(false); } @@ -487,18 +487,18 @@ void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { // make sure that a real sit is still recognized so we won't be stuck in sitting unable to change state _sitStandStateCount++; if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { - _sumUserHeightSensorSpace = newHeightReading; + _averageUserHeightSensorSpace = newHeightReading; _tippingPoint = newHeightReading; // here we stay in sit state but reset the average height setIsInSittingState(true); } } else { // sanity check if average height greater than 5ft they are not sitting(or get off your dangerous barstool please) - if (_sumUserHeightSensorSpace > SITTING_UPPER_BOUND) { + if (_averageUserHeightSensorSpace > SITTING_UPPER_BOUND) { setIsInSittingState(false); } else { // tipping point is average height when sitting. - _tippingPoint = _sumUserHeightSensorSpace; + _tippingPoint = _averageUserHeightSensorSpace; _sitStandStateCount = 0; } } @@ -507,7 +507,7 @@ void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) { _sitStandStateCount++; if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { - _sumUserHeightSensorSpace = newHeightReading; + _averageUserHeightSensorSpace = newHeightReading; _tippingPoint = newHeightReading; setIsInSittingState(true); } @@ -519,7 +519,7 @@ void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { } } else { // if you are away then reset the average and set state to standing. - _sumUserHeightSensorSpace = _userHeight.get(); + _averageUserHeightSensorSpace = _userHeight.get(); _tippingPoint = _userHeight.get(); setIsInSittingState(false); } @@ -559,7 +559,7 @@ void MyAvatar::update(float deltaTime) { controller::Pose newHeightReading = getControllerPoseInSensorFrame(controller::Action::HEAD); if (newHeightReading.isValid()) { int newHeightReadingInCentimeters = glm::floor(newHeightReading.getTranslation().y * CENTIMETERS_PER_METER); - _sumUserHeightSensorSpace = lerp(_sumUserHeightSensorSpace, newHeightReading.getTranslation().y, 0.01f); + _averageUserHeightSensorSpace = lerp(_averageUserHeightSensorSpace, newHeightReading.getTranslation().y, 0.01f); _recentModeReadings.insert(newHeightReadingInCentimeters); setCurrentStandingHeight(computeStandingHeightMode(newHeightReading)); setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); @@ -3910,7 +3910,7 @@ void MyAvatar::setIsInSittingState(bool isSitting) { void MyAvatar::setIsSitStandStateLocked(bool isLocked) { _lockSitStandState.set(isLocked); _sitStandStateCount = 0; - _sumUserHeightSensorSpace = _userHeight.get(); + _averageUserHeightSensorSpace = _userHeight.get(); _tippingPoint = _userHeight.get(); if (!isLocked) { // always start the auto transition mode in standing state. diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 59f9145404..d1ba0fd9cf 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1832,7 +1832,7 @@ private: // height of user in sensor space, when standing erect. ThreadSafeValueCache _userHeight { DEFAULT_AVATAR_HEIGHT }; - float _sumUserHeightSensorSpace { _userHeight.get() }; + float _averageUserHeightSensorSpace { _userHeight.get() }; bool _sitStandStateChange { false }; ThreadSafeValueCache _lockSitStandState { false }; From 5f229280066eb3b6a081a14c08fc492039d7be17 Mon Sep 17 00:00:00 2001 From: sam gateau Date: Wed, 17 Oct 2018 12:12:13 -0700 Subject: [PATCH 128/362] consolidating the tools under luci --- scripts/developer/utilities/render/luci.js | 259 +++++++-------------- 1 file changed, 90 insertions(+), 169 deletions(-) diff --git a/scripts/developer/utilities/render/luci.js b/scripts/developer/utilities/render/luci.js index 40fb6e01d7..cffeb615c9 100644 --- a/scripts/developer/utilities/render/luci.js +++ b/scripts/developer/utilities/render/luci.js @@ -11,210 +11,131 @@ // (function() { - var TABLET_BUTTON_NAME = "LUCI"; - var QMLAPP_URL = Script.resolvePath("./deferredLighting.qml"); - var ICON_URL = Script.resolvePath("../../../system/assets/images/luci-i.svg"); - var ACTIVE_ICON_URL = Script.resolvePath("../../../system/assets/images/luci-a.svg"); - - - var onLuciScreen = false; - - function onClicked() { - if (onLuciScreen) { - tablet.gotoHomeScreen(); - } else { - tablet.loadQMLSource(QMLAPP_URL); - } - } - - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - var button = tablet.addButton({ - text: TABLET_BUTTON_NAME, - icon: ICON_URL, - activeIcon: ACTIVE_ICON_URL - }); - - var hasEventBridge = false; - - function wireEventBridge(on) { - if (!tablet) { - print("Warning in wireEventBridge(): 'tablet' undefined!"); - return; - } - if (on) { - if (!hasEventBridge) { - tablet.fromQml.connect(fromQml); - hasEventBridge = true; - } - } else { - if (hasEventBridge) { - tablet.fromQml.disconnect(fromQml); - hasEventBridge = false; - } - } - } - - function onScreenChanged(type, url) { - if (url === QMLAPP_URL) { - onLuciScreen = true; - } else { - onLuciScreen = false; - } - - button.editProperties({isActive: onLuciScreen}); - wireEventBridge(onLuciScreen); - } - - button.clicked.connect(onClicked); - tablet.screenChanged.connect(onScreenChanged); + var AppUi = Script.require('appUi'); var moveDebugCursor = false; - Controller.mousePressEvent.connect(function (e) { + var onMousePressEvent = function (e) { if (e.isMiddleButton) { moveDebugCursor = true; setDebugCursor(e.x, e.y); } - }); - Controller.mouseReleaseEvent.connect(function() { moveDebugCursor = false; }); - Controller.mouseMoveEvent.connect(function (e) { if (moveDebugCursor) setDebugCursor(e.x, e.y); }); + }; + Controller.mousePressEvent.connect(onMousePressEvent); + var onMouseReleaseEvent = function () { + moveDebugCursor = false; + }; + Controller.mouseReleaseEvent.connect(onMouseReleaseEvent); + var onMouseMoveEvent = function (e) { + if (moveDebugCursor) { + setDebugCursor(e.x, e.y); + } + }; + Controller.mouseMoveEvent.connect(onMouseMoveEvent); function setDebugCursor(x, y) { - nx = 2.0 * (x / Window.innerWidth) - 1.0; - ny = 1.0 - 2.0 * ((y) / (Window.innerHeight)); + var nx = 2.0 * (x / Window.innerWidth) - 1.0; + var ny = 1.0 - 2.0 * ((y) / (Window.innerHeight)); - Render.getConfig("RenderMainView").getConfig("DebugDeferredBuffer").size = { x: nx, y: ny, z: 1.0, w: 1.0 }; + Render.getConfig("RenderMainView").getConfig("DebugDeferredBuffer").size = { x: nx, y: ny, z: 1.0, w: 1.0 }; } - - - var Page = function(title, qmlurl, width, height) { - + function Page(title, qmlurl, width, height) { this.title = title; - this.qml = Script.resolvePath(qmlurl); + this.qml = qmlurl; this.width = width; this.height = height; - this.window = null; + this.window; + + print("Page: New Page:" + JSON.stringify(this)); + } + + Page.prototype.killView = function () { + print("Page: Kill window for page:" + JSON.stringify(this)); + if (this.window) { + print("Page: Kill window for page:" + this.title); + //this.window.closed.disconnect(function () { + // this.killView(); + //}); + this.window.close(); + this.window = false; + } }; - Page.prototype.createView = function() { - if (this.window == null) { - var window = Desktop.createWindow(this.qml, { + Page.prototype.createView = function () { + var that = this; + if (!this.window) { + print("Page: New window for page:" + this.title); + this.window = Desktop.createWindow(Script.resolvePath(this.qml), { title: this.title, presentationMode: Desktop.PresentationMode.NATIVE, size: {x: this.width, y: this.height} }); - this.window = window - this.window.closed.connect(this.killView); + this.window.closed.connect(function () { + that.killView(); + }); } }; - Page.prototype.killView = function() { - if (this.window !== undefined) { - this.window.closed.disconnect(this.killView); - this.window.close() - this.window = undefined - } + + var Pages = function () { + this._pages = {}; }; - var pages = [] - pages.push_back(new Page('Render Engine', 'engineInspector.qml', 300, 400)) + Pages.prototype.addPage = function (command, title, qmlurl, width, height) { + this._pages[command] = new Page(title, qmlurl, width, height); + }; - + Pages.prototype.open = function (command) { + print("Pages: command = " + command); + if (!this._pages[command]) { + print("Pages: unknown command = " + command); + return; + } + this._pages[command].createView(); + }; - var engineInspectorView = null - function openEngineInspectorView() { - - /* if (engineInspectorView == null) { - var qml = Script.resolvePath('engineInspector.qml'); - var window = Desktop.createWindow(qml, { - title: 'Render Engine', - presentationMode: Desktop.PresentationMode.NATIVE, - size: {x: 300, y: 400} - }); - engineInspectorView = window - window.closed.connect(killEngineInspectorView); + Pages.prototype.clear = function () { + for (var p in this._pages) { + print("Pages: kill page: " + p); + this._pages[p].killView(); + delete this._pages[p]; } - } + this._pages = {}; + }; + var pages = new Pages(); - function killEngineInspectorView() { - if (engineInspectorView !== undefined) { - engineInspectorView.closed.disconnect(killEngineInspectorView); - engineInspectorView.close() - engineInspectorView = undefined - } - } -*/ - var cullInspectorView = null - function openCullInspectorView() { - if (cullInspectorView == null) { - var qml = Script.resolvePath('culling.qml'); - var window = Desktop.createWindow(qml, { - title: 'Cull Inspector', - presentationMode: Desktop.PresentationMode.NATIVE, - size: {x: 400, y: 600} - }); - cullInspectorView = window - window.closed.connect(killCullInspectorView); - } - } - - function killCullInspectorView() { - if (cullInspectorView !== undefined) { - cullInspectorView.closed.disconnect(killCullInspectorView); - cullInspectorView.close() - cullInspectorView = undefined - } - } - - var engineLODView = null - function openEngineLODView() { - if (engineLODView == null) { - engineLODView = Desktop.createWindow(Script.resolvePath('lod.qml'), { - title: 'Render LOD', - flags: Desktop.ALWAYS_ON_TOP, - presentationMode: Desktop.PresentationMode.NATIVE, - size: {x: 300, y: 500}, - }); - engineLODView.closed.connect(killEngineLODView); - } - } - function killEngineLODView() { - if (engineLODView !== undefined) { - engineLODView.closed.disconnect(killEngineLODView); - engineLODView.close() - engineLODView = undefined - } - } - - + pages.addPage('openEngineView', 'Render Engine', 'engineInspector.qml', 300, 400); + pages.addPage('openEngineLODView', 'Render LOD', 'lod.qml', 300, 400); + pages.addPage('openCullInspectorView', 'Cull Inspector', 'culling.qml', 300, 400); function fromQml(message) { - switch (message.method) { - case "openEngineView": - openEngineInspectorView(); - break; - case "openCullInspectorView": - openCullInspectorView(); - break; - case "openEngineLODView": - openEngineLODView(); - break; - } + if (pages.open(message.method)) { + return; + } } + var ui; + function startup() { + ui = new AppUi({ + buttonName: "LUCI", + home: Script.resolvePath("deferredLighting.qml"), + additionalAppScreens: Script.resolvePath("engineInspector.qml"), + onMessage: fromQml, + normalButton: Script.resolvePath("../../../system/assets/images/luci-i.svg"), + activeButton: Script.resolvePath("../../../system/assets/images/luci-a.svg") + }); + } + startup(); Script.scriptEnding.connect(function () { - if (onLuciScreen) { - tablet.gotoHomeScreen(); - } - button.clicked.disconnect(onClicked); - tablet.screenChanged.disconnect(onScreenChanged); - tablet.removeButton(button); - - killEngineInspectorView(); - killCullInspectorView(); - killEngineLODWindow(); + Controller.mousePressEvent.disconnect(onMousePressEvent); + Controller.mouseReleaseEvent.disconnect(onMouseReleaseEvent); + Controller.mouseMoveEvent.disconnect(onMouseMoveEvent); + pages.clear(); + // killEngineInspectorView(); + // killCullInspectorView(); + // killEngineLODWindow(); }); -}()); \ No newline at end of file +}()); From 0ab8a0ba5ff6831e5949758d36a7248d367e775c Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Thu, 18 Oct 2018 01:23:10 +0300 Subject: [PATCH 129/362] FB19396 - QML warning qrc:///qml/hifi/tablet/WindowRoot.qml:132: TypeError: Cannot read property of null --- .../resources/qml/hifi/tablet/WindowRoot.qml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/tablet/WindowRoot.qml b/interface/resources/qml/hifi/tablet/WindowRoot.qml index 9c027308b8..d55ec363f0 100644 --- a/interface/resources/qml/hifi/tablet/WindowRoot.qml +++ b/interface/resources/qml/hifi/tablet/WindowRoot.qml @@ -129,8 +129,21 @@ Windows.ScrollingWindow { height: pane.scrollHeight width: pane.contentWidth - anchors.left: parent.left - anchors.top: parent.top + + // this might be looking not clear from the first look + // but loader.parent is not tabletRoot and it can be null! + // unfortunately we can't use conditional bindings here due to https://bugreports.qt.io/browse/QTBUG-22005 + + onParentChanged: { + if (parent) { + anchors.left = Qt.binding(function() { return parent.left }) + anchors.top = Qt.binding(function() { return parent.top }) + } else { + anchors.left = undefined + anchors.top = undefined + } + } + signal loaded; onWidthChanged: { From 9cd3c35cc6858473c012e4f380b7d92f9a4381e1 Mon Sep 17 00:00:00 2001 From: David Back Date: Wed, 17 Oct 2018 18:18:51 -0700 Subject: [PATCH 130/362] style changes and fix hidden top row --- scripts/system/html/css/edit-style.css | 58 ++++++++++++++------------ scripts/system/html/entityList.html | 28 ++++++------- scripts/system/html/js/entityList.js | 7 +++- 3 files changed, 52 insertions(+), 41 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 088b0952ae..1b0094cfb7 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -273,7 +273,6 @@ input[type="number"] { height: 28px; width: 124px; } - input[type=number] { padding-right: 3px; } @@ -1085,66 +1084,68 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { bottom: 0; } +#filter-type-selectBox select { + border-radius: 14.5px; +} #filter-type-checkboxes { position: absolute; z-index: 2; - top: 28px; + top: 48px; display: none; border: none; } #filter-type-checkboxes div { position: relative; - height: 25px; + height: 22px; } #filter-type-checkboxes span { position: relative; top: 3px; font-family: hifi-glyphs; - font-size: 16px; - color: #404040; - padding-left: 12px; - padding-right: 12px; + font-size: 13px; + color: #000000; + padding-left: 6px; + padding-right: 4px; } #filter-type-checkboxes label { - position: relative; - top: -13px; - z-index: 3; + position: absolute; + top: -20px; + z-index: 2; display: block; font-family: FiraSans-SemiBold; - color: #404040; + font-size: 11px; + color: #000000; background-color: #afafaf; + width: 200px; + height: 22px; padding-top: 1px; - padding-right: 12px; - height: 24px; } #filter-type-checkboxes label:hover { background-color: #1e90ff; } -#filter-type-checkboxes input[type=checkbox] { - position: relative; - top: 6px; - right: -10px; - z-index: 4; - display: block; -} #filter-type-checkboxes input[type=checkbox] + label { - background-image: none; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH4goSADUOYnF4LQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAIMSURBVFjD7ZmxkqowFIZ/7mwJPen1AezV3t6hFvrQhweAHvrQ8wL2xt4HwD7ppd+tvHOvu0gCYdEZTsmAfpNzzpcTcAB84o3iD94sZuCx4+Pxwvl8dl4JcL1ef84lMQPPwBZDSgkp5XsASylBKUUYhhBCvDbw7XYDpRRKKTRNA8YYOOevC5ymKZRS/13jnHdCTwLMOW8tAc45GGNomuY1gKuq6lxFIQQopdMDXy4X5HmudW8URdMCSynBGNOG3Ww20wHf9dVWl4+wbav7a8CMsW9G+Cm22+1T2F8BzvMc1+u18z5CCJIkseNhKSX2+z2qqjLWl84zhBAURQHXde0A31Oa57nWbqSrLwDwPA9FUcD3fTtb82NKu8QOAHVda+srSRJt2E7gtpQKIXA4HH6csmzpyxj4dDo9TalSCpRS1HX9TV86RujSlxGwlBJpmnY+rJRCGIZ/s2BTX9qnZgBwHAee52mJ/l7nx+PRqr6MVtj3fZRlaVRf/5aGDX0Z17DrusiyrHfqhuqrt9aiKEIcx4OBTfU1aOMIggBlWYIQ0utP+uhr8CyxXC5RFIUxdBAE1srKePgxbcbVamWlnAZNa7rNSAhBlmWv8yLlWTPa0Nco83BbM2ZZZsUIowzwj80YxzEWi8VoB4IPGz9yb0YhBHa73agnGGtHJNd1R4ed9FVV33Awf6ebgd8b+Av9A/rq6s3hjgAAAABJRU5ErkJggg=='); + background-size: 11px 11px; + background-position: top 5px left 14px; } #filter-type-checkboxes input[type=checkbox]:checked + label { - background-image: none; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH4goSADMveELP9QAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAIqSURBVFjD7ZmxkqowFIb/7GwJPfT6APZib+9QC33o4QGghz70vIC9sfcBsE966bPNWlxnlQTDRWc4JUT4hpPz5SQSAAofFF/4sJiBx47v+wun04m8E+B6vVbzlJiBZ2CLIYRQQgj1EcBCCEUpRRRF4Jyrtwa+Xq+glEJKia7rkKYpGGPqbYHzPFdSyn+uMcZ6oScBZowpzvmje0jTVHVd9x7ATdMoxtjTMZxzUErV5MDn81mVZak1No7jab+wEEKlaaoNGwQBmQz4pq9H8/IeNo5jMmnRpWmKeyP8FZvN5insfwEuy1JdLpfecb7vI8uy3tb2Szelu91ONU1jtP9jjKmmabRgq6qC4zh2VrpbSsuy1FqNdPUFAK7roqoqeJ6ntXH4Mk1pn9gBoG1bbX1lWaYN2wv8KKWcc+z3+z+7LFv6MgY+Ho9PUyqlBKUUbduqe33pGKFPX0bAQgiV53nvj6WUiKIIt2K0qS/tXTMAEELguq6W6H/nOQ6Hg1V9GX1hz/NIXdckCALtB7Vta1VfxnPYcRwURUEeNSGmYaqvwVqL45gkSfIysKm+Xlo4wjAkdV3D9/1BLxmir5d7ieVySaqqMoYOw3CwEV5ufkyLcbVaIUkSq2d1xt2abjH6vo+iKKwfLA5uL58Vow19jdIPPyrGoiisGGGUBv6+GJMkwWKxGO2M+dvGQ36LEZxztd1uRz0Qt7ZFchwHY8NOelQ1NAjm/+lm4M8G/gH2zx33BSr7jAAAAABJRU5ErkJggg=='); + background-size: 11px 11px; + background-position: top 5px left 14px; } #filter-type-checkboxes input[type=checkbox]:hover + label { background-color: #1e90ff; } #filter-search-and-icon { - position: absolute; - left: 120px; - width: calc(100% - 300px); + position: relative; + left: 118px; + width: calc(100% - 126px); } #filter-in-view { position: absolute; + top: 0px; right: 126px; } @@ -1152,13 +1153,18 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { position: relative; float: right; margin-right: -168px; - top: -17px; + top: -45px; } #filter-radius-and-unit label { margin-left: 2px; } #filter-radius-and-unit input { width: 120px; + border-radius: 14.5px; + font-style: italic; +} +#filter-radius-and-unit input[type=number]::-webkit-inner-spin-button { + display: none; } #entity-table-scroll { diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 8371e2cbd7..dc022c9ab9 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -30,20 +30,20 @@
-
-
- -
-
-
- -
-
-
- Y -
+
+
+ +
+
+
+ +
+
+
+ Y +
diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 400cdc7dce..66ad08e27c 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -23,6 +23,7 @@ const FILTER_IN_VIEW_ATTRIBUTE = "pressed"; const WINDOW_NONVARIABLE_HEIGHT = 227; const NUM_COLUMNS = 12; const EMPTY_ENTITY_ID = "0"; +const MAX_LENGTH_RADIUS = 9; const DELETE = 46; // Key code for the delete key. const KEY_P = 80; // Key code for letter p used for Parenting hotkey. @@ -203,7 +204,11 @@ function loaded() { elFilterSearch.onsearch = refreshEntityList; elFilterInView.onclick = toggleFilterInView; elFilterRadius.onchange = onRadiusChange; - elInfoToggle.onclick = toggleInfo; + elFilterRadius.oninput = function(event) { + if (event.target.value.length > MAX_LENGTH_RADIUS) { + event.target.value = event.target.value.slice(0, MAX_LENGTH_RADIUS); + } + } // create filter type dropdown checkboxes with label and icon for each type elFilterTypeSelectBox.onclick = toggleTypeDropdown; From 9e35af0c4a3ff246d3fe32410c9b67952f22e265 Mon Sep 17 00:00:00 2001 From: David Back Date: Wed, 17 Oct 2018 18:24:09 -0700 Subject: [PATCH 131/362] remove space --- scripts/system/html/entityList.html | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index dc022c9ab9..434f8a5f87 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -100,7 +100,6 @@ -
There are no entities to display. Please check your filters or create an entity to begin.
From 684f7d5689b78def61ca3e9f6c035297669e6fe1 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 3 Oct 2018 19:35:03 +0200 Subject: [PATCH 132/362] move export selection button to bottom of the list --- scripts/system/html/css/edit-style.css | 9 +++++++++ scripts/system/html/entityList.html | 13 ++++++++++--- scripts/system/html/js/entityList.js | 18 ++++++------------ 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 8d334609a6..fa64960487 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1063,6 +1063,15 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { position: relative; /* New positioning context. */ } +#footer-text { + float: right; + margin-right: 0; +} + +#entity-list-footer { + padding-top: 38px; +} + #search-area { padding-right: 168px; padding-bottom: 24px; diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 7eed78ecf3..cc7afe3bdb 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -1,4 +1,4 @@ -
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index fcc457eb7f..060ed1f67a 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1059,13 +1059,13 @@ const GROUPS = [ { label: "Grabbable", type: "bool", - propertyID: "grabbable", + propertyID: "grab.grabbable", column: 1, }, { label: "Triggerable", type: "bool", - propertyID: "triggerable", + propertyID: "grab.triggerable", column: 2, }, { @@ -1075,9 +1075,9 @@ const GROUPS = [ column: 1, }, { - label: "Ignore inverse kinematics", + label: "Follow Controller", type: "bool", - propertyID: "ignoreIK", + propertyID: "grab.grabFollowsController", column: 2, }, { @@ -2767,7 +2767,7 @@ function loaded() { showUserDataTextArea(); showSaveUserDataButton(); showNewJSONEditorButton(); - + deleteJSONMaterialEditor(); getPropertyInputElement("materialData").value = ""; showMaterialDataTextArea(); @@ -2937,46 +2937,6 @@ function loaded() { } } - let elGrabbable = getPropertyInputElement("grabbable"); - let elTriggerable = getPropertyInputElement("triggerable"); - let elIgnoreIK = getPropertyInputElement("ignoreIK"); - elGrabbable.checked = getPropertyInputElement("dynamic").checked; - elTriggerable.checked = false; - elIgnoreIK.checked = true; - let grabbablesSet = false; - let parsedUserData = {}; - try { - parsedUserData = JSON.parse(selectedEntityProperties.userData); - if ("grabbableKey" in parsedUserData) { - grabbablesSet = true; - let grabbableData = parsedUserData.grabbableKey; - if ("grabbable" in grabbableData) { - elGrabbable.checked = grabbableData.grabbable; - } else { - elGrabbable.checked = true; - } - if ("triggerable" in grabbableData) { - elTriggerable.checked = grabbableData.triggerable; - } else if ("wantsTrigger" in grabbableData) { - elTriggerable.checked = grabbableData.wantsTrigger; - } else { - elTriggerable.checked = false; - } - if ("ignoreIK" in grabbableData) { - elIgnoreIK.checked = grabbableData.ignoreIK; - } else { - elIgnoreIK.checked = true; - } - } - } catch (e) { - // TODO: What should go here? - } - if (!grabbablesSet) { - elGrabbable.checked = true; - elTriggerable.checked = false; - elIgnoreIK.checked = true; - } - if (selectedEntityProperties.type === "Image") { let imageLink = JSON.parse(selectedEntityProperties.textures)["tex.picture"]; getPropertyInputElement("image").value = imageLink; @@ -3119,20 +3079,6 @@ function loaded() { elDiv.insertBefore(elStaticMaterialData, elMaterialData); elDiv.insertBefore(elMaterialDataEditor, elMaterialData); - // User Data Fields - let elGrabbable = getPropertyInputElement("grabbable"); - let elTriggerable = getPropertyInputElement("triggerable"); - let elIgnoreIK = getPropertyInputElement("ignoreIK"); - elGrabbable.addEventListener('change', function() { - userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, true); - }); - elTriggerable.addEventListener('change', function() { - userDataChanger("grabbableKey", "triggerable", elTriggerable, elUserData, false, ['wantsTrigger']); - }); - elIgnoreIK.addEventListener('change', function() { - userDataChanger("grabbableKey", "ignoreIK", elIgnoreIK, elUserData, true); - }); - // Special Property Callbacks let elParentMaterialNameString = getPropertyInputElement("materialNameToReplace"); let elParentMaterialNameNumber = getPropertyInputElement("submeshToReplace"); From 8b922ad7ccb857188f2c5a74bf881d551d7d8434 Mon Sep 17 00:00:00 2001 From: Kerry Ivan Kurian Date: Thu, 18 Oct 2018 11:07:45 -0700 Subject: [PATCH 139/362] Add missing dependency --- assignment-client/src/Agent.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 5f1e1ca74a..0b590a6d27 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -57,6 +57,7 @@ #include "RecordingScriptingInterface.h" #include "AbstractAudioInterface.h" #include "AgentScriptingInterface.h" +#include "ResourceRequestObserver.h" static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10; @@ -99,6 +100,8 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); + // Needed to ensure the creation of the DebugDraw instance on the main thread DebugDraw::getInstance(); From 77b6389671b8cdb22a4bf62c54e620bde83b9da4 Mon Sep 17 00:00:00 2001 From: Kerry Ivan Kurian Date: Thu, 18 Oct 2018 12:38:18 -0700 Subject: [PATCH 140/362] Correct location of dependency --- assignment-client/src/Agent.cpp | 3 --- assignment-client/src/AssignmentClient.cpp | 13 ++++++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 0b590a6d27..5f1e1ca74a 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -57,7 +57,6 @@ #include "RecordingScriptingInterface.h" #include "AbstractAudioInterface.h" #include "AgentScriptingInterface.h" -#include "ResourceRequestObserver.h" static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10; @@ -100,8 +99,6 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); - // Needed to ensure the creation of the DebugDraw instance on the main thread DebugDraw::getInstance(); diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index 426f3ce6fc..06b3f4da86 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -35,6 +35,8 @@ #include "AssignmentClientLogging.h" #include "AssignmentFactory.h" +#include "ResourceRequestObserver.h" + const QString ASSIGNMENT_CLIENT_TARGET_NAME = "assignment-client"; const long long ASSIGNMENT_REQUEST_INTERVAL_MSECS = 1 * 1000; @@ -49,6 +51,7 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); auto addressManager = DependencyManager::set(); @@ -159,7 +162,7 @@ void AssignmentClient::setUpStatusToMonitor() { void AssignmentClient::sendStatusPacketToACM() { // tell the assignment client monitor what this assignment client is doing (if anything) auto nodeList = DependencyManager::get(); - + quint8 assignmentType = Assignment::Type::AllTypes; if (_currentAssignment) { @@ -170,7 +173,7 @@ void AssignmentClient::sendStatusPacketToACM() { statusPacket->write(_childAssignmentUUID.toRfc4122()); statusPacket->writePrimitive(assignmentType); - + nodeList->sendPacket(std::move(statusPacket), _assignmentClientMonitorSocket); } @@ -256,10 +259,10 @@ void AssignmentClient::handleCreateAssignmentPacket(QSharedPointer message) { const HifiSockAddr& senderSockAddr = message->getSenderSockAddr(); - + if (senderSockAddr.getAddress() == QHostAddress::LocalHost || senderSockAddr.getAddress() == QHostAddress::LocalHostIPv6) { - + qCDebug(assignment_client) << "AssignmentClientMonitor at" << senderSockAddr << "requested stop via PacketType::StopNode."; QCoreApplication::quit(); } else { @@ -312,6 +315,6 @@ void AssignmentClient::assignmentCompleted() { nodeList->setOwnerType(NodeType::Unassigned); nodeList->reset(); nodeList->resetNodeInterestSet(); - + _isAssigned = false; } From 11add70ef2b31986eb089eef38fa39c7d417e8d4 Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 18 Oct 2018 14:58:51 -0700 Subject: [PATCH 141/362] style tweaks --- scripts/system/html/css/edit-style.css | 109 +++++++++++---------- scripts/system/html/js/entityProperties.js | 6 +- 2 files changed, 59 insertions(+), 56 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 30dc959ef3..b50b5233f5 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -473,7 +473,7 @@ input[type=checkbox]:checked + label:hover { #properties-list fieldset { position: relative; /* 0.1px on the top is to prevent margin collapsing between this and it's first child */ - margin: 21px -21px 0 -21px; + margin: 0 -21px 21px -21px; padding: 0.1px 21px 0 21px; border: none; border-top: 1px rgb(90,90,90) solid; @@ -500,11 +500,6 @@ input[type=checkbox]:checked + label:hover { box-shadow: none; } -#properties-list > fieldset#properties-header { - margin-top: 0; - padding-bottom: 0; -} - #properties-list > fieldset > legend { position: relative; display: table; @@ -523,7 +518,7 @@ input[type=checkbox]:checked + label:hover { box-shadow: 0 -1px 0 rgb(37,37,37), 0 4px 4px 0 rgba(0,0,0,0.75); } -div.section-header, .sub-section-header, hr { +div.section-header, hr { display: table; width: 100%; margin: 21px -21px 0 -21px; @@ -536,17 +531,6 @@ div.section-header, .sub-section-header, hr { outline: none; } - - -.column .sub-section-header { - background-image: none; - padding-top: 0; -} - -.sub-section-header, .no-collapse, hr { - background: #404040 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAjSURBVBhXY1RVVf3PgARYjIyMoEwIYHRwcEBRwQSloYCBAQCwjgPMiI7W2QAAAABJRU5ErkJggg==) repeat-x top left; -} - div.section-header:first-child { margin-top: -2px; padding-top: 0; @@ -554,10 +538,6 @@ div.section-header:first-child { height: auto; } -.sub-section-header { - margin-bottom: -10px; -} - #properties-list > fieldset > legend span, .section-header span { font-family: HiFi-Glyphs; font-size: 30px; @@ -646,7 +626,7 @@ hr { } .property.range label{ - display: block; + padding-bottom: 3px; } .property.range input[type=number]{ margin-left: 0.8rem; @@ -956,12 +936,11 @@ div.refresh input[type="button"] { } #properties-list fieldset .two-column { - padding-top: 21px; + padding-top: 10px; display: flex; } -#properties-list .two-column fieldset { - /*display: table-cell;*/ +#properties-list .two-column fieldset { width: 50%; margin: 0; padding: 0; @@ -969,19 +948,27 @@ div.refresh input[type="button"] { box-shadow: none; } +#properties-list .two-column .column { + position: relative; + top: -10px; +} + #properties-list .two-column fieldset legend { - display: table; width: 100%; margin: 21px -21px 0 -21px; - padding: 0 0 0 21px; + padding: 6px 0 0 21px; font-family: Raleway-Regular; font-size: 12px; color: #afafaf; - height: 20px; + height: 10px; text-transform: uppercase; outline: none; } +#properties-list .two-column + .property { + margin-top: 6px; +} + fieldset .checkbox-sub-props { margin-top: 0; } @@ -1321,16 +1308,11 @@ th#entity-hasScript { border-right: 1px solid #575757; } - -#no-entities { - display: none; - position: absolute; - top: 80px; - padding: 12px; - font-family: FiraSans-SemiBold; - font-size: 15px; - font-style: italic; - color: #afafaf; +#properties-base { + border-top: none !important; + box-shadow: none !important; + margin-bottom: 5px !important; + top: -15px; } #properties-base #property-type-icon { @@ -1351,14 +1333,23 @@ th#entity-hasScript { #properties-base #div-property-locked { position: absolute; - top: 0px; + top: 6px; right: 140px; + display: table-cell; + vertical-align: middle; } #properties-base #div-property-visible { position: absolute; - top: 20px; + top: 26px; right: 20px; + display: table-cell; + vertical-align: middle; +} + +#properties-base #div-property-locked label, +#properties-base #div-property-visible label { + background-position-y: 1px; } #properties-base .checkbox label span { @@ -1367,18 +1358,13 @@ th#entity-hasScript { padding-right: 6px; vertical-align: top; position: relative; - top: -2px; + top: -4px; } #properties-base input[type=checkbox]:checked + label span { color: #ffffff; } -#properties-base + hr { - margin-top: 12px; -} - - #id label { width: 24px; } @@ -1409,22 +1395,26 @@ input#property-scale-button-reset { z-index: 99; position: absolute; width: 96%; - padding-left:1%; - margin-top:5px; - margin-bottom:10px; + padding-left: 1%; + margin-top: 5px; + margin-bottom: 10px; background-color: #2e2e2e; } #property-userData-saved, #property-materialData-saved { - margin-top:5px; - font-size:16px; - display:none; + margin-top: 5px; + font-size: 16px; + display: none; } +#div-property-serverScripts-status label { + position: relative; + top: 8px; +} #property-serverScripts-status { position: relative; - top: -3px; + top: 5px; right: -20px; } @@ -1439,6 +1429,17 @@ input#property-scale-button-reset { flex-direction: column; } +#no-entities { + display: none; + position: absolute; + top: 80px; + padding: 12px; + font-family: FiraSans-SemiBold; + font-size: 15px; + font-style: italic; + color: #afafaf; +} + input[type=button]#export { height: 38px; width: 180px; diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 060ed1f67a..1a52cf939d 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -3024,13 +3024,15 @@ function loaded() { let serverScriptProperty = properties["serverScripts"]; let elServerScript = serverScriptProperty.elInput; let serverScriptElementID = serverScriptProperty.elementID; + let serverScriptStatusElementID = serverScriptElementID + "-status"; let elDiv = document.createElement('div'); elDiv.className = "property"; + elDiv.setAttribute("id", "div-" + serverScriptStatusElementID); let elLabel = document.createElement('label'); - elLabel.setAttribute("for", serverScriptElementID + "-status"); + elLabel.setAttribute("for", serverScriptStatusElementID); elLabel.innerText = "Server Script Status"; let elServerScriptStatus = document.createElement('span'); - elServerScriptStatus.setAttribute("id", serverScriptElementID + "-status"); + elServerScriptStatus.setAttribute("id", serverScriptStatusElementID); elDiv.appendChild(elLabel); elDiv.appendChild(elServerScriptStatus); elServerScript.parentNode.appendChild(elDiv); From 45a92fd53745bf961e028cbf6dd9342ca8c1485b Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 18 Oct 2018 15:12:38 -0700 Subject: [PATCH 142/362] style tweaks --- scripts/system/html/css/edit-style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index b50b5233f5..8205fcd340 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -956,7 +956,7 @@ div.refresh input[type="button"] { #properties-list .two-column fieldset legend { width: 100%; margin: 21px -21px 0 -21px; - padding: 6px 0 0 21px; + padding: 16px 0 0 21px; font-family: Raleway-Regular; font-size: 12px; color: #afafaf; From 9f91238945ed9f5e27116a7be8a334285b724f39 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 18 Oct 2018 15:19:29 -0700 Subject: [PATCH 143/362] UX/UI improvements --- .../marketplaceItemTester/ItemUnderTest.qml | 352 ++++++++++++ .../MarketplaceItemTester.qml | 499 +++++++----------- .../marketplaceItemTester/spinner.gif | Bin 46135 -> 59412 bytes 3 files changed, 555 insertions(+), 296 deletions(-) create mode 100644 interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml new file mode 100644 index 0000000000..4852158df9 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml @@ -0,0 +1,352 @@ +// +// ItemUnderTest +// qml/hifi/commerce/marketplaceItemTester +// +// Load items not in the marketplace for testing purposes +// +// Created by Kerry Ivan Kurian on 2018-10-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 +// + +import QtQuick 2.10 +import QtQuick.Controls 2.3 +import Hifi 1.0 as Hifi +import "qrc:////qml//styles-uit" as HifiStylesUit +import "qrc:////qml//controls-uit" as HifiControlsUit + +Rectangle { + id: root; + color: hifi.colors.baseGray + width: parent.width - 16 + height: childrenRect.height + itemHeaderContainer.anchors.topMargin + detailsContainer.anchors.topMargin + + property var detailsExpanded: false + + property var actions: { + "forward": function(resource, assetType, resourceObjectId){ + switch(assetType) { + case "application": + Commerce.openApp(resource); + break; + case "avatar": + MyAvatar.useFullAvatarURL(resource); + break; + case "content set": + urlHandler.handleUrl("hifi://localhost/0,0,0"); + Commerce.replaceContentSet(toUrl(resource), ""); + break; + case "entity": + case "wearable": + rezEntity(resource, assetType, resourceObjectId); + break; + default: + print("Marketplace item tester unsupported assetType " + assetType); + } + }, + "trash": function(resource, assetType){ + if ("application" === assetType) { + Commerce.uninstallApp(resource); + } + sendToScript({ + method: "tester_deleteResourceObject", + objectId: resourceListModel.get(index).id}); + resourceListModel.remove(index); + } + } + + Item { + id: itemHeaderContainer + anchors.top: parent.top + anchors.topMargin: 8 + anchors.left: parent.left + anchors.leftMargin: 8 + width: parent.width - 16 + height: childrenRect.height + + Item { + id: itemNameContainer + width: parent.width * 0.5 + height: childrenRect.height + + HifiStylesUit.RalewaySemiBold { + id: resourceName + height: paintedHeight + width: parent.width + text: { + var match = resource.match(/\/([^/]*)$/); + return match ? match[1] : resource; + } + size: 14 + color: hifi.colors.white + wrapMode: Text.WrapAnywhere + } + + HifiStylesUit.RalewayRegular { + id: resourceUrl + anchors.top: resourceName.bottom; + anchors.topMargin: 4; + height: paintedHeight + width: parent.width + text: resource + size: 12 + color: hifi.colors.faintGray; + wrapMode: Text.WrapAnywhere + } + } + + HifiControlsUit.ComboBox { + id: comboBox + anchors.left: itemNameContainer.right + anchors.leftMargin: 4 + anchors.verticalCenter: itemNameContainer.verticalCenter + height: 30 + width: parent.width * 0.3 - anchors.leftMargin + + model: [ + "application", + "avatar", + "content set", + "entity", + "wearable", + "unknown" + ] + + currentIndex: (("entity or wearable" === assetType) ? + model.indexOf("unknown") : model.indexOf(assetType)) + + Component.onCompleted: { + onCurrentIndexChanged.connect(function() { + assetType = model[currentIndex]; + sendToScript({ + method: "tester_updateResourceObjectAssetType", + objectId: resourceListModel.get(index)["resourceObjectId"], + assetType: assetType }); + }); + } + } + + Button { + id: actionButton + property var glyphs: { + "application": hifi.glyphs.install, + "avatar": hifi.glyphs.avatar, + "content set": hifi.glyphs.globe, + "entity": hifi.glyphs.wand, + "trash": hifi.glyphs.trash, + "unknown": hifi.glyphs.circleSlash, + "wearable": hifi.glyphs.hat, + } + property int color: hifi.buttons.blue; + property int colorScheme: hifi.colorSchemes.dark; + anchors.left: comboBox.right + anchors.leftMargin: 4 + anchors.verticalCenter: itemNameContainer.verticalCenter + width: parent.width * 0.10 - anchors.leftMargin + height: width + enabled: comboBox.model[comboBox.currentIndex] !== "unknown" + + onClicked: { + root.actions["forward"](resource, comboBox.currentText, resourceObjectId); + } + + background: Rectangle { + radius: 4; + gradient: Gradient { + GradientStop { + position: 0.2 + color: { + if (!actionButton.enabled) { + hifi.buttons.disabledColorStart[actionButton.colorScheme] + } else if (actionButton.pressed) { + hifi.buttons.pressedColor[actionButton.color] + } else if (actionButton.hovered) { + hifi.buttons.hoveredColor[actionButton.color] + } else { + hifi.buttons.colorStart[actionButton.color] + } + } + } + GradientStop { + position: 1.0 + color: { + if (!actionButton.enabled) { + hifi.buttons.disabledColorFinish[actionButton.colorScheme] + } else if (actionButton.pressed) { + hifi.buttons.pressedColor[actionButton.color] + } else if (actionButton.hovered) { + hifi.buttons.hoveredColor[actionButton.color] + } else { + hifi.buttons.colorFinish[actionButton.color] + } + } + } + } + } + + contentItem: Item { + HifiStylesUit.HiFiGlyphs { + id: rezIcon; + text: actionButton.glyphs[comboBox.model[comboBox.currentIndex]]; + anchors.fill: parent + size: 30; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + color: enabled ? hifi.buttons.textColor[actionButton.color] + : hifi.buttons.disabledTextColor[actionButton.colorScheme] + } + } + } + + Button { + id: trashButton + property int color: hifi.buttons.red; + property int colorScheme: hifi.colorSchemes.dark; + anchors.left: actionButton.right + anchors.verticalCenter: itemNameContainer.verticalCenter + anchors.leftMargin: 4 + width: parent.width * 0.10 - anchors.leftMargin + height: width + + onClicked: { + root.actions["trash"](resource, comboBox.currentText, resourceObjectId); + } + + background: Rectangle { + radius: 4; + gradient: Gradient { + GradientStop { + position: 0.2 + color: { + if (!trashButton.enabled) { + hifi.buttons.disabledColorStart[trashButton.colorScheme] + } else if (trashButton.pressed) { + hifi.buttons.pressedColor[trashButton.color] + } else if (trashButton.hovered) { + hifi.buttons.hoveredColor[trashButton.color] + } else { + hifi.buttons.colorStart[trashButton.color] + } + } + } + GradientStop { + position: 1.0 + color: { + if (!trashButton.enabled) { + hifi.buttons.disabledColorFinish[trashButton.colorScheme] + } else if (trashButton.pressed) { + hifi.buttons.pressedColor[trashButton.color] + } else if (trashButton.hovered) { + hifi.buttons.hoveredColor[trashButton.color] + } else { + hifi.buttons.colorFinish[trashButton.color] + } + } + } + } + } + + contentItem: Item { + HifiStylesUit.HiFiGlyphs { + id: trashIcon; + text: hifi.glyphs.trash + anchors.fill: parent + size: 22; + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: enabled ? hifi.buttons.textColor[trashButton.color] + : hifi.buttons.disabledTextColor[trashButton.colorScheme] + } + } + } + } + + Item { + id: detailsContainer + + width: parent.width - 16 + height: root.detailsExpanded ? 300 : 26 + anchors.top: itemHeaderContainer.bottom + anchors.topMargin: 12 + anchors.left: parent.left + anchors.leftMargin: 8 + + HifiStylesUit.HiFiGlyphs { + id: detailsToggle + anchors.left: parent.left + anchors.leftMargin: -4 + anchors.top: parent.top + anchors.topMargin: -2 + width: 22 + text: root.detailsExpanded ? hifi.glyphs.minimize : hifi.glyphs.maximize + color: hifi.colors.white + size: 22 + MouseArea { + anchors.fill: parent + onClicked: root.detailsExpanded = !root.detailsExpanded + } + } + + ScrollView { + id: detailsTextContainer + anchors.top: parent.top + anchors.left: detailsToggle.right + anchors.leftMargin: 4 + anchors.right: parent.right + height: detailsContainer.height - (root.detailsExpanded ? (copyToClipboardButton.height + copyToClipboardButton.anchors.topMargin) : 0) + clip: true + + TextArea { + id: detailsText + readOnly: true + color: hifi.colors.white + text: { + if (root.detailsExpanded) { + return resourceAccessEventText + } else { + return (resourceAccessEventText.split("\n").length - 1).toString() + " resources loaded..." + } + } + font: Qt.font({ family: "Courier", pointSize: 8, weight: Font.Normal }) + wrapMode: TextEdit.NoWrap + + background: Rectangle { + anchors.fill: parent; + color: hifi.colors.baseGrayShadow; + border.width: 0; + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (root.detailsExpanded) { + detailsText.selectAll(); + } else { + root.detailsExpanded = true; + } + } + } + } + + HifiControlsUit.Button { + id: copyToClipboardButton; + visible: root.detailsExpanded + color: hifi.buttons.noneBorderlessWhite + colorScheme: hifi.colorSchemes.dark + + anchors.top: detailsTextContainer.bottom + anchors.topMargin: 8 + anchors.right: parent.right + width: 150 + height: 30 + text: "Copy to Clipboard" + + onClicked: { + Window.copyToClipboard(detailsText.text); + } + } + } +} diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml index f06612d035..89b1dd3915 100644 --- a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml @@ -11,14 +11,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import QtQuick.Dialogs 1.0 +import QtQuick 2.10 import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 import Hifi 1.0 as Hifi -import "../../../styles-uit" as HifiStylesUit -import "../../../controls-uit" as HifiControlsUit +import "qrc:////qml//styles-uit" as HifiStylesUit +import "qrc:////qml//controls-uit" as HifiControlsUit @@ -29,20 +27,208 @@ Rectangle { property string resourceAccessEventText property var nextResourceObjectId: 0 property var startDate - signal sendToScript(var message) HifiStylesUit.HifiConstants { id: hifi } ListModel { id: resourceListModel } - color: hifi.colors.white + color: hifi.colors.darkGray - AnimatedImage { - id: spinner; - source: "spinner.gif" - width: 74; - height: width; - anchors.verticalCenter: parent.verticalCenter; - anchors.horizontalCenter: parent.horizontalCenter; + Component.onCompleted: startDate = new Date() + + // + // TITLE BAR START + // + Item { + id: titleBarContainer + // Size + width: root.width + height: 50 + // Anchors + anchors.left: parent.left + anchors.top: parent.top + + // Title bar text + HifiStylesUit.RalewaySemiBold { + id: titleBarText + text: "Marketplace Item Tester" + // Text size + size: 24 + // Anchors + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: 16 + width: paintedWidth + // Style + color: hifi.colors.lightGrayText + // Alignment + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: 1 + } + } + // + // TITLE BAR END + // + + Rectangle { + id: spinner + z: 999 + anchors.top: titleBarContainer.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: buttonContainer.top + color: hifi.colors.darkGray + + AnimatedImage { + source: "spinner.gif" + width: 74 + height: width + anchors.centerIn: parent + } + } + + Rectangle { + id: instructionsContainer + z: 998 + color: hifi.colors.darkGray + visible: resourceListModel.count === 0 && !spinner.visible + anchors.top: titleBarContainer.bottom + anchors.topMargin: 20 + anchors.left: parent.left + anchors.leftMargin: 20 + anchors.right: parent.right + anchors.rightMargin: 20 + anchors.bottom: buttonContainer.top + anchors.bottomMargin: 20 + + HifiStylesUit.RalewayRegular { + text: "Use Marketplace Item Tester to test out your items before submitting them to the Marketplace." + + "\n\nUse one of the buttons below to load your item." + // Text size + size: 20 + // Anchors + anchors.fill: parent + // Style + color: hifi.colors.lightGrayText + wrapMode: Text.Wrap + // Alignment + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + + ListView { + id: itemList + visible: !instructionsContainer.visible + anchors.top: titleBarContainer.bottom + anchors.topMargin: 20 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: buttonContainer.top + anchors.bottomMargin: 20 + ScrollBar.vertical: ScrollBar { + visible: !instructionsContainer.visible + policy: ScrollBar.AlwaysOn + parent: itemList.parent + anchors.top: itemList.top + anchors.right: itemList.right + anchors.bottom: itemList.bottom + width: 16 + } + clip: true + model: resourceListModel + spacing: 8 + + delegate: ItemUnderTest { + } + } + + Item { + id: buttonContainer + + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.bottom: parent.bottom + anchors.bottomMargin: 12 + height: 40 + + property string currentAction + property var actions: { + "Load File": function() { + buttonContainer.currentAction = "load file"; + Window.browseChanged.connect(onResourceSelected); + Window.browseAsync("Please select a file (*.app.json *.json *.fst *.json.gz)", "", "Assets (*.app.json *.json *.fst *.json.gz)"); + }, + "Load URL": function() { + buttonContainer.currentAction = "load url"; + Window.promptTextChanged.connect(onResourceSelected); + Window.promptAsync("Please enter a URL", ""); + } + } + + function onResourceSelected(resource) { + // It is possible that we received the present signal + // from something other than our browserAsync window. + // Alas, there is nothing we can do about that so charge + // ahead as though we are sure the present signal is one + // we expect. + print("!!!! resource selected"); + switch(currentAction) { + case "load file": + Window.browseChanged.disconnect(onResourceSelected); + break + case "load url": + Window.promptTextChanged.disconnect(onResourceSelected); + break; + } + if (resource) { + print("!!!! building resource object"); + var resourceObj = buildResourceObj(resource); + print("!!!! installing resource object"); + installResourceObj(resourceObj); + print("!!!! notifying script of resource object"); + sendToScript({ + method: 'tester_newResourceObject', + resourceObject: resourceObj + }); + } + } + + HifiControlsUit.Button { + enabled: !spinner.visible + anchors.right: parent.horizontalCenter + anchors.rightMargin: width/4 + anchors.verticalCenter: parent.verticalCenter + color: hifi.buttons.blue + fontSize: 20 + text: "Load File" + width: parent.width / 3 + height: parent.height + onClicked: buttonContainer.actions[text]() + } + + HifiControlsUit.Button { + enabled: !spinner.visible + anchors.left: parent.horizontalCenter + anchors.leftMargin: width/4 + anchors.verticalCenter: parent.verticalCenter + color: hifi.buttons.blue + fontSize: 20 + text: "Load URL" + width: parent.width / 3 + height: parent.height + onClicked: buttonContainer.actions[text]() + } } function fromScript(message) { @@ -117,285 +303,6 @@ Rectangle { itemType: entityType, itemId: resourceObjectId }); } - - Component.onCompleted: startDate = new Date() - - ColumnLayout { - id: rootColumn - spacing: 30 - - HifiStylesUit.RalewayRegular { - id: rootHeader - text: "Marketplace Item Tester" - height: 40 - width: paintedWidth - size: 22 - color: hifi.colors.black - anchors.top: parent.top - anchors.topMargin: 20 - anchors.left: parent.left - anchors.leftMargin: 12 - } - - Rectangle { - height: root.height - 100 - width: root.width - anchors.left: parent.left - - ScrollView { - id: scrollView - anchors.fill: parent - anchors.rightMargin: 12 - anchors.bottom: parent.top - anchors.bottomMargin: 20 - anchors.leftMargin: 12 - verticalScrollBarPolicy: Qt.ScrollBarAlwaysOn - - frameVisible: false - - contentItem: ListView { - spacing: 20 - height: 200 - model: resourceListModel - interactive: false - - delegate: Column { - spacing: 8 - - RowLayout { - id: listRow - width: scrollView.width - 20 - anchors.rightMargin: scrollView.rightMargin - spacing: 5 - - property var actions: { - "forward": function(resource, assetType, resourceObjectId){ - switch(assetType) { - case "application": - Commerce.openApp(resource); - break; - case "avatar": - MyAvatar.useFullAvatarURL(resource); - break; - case "content set": - urlHandler.handleUrl("hifi://localhost/0,0,0"); - Commerce.replaceContentSet(toUrl(resource), ""); - break; - case "entity": - case "wearable": - rezEntity(resource, assetType, resourceObjectId); - break; - default: - print("Marketplace item tester unsupported assetType " + assetType); - } - }, - "trash": function(resource, assetType){ - if ("application" === assetType) { - Commerce.uninstallApp(resource); - } - sendToScript({ - method: "tester_deleteResourceObject", - objectId: resourceListModel.get(index).id}); - resourceListModel.remove(index); - } - } - - Column { - Layout.preferredWidth: scrollView.width * .6 - spacing: 5 - Text { - width: listRow.width * .6 - text: { - var match = resource.match(/\/([^/]*)$/); - return match ? match[1] : resource; - } - font.pointSize: 12 - horizontalAlignment: Text.AlignBottom - wrapMode: Text.WrapAnywhere - } - Text { - width: listRow.width * .6 - text: resource - font.pointSize: 8 - horizontalAlignment: Text.AlignBottom - wrapMode: Text.WrapAnywhere - } - } - - ComboBox { - id: comboBox - - Layout.preferredWidth: listRow.width * .2 - - model: [ - "application", - "avatar", - "content set", - "entity", - "wearable", - "unknown" - ] - - currentIndex: (("entity or wearable" === assetType) ? - model.indexOf("unknown") : model.indexOf(assetType)) - - Component.onCompleted: { - onCurrentIndexChanged.connect(function() { - assetType = model[currentIndex]; - sendToScript({ - method: "tester_updateResourceObjectAssetType", - objectId: resourceListModel.get(index)["resourceObjectId"], - assetType: assetType }); - }); - } - } - - Repeater { - model: [ "forward", "trash" ] - - HifiStylesUit.HiFiGlyphs { - property var glyphs: { - "application": hifi.glyphs.install, - "avatar": hifi.glyphs.avatar, - "content set": hifi.glyphs.globe, - "entity": hifi.glyphs.wand, - "trash": hifi.glyphs.trash, - "unknown": hifi.glyphs.circleSlash, - "wearable": hifi.glyphs.hat, - } - text: (("trash" === modelData) ? - glyphs.trash : - glyphs[comboBox.model[comboBox.currentIndex]]) - size: ("trash" === modelData) ? 22 : 30 - color: hifi.colors.black - horizontalAlignment: Text.AlignHCenter - MouseArea { - anchors.fill: parent - onClicked: { - listRow.actions[modelData](resource, comboBox.currentText, resourceObjectId); - } - } - } - } - } - - Rectangle { - id: detailsContainer - - width: scrollView.width - 20 - height: resourceDetails.isOpen ? 300 : 20 - anchors.left: parent.left - - HifiStylesUit.HiFiGlyphs { - id: detailsToggle - anchors.top: parent.top - text: resourceDetails.isOpen ? hifi.glyphs.minimize : hifi.glyphs.maximize - color: hifi.colors.black - size: 22 - verticalAlignment: Text.AlignBottom - MouseArea { - anchors.fill: parent - onClicked: resourceDetails.isOpen = !resourceDetails.isOpen - } - } - - TextArea { - id: resourceDetails - - property var isOpen: false - - width: detailsContainer.width - 20 - height: detailsContainer.height - anchors.top: parent.top - anchors.left: detailsToggle.left - anchors.leftMargin: 20 - verticalScrollBarPolicy: isOpen ? Qt.ScrollBarAsNeeded : Qt.ScrollBarAlwaysOff - frameVisible: isOpen - readOnly: true - - text: { - if (isOpen) { - return resourceAccessEventText - } else { - return (resourceAccessEventText.split("\n").length - 1).toString() + " resources loaded..." - } - } - font: Qt.font({ family: "Courier", pointSize: 8, weight: Font.Normal }) - wrapMode: TextEdit.NoWrap - } - } - - Rectangle { - width: listRow.width - height: 1 - color: hifi.colors.black - } - } - } - } - } - - Row { - id: rootActions - spacing: 20 - - anchors.left: parent.left - anchors.leftMargin: root.width / 6 - 10 - anchors.bottomMargin: 40 - anchors.bottom: parent.bottom - - property string currentAction - property var actions: { - "Load File": function(){ - rootActions.currentAction = "load file"; - Window.browseChanged.connect(onResourceSelected); - Window.browseAsync("Please select a file (*.app.json *.json *.fst *.json.gz)", "", "Assets (*.app.json *.json *.fst *.json.gz)"); - }, - "Load URL": function(){ - rootActions.currentAction = "load url"; - Window.promptTextChanged.connect(onResourceSelected); - Window.promptAsync("Please enter a URL", ""); - } - } - - function onResourceSelected(resource) { - // It is possible that we received the present signal - // from something other than our browserAsync window. - // Alas, there is nothing we can do about that so charge - // ahead as though we are sure the present signal is one - // we expect. - print("!!!! resource selected"); - switch(currentAction) { - case "load file": - Window.browseChanged.disconnect(onResourceSelected); - break - case "load url": - Window.promptTextChanged.disconnect(onResourceSelected); - break; - } - if (resource) { - print("!!!! building resource object"); - var resourceObj = buildResourceObj(resource); - print("!!!! installing resource object"); - installResourceObj(resourceObj); - print("!!!! notifying script of resource object"); - sendToScript({ - method: 'tester_newResourceObject', - resourceObject: resourceObj }); - } - } - - Repeater { - model: [ "Load File", "Load URL" ] - HifiControlsUit.Button { - color: hifi.buttons.blue - fontSize: 20 - text: modelData - width: root.width / 3 - height: 40 - onClicked: rootActions.actions[text]() - } - } - } - } + + signal sendToScript(var message) } diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif b/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif index 00f75ae62fa59b28a7813ca00c4185390e3e51da..0536bd1884f1cb4a814b729803b1996c8347d182 100644 GIT binary patch literal 59412 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�)#8TsS*rVAU%J0tpt=B2=IG*djp3b$!Y|O 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 L8@}NiK8F7Q4CAa& literal 46135 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 V-}`%i@9+J+zxVh4-rv9A{u}xRWtji~ From 4d7bc3094aed9445a5830edfb850c22839d7b2e4 Mon Sep 17 00:00:00 2001 From: Kerry Ivan Kurian Date: Thu, 18 Oct 2018 16:14:53 -0700 Subject: [PATCH 144/362] Fix regression --- .../hifi/commerce/marketplaceItemTester/ItemUnderTest.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml index 4852158df9..dcb67f3f12 100644 --- a/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml @@ -52,7 +52,7 @@ Rectangle { } sendToScript({ method: "tester_deleteResourceObject", - objectId: resourceListModel.get(index).id}); + objectId: resourceListModel.get(index).resourceObjectId}); resourceListModel.remove(index); } } @@ -83,7 +83,7 @@ Rectangle { color: hifi.colors.white wrapMode: Text.WrapAnywhere } - + HifiStylesUit.RalewayRegular { id: resourceUrl anchors.top: resourceName.bottom; @@ -311,7 +311,7 @@ Rectangle { } font: Qt.font({ family: "Courier", pointSize: 8, weight: Font.Normal }) wrapMode: TextEdit.NoWrap - + background: Rectangle { anchors.fill: parent; color: hifi.colors.baseGrayShadow; From d7d49ed84e66f9517b7adeaa9a1baf0fd7b71f2b Mon Sep 17 00:00:00 2001 From: Kerry Ivan Kurian Date: Thu, 18 Oct 2018 16:25:26 -0700 Subject: [PATCH 145/362] Remove cruft --- .../marketplaceItemTester/MarketplaceItemTester.qml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml index 89b1dd3915..5f2268132c 100644 --- a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml @@ -77,7 +77,7 @@ Rectangle { // // TITLE BAR END // - + Rectangle { id: spinner z: 999 @@ -281,15 +281,6 @@ Rectangle { } } - function addAllInstalledAppsToList() { - var i, apps = Commerce.getInstalledApps().split(","), len = apps.length; - for(i = 0; i < len - 1; ++i) { - if (i in apps) { - resourceListModel.append(buildResourceObj(apps[i])); - } - } - } - function toUrl(resource) { var httpPattern = /^http/i; return httpPattern.test(resource) ? resource : "file:///" + resource; @@ -303,6 +294,6 @@ Rectangle { itemType: entityType, itemId: resourceObjectId }); } - + signal sendToScript(var message) } From b9a2f19d302f87b74a13d75f45a0285883914d3d Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 18 Oct 2018 16:39:50 -0700 Subject: [PATCH 146/362] made all the suggested corrections except the dropdown qml. --- .../resources/qml/hifi/avatarapp/Settings.qml | 36 +++++++++--- interface/src/avatar/MyAvatar.cpp | 55 ++++++++++--------- interface/src/avatar/MyAvatar.h | 9 +-- 3 files changed, 62 insertions(+), 38 deletions(-) diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index 8749079940..fd72d70106 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -354,14 +354,36 @@ Rectangle { // "Lock State" Checkbox HifiControlsUit.CheckBox { - id: lockSitStandStateCheckbox; - visible: activeTab == "nearbyTab"; - anchors.right: reloadNearbyContainer.left; - anchors.rightMargin: 20; - checked: settings.lockStateEnabled; - text: "lock"; - boxSize: 24; + id: lockSitStandStateCheckbox + visible: activeTab == "nearbyTab" + anchors.right: reloadNearbyContainer.left + anchors.rightMargin: 20 + checked: settings.lockStateEnabled + text: "lock" + boxSize: 24 } + + // sit stand combo box + HifiControlsUit.ComboBox { + id: boxy + //textRole: "text" + currentIndex: 2 + model: ListModel { + id: cbItems + ListElement { text: "Force Sitting"; color: "Yellow" } + ListElement { text: "Force Standing"; color: "Green" } + ListElement { text: "Auto Mode"; color: "Brown" } + ListElement { text: "Disable Recentering"; color: "Red" } + } + //displayText: "fred" + //label: cbItems.get(currentIndex).text + width: 200 + onCurrentIndexChanged: { + console.debug(cbItems.get(currentIndex).text + ", " + cbItems.get(currentIndex).color) + console.debug("line 2") + } + } + } ColumnLayout { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 9fe91e44b9..2c9b83b636 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -464,20 +464,19 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { } } -void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { +void MyAvatar::updateSitStandState(float newHeightReading, float dt) { const float STANDING_HEIGHT_MULTIPLE = 1.2f; const float SITTING_HEIGHT_MULTIPLE = 0.833f; - const int SITTING_COUNT_THRESHOLD = 240; - const int STANDING_COUNT_THRESHOLD = 20; - const int SQUATTY_COUNT_THRESHOLD = 600; + const float SITTING_TIMEOUT = 4.0f; // 4 seconds + const float STANDING_TIMEOUT = 0.3333f; // 1/3 second const float SITTING_UPPER_BOUND = 1.52f; if (!getIsSitStandStateLocked() && !getIsAway() && qApp->isHMDMode()) { if (getIsInSittingState()) { if (newHeightReading > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { // if we recenter upwards then no longer in sitting state - _sitStandStateCount++; - if (_sitStandStateCount > STANDING_COUNT_THRESHOLD) { + _sitStandStateTimer += dt; + if (_sitStandStateTimer > STANDING_TIMEOUT) { _averageUserHeightSensorSpace = newHeightReading; _tippingPoint = newHeightReading; setIsInSittingState(false); @@ -485,8 +484,8 @@ void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { } else if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) { // if we are mis labelled as sitting but we are standing in the real world this will // make sure that a real sit is still recognized so we won't be stuck in sitting unable to change state - _sitStandStateCount++; - if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sitStandStateTimer += dt; + if (_sitStandStateTimer > SITTING_TIMEOUT) { _averageUserHeightSensorSpace = newHeightReading; _tippingPoint = newHeightReading; // here we stay in sit state but reset the average height @@ -499,14 +498,14 @@ void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { } else { // tipping point is average height when sitting. _tippingPoint = _averageUserHeightSensorSpace; - _sitStandStateCount = 0; + _sitStandStateTimer = 0.0f; } } } else { // in the standing state if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) { - _sitStandStateCount++; - if (_sitStandStateCount > SITTING_COUNT_THRESHOLD) { + _sitStandStateTimer += dt; + if (_sitStandStateTimer > SITTING_TIMEOUT) { _averageUserHeightSensorSpace = newHeightReading; _tippingPoint = newHeightReading; setIsInSittingState(true); @@ -514,7 +513,7 @@ void MyAvatar::updateSitStandState(float newHeightReading, float angleHeadUp) { } else { // use the mode height for the tipping point when we are standing. _tippingPoint = getCurrentStandingHeight(); - _sitStandStateCount = 0; + _sitStandStateTimer = 0.0f; } } } else { @@ -530,6 +529,7 @@ void MyAvatar::update(float deltaTime) { const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); const float PERCENTAGE_WEIGHT_HEAD_VS_SHOULDERS_AZIMUTH = 0.0f; // 100 percent shoulders const float COSINE_THIRTY_DEGREES = 0.866f; + const float SQUATTY_TIMEOUT = 30.0f; // 30 seconds float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); @@ -575,19 +575,17 @@ void MyAvatar::update(float deltaTime) { } float angleSpine2 = glm::dot(upSpine2, glm::vec3(0.0f, 1.0f, 0.0f)); if (getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPositionAvatarSpace.y - SQUAT_THRESHOLD) && (angleSpine2 > COSINE_THIRTY_DEGREES)) { - _squatCount++; + _squatTimer += deltaTime; + if (_squatTimer > SQUATTY_TIMEOUT) { + _squatTimer = 0.0f; + _follow._squatDetected = true; + } } else { - _squatCount = 0; + _squatTimer = 0.0f; } - glm::vec3 headUp = newHeightReading.getRotation() * glm::vec3(0.0f, 1.0f, 0.0f); - if (glm::length(headUp) > 0.0f) { - headUp = glm::normalize(headUp); - } - float angleHeadUp = glm::dot(headUp, glm::vec3(0.0f, 1.0f, 0.0f)); - // put update sit stand state counts here - updateSitStandState(newHeightReading.getTranslation().y, angleHeadUp); + updateSitStandState(newHeightReading.getTranslation().y, deltaTime); if (_drawAverageFacingEnabled) { auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); @@ -3893,8 +3891,8 @@ void MyAvatar::setIsInWalkingState(bool isWalking) { } void MyAvatar::setIsInSittingState(bool isSitting) { - _sitStandStateCount = 0; - _squatCount = 0; + _sitStandStateTimer = 0.0f; + _squatTimer = 0.0f; // on reset height we need the count to be more than one in case the user sits and stands up quickly. _isInSittingState.set(isSitting); setResetMode(true); @@ -3909,7 +3907,8 @@ void MyAvatar::setIsInSittingState(bool isSitting) { void MyAvatar::setIsSitStandStateLocked(bool isLocked) { _lockSitStandState.set(isLocked); - _sitStandStateCount = 0; + _sitStandStateTimer = 0.0f; + _squatTimer = 0.0f; _averageUserHeightSensorSpace = _userHeight.get(); _tippingPoint = _userHeight.get(); if (!isLocked) { @@ -4155,7 +4154,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons return stepDetected; } -bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { +bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { const float CYLINDER_TOP = 0.1f; const float CYLINDER_BOTTOM = -1.5f; const float SITTING_BOTTOM = -0.02f; @@ -4179,8 +4178,7 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(MyAvatar& myAvatar, const gl // in the standing state returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); // finally check for squats in standing - if ((myAvatar._squatCount > SQUATTY_COUNT_THRESHOLD) && !myAvatar.getIsSitStandStateLocked()) { - myAvatar._squatCount = 0; + if (_squatDetected) { returnValue = true; } } @@ -4216,6 +4214,9 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Vertical); + if (_squatDetected) { + _squatDetected = false; + } } } else { if (!isActive(Rotation) && getForceActivateRotation()) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index d1ba0fd9cf..be8b5fa1b2 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1116,7 +1116,7 @@ public: float getSprintSpeed() const; void setSitStandStateChange(bool stateChanged); float getSitStandStateChange() const; - void updateSitStandState(float newHeightReading, float angleHeadUp); + void updateSitStandState(float newHeightReading, float dt); QVector getScriptUrls(); @@ -1744,7 +1744,7 @@ private: float getMaxTimeRemaining() const; void decrementTimeRemaining(float dt); bool shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; - bool shouldActivateVertical(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; + bool shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateHorizontalCG(MyAvatar& myAvatar) const; void prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& bodySensorMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput); @@ -1757,6 +1757,7 @@ private: void setForceActivateHorizontal(bool val); bool getToggleHipsFollowing() const; void setToggleHipsFollowing(bool followHead); + bool _squatDetected { false }; std::atomic _forceActivateRotation { false }; std::atomic _forceActivateVertical { false }; std::atomic _forceActivateHorizontal { false }; @@ -1843,8 +1844,8 @@ private: float _walkSpeedScalar { AVATAR_WALK_SPEED_SCALAR }; bool _isInWalkingState { false }; ThreadSafeValueCache _isInSittingState { false }; - int _sitStandStateCount { 0 }; - int _squatCount { 0 }; + float _sitStandStateTimer { 0.0f }; + float _squatTimer { 0.0f }; float _tippingPoint { _userHeight.get() }; // load avatar scripts once when rig is ready From 48a48883b0af734d8bf4bba46a5b2cf99b1e4ab5 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 18 Oct 2018 16:42:22 -0700 Subject: [PATCH 147/362] Add logging to the windows installer to track install options and install steps --- cmake/templates/CPackProperties.cmake.in | 1 + cmake/templates/NSIS.template.in | 33 +++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/cmake/templates/CPackProperties.cmake.in b/cmake/templates/CPackProperties.cmake.in index cb6474b010..73f52d72bf 100644 --- a/cmake/templates/CPackProperties.cmake.in +++ b/cmake/templates/CPackProperties.cmake.in @@ -54,3 +54,4 @@ set(CLIENT_COMPONENT_CONDITIONAL "@CLIENT_COMPONENT_CONDITIONAL@") set(INSTALLER_TYPE "@INSTALLER_TYPE@") set(APP_USER_MODEL_ID "@APP_USER_MODEL_ID@") set(BYPASS_SIGNING "@BYPASS_SIGNING@") +set(HF_CMAKE_DIR "@HF_CMAKE_DIR@") diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index fd48a792dc..4a9b67d843 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -28,6 +28,11 @@ !include "WinVer.nsh" +;-------------------------------- +;Include Installer Logging + !addincludedir "@HF_CMAKE_DIR@\installer" + !include "TextLog.nsh" + ;-------------------------------- ; Utilities and Functions ;-------------------------------- @@ -375,6 +380,10 @@ Var GAClientID !insertmacro CreateGUID $GAClientID !macroend +!macro LogStep Category Action Label Value + ${LogText} "Step: ${Category} ${Action} ${Label} ${Value}" +!macroend + !macro GoogleAnalytics Category Action Label Value ${If} "@GA_TRACKING_ID@" != "" Push $0 @@ -557,11 +566,13 @@ Var Express !macro MaybeSkipPage ; Check if Express is set, if so, abort the post install options page ${If} $Express == "1" + ${LogText} "Express Install: Skipping Post Install Options Page" Abort ${EndIf} !macroend !macro DownloadSlideshowImages + ${LogText} "Download Slideshow Images" InitPluginsDir Push $0 @@ -583,32 +594,40 @@ Var Express !macroend Function OnUserAbort + !insertmacro LogStep "Installer" "Abort" "User Abort" "" !insertmacro GoogleAnalytics "Installer" "Abort" "User Abort" "" FunctionEnd Function PageWelcomePre + !insertmacro LogStep "Installer" "Welcome" "" "" !insertmacro GoogleAnalytics "Installer" "Welcome" "" "" !insertmacro DownloadSlideshowImages FunctionEnd Function PageLicensePre + !insertmacro LogStep "Installer" "License" "" "" !insertmacro GoogleAnalytics "Installer" "License" "" "" FunctionEnd Function PageDirectoryPre !insertmacro MaybeSkipPage + !insertmacro LogStep "Installer" "Directory" "" "" !insertmacro GoogleAnalytics "Installer" "Directory" "" "" FunctionEnd Function PageStartMenuPre !insertmacro MaybeSkipPage + !insertmacro LogStep "Installer" "StartMenu" "" "" !insertmacro GoogleAnalytics "Installer" "StartMenu" "" "" FunctionEnd Function PageComponentsPre !insertmacro MaybeSkipPage + !insertmacro LogStep "Installer" "Components" "" "" !insertmacro GoogleAnalytics "Installer" "Components" "" "" FunctionEnd Function PageInstallFilesPre + !insertmacro LogStep "Installer" "Install" "" "" !insertmacro GoogleAnalytics "Installer" "Install" "" "" FunctionEnd !macro SetInstallOption Checkbox OptionName Default + ${LogText} "SetInstallOption ${OptionName} ${Default}" ; reads the value for the given install option to the registry ReadRegStr $0 HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" @@ -625,6 +644,7 @@ FunctionEnd !macroend Function InstallTypesPage + !insertmacro LogStep "Installer" "Install Types" "" "" !insertmacro GoogleAnalytics "Installer" "Install Types" "" "" !insertmacro MUI_HEADER_TEXT "Choose Installation Type" "Express or Custom Install" @@ -688,6 +708,7 @@ FunctionEnd Function StartInstallSlideshow ; create a slideshow file based on what files we have available + ${LogText} "Start Installs Slideshow" ; stash $0 and $1 Push $0 @@ -730,7 +751,11 @@ Function StartInstallSlideshow FunctionEnd Function PostInstallOptionsPage + + ${LogText} "Install Directory: $INSTDIR" + !insertmacro MaybeSkipPage + !insertmacro LogStep "Installer" "Post Install Options" "" "" !insertmacro GoogleAnalytics "Installer" "Post Install Options" "" "" !insertmacro MUI_HEADER_TEXT "Setup Options" "" @@ -1225,6 +1250,7 @@ Section "-Core installation" ; Handle whichever post install options were set Call HandlePostInstallOptions + !insertmacro LogStep "Installer" "Done" "" "" !insertmacro GoogleAnalytics "Installer" "Done" "" "" SectionEnd @@ -1232,7 +1258,6 @@ SectionEnd !macro PromptForRunningApplication applicationName displayName action prompter !define UniqueID ${__LINE__} - Prompt_${UniqueID}: ${nsProcess::FindProcess} ${applicationName} $R0 @@ -1478,6 +1503,11 @@ InstallDirRegKey HKLM "Software\@CPACK_PACKAGE_VENDOR@\@CPACK_PACKAGE_INSTALL_RE Function .onInit + Delete "$TEMP\hifi_install.log" + ${LogSetFileName} "$TEMP\hifi_install.log" + ${LogSetOn} + ${LogText} "In .onInit" + !ifdef INNER ; If INNER is defined, then we aren't supposed to do anything except write out ; the installer. This is better than processing a command line option as it means @@ -1495,6 +1525,7 @@ Function .onInit !insertmacro GoogleAnalytics "Installer" "Start" "$CampaignName" "" ; make sure none of the installed applications are still running + ${LogText} "Checking For Running Applications" !insertmacro CheckForRunningApplications "installed" "Installer" ${nsProcess::Unload} From 659302d20fe004759f08276f22074bf6b774fec1 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 18 Oct 2018 17:06:09 -0700 Subject: [PATCH 148/362] Include TextLog logging include file for NSIS --- cmake/installer/TextLog.nsh | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 cmake/installer/TextLog.nsh diff --git a/cmake/installer/TextLog.nsh b/cmake/installer/TextLog.nsh new file mode 100644 index 0000000000..a61ea1531e --- /dev/null +++ b/cmake/installer/TextLog.nsh @@ -0,0 +1,68 @@ +# TextLog.nsh v1.1 - 2005-12-26 +# Written by Mike Schinkel [http://www.mikeschinkel.com/blog/] + +Var /GLOBAL __TextLog_FileHandle +Var /GLOBAL __TextLog_FileName +Var /GLOBAL __TextLog_State + +!define LogMsg '!insertmacro LogMsgCall' +!macro LogMsgCall _text + Call LogSetOn + Push "${_text}" + Call LogText + Call LogSetOff +!macroend + + +!define LogText '!insertmacro LogTextCall' +!macro LogTextCall _text + Push "${_text}" + Call LogText +!macroend + +Function LogText + Exch $0 ; pABC -> 0ABC + FileWrite $__TextLog_FileHandle "$0$\r$\n" + Pop $0 ; 0ABC -> ABC +FunctionEnd + +!define LogSetFileName '!insertmacro LogSetFileNameCall' +!macro LogSetFileNameCall _filename + Push "${_filename}" + Call LogSetFileName +!macroend + +Function LogSetFileName + Exch $0 ; pABC -> 0ABC + StrCpy $__TextLog_FileName "$0" + StrCmp $__TextLog_State "open" +1 +3 + Call LogSetOff + Call LogSetOn + Pop $0 ; 0ABC -> ABC +FunctionEnd + +!define LogSetOn '!insertmacro LogSetOnCall' +!macro LogSetOnCall + Call LogSetOn +!macroend + +Function LogSetOn + StrCmp $__TextLog_FileName "" +1 AlreadySet + StrCpy $__TextLog_FileName "$INSTDIR\install.log" +AlreadySet: + StrCmp $__TextLog_State "open" +2 + FileOpen $__TextLog_FileHandle "$__TextLog_FileName" a + FileSeek $__TextLog_FileHandle 0 END + StrCpy $__TextLog_State "open" +FunctionEnd + +!define LogSetOff '!insertmacro LogSetOffCall' +!macro LogSetOffCall + Call LogSetOff +!macroend + +Function LogSetOff + StrCmp $__TextLog_State "open" +1 +2 + FileClose $__TextLog_FileHandle + StrCpy $__TextLog_State "" +FunctionEnd \ No newline at end of file From 12d092609b39ad125c57f7f038501ee1d4c77215 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 18 Oct 2018 17:20:26 -0700 Subject: [PATCH 149/362] Do not show login dialog if requested not to on the command line. --- interface/src/Application.cpp | 40 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1bd6af2eba..ba51ff9cec 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2330,23 +2330,29 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(&AndroidHelper::instance(), &AndroidHelper::enterForeground, this, &Application::enterForeground); AndroidHelper::instance().notifyLoadComplete(); #else - static int CHECK_LOGIN_TIMER = 3000; - QTimer* checkLoginTimer = new QTimer(this); - checkLoginTimer->setInterval(CHECK_LOGIN_TIMER); - checkLoginTimer->setSingleShot(true); - connect(checkLoginTimer, &QTimer::timeout, this, []() { - auto accountManager = DependencyManager::get(); - auto dialogsManager = DependencyManager::get(); - if (!accountManager->isLoggedIn()) { - Setting::Handle{"loginDialogPoppedUp", false}.set(true); - dialogsManager->showLoginDialog(); - QJsonObject loginData = {}; - loginData["action"] = "login dialog shown"; - UserActivityLogger::getInstance().logAction("encourageLoginDialog", loginData); - } - }); - Setting::Handle{"loginDialogPoppedUp", false}.set(false); - checkLoginTimer->start(); + // Do not show login dialog if requested not to on the command line + const QString HIFI_NO_LOGIN_COMMAND_LINE_KEY = "--no-login"; + int index = arguments().indexOf(HIFI_NO_LOGIN_COMMAND_LINE_KEY); + if (index == -1) { + // request not found + static int CHECK_LOGIN_TIMER = 3000; + QTimer* checkLoginTimer = new QTimer(this); + checkLoginTimer->setInterval(CHECK_LOGIN_TIMER); + checkLoginTimer->setSingleShot(true); + connect(checkLoginTimer, &QTimer::timeout, this, []() { + auto accountManager = DependencyManager::get(); + auto dialogsManager = DependencyManager::get(); + if (!accountManager->isLoggedIn()) { + Setting::Handle{ "loginDialogPoppedUp", false }.set(true); + dialogsManager->showLoginDialog(); + QJsonObject loginData = {}; + loginData["action"] = "login dialog shown"; + UserActivityLogger::getInstance().logAction("encourageLoginDialog", loginData); + } + }); + Setting::Handle{ "loginDialogPoppedUp", false }.set(false); + checkLoginTimer->start(); + } #endif } From a4adf2c71678853b4edd3737177585b3e1a5a9ab Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 18 Oct 2018 17:39:24 -0700 Subject: [PATCH 150/362] Fix name of Particles to ParticleEffect for default properties in edit.js --- scripts/system/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 96d3d763b0..71b4b2c54d 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -419,7 +419,7 @@ const DEFAULT_ENTITY_PROPERTIES = { sourceUrl: "https://highfidelity.com/", dpi: 30, }, - Particles: { + ParticleEffect: { lifespan: 1.5, maxParticles: 10, textures: "https://content.highfidelity.com/DomainContent/production/Particles/wispy-smoke.png", From 51d767ac2bd9989f67f18a53e029f6e6796985a2 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Thu, 18 Oct 2018 17:46:32 -0700 Subject: [PATCH 151/362] Reasonable defaults for position and rotation filtering for vive tracked joints. --- interface/resources/controllers/vive.json | 33 +++++++++++++++++------ 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 42be6f3f04..24b1587691 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -53,15 +53,32 @@ { "from": "Vive.LeftHand", "to": "Standard.LeftHand" }, { "from": "Vive.RightHand", "to": "Standard.RightHand" }, - - { "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot" }, - { "from": "Vive.RightFoot", "to" : "Standard.RightFoot" }, - { "from": "Vive.Hips", "to" : "Standard.Hips" }, - { "from": "Vive.Spine2", "to" : "Standard.Spine2" }, - { "from": "Vive.Head", "to" : "Standard.Head" }, - { "from": "Vive.RightArm", "to" : "Standard.RightArm" }, - { "from": "Vive.LeftArm", "to" : "Standard.LeftArm" }, + + { + "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot", + "filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.15}] + }, + { + "from": "Vive.RightFoot", "to" : "Standard.RightFoot", + "filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.15}] + }, + { + "from": "Vive.Hips", "to" : "Standard.Hips", + "filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.15}] + }, + { + "from": "Vive.Spine2", "to" : "Standard.Spine2", + "filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.15}] + }, + { + "from": "Vive.RightArm", "to" : "Standard.RightArm", + "filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.15}] + }, + { + "from": "Vive.LeftArm", "to" : "Standard.LeftArm", + "filters" : [{"type" : "exponentialSmoothing", "rotation" : 0.15, "translation": 0.15}] + }, { "from": "Vive.TrackedObject00", "to" : "Standard.TrackedObject00" }, { "from": "Vive.TrackedObject01", "to" : "Standard.TrackedObject01" }, From bb5c042f16bc0dd86785861af429db54925a572d Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Thu, 18 Oct 2018 17:51:20 -0700 Subject: [PATCH 152/362] Blend network animation --- .../resources/avatar/network-animation.json | 140 ++++++++++++++++++ interface/src/avatar/AvatarManager.cpp | 37 +---- interface/src/avatar/MyAvatar.cpp | 17 --- interface/src/avatar/MyAvatar.h | 2 - libraries/animation/src/AnimClip.h | 2 + libraries/animation/src/Rig.cpp | 117 ++++++++------- libraries/animation/src/Rig.h | 23 ++- .../src/avatars-renderer/Avatar.cpp | 24 +-- .../src/avatars-renderer/Avatar.h | 21 --- .../controllers/controllerModules/teleport.js | 2 - 10 files changed, 241 insertions(+), 144 deletions(-) create mode 100644 interface/resources/avatar/network-animation.json diff --git a/interface/resources/avatar/network-animation.json b/interface/resources/avatar/network-animation.json new file mode 100644 index 0000000000..0ba4ea465c --- /dev/null +++ b/interface/resources/avatar/network-animation.json @@ -0,0 +1,140 @@ +{ + "version": "1.1", + "root": { + "id": "userAnimStateMachine", + "type": "stateMachine", + "data": { + "currentState": "idleAnim", + "states": [ + { + "id": "idleAnim", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "postTransitAnim", "state": "postTransitAnim" }, + { "var": "preTransitAnim", "state": "preTransitAnim" } + ] + }, + { + "id": "preTransitAnim", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "idleAnim", "state": "idleAnim" }, + { "var": "transitAnim", "state": "transitAnim" } + ] + }, + { + "id": "transitAnim", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "preTransitAnim", "state": "preTransitAnim" }, + { "var": "postTransitAnim", "state": "postTransitAnim" } + ] + }, + { + "id": "postTransitAnim", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "transitAnim", "state": "transitAnim" }, + { "var": "idleAnim", "state": "idleAnim" } + ] + }, + { + "id": "userAnimA", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "idleAnim", "state": "idleAnim" }, + { "var": "userAnimB", "state": "userAnimB" } + ] + }, + { + "id": "userAnimB", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "idleAnim", "state": "idleAnim" }, + { "var": "userAnimA", "state": "userAnimA" } + ] + } + ] + }, + "children": [ + { + "id": "idleAnim", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "preTransitAnim", + "type": "clip", + "data": { + "url": "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx", + "startFrame": 0.0, + "endFrame": 10.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "transitAnim", + "type": "clip", + "data": { + "url": "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx", + "startFrame": 11.0, + "endFrame": 11.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "postTransitAnim", + "type": "clip", + "data": { + "url": "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx", + "startFrame": 22.0, + "endFrame": 49.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "userAnimA", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "userAnimB", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } +} diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index c3f6579e90..dd56c03d26 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -83,18 +83,10 @@ AvatarManager::AvatarManager(QObject* parent) : const int AVATAR_TRANSIT_FRAME_COUNT = 11; // Based on testing const float AVATAR_TRANSIT_FRAMES_PER_METER = 0.5f; // Based on testing - const QString START_ANIMATION_URL = "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx"; - const QString MIDDLE_ANIMATION_URL = "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx"; - const QString END_ANIMATION_URL = "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx"; - _transitConfig._totalFrames = AVATAR_TRANSIT_FRAME_COUNT; _transitConfig._triggerDistance = AVATAR_TRANSIT_TRIGGER_DISTANCE; _transitConfig._framesPerMeter = AVATAR_TRANSIT_FRAMES_PER_METER; _transitConfig._isDistanceBased = true; - - _transitConfig._startTransitAnimation = AvatarTransit::TransitAnimation(START_ANIMATION_URL, 0, 10); - _transitConfig._middleTransitAnimation = AvatarTransit::TransitAnimation(MIDDLE_ANIMATION_URL, 15, 0); - _transitConfig._endTransitAnimation = AvatarTransit::TransitAnimation(END_ANIMATION_URL, 22, 27); } AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { @@ -143,30 +135,22 @@ void AvatarManager::setSpace(workload::SpacePointer& space ) { } void AvatarManager::handleTransitAnimations(AvatarTransit::Status status) { - auto startAnimation = _transitConfig._startTransitAnimation; - auto middleAnimation = _transitConfig._middleTransitAnimation; - auto endAnimation = _transitConfig._endTransitAnimation; - - const float REFERENCE_FPS = 30.0f; switch (status) { case AvatarTransit::Status::STARTED: qDebug() << "START_FRAME"; - _myAvatar->overrideNetworkAnimation(startAnimation._animationUrl, REFERENCE_FPS, false, startAnimation._firstFrame, - startAnimation._firstFrame + startAnimation._frameCount); + _myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("preTransitAnim"); break; case AvatarTransit::Status::START_TRANSIT: qDebug() << "START_TRANSIT"; - _myAvatar->overrideNetworkAnimation(middleAnimation._animationUrl, REFERENCE_FPS, false, middleAnimation._firstFrame, - middleAnimation._firstFrame + middleAnimation._frameCount); + _myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("transitAnim"); break; case AvatarTransit::Status::END_TRANSIT: qDebug() << "END_TRANSIT"; - _myAvatar->overrideNetworkAnimation(endAnimation._animationUrl, REFERENCE_FPS, false, endAnimation._firstFrame, - endAnimation._firstFrame + endAnimation._frameCount); + _myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("postTransitAnim"); break; case AvatarTransit::Status::ENDED: qDebug() << "END_FRAME"; - _myAvatar->restoreNetworkAnimation(); + _myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("idleAnim"); break; case AvatarTransit::Status::PRE_TRANSIT: break; @@ -188,9 +172,6 @@ void AvatarManager::updateMyAvatar(float deltaTime) { AvatarTransit::Status status = _myAvatar->updateTransit(deltaTime, _myAvatar->getNextPosition(), _transitConfig); handleTransitAnimations(status); - bool sendFirstTransitPacket = (status == AvatarTransit::Status::STARTED); - bool sendCurrentTransitPacket = (status != AvatarTransit::Status::IDLE && (status != AvatarTransit::Status::POST_TRANSIT || status != AvatarTransit::Status::ENDED)); - _myAvatar->update(deltaTime); render::Transaction transaction; _myAvatar->updateRenderItem(transaction); @@ -199,19 +180,13 @@ void AvatarManager::updateMyAvatar(float deltaTime) { quint64 now = usecTimestampNow(); quint64 dt = now - _lastSendAvatarDataTime; - if (sendFirstTransitPacket || (dt > MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS && !_myAvatarDataPacketsPaused)) { + if (dt > MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS && !_myAvatarDataPacketsPaused) { // send head/hand data to the avatar mixer and voxel server - PerformanceTimer perfTimer("send"); - if (sendFirstTransitPacket) { - _myAvatar->overrideNextPacketPositionData(_myAvatar->getTransit()->getEndPosition()); - } else if (sendCurrentTransitPacket) { - _myAvatar->overrideNextPacketPositionData(_myAvatar->getTransit()->getCurrentPosition()); - } + PerformanceTimer perfTimer("send"); _myAvatar->sendAvatarDataPacket(); _lastSendAvatarDataTime = now; _myAvatarSendRate.increment(); } - } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 152215e717..b347963cf1 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1124,15 +1124,6 @@ void MyAvatar::overrideAnimation(const QString& url, float fps, bool loop, float _skeletonModel->getRig().overrideAnimation(url, fps, loop, firstFrame, lastFrame); } -void MyAvatar::overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "overrideNetworkAnimation", Q_ARG(const QString&, url), Q_ARG(float, fps), - Q_ARG(bool, loop), Q_ARG(float, firstFrame), Q_ARG(float, lastFrame)); - return; - } - _skeletonModel->getRig().overrideNetworkAnimation(url, fps, loop, firstFrame, lastFrame); -} - void MyAvatar::restoreAnimation() { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "restoreAnimation"); @@ -1141,14 +1132,6 @@ void MyAvatar::restoreAnimation() { _skeletonModel->getRig().restoreAnimation(); } -void MyAvatar::restoreNetworkAnimation() { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "restoreNetworkAnimation"); - return; - } - _skeletonModel->getRig().restoreNetworkAnimation(); -} - QStringList MyAvatar::getAnimationRoles() { if (QThread::currentThread() != thread()) { QStringList result; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 9770a5bb1a..16b765711a 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -376,7 +376,6 @@ public: * }, 3000); */ Q_INVOKABLE void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); - Q_INVOKABLE void overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); /**jsdoc * The avatar animation system includes a set of default animations along with rules for how those animations are blended together with @@ -393,7 +392,6 @@ public: * }, 3000); */ Q_INVOKABLE void restoreAnimation(); - Q_INVOKABLE void restoreNetworkAnimation(); /**jsdoc * Each avatar has an avatar-animation.json file that defines which animations are used and how they are blended together with procedural data diff --git a/libraries/animation/src/AnimClip.h b/libraries/animation/src/AnimClip.h index eba361fd4c..92f4c01dcc 100644 --- a/libraries/animation/src/AnimClip.h +++ b/libraries/animation/src/AnimClip.h @@ -51,6 +51,8 @@ public: bool getMirrorFlag() const { return _mirrorFlag; } void setMirrorFlag(bool mirrorFlag) { _mirrorFlag = mirrorFlag; } + float getFrame() const { return _frame; } + void loadURL(const QString& url); protected: diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index b9e654964a..df7e322da7 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -31,6 +31,7 @@ #include "AnimSkeleton.h" #include "AnimUtil.h" #include "IKTarget.h" +#include "PathUtils.h" static int nextRigId = 1; @@ -133,41 +134,28 @@ void Rig::overrideAnimation(const QString& url, float fps, bool loop, float firs _animVars.set("userAnimB", clipNodeEnum == UserAnimState::B); } -void Rig::overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { - UserAnimState::ClipNodeEnum clipNodeEnum; - if (_networkAnimState.clipNodeEnum == UserAnimState::None || _networkAnimState.clipNodeEnum == UserAnimState::B) { - clipNodeEnum = UserAnimState::A; +void Rig::triggerNetworkAnimation(const QString& animName) { + _networkVars.set("idleAnim", false); + _networkVars.set("preTransitAnim", false); + _networkVars.set("transitAnim", false); + _networkVars.set("postTransitAnim", false); + _sendNetworkNode = true; + + if (animName == "idleAnim") { + _networkVars.set("idleAnim", true); + _networkAnimState.clipNodeEnum = NetworkAnimState::Idle; + _sendNetworkNode = false; + } else if (animName == "preTransitAnim") { + _networkVars.set("preTransitAnim", true); + _networkAnimState.clipNodeEnum = NetworkAnimState::PreTransit; + } else if (animName == "transitAnim") { + _networkVars.set("transitAnim", true); + _networkAnimState.clipNodeEnum = NetworkAnimState::Transit; + } else if (animName == "postTransitAnim") { + _networkVars.set("postTransitAnim", true); + _networkAnimState.clipNodeEnum = NetworkAnimState::PostTransit; } - else { - clipNodeEnum = UserAnimState::B; - } - if (_networkNode) { - // find an unused AnimClip clipNode - _sendNetworkNode = true; - std::shared_ptr clip; - if (clipNodeEnum == UserAnimState::A) { - clip = std::dynamic_pointer_cast(_networkNode->findByName("userAnimA")); - } - else { - clip = std::dynamic_pointer_cast(_networkNode->findByName("userAnimB")); - } - if (clip) { - // set parameters - clip->setLoopFlag(loop); - clip->setStartFrame(firstFrame); - clip->setEndFrame(lastFrame); - const float REFERENCE_FRAMES_PER_SECOND = 30.0f; - float timeScale = fps / REFERENCE_FRAMES_PER_SECOND; - clip->setTimeScale(timeScale); - clip->loadURL(url); - } - } - // store current user anim state. - _networkAnimState = { clipNodeEnum, url, fps, loop, firstFrame, lastFrame }; - // notify the userAnimStateMachine the desired state. - _networkVars.set("userAnimNone", false); - _networkVars.set("userAnimA", clipNodeEnum == UserAnimState::A); - _networkVars.set("userAnimB", clipNodeEnum == UserAnimState::B); + } void Rig::restoreAnimation() { @@ -182,13 +170,12 @@ void Rig::restoreAnimation() { } void Rig::restoreNetworkAnimation() { - _sendNetworkNode = false; - if (_networkAnimState.clipNodeEnum != UserAnimState::None) { - _networkAnimState.clipNodeEnum = UserAnimState::None; - // notify the userAnimStateMachine the desired state. - _networkVars.set("userAnimNone", true); - _networkVars.set("userAnimA", false); - _networkVars.set("userAnimB", false); + if (_networkAnimState.clipNodeEnum != NetworkAnimState::Idle) { + _networkAnimState.clipNodeEnum = NetworkAnimState::Idle; + _networkVars.set("idleAnim", true); + _networkVars.set("preTransitAnim", false); + _networkVars.set("transitAnim", false); + _networkVars.set("postTransitAnim", false); } } @@ -1137,6 +1124,24 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons _internalPoseSet._relativePoses = _animNode->evaluate(_animVars, context, deltaTime, triggersOut); if (_networkNode) { _networkPoseSet._relativePoses = _networkNode->evaluate(_networkVars, context, deltaTime, networkTriggersOut); + float alpha = 1.0f; + std::shared_ptr clip; + if (_networkAnimState.clipNodeEnum == NetworkAnimState::PreTransit) { + clip = std::dynamic_pointer_cast(_networkNode->findByName("preTransitAnim")); + if (clip) { + alpha = (clip->getFrame() - clip->getStartFrame()) / 6.0f; + } + } else if (_networkAnimState.clipNodeEnum == NetworkAnimState::PostTransit) { + clip = std::dynamic_pointer_cast(_networkNode->findByName("postTransitAnim")); + if (clip) { + alpha = (clip->getEndFrame() - clip->getFrame()) / 6.0f; + } + } + if (_sendNetworkNode) { + for (int i = 0; i < _networkPoseSet._relativePoses.size(); i++) { + _networkPoseSet._relativePoses[i].blend(_internalPoseSet._relativePoses[i], (alpha > 1.0f ? 1.0f : alpha)); + } + } } if ((int)_internalPoseSet._relativePoses.size() != _animSkeleton->getNumJoints()) { // animations haven't fully loaded yet. @@ -1798,10 +1803,10 @@ void Rig::initAnimGraph(const QUrl& url) { // load the anim graph _animLoader.reset(new AnimNodeLoader(url)); - _networkLoader.reset(new AnimNodeLoader(url)); - + auto networkUrl = PathUtils::resourcesUrl("avatar/network-animation.json"); + _networkLoader.reset(new AnimNodeLoader(networkUrl)); std::weak_ptr weakSkeletonPtr = _animSkeleton; - connect(_animLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr](AnimNode::Pointer nodeIn) { + connect(_animLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr, url](AnimNode::Pointer nodeIn) { _animNode = nodeIn; // abort load if the previous skeleton was deleted. @@ -1828,10 +1833,10 @@ void Rig::initAnimGraph(const QUrl& url) { emit onLoadComplete(); }); connect(_animLoader.get(), &AnimNodeLoader::error, [url](int error, QString str) { - qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; + qCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; }); - connect(_networkLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr](AnimNode::Pointer nodeIn) { + connect(_networkLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr, networkUrl](AnimNode::Pointer nodeIn) { _networkNode = nodeIn; // abort load if the previous skeleton was deleted. auto sharedSkeletonPtr = weakSkeletonPtr.lock(); @@ -1839,16 +1844,22 @@ void Rig::initAnimGraph(const QUrl& url) { return; } _networkNode->setSkeleton(sharedSkeletonPtr); - if (_networkAnimState.clipNodeEnum != UserAnimState::None) { + if (_networkAnimState.clipNodeEnum != NetworkAnimState::Idle) { // restore the user animation we had before reset. - UserAnimState origState = _networkAnimState; - _networkAnimState = { UserAnimState::None, "", 30.0f, false, 0.0f, 0.0f }; - overrideNetworkAnimation(origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame); + NetworkAnimState origState = _networkAnimState; + _networkAnimState = { NetworkAnimState::Idle, "", 30.0f, false, 0.0f, 0.0f }; + if (_networkAnimState.clipNodeEnum == NetworkAnimState::PreTransit) { + triggerNetworkAnimation("preTransitAnim"); + } else if (_networkAnimState.clipNodeEnum == NetworkAnimState::Transit) { + triggerNetworkAnimation("transitAnim"); + } else if (_networkAnimState.clipNodeEnum == NetworkAnimState::PostTransit) { + triggerNetworkAnimation("postTransitAnim"); + } } - // emit onLoadComplete(); + }); - connect(_networkLoader.get(), &AnimNodeLoader::error, [url](int error, QString str) { - qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; + connect(_networkLoader.get(), &AnimNodeLoader::error, [networkUrl](int error, QString str) { + qCritical(animation) << "Error loading" << networkUrl.toDisplayString() << "code = " << error << "str =" << str; }); } } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 37d1ec1dd3..af786a5e70 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -113,7 +113,7 @@ public: void destroyAnimGraph(); void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); - void overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); + void triggerNetworkAnimation(const QString& animName); void restoreAnimation(); void restoreNetworkAnimation(); @@ -323,6 +323,25 @@ protected: RigRole _state { RigRole::Idle }; RigRole _desiredState { RigRole::Idle }; float _desiredStateAge { 0.0f }; + + struct NetworkAnimState { + enum ClipNodeEnum { + Idle = 0, + PreTransit, + Transit, + PostTransit + }; + NetworkAnimState() : clipNodeEnum(NetworkAnimState::Idle) {} + NetworkAnimState(ClipNodeEnum clipNodeEnumIn, const QString& urlIn, float fpsIn, bool loopIn, float firstFrameIn, float lastFrameIn) : + clipNodeEnum(clipNodeEnumIn), url(urlIn), fps(fpsIn), loop(loopIn), firstFrame(firstFrameIn), lastFrame(lastFrameIn) {} + + ClipNodeEnum clipNodeEnum; + QString url; + float fps; + bool loop; + float firstFrame; + float lastFrame; + }; struct UserAnimState { enum ClipNodeEnum { @@ -357,7 +376,7 @@ protected: }; UserAnimState _userAnimState; - UserAnimState _networkAnimState; + NetworkAnimState _networkAnimState; std::map _roleAnimStates; float _leftHandOverlayAlpha { 0.0f }; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index b43e1c23f6..705ebc0b13 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -114,12 +114,10 @@ void Avatar::setShowNamesAboveHeads(bool show) { } AvatarTransit::Status AvatarTransit::update(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config) { - bool checkDistance = (!_isActive || (_status == Status::POST_TRANSIT || _status == Status::ENDED)); float oneFrameDistance = _isActive ? glm::length(avatarPosition - _endPosition) : glm::length(avatarPosition - _lastPosition); - const float TRANSIT_TRIGGER_MAX_DISTANCE = 30.0f; float scaledMaxTransitDistance = TRANSIT_TRIGGER_MAX_DISTANCE * _scale; - if (oneFrameDistance > config._triggerDistance && checkDistance) { + if (oneFrameDistance > config._triggerDistance) { if (oneFrameDistance < scaledMaxTransitDistance) { start(deltaTime, _lastPosition, avatarPosition, config); } else { @@ -132,12 +130,9 @@ AvatarTransit::Status AvatarTransit::update(float deltaTime, const glm::vec3& av const float SETTLE_ABORT_DISTANCE = 0.1f; float scaledSettleAbortDistance = SETTLE_ABORT_DISTANCE * _scale; - bool abortPostTransit = (_status == Status::POST_TRANSIT && _purpose == Purpose::SIT) || - (_isActive && oneFrameDistance > scaledSettleAbortDistance && _status == Status::POST_TRANSIT); - if (abortPostTransit) { + if (_isActive && oneFrameDistance > scaledSettleAbortDistance && _status == Status::POST_TRANSIT) { reset(); _status = Status::ENDED; - _purpose = Purpose::UNDEFINED; } return _status; } @@ -154,10 +149,13 @@ void AvatarTransit::start(float deltaTime, const glm::vec3& startPosition, const _transitLine = endPosition - startPosition; _totalDistance = glm::length(_transitLine); _easeType = config._easeType; - const float REFERENCE_FRAMES_PER_SECOND = 30.0f; - _preTransitTime = (float)config._startTransitAnimation._frameCount / REFERENCE_FRAMES_PER_SECOND; - _postTransitTime = (float)config._endTransitAnimation._frameCount / REFERENCE_FRAMES_PER_SECOND; + const float REFERENCE_FRAMES_PER_SECOND = 30.0f; + const float PRE_TRANSIT_FRAME_COUNT = 10.0f; + const float POST_TRANSIT_FRAME_COUNT = 27.0f; + + _preTransitTime = PRE_TRANSIT_FRAME_COUNT / REFERENCE_FRAMES_PER_SECOND; + _postTransitTime = POST_TRANSIT_FRAME_COUNT / REFERENCE_FRAMES_PER_SECOND; int transitFrames = (!config._isDistanceBased) ? config._totalFrames : config._framesPerMeter * _totalDistance; _transitTime = (float)transitFrames / REFERENCE_FRAMES_PER_SECOND; _totalTime = _transitTime + _preTransitTime + _postTransitTime; @@ -206,7 +204,6 @@ AvatarTransit::Status AvatarTransit::updatePosition(float deltaTime) { _currentPosition = _endPosition; if (nextTime >= _totalTime) { _isActive = false; - _purpose = Purpose::UNDEFINED; status = Status::ENDED; } else if (_currentTime < _totalTime - _postTransitTime) { status = Status::END_TRANSIT; @@ -2007,11 +2004,6 @@ void Avatar::setTransitScale(float scale) { _transit.setScale(scale); } -void Avatar::setTransitPurpose(int purpose) { - std::lock_guard lock(_transitLock); - _transit.setPurpose(static_cast(purpose)); -} - void Avatar::overrideNextPacketPositionData(const glm::vec3& position) { std::lock_guard lock(_transitLock); _overrideGlobalPosition = true; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index d375909609..2995c0f7b4 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -71,21 +71,6 @@ public: EASE_IN_OUT }; - enum Purpose { - UNDEFINED = 0, - TELEPORT, - SIT - }; - - struct TransitAnimation { - TransitAnimation(){}; - TransitAnimation(const QString& animationUrl, int firstFrame, int frameCount) : - _firstFrame(firstFrame), _frameCount(frameCount), _animationUrl(animationUrl){}; - int _firstFrame; - int _frameCount; - QString _animationUrl; - }; - struct TransitConfig { TransitConfig() {}; int _totalFrames { 0 }; @@ -93,9 +78,6 @@ public: bool _isDistanceBased { false }; float _triggerDistance { 0 }; EaseType _easeType { EaseType::EASE_OUT }; - TransitAnimation _startTransitAnimation; - TransitAnimation _middleTransitAnimation; - TransitAnimation _endTransitAnimation; }; AvatarTransit() {}; @@ -105,7 +87,6 @@ public: glm::vec3 getCurrentPosition() { return _currentPosition; } glm::vec3 getEndPosition() { return _endPosition; } void setScale(float scale) { _scale = scale; } - void setPurpose(const Purpose& purpose) { _purpose = purpose; } void reset(); private: @@ -130,7 +111,6 @@ private: EaseType _easeType { EaseType::EASE_OUT }; Status _status { Status::IDLE }; float _scale { 1.0f }; - Purpose _purpose { Purpose::UNDEFINED }; }; class Avatar : public AvatarData, public scriptable::ModelProvider { @@ -457,7 +437,6 @@ public: AvatarTransit::Status updateTransit(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config); void setTransitScale(float scale); - Q_INVOKABLE void setTransitPurpose(int purpose); void overrideNextPacketPositionData(const glm::vec3& position); diff --git a/scripts/system/controllers/controllerModules/teleport.js b/scripts/system/controllers/controllerModules/teleport.js index d31f207b19..bf5022cdaf 100644 --- a/scripts/system/controllers/controllerModules/teleport.js +++ b/scripts/system/controllers/controllerModules/teleport.js @@ -781,13 +781,11 @@ Script.include("/~/system/libraries/controllers.js"); if (target === TARGET.NONE || target === TARGET.INVALID) { // Do nothing } else if (target === TARGET.SEAT) { - MyAvatar.setTransitPurpose(2); 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.setTransitPurpose(1); MyAvatar.goToLocation(result.intersection, true, HMD.orientation, false, shouldLandSafe); HMD.centerUI(); MyAvatar.centerBody(); From e499309c8e38265d0d76237984f8c3ad77dd99ba Mon Sep 17 00:00:00 2001 From: Flame Soulis Date: Fri, 19 Oct 2018 06:49:48 -0400 Subject: [PATCH 153/362] Upload interstitial page sound and update link --- .../assets/sounds/crystals_and_voices.mp3 | Bin 0 -> 254192 bytes scripts/system/interstitialPage.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 scripts/system/assets/sounds/crystals_and_voices.mp3 diff --git a/scripts/system/assets/sounds/crystals_and_voices.mp3 b/scripts/system/assets/sounds/crystals_and_voices.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ee9073b0156e2a341fe75e22646f1a52b2a2adae GIT binary patch literal 254192 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 z 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 Date: Fri, 19 Oct 2018 08:16:14 -0400 Subject: [PATCH 154/362] Made MP3 constistant 128kbps and fixed path resolving --- .../assets/sounds/crystals_and_voices.mp3 | Bin 254192 -> 334366 bytes scripts/system/interstitialPage.js | 64 +++++++++--------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/scripts/system/assets/sounds/crystals_and_voices.mp3 b/scripts/system/assets/sounds/crystals_and_voices.mp3 index ee9073b0156e2a341fe75e22646f1a52b2a2adae..1dd2037e6bfce5d4db21e733dd8a31b4d5dae84b 100644 GIT binary patch literal 334366 zcmdp+Ra8}P)b%%q?nb&h58d6}NJ@8?q;lw%Zcw_rL!=u7=@0?wmQ+F5@8Cb)_xij4 zp4)x1#`ryRK5Narz{g|+;Qw)y-R#@}0Kf(Scq#yqloEg-BBNno;}X0gA*ZCFV`O3D zg!1wWiHJ+d$SbO-Y3k^`HZpxI4|?B?O^>mT$E78V&Dmynd2o|TjTv8c4Xs;0iN zrLD84w|{VCY;tCHesN{(`;VQygX7bSUpM!E9-jaJ5LS~`m*?da7WRUx{=W(dVDv~z z3IG6msiV-+neP8S{C|7E7eFYXH^|b;XV7H+{upHv76A1)4@V8EG@&LyGg6qz0ssz( z8Kl!Y4(PwK<&mspvx(V<0e6}z(nhdqEBfXl{WN6i+COB`%k1AFwJiAX!PvC$8$gX6 zeM1Y7Tw4I}qCWkwWC24Q=qmwKA!uRjWPxr5c2@1P_x=`FCHHUgZy>gRuqFtOhve^x zCnc?^onpovoTX`?8Z2i1~X)|{=k+k=k`wGZIC z)h9>rzGC5dV)p5>1_Ym{&cE-sF(23o#H4j6wWm(dXTo=};B-lJDiuY_R>=%?s&1|1 z3GSo+?j~F2|KmsJnUDrb_>@j{pY(yZE8tU&Tf&lNMV`it{T&G2HuUnhjVBlv&@-0P z@#{@dv!GCtQftQnv&ONIF9@}>h2<`(^goflt~WdCKNFl z$kYfo)Ud711s_upcu^qVoQGQsaef+{J4CrdSk-76aO(DUWBE}_?y_PU$46q~rnL~< zKFBSvZ!PRPADN;(+zM5Z@uyZZj}+asD0>^G&pN$~u*;`?*oDF}kkHG$tg zmYmtz`yxnLMGB9S3@mH*|1}`t6^FaQNJ&l%h2$_ne}6!;C$^f?>M_{n*AIBk!RE2F zmtEhzF4P+HWx9KqdA}uC-_r-Ke?0&3#J=boczXQQ?FWM3(-Y>~lhL#41M-s!9xTf6 zC^U6Sp|}_T*h^V+Ng41mrfEI5;JL0rbZ~t&upNb_6x3w!m~Dfp4{^Zn{AFA-C#Mx^ zIjYiWZv8l+uioM;d8*A_nV}0^kmUXC^yCEIY_-`s1?r({g%)QJ8u~gm zqCh|@Vrs63HTLNXX(KkenxoC$c$T(d*HWhgP#n{YJPwI(>@>Fz4Bw=+;vzHQEM^R+l1 zffIi@3%dQD-Usxpu~j^=Y?-@*;9bq*(@CjuRds*V(wHFb0xJsWI2o){(e9 z0nG=?BN`u$V;Dj;A)2n%&kt|$jcra0zh7f(c76Y`7%CTI7r+%ulf_bdu|rwR<(-a? z9%&qtSPpv*79luQc6GEG@!=i_& zp{D(&QUDIwt&${H)9ccwz?l>FY3Es}Ml=4f6Osk5snf}WvZyAp6W;$`*y@^gSowZ%}-lTGkQl! zM6MVrd?Db2uf*uj;)=0eiEOZMXfP~=k=b%Z=csDJEPq8@ z(;;^ee4_8jg0X&wmN|32m;9>x{xvc~V8dbXX0l8O0&!1Ch6ii?nbx zN0as1)0UMvJVOCS29o8jr56p$YcfE=r0})e#?khjl+cBZ0#9+*d*8% zy7yXYHeM^nKQ|gdd8#~9``X~zy!E(cr+y6PyuPcreg5Qr3?8riTkmaVqaW%{(Rah- z#K=hixV(xmxfhTYrvoWPjZ3Ga7WBgt5LDDWEiC64_NIgMt-q8MD^6S}O(7#4(M`Qt zCod0uA*2~IWau>9;!Gf8Ez4Z)%_xLn8cm|UV=L{I_;X^qu=4bP;#SpHV_$Z_V`^A_ zP{~Syi=(o=W1L2nt@#NJ{2jQYao2fNtOmb>U)TZ*2OLaYQ<;J*lO2h6#(vg^l@Mm@ z6@T-A4B&>j{xbWkg-uf#{5gz=vW={xom`$;OQPl0xhC!H=I>9`cFui2YSI@v(0a$_ zTwS$|AO{Gu?6K8Omrq*{;swzKjK68ZVc+%?^sh^Y?-{+Iv+A)X2b9qguqkL#2nmnKgV@!%<_ z-*g@{ReBWsOAbQ{km7vaP9BZG)xt1j;e%G5tquUxzEea9NSMs4Z2gd8fS`fC&Pm-7G71hQULv(Td^4&DU)sQp zmGpr3&eAp>jQ=AKE!N$(ebHVM4k7N@B&O7=yV-bvejp!hZ1z`8w(7_tx9as@O+#?< zbx|DF(~>&T<*u%}YBI>d+qVM$%QyxCysnyxgqcTsYHPa$-1j={m9H6 zRhl5jCbnL8)V!q#f>+*)(#u#zmNFwWxzSX5a4iKi^I+It>Fb{zCZG}0dtCU zFNCCG!Yv)-Rb9k*YcMjp+VeD2-msXz14PhXqAVdbk)r2Cg^O<@e zR&9szs6v&s=c?j}3}Dl@x#X0t!o4i}8H{JovkNw-f%z)XA3~<|tD|LvDryWLXCEbg z^no5mNXktP=;R3m?Q68djht2vlCXWN0hH9a&`50c*-R20i+^!0>8HMPnVCbnMjBfU( zFNAnu;c(=mqB$1Z)^ypU-pZ`#4mm7!;>bI#Z9mIq$aeq~;ndH=&>_S2ud8VS%(NB> zdN#wDZkjV5z@PU!0Ksq@|1_bvis;e7x=`t+!aJL%F%F0k6@hTPFSL7>0<{Qzl>tS- z_1Ueysg=PT1c@e4`9Hu#5P61q*(L)3B+abUWwpYNk!Ob>Dv({K|Jh~0h5RaD@n{;? zfiw-vt7r7^x^L?PT{N2mTA(kyPa0)ilTbi8>f zo|EOoN;5PbsQ-a|(`R%Yk_s;k7i&)i^!E?sAdVuG%4JJOpoLcHG%gFt4AQnJ$!Yv+ z7jzPzgtf2`XOd*n;nJ-Eu^e{!o+8A097#Do-tX-zj^0%+y-zH^6Sec52D^>!A6^Kt zCxk0Ijpq4l&zaF>FL?(GzTAe*>rxhVW#f7!ohLj zrZq)DkX+k|104{MAea?=@eN4qB@KPVa_DL}B!M*Fj6B`a9vnjQgF<*YDHpPkFKM~Du@=M!86%`Gquy#1CbYTw9h}=`hD$v`0H>rf-1Y@6Ugc=I7?r2njMVXb?;Xgat%=c#gIwe(1VZ&o(Zg zzMphDIPvVMd3f=zC+GGbnLe%s9~92qN|9K(t5IE~IJNPz3JOc9sdLCp z=^%-6qzm^5d~^4KB5YzOlVS;oJckpxUkYMACkJ1|3Tcxgf1qEWGZP*uEjM~0M3oRu zMLv?2<2Ywq5;Nq@B7~{rN2K9sFP)A{yEa{VRK>4hQboaea%mDb_s3H2E0cl4BQ=2} z2;MuIsC0H`S%y;kOVg;AMx4VjAC1xBYad~VgPOU!$HiND~)DE zA{a=cLv=clg_*o6c7LH6EE>4bhpB8Fp>+$RY^8LlS)(oQ#V|uIbb3Q~T35RXf~Tg8 z*Ow@0XuVvNjKr{DH!~WVa$GzZdJC(+8m0m-;P0siGZs~#JW$b|Xr;BDhbK-$F!@UE z3VP&C;S=xce|1iafoN%W4NOSdV;^F!{7%rs+2e{YTa?H0I_2yQ<8`JCJC z>3*2lGbQS#VS zDzIw7P}I6;1X?kSCu8yGT|e=o0^>u^1+T5Zps zk~ob{4-O2BR%%?H?m_VWk@YST;S>e{KBXzqgjoPm606$KCzV2}%V8>p?Jsky4EW;c zdK{U)JhDBcSkrV3OB$62ntaQR{+K)ck{=YHM68>t##@3c(_2bC#KjUKuI@&u6RKM# z;@h9~>6DxHLLNfE^(|invje;wuXzE2;!MJ(3TOvud9anvbTn<`91E)*0kzbS~<$809VPOlZ&Fdx#3Q z_x^hG`$P+-(0j>Jv-Gi4*5KWujiHoT^arQAo21u9Z^4HU@IoMTm>QrC|IoQE)j<=Q znmhz^B?(X~ao!11(8hYT*=(MHgxnI104pQiYZy<_w5KDDNRO)@&HVC7+GM?!Hg%BP z@+~#xVXbLzU6k_Yd5Co8n?Z^s{aK52IVo`h@)wsJ`}_umrrcxNE`Yb~5Y>cA>8`Mr zJFT-L#<-H4`p43sp$DV(>^*|9Ya&Gqw^25rPI9uA?xQEe2fwI(ep6FPZDrEFm3E{a zccLU+iARB%!d^oTeR%P$_LF`FPC>fTqRO}YN0E3$e7uqr!{2I^G`KQen>Drm zX(S~unYvuMaV_}hInHF(@q5#)@&uQE&iq?f&;oKas;pHkK1=53x+qc)otm%Y+7Zyz z#=r5qA3{LTtl;-jKY)&M*GNobq>_R}%jx4}J?|S~$Hym9mQ>pHA0ar%BZJaHfZ?`9 zc#g3pU-Xb+QndG}v*d zF#Q(~T$F;<7ol2yEd!TE4ujN|(%1BE9HOaAsdDAU@3a{4)jVG{Zt&%1l1JvEQt!+o zegC*CZ#7PjgrA$`+^!__EMt2?e2OahJShoYvb;3K&iM3SJ}C3P%2p&NfQ~dWLUvWs z2^?LTT#NUYS|jc+D?UoHz&gcyBA;qnbKxrH&g)L`a35qbC_NGf=Cp*_Z%SoJoL8+K z((!gzT54)up5Bc&027_xOkJ`-_r6vHg*q8qjv|YBD_Z#?KSO{ZwEObk{n^H!`!Aor z#HXx|*k^0|J*LLoLD%_q+y>LU4Z;{`U(N4jV$fJXV(7&T&jqW{$lD@e<0faWFD4UN zh;-s+ zxiD>;*(13M#G38cY0aa>`mXw`ho;RGPL7UX$<#uowjteBX8sk$?+MFkv0^EAP-mvd!L&m#iG;|E=; zmwxw@hl{0Dd;JWiYP7O@g=GjAZp*e6i)|6**B3$*FuY+}WnDD_A$_^3N!t>$ zI8$HAS`JojNo;%m^fos#)9#u$@{uo@t@|fTU4)6MZw1dv%;&4h- zwj*kVdOsm^Y>w%LYxEk|5FN%;Jk6k6^8)YaHFvqW(AvUtGYKj?!M#aTA%yjVr+W-3jU3A9PiKp_Ff(KE5y5v3`TaXe3pgJMUS=PCri^k z!d{a>UG@;faJjMfjFt?nB>p|4u{h{7AKo|^2jS?cvvIbM!$Vso0R{ZiU9EHN@;})h zYQ~vnaI?=D&v!u6?hwBiKEX2%b0$oOT4$3tAla@u9FF9TaNO7!v+GNG*ZCxRa_={NW=-JyfqQ5sThOevN<}5^wUWP)`OTevA&wbb9dh;;-1<~tSpOO;E~{t^?G?`Frp5K;jk4Win|yF}!=2-OspN1Ac@ z8M3wP?%XQ=G?|5 zsEFa>hOre2NwpJpW`}oC7{OCWH^C$-xvJD#LT2AlVq&kHDkr&ujE+M|5PTpkWFT`~ zajAp-Yb&FW-9=a=fjc$((1VYX4|Ddfe3ui+=h;R4jkY80DavaJ89nmwA1QW4a>^s0 zSF}t-@~_I^k38ELj5dLBBdLQ-b8igOSeD!%0K?nDe|0p|mg)*F#&}WrJfq1+vYcfS zziNxsCO%F-*?w?oykz$d)}=?;PV9L)z5Pib8k%rNibT%-`hX?YaaS|W zKZi{{eSt;~%aQNCUL(TwW6cJOTdC*FlhqZY&-Wmw$C3009LgwLLKe9w3a39QUKu`2 zIb`-|VX9xlCTX?92XCGF#1RM4q)(}ePb*PZZ`@AVLvv{Nm}qf0&>N3AN48Z+(L*PU zsE%rolnz|6*VgWM)(*Hz3-clq>*m*fW?2?U2wx(z+Dwqy6^Y4spAKmlAnE|_O7pRW|sgEH63Pig0l0L@%Ea%8qUIr53R??E4Ww{hRUdv9W2vrg>y zX>(~SggmeDbd#a7uO+3f?H%+e%;pC$hdOy`@m2C<6^)iSSIsi*8~)N8T!ghcxWaB` zggs`dMqN@lf_)P&p$fRtpZ?1L_wSPZ{B-4^4^@uIFNAzxkaR{JXACkmBl@)`%cJPY zWM4K7XjPFl(IJ0ykpyWrrIt;z)(WkPG{vVk2XVo(AoxTYF9pX8)AJpMx$8eR2RgVC z21>GbAP$Gwr*y7}?hbQ#{R>hR$SuE@0dshJ8*_O`Y7vmM$fc?U#kB!H?2R zzR^`iJ(S_cd*YgW_!E8~x=&vURTTfxtAbqO>2@(oFb zIa)g@Nah_1wF{z-LNNn6xWUIIx@;zdPrHhZUL47)uP9Mp+Srk*RUZ^owP>RTl{Qql zCUv}9UJ+SN_r^Jw1i`k~npjTf3@~bxdSQBvdDsh~XaLgAGNtQ6F7!H51XFBiAZ{^7 zwHB&;<&ts0l9eHn921eIWUV44$}B=``lZYhbrAt!ti|NxD~b$g?qtW^@YYXREk3ah1gnr}aN z?;Q?X<9;X7e?Lx&2(LzA-Qg?BoNrPLj)AtuBD%AaG3^v#pZh zz&9g~%I%U~=2%fAq=Ez)cK7W&W{Hu>Xy8~(|1LhhvqBarRDI^FLm#ZEJ1j7mQWUAY z47QwF!)7o_Ub3m*OZ_B-Ok_&L3zOd?mGfUw)0F`$)+!#Yj3@@%;?uET-?6u0T}MYsb01r)6Riae+H4{1>j_r@Es=aLA(fx1wB&6Wtr{wI5Y={Nqb zf@i(U+uwio>&!o0rX2c**-;5EOq#YFxF>Vx7eb|g%pkjlt{Q2unQefR?V*2M={MP0 zv9cyh!hM#CA`hJ@zs-NM%4;VlVZ|*2s#MJ$Z$2r$yzCEEg#bWJ$Daz1XTkE_vMgG| z;trPwPGM6~K<|iaExzPKGvsT%YHnI(Zi5O*MQzp_G*?Oex)o_DizC7VLt;ilheACa zIx#ecypY-Nb{Vd3USbe800Icp!*LUqe!(EbtyZ8ne4Fmg8#c0?gI(ZtHJLIvBgf|g zw}J8+*uJoYlcED9`eX`%AdOdSrJqWjaz@!q7ZYNfnRgY3L-DYzUhqOIQ>>)V;5jG~xlP7YC6`_KdnY#`;dk@VH{k7Wz8~S^hUE12D>~mK zb`caOxhnrkOxD%SB~TF(KpAm8nNEIyLS`WCpT~Pa5G+4P9VXgPAM$NRG7sl-LEi3Q zf&_=sAksE&(JWW1)Iu1=K9uVg`e~lnd^MHfd}b88K02_+9FQf-TB}|2b~vhimv5QC z9&lyDb%-*YSyZk@cI3$QqTFp<+CDu@M_RKR8S=$TuA?sB(I4$mgN>aF)J$q-6X;16 z!^w{$iAq`M()7|rS#N^AzqjFP`Z{X9^}xE)_Wj+^_x_OtBXdwU(V&^=?sJz%U*zra zr_((c0JxRXO}iej7$;)!Q%diV%G`ZW?(bf&ZSEkc9YI2 ziZFTO=v=)hmxUFk&_y20u*~*n{Dsgk0Qto}bsE6p|C%9AU7X1;?q3~0hd640CgFi? zM(lwG9wSr&z35}9&x)f-r=xI+nYp0f)}1--d1bne;qu4y*6jN#ZC?!EnQl??x`A#0 zz=6Cnq1I$WCf4$nrbHe^>@tJmj;`|}G>R_t_QmmJ<3 z*Bc*dU%>~*)G>wz6rEZq?PN3e-iijYu39y$fgtF21OotokUtsWktHUf_24ipI~0zF zS$heYlNumCh`o*sc;{&4ik=_OQTN$#M>pG%?}qRP z955`nfL5VR;Nq)Xh*aX(;+qn!SkaH&xJG^mC!>a=ws+(v*gwo285=2hhg9 zZoQJ&Lz!^Yt(7H$XOASUr?QfX^Yg;I)?QVhRDlZ=2-F_1k}>Vc!%*@p3PH zY_|ed00mYR;#O=TGyEgEO5#1eq93d&fwmj+qUt0pC%$_VqB1CY*Q=)AY@yME?YZBZ z&IPl4mpXzbH&Q8a#iagf`|vTpv#L>2L9`7WrLDt%0ljX%CU0{B1I+3QCv>3jnEa~g z=f|?-_)Fsv=BRd!R{q$K0F_y8If#rMUZ~37uN!%WJOyuk&dm^z%m#m$DN_;3#iK+k zq32}VkIpJg=4gCwul2ISk|%ApMNG+y!QoL-ctpEV`9sssft*95S5?um89@HM4Lg$BD{-83&t^7# zzNyb22QOV*FMv;>PUj-f)9Gbf1K2m_(c)T(SMR22YS_j9mCmYLv)?1Bl9@O78el_)HDBet*eih&YU?n7Ob6tNzSEGm>^a zoYgve##K9qfBcm1`ZasaETdj<(f{!19)%!%X6Y0vJrN z5@08^UrvvqKd~`ez4PeWDM`po++Q@ys^t%q`-7?ro;qf=U|nhEvZ8SfrP$kYigl%F zU!9hp&UyvD(%0c|FD7@8FbHx^^d!kjnMTE`xQ0ie=Z`VFv3m^oVbRZgravn5X}WtA zytL)UcKhNqsBMj?y4Qr-lDTy5QQXAR5<~d3xN!IoIie%aX*)6nHdO~4dXX7a-u}W^ z))`?m)g7D-BfR9BKKH>g?fidl5HM98hg=zU@;hkz%6P;&&MC5x8tZ{HsMxXSAbC@~;O6*%T+*h>|0nj@Qv33Z)mClEAxPvwg9E+)9DrMxLpcbm}sz z9+dJutoAzs((CE_pFFgMk)~s29Zq~}YA1A@kZ$o5cjwlSmwnPZy=48b{pQOyGEG7# z^!pzcX!2*WnIOm}(%X?9mHt`vn8|wjU8D#~^g_SAz1bMd*7aFBR1F%#qpal+X*I@~ z_EB-=>+9YU+cFV}Ea^QTUo$AtGJ0>nu8TSlY*ObqdZ8u4ztm5y)sRezk}MKKo+b?= zgj0~qF(iXC-1}?r#*@Xbee7?E!qIuhr!BL0nQJ|5EVaw!0oAmZoqzn-w^4`h2+39S zpbsmOJjYBggiZii9A*{W93v0Q17Q)@U4IO|DUQ0v#+}q0`oysehv$X|eM3q)QsT6S zi1vN}Aj@clXOaDhlE&o+{R_{C&FOFGx#@8I3R8$dFCjCnQ1?V@-g51gvi7(TSH?ck zRCvyM+$sA5lD*Fr!gGeyj7a)!d#oVyF@ZeXe%Owm!Sm_L3BDF9J^j{P@!oQ@7ke5C2Y3->1qHWkQ z5~6&n5AiYoF6ARm6H|dY>R-E4PO{VDKG)tHns85J$v9RNqknj18oC2M&{4mz^`TAo z%y(531NduF!O08C&rd<(P7UB~pkwGF1W;}*5>e`wUgeE(pF-jSat1;fQ2Swmb&SUQ zjEM7@YPF&Q9oSm(Mtl2F)>73N)p$Ueg>uf+$-s_0p}rIgNuVpVlcGi>v^*h92@ZoW zU|{mUpAWk*nI`D;l)Fr@!QO%~lmT!;6BH z?U^#BIQbKav$D2q^VY6L{8i1zuf;p~4M8u$nbTw_P28qqhI6(-qiW9llVzTu04*3%2I}t z=*e52;y>nC%jziB>K3$~bjfK+@!uO>@=J|!6B#H10tCFrqR!lrh4+BcoAAW9=wp(Q zsN5F#k%`Q7n09$DIGyh|Au%H0W)Dz$g@4(@`zCflS#!ra#h;aqS$O^2ps^f9$CpW2 z7uG%S(dpwNnSUjZ>42vZ1C67RvF^(x7}r-^NKmYv#*rAxC56ywZim7?u5%rtd(g6- zP3TOckr_*TvNMcES&6pl9UB)X{IE|{TX|%OjN@F;oPl-;gH4A5__RM8!h1`RSqYE>h~mj8adChS<2Qr6xC3jx0i0w#B5O1q{NQ#Y zc5X~=;Zc7qmZ0tKI^ReyEBIwRBR^r^Fn+IA1h+2oU!QDthtkH~x3U%iEp&D^e$i|2CPrc!(n8@-3~S{=ZRn)1$r9zVC;cm_ zKcOR08oQ~gSz?dCTV=j+sXw&ax)}l?QFtNn^S%YRcID*gRVX?AacJOAji$3g;v!Iyq< zQ~o@4--prh>U2`qR5d7x&4gk0>m)n74Y?F?$^`Fy<*%l&`tL8PfoC#A$Dy#a3HY=$ zJ*_D(uqi~`66TqI&m4Qtp5SGMHuB!$HWka!d7tKyPBFfG|LZUIlf;=4_BY4%RIFF<*4!V<`js0+NGcB&{+g*dS#Dw&ssZ06PtU9{3KoE>S z26`80@hGz7r2 z@hdMR;dxjhO!&DnXU63hNY{#uajW7S-9;3XZ{7QL7wTrfSWKwT^2bwmxtY|59H9Le zgW*t*TxW0SxT&VtHY46R&58>l*)iFdjMSs~l>7ld;mmDDTPl5kfQOV{iNdFJSz)ZF z9YYv7f(7!!qPL#DKUcPfocT(a#p=JIRGk0%m`n#Th^k2LBuffdruBLZ z=Chpa`KSGEYpz(^&f#6vdZ|=iOz4#k3}6O)dGfqXh&@iz(+VR2IbL7JW4;P;L+x!J zd&a36R$4HKg;r?-pJ{UR=UTSOCtiWEtAZfbsf?c<>Y&58P;fo_Iz3Y4<+q z-3hua80Vqv!bc8K$JB1TTLHl%@XR+1vEP;;i(ZKsRbCZ2e%-1r6`8V2@2xrOtiP3j z@CKr~t1I%R;B(AMg(r>`)q0|L%ZWU=h`bTsGUWWOXtAip69(PUdLeX$052z`l$S&5 zVYeF#?~N*fvA4^jVTm}9Pv+1TUGz3g$G5mEZAz}|2{M9!?)!ZAGQ?B@03dh{OB2|Z zUGU%&_<%3ehpK}fU5g(}=lSjVwsvI4a9*(}5*_xvu%Oj|)I6_9PTm4?AL*!a-&H_b zHg8=;k>ICa)6{DG_F+CqL|d}gj`&r*Ry9*qKOFrlw^q>)u`JS2k;Gz_DA45jMaS#) zx{O}X zuW!HWd$VCAk3_R$p=(oFg@E8yhynnEiMxjOaooW1B2F*wzzt5p1JWOC4M?|s4`At$ zxfK@|2Te^WfERmbvm5`ceBK6irI1QRi6zEPpsCFGH-6b=`A*y)U}JhbC)C94vD%M& zTgMamg1@Dns<$5jg=w_!u7b1@vaAGm_f$&jAbM28*c$n#{q+7Xh3KP- zws)$WrPl4lAvi4&L`Ua2)cqut3N1L$=?vD+QH35Y>9uc$tuKUj0LV0hrmmqfyHRwN zv1^zE2H$UC^=xlk{|vk1(!_43kW;tB3?(bfe1YmJbFdF|m;Q<_(BLJh=_ z)pb{w3g@I1W%o8BfTZMBcwTB9?PG7RX6*AXhrPab*kW8Qbhu!x%X^{rRO=EU zDwF!PurxruO}Bb>>BpJvySko&As{*_W_>I-kjdcX|Ar5cmdI1k4rQTr;!Bp2ZQQer zZOI@hcR9;q8l6G=t6Me_xh#erPq97wI4H$i(n;?zMi6u7*~SsN)~w_H3ThEAcq90) zY~%NF+ADt}syy6z1nLj(#Fb$PI&xuWiSR~i4wHipG-ON++=i`Dud$*3=uXAnkX9&Y z`3%Jw>ppZe4$phzfpNrS%6G)1uQ=0N9`!jFf-~zhk`Nxxn}QBlSjvlUIu{ zj6Tt`icl0n!qj0#a_mRKUvN$tfL0{Xx1xP{|9;F$Luj)1^YPCImlVu3)F%f5`8PlI zv#Utxtfmm=SoPT)FkQcng@0H@hQkHIFR@^QRy`kQLGZ3Y_ZI*Fm+hEkbd^U(GZnk? zGm9}63JUd$iOhNg)D5pkM&mrF4%gclXpbE=+MYR7G_7eU55RHIAMo!uGPGbN352$= zWmn9lR_KGvPyB&(qASz{?34=UAPDxo{)GWB;48m!O+b}daDeHO$C>^?EbB;5J?ssd z=pfu9x|!3PnI`CxrF{H`BU1O(IOJ0#)~BtZNc>CTp{2NCGfv}sf(CnClP5Y7Iybn8 z5n}|5QO7D6LMQ`1eIih=d)$p6E83s=Yfe!5O0&CmJLy;a7XC;V*ZzcOv!%$;pG>#2 z;Kj+=G+q@J-B&mA-lJr*0;qDxRq%G-=tZw-z7GBGJM@NDS+tAC!#=#q%=JJ5{p>fJ zhL~!oRxWc}I2Pl+@{zBVGJHz?mZI>7bJa<}RbZaigc?I$SMAjd4q7;+m{m&yRzoEC zyy`l@PBOM+$49#rY>Bk>S_b)By(r2><&Z2BS??;|zoa?76mRH_- z4t*y}`?U4G;X42TY$kwCDj;Rb=+=|7VveNaOx6mqXtYSCR+9S#7iul<&|H}fMRb%` z6F?{~)g6XUEiUxfoP;SfdD^2MjL1%~U~BHI=fC-F^A%;hOLE&2zB&Tefq$UmTyp7L zH267%($uv2x;U>=K=+Xi2B6<}>A%Ah`o@=rS2?}sAQXc#NbPfGz|^lt#G=l&jju_7 zqoBt)GL-w?ckM3{s!PyH;gX}qH*D8we2;(ctGRPd{4zG-Kk$)J28aH%QTmZ0m6~f! z%?ePCV;J&{T!IQ;4nx3G7$QO>LpoMZIL!>@0{5a9+$&m~OL)_nh?eRI}TeCJY5$t65Nh$R?PaYTv^ucm={8OFvy~Wrn4I9vjOsny2W7p%&$iV z4X7tvA#NGPSggnE_tQ1iIJMKxS&OH=RU9INfiZahK=Gjnp!OiOQ{n-uKVg{J|l(gSRf-S_7ELWIDm&;r+4|q`{mSC?k+=<3PJy;zFyquWGfc4zx!q0`1@2UmHCm)l$;8kxL-z{b9fwz|rR6=$8+;FR z6OB9hk;6r;w@mLTD=naxA5;E5pBRXn#UsN#`4{^_N$j zT37pRYGqq=%=7bbO6@)&5~a)PJT@6z0|@?p7d#9g08C#7CJS)2lnafb1aM<$>Es8J zyG4`W9CHmlnr{Q%-J?LYjEGy@&HSmPG%8wkrhg785%v29*Xl^u^I2-@qS8u+6t`i4 z_BI*4W~ng+2lJ^8w&UETouLnKV zld9KWLO&O5zB97bn@}O1Dou;&eoO@CRaCPdA4367JR6@fe2nV$shakc^{(goWTm38 zd?;7?CJ1gmn|elJ^a~Nca%~lKjQJ4t_Lob)9TxphJPN*sfUVTiX#?7~qR#5awbux~ z&FV>4*CVe#uRXtCV)I=Iw_#It?|3pInM!V*`>QS4^DO#5P6xv zFAEWJIa1G?LOFcWUFi~G^svS=B_wil#JaN8sU76sO1vi26eBAEEjSSydxw!?J%Zcz zem~BiEh>XwZ)iRuv>cpu&s?~?xfBtu_;1zc5C+d7F|(k`c57ELVeA{+FAqOi zE*W(yPszUiq`mjmK-JxaulH$PzRL_ZrDw~PY~qruI;9{paxexSS!L+x{$-DF zYzc+~0DFl@%^wD%%g1eytN2-Y7%_2jH5u4EDfJm9478jsgZvW3+R>lIoN?Zz;uMz4 zh%$63UzhbAgu4jdZ_Sd<z+^n{K67m*o#O0W)TsC5Q>b}^PgCG;RCQ;sgktyEc zSECgex8|?5?E}Gwzkz^Dd#=|8ts85e5sFnPma>*OMpx5@f3#nUrB?FA*;N$heOr=B z5rbEv;KH|?M6zy&=UT@9A>l0in*O^e{@v*AuF>5fB{;gfLAq0rR*})&NQyL!5|Az# zNQX4ig3=|8QhQ$fo@4s$Sygw9JiH~Ck=G|Wkf z^ajxJWTc*c5!Rsc8T(2%o5l=J7S5EyP|QUQqqvJNlg@kjp3ldRJ8-DESIAYr9om)l zkPx=qJ-BOnJFEMPgfqPO8hk6(GQ{%yl!JlPtBsO0FsSOyTj_Im*Y})AEnJ5{jR z^EUB-OIVh!gbtiKqU(f2^fHd1rP+&OiVkJzG%}47BZOaA4J80nH6R~Mn1I@_t?9Cp z`n`kVJKMhk6oiHm{e+-A=DvnD?|C44rAR>NP|1?GvJ|~kB>XW6HAJfl0?(;9-G4{( zHj?pnXRIUND`lnS@#JKbyRdZ#80m7vKKouNXoZEAYEQpyMm&$XImit?%%xMWf zFM019ePgqF%DH3rKnYwnIA$~ooWS<3SBPEqm&9>}XU?g6jO+oPP@?^E?;wfGsr5=S zmH{+gakU~>i?1;}ToZ{z{`}GgYL><8hsE-z<|R*Q6m4%;4Sft#7KH-xS*XaE*y5&9+ zFeQY7k5ij?aop1&vsSS-`i7^EFFTN1go2BF^P4!TAQ=K&$=IR!$C3T+f_U~B8H}=2vY_dqz%dPI5QVp3ad4Ct| zOGkQqSJiANc5qecQ*HiH^92%lRjK-9P;j=j2GCU?=!B@sJN$#RH5g?*O5*8Gmvci> zVQeC4gMH~rS`p#;K}nFk1k@)&2QU;n?om3wLcn3mMY*;MpdlxsG(7-Nq~w@>NI?Ms zsH7kQEFeIQ`vL^A#^A-v1cw0`(7y-{L>z1iMha!@wJQ|y6-@cX2t`$Cz=;ucX2Fz$ zwnG#Ed%+EnO&O5R!0SqKoGt+>7_9u)GG;nnu|SQLM*T@(Td0v|PEKf1v9bOgFZ-9d zmd2wppXxCfX@Q2qeT)jjb4wXoepIB^e77q?MUoaLtHLN%cDJilNn7Y!fm|1D*{XI_*;kpPLjdvOQ@P=7n~5<^2xFT^@! zzS>cCW9HCt=-aCXSw0bRhk-A+H78vFe5=x>_G2DU!t-XX<1!*5uj~q!(j>OcBKGw& z6LuG!H%RuKB^+R$n|cTLj2l=?mB}_Noa)bCJKv zcT(Gpp;01)@TFsud=iEM(`e$U{O9|0_?5UwhPNA3U8`X@MG9)(4?77gDR5-#pfwJ++)OCC{#c1p~!r_RNK98bA zj_wr_OTSDp@SSwhbs4(AI#d+)SqkD9}NJ2#$EOUi?b-U z`}5P9?1_Q%R7#V~(s1m0A&OAg+!^ut7^#K{=@_q2UrHv?SxV|THH`z|?1C=Am*rKQ@kN{i5PwH0;|eXdQ4>rUDRUnqcf+(;BfkBXj?X79jOFE?%y zWl1_~BR0 zcRLW2kl)&Kl*cohum(FN-CkcZ<-MBtT5N*y!pYxZ;%lvI0*ZuN%2Z=^z-LI8eD*s=Nsi^7S&aNRT9-%j-EsPqV}c(yX7#-gkr zpJ^?O+R##i=Yz^~&9TFO1~$d1Vyv$JKk-5Wf{S>GptmlIN)9C*w63V(uGZ? z^Nk{J<42F-W^&H}Q~+gpsx1B^M~zF}u639_Y?B&$Km=tRiS|`ACXu`IJqq93x9|y` zoV+b_UuV4UNkspJ#`wX&Ljr~_f8}Z{wp^Hfcx7<9K5%PpvWB{pva>eFzL$0_gO&<3 z)yj$esPYTBc<(g0X;sHqF_#{}pob0jKu#ZC-~dm5sEq&)zzRC70>aeMDA$dLjjb}@YnDv*@2Uf*vaF>VU98Vm|)4J{%%jUddK=?7f?#rPKuaH}Tv}58ZS0j3O-;cVixGxO|%0hYUS42Sw7s;*ZIrqWuK4 ziU9G9O4_zNCT&wh9iyptgdAnkME`Yss6<#hr`gSv&*o}-sBrGl3lozWC- zGcR>1{aE-s;xB8af+g$nUbphNhI@}SgJ=8PbM*^ZhEEj_+7CFjkS1a+A^9W$?{Y-~ zq#6!pxCmh2QvOs!!2r07R;@G0WJ4RJiq~d`J5GE}zarbqTbmeu6;8HekSRbti^i3q zC?^H^M?u4*3V&@ufN!beCpI?BTMS;01u-pN}u=wRr$}7o|ytmpC`HG;lYQm6%isp zO=WR><}=2~Xw_-eG}KB?A)$dsxL$K0CbZOZR3F>39*usVTT`R*VgCHDE|kBv8;`GBO(dQ z$h5-&m2}KdOS-^3onjtyUh*lfx*RFA(oZp3s4$kEOQ_RjNI9+1gL7*4ybybbWEeH} zbI*Id;+}^fj$or%T6B)Ivu z*7e6l=ffv3juK7GJo2ZJZ1JS-Z!VZ7DZw5qZ%hLfIE-i1=&_AgOX^{y59=7WHfbY(elY#eKq zM7LFMA1aPO7sYE|y>g3`sTZX&UT)TIni3NTwmCnM{4WsS@V$hv6r5f@21Tk>+vY5v zp{>f-5iCX|V$KW!w+d^&gErS$3ESz(rnW8$Uj2$D{looy@{LuipY7-POvTzXonud9!n^ zH!4eyl6TL!rU&OVoCo0q8l+G1=KK2!EtP+1J7X+mVJe#?BW~d)EiZhBshxe>Xlm>kWU(n*K;+n~Cy|u)EdG9HBJ}X<8)zabtRLv;I zRh^PxW+J$+!s%IbhA{I{FUHFeNJ2#{<+Sz*DG6t@lhr^pP^k2b>tLfiobLrL3I+xP zj!u5d*JHYO*cDnbl+RdadIxm0F5^9rjWAM1p}e<8s15mLMZ<;Ghg92zK6-SUl?u#+ zgtt|(0`4uQZDHxY=aG)9FUZN9LUU|f^orwxKFZ^_Q|&Wkx>>U5^OnuEMDx_-%dB@W z%>lECgDaU(OabfsO)$o(4$h$R=D+XX=w(3TzYIcQphV8H)y@|wqPPAyDbu-Si;Il1 zy7sHImVaiDHfF2o=W{Coc(p@&x5k-$nyivhUu6-jG+)@Z=fu}gS?$o^U^I%v{&D?S z=bxDH9HaBn^p8oLK_r(s%CWa~sn#C53hMd8%dW98Sn|!f%^i=>3o$LUhrgT)B{nMu zwqDiVZYH0b9Z{Z2=sN(e5t%;3mF&`0e4}ZzM;CjUC{ZhoW}}$N>3f;D!zA{kHC@hj z-feNW!%zVjpnp$mM9pd=@=m@`@mZ#i7n(p?TQEdF)q-g|Zm&maa(2J{H>Zj?u8@Jn zSA~!nUljaa(>bfT#V;o!XxNTax{^G3cH1DpJsn16I{^qYfv3#KFIQjvxg31U<4tgNe$D78uNEx5Oi`W6CqcR%*J zs)*hF?lpM*?v>>wa(ly_tDN{}QTZNEk@YB6(U$(xD~X)KB)Ml1T8+?VMnT6Sbof+Z zTK?K5tKy}iM(@IJIRUcrzLe=CB{7fo*Af0HUtCW?ibr01H1FPX{aD7PNwf^PVbIPq zEJh4#ge!7M`k^KmaL5+leb6pJUJ+jgV_sE-p#e1>^6LFcUCx}!-g3MuL=3=esio@o zPB!K=N%qn}>B{v##b4bA7idfyvkp?Xq75^jb4)FGp83w`TofaHa+mAAi(BaSC8Tv$ zPPsaCy_D^Zdm^+CprDJ*x)I2_TRQ}Kc(6v{WXE%BZf^Ak5tSu&71q0~Ad&lyKW|%I z0IZtO2QwwGcJ6tm^XO7d#b5!5GH_1cyG=z)_8y0{E?QRZBzF z2LJILO#Zmt9FMdA;EuwCns+;D| zgA`jpq9x19+1G%_!|WSol>;!86ahOMs z2(MpTkz~+0I+~<$GeZ>FtAZ7zAm8n@swpHCtjih0%qse0kUAg=E~5FY_3}PNCvN1| zmJ!;2gcADxijGUY;t{dljurO7635v{bL;HR!$1J=6YkI8 z)sXrZv*3GB;a-loDl8QiriM|fTE)U|se(8bk>xR)iZSxd=Uy!3HXg^{pEh+USnpAX z+j67Gev9Nq|GuNtHSp^E!w~uv#De|u5_$7<<#I=42>=k#3s7YYoXCb}c~2X2Zz)ZK zwh2>YGlNJMB-|>N008(MC@}3E=kvf!3_k^*Zw}m!5CUv&vH`SE z#A2^Q@p6(-&}fPzbmNWe91meS#15S)Uvd3VlgUU`6A?%=*OHA!OeXJguA zq}JghqWNzxgnYn;0U&^FL5)0x%Sxbnb;1pW)HtF<79>(*P6mdhgp&P>R^z9nCY$d*IgO3pP4{LA@@$~L1hzOb-u|B}Cuzy>GEGNxqPH9=R0|58r(0q{?V zYG=0Usl`B$t4DYs?#oSH%`Q#Zw^zS_I5cgRP@++e!PV*JNwdUIQxaC&#? z^hW+ikSYZnmJr_?s+|Kz6LvBac+Du4WifI-ZTGxV{EO0LjI2)VE0>8V!o#7zrbb zQ_-22iUkW5^nYAA#$XFb8CIsBo&0K#Ijj~w&0eM#H~T5`VPbAf-kUd$<21YE>Q9fS zecISm=I}8AzgyyF#j9dex&@$bDu&5C499(R(W@36UpFGcM zliXXX1hw|{FI<`|bD2$59|q?>(g-c9zUQL){Et+M+B556Up2^%a0Yn&%TANZ%VcvS zG$AqR=@F<#${)c90T@|5wqpm`i<}Gt)+?`##I1mh8G|~Sag&{`Obd64MRPE}A zR*+g)k@1)C>(?E$fT-J4I?-$DyJlsEgSJE$|KG7g5tB&g&FVzN$N$zt2dF4iOlq^q zJOVcM?RKxQBynQu*gT(4^#&7{T@4j+EG#R!-8}V)$1VT_!g%GD$n9KJzrZr13WMi3?ZrzqWZa#sx~)Q9$_*n5Zb9^Bz&%G@RxOL)nKzqUTE^Zf#lud zTpPNCy}2uhkTC!Ecg1S?ug6H_PD{uGAtB0q#4+EL8qZwGZ@E&9O&!rTlX?Oon0%b4 zPTzbO8Bou$V$E=r)IaxnE;ml>>uP1UaZ!1$w&~+}8oN|PpF4wV;X7Oi+vS>|HH+UU z2O+fk2k6@E?#Wp3cp!xUyP+gMBQ`1;+h98y$Rr<3WuSa6S>Z5iq1Qif1pRVRx!kkF zR-n@qNHKubxgXs%Xyv>?`0BGQMxl@X?=x z($#xU66ZdaTb_HfCaSaky49BDO}Xi?e)XrPPBi7# zYH}f(xw)+Y>j<@?;^gTIL|)$312F!-5?rsrUYhfZ`?(d+C6L1Sa+u&6wYe*tmhfHT3CkeYdi-pHUm2ATml`J-sg9#{ zkkZSiHne{Upkx5`1M>o8l+SPt#7;1TZ5SkaeX#7&Q1JvS!is4&Pu3M8`T)G&4J#Q(9vDA?S6Ld^1<+PKFlac^HgYjopqrzsOq}JbBEZc`L{JguJVY2y2iC3h!xc%Qd6aju7Hzc_91HYq7{ z$-kz}Wnxu)dIjHmY-SlcwE&RUY=ykCOVzZky!=X%+MjLaJbx85-0aRsUH_6wyP7>K zwz_XgJ=hWEU(+r0Bghe4%h%FD@3{&=SsM%sw3xBk?p<%uI6tT|&)Gn2eBuU@hW{km zCCXy)du$3|J+6x7v-Cg2`ceFLx{`ja*RUydyb{o@99ti?3yO@w!28@7TP8AAXJ}>l z{S6guv!0Y|tu$tpu8-2_NW7aK00lt2fVvNQin2&dc~nc3K^}N?BZ3l@#~n4RknW># zz>?dpnt80z=3j?jA%B@ zQl58wZnh{KKAUT6?){r$AWFwr^sfYNeX<*)J)JE7+4pZ}y6;5K-B&}S_G;BNQ6Y(c zqcM}+qd+&RApjes- z{3&s^m{>(s(b(SnFUKZ^1QNgaL8&Od-=C4Nx5<4tLfb*UlWcc0GdO1GCWl;pMD*W; zKet^eWk~@Nd4$|IS|bHuK#xgbMw5V=!6}dTh~VdJb_Ue&C`e%&ly#L6H3IW?Ud5c# zNM#9*I7?k#{&mxM_@I(wj2RI`A0uFrY=@5iaY=ib$M)lQ3EsvWT3fM(Yc0!S68Qw! zkC5u8UULWo2m#L~EIR@e5I&4w3fp>;DMc_PgJT>g@HSH3*_bPI_djqiLFluV&HyK>$R)c$msvuwiCeh-0D&;v^oapmAO8cTuBNa z=(YAad!uu=Ervc6!q1w&`yxb?;Z|J51IJj)CZS?q41UkjW0*oC3cT$OmLl?8TtQs^0xA zaxu1^uKE(UGI*S9+D}s*auMKL!YPf`DgL`|fTGHvp_oN8rCO@UhqHmM&_^tf&T5fq z+wsTXRm(%e*jT8!Qa%T3p!BN9F6Dx~h`GtFVipEBRNRy{hqP zj=rdxR{u4D1pt2v#Z<|LjAu@e`6J&XcMc%kHz84VI~Z-@bH@C=3Y$0MLPZSm5?^$2yo}Hq z!H790*Kd?w0gkf-Boa9if?O#9vy4Mo7^^D1`_ZWq>^5cvtf6xB;Tn33&$cF;} zg9e;$gUiM%Ed@DzAC4!1csYwgj>7#h-Gisq(d4fG5u5mZZKk)gw%9n?Q0jGMn#p_e znbXw4p%%kT``6MhjS{cQn#C9P3bt9(Upx`o!bc(JQKiW?JvC=*A8{YZN=$UxZ?z73OqM{-56XavelUd==`Yn+LLjH^LX7gUuSq@4j zqs|f>$dE0C$jDXy!nAKUO`$wKC#aO14^S1H5aUQ{H{$jUk}kIsUN>WW*xXGVs67|! zRO9ei^jZYOd78_yot;rXK78^om<9llC?J}q1Niy~cJYY|t_X-@qd~$55v^cKEA!|S z%^4{jmRg-gEv?sA%#+-eyq$PTcS~_fcp_Jc{i~gzjxaBHd5oJY@&j_~-W=nb3Jwl| z%q6VlO*J8;od#FY4@wNv$CPSxZ@W%}yyh*%xDx;YT zItIaX9ef@i4K_JFK1IBbEneYPj8}Uiw1XPX!#bRoYlE<|H&ydNm%&j;dywdSH$6*N?^g*4E(S3fSgmL z%bs9t_zI^Bjb$sgs?US8-k@mNDf|6OVK)@Jm=|sH#!X{)Z4rA-eLLh1GWp34iPU;` z>0ZBayXOU-!~U;N7;6B)6-?25gOJj3L(QGk*4nz>$ zL0lKIM8>^GZ7z5Sm1?zB|dnAEP0C$ZN}cQbm!H80B^dVU8xr&@QkY?1A< zCdX$Y-u)|RMa~Icbg>p88fn>D#?q?Sm5ktj?zjL96fl~zBPI#DJL8Uz#vnhpl4&Ry z!pf=PyJZcY1yxIJTA!rxd8JKn@{V%a;>E87^PWUjyB?j8D6e?dDdT#X^l){3mfa|f zjT3FAyIFfS+&e%=zgSy?e0U ziU~&|uMto8#fJtC?f8-(%WZNxsq#6=Wn*hti{yAPA?U>5O0*(cHl_qkD{rx*H)U;X zB;ogd7K0-_*V-Q!@64S-9O_F_j+6eF%(s=S{mcH$H7a#O1^%oHd5H`_KJFrsKhP4W zzwJB6~CJO?R-UCX`)T1G?V`*^!+sW zqsQ_BB`;Ym=}&5pAgR>rz=<}R>%UhY!pH?{24DX&d53|KamjUXU3Yvxi9|l;?ZW`J z8LA;#MCv5?^e|qNYE>cwF`>@YU@Y{g+`dy4C3I~~%hSmBEt!?xu?#E1$gFt|3$hpt z{YoKLVHr)jeN%7qom=sY2Q~9eV>p08zR&2sShBfDT0MhAB9Db<{305?C&6#WySV{~~cW>uPY)l=(%*e9zG?p7Ad$@aEvSSl-B3pPYf(Q@kB zy4R$vFjK1s^WQa2BocYgp*oKM0DlC98Z$Vue9=80u4Qw6DqBjc39gl(7$&XceZeCs zuc!+|ZH!A%K>pFg-G=#QmKJ;$^j2{r)fB#vcFb~3mx(IGr1oV#UOenB0=V)mwch@A z;A*!Nufqktms-u!j4(`2)J_N%E;@)P5rZOcoimOg5u&6YEbB}efBra7ly`~y#&~5e z*Cr8jK`_L)H+JY@r9nbQOq!H~idgk4pM%HUi9RK{4R!r3$t1_5>Fx^3IW&)+Dss0gFYR^iKaeiO8Z4_5t z;pUM`;_;$$=i#WOU*zN}S%n&dvVUY{{`iF+3;>D3zSU3bXgD`9o(c`x@YBlMU?>^n zP4VfyAkm1TPTNU^|9YMZ9z7ff*$SGx!Y07zvqr2ap$JR_)9&J+i4|-<}01)o0zBz2UCLgjL3|AYdoa%2BTSA?P*1)!3qae+@&9AS+sY! zc1BdahqC+l>T0ve=(yf0%AwRDT~hBo7GyOmp+Wm%hON5qH{)lS}(9$`}4=8 zdv6;61;o&#Qm2+4q)brf@KEHU6j9<_pQpidvAq_^B-1B4xFQD2VzJAeW1fw$D)ZM1a%XZS+#I44X8PQt{p-n ztmo#LZn|H6jIsu@D@c^kw$zmzJSd7N-?}!li#jK08EY-4+DP$OHFYn?2^i7CQwKLaL$4)WF%;R?;c8m`vuXlK;7Jk`8R}K8n?d!}CB=Rzt zQ2>;SfDBNeN+d|*3(;jvc`+MCj}llztyzFa{5UMwyaT~F6DB5^Hq-0)@&Yv<}t zY__&3;aRu)_m%dRT%KSz0SK(v@ps`vj~ZQYnOMlr_rXxaR*JAF)9%7ipX>vMFAO^s z_T8|JGV__u>1fOE?1CgP4kZD0J^seabTI5JT48U)CP!$I7Q^$mMn*^7_*a^+n{=~= zze<&J?Pk?OEH@mLUiV7;($kWd`5z0fM`b?yq<+XX&$IzHD*#cIRymF8x`(IupxbLb zfNc<^s%M;tR=Bay{*W>8G7ICW!<7UpNoDZ0zqDxi-LqiW^ogFQN)h~9 z=o@87ntJMTr?}tjS%%U(f)Ms)ro!GkQFwu|Or9>$y=lp%i9_X-$|rRU+zNRXyM>Tk z?MUS?l(*E!1<0qI)AraNz$}d<6Tv0zAYZ~PD>OYzFN=lFN*E&|q4tdg%l@w&HRiNcJ!GuG)$dnjgvt?~eql|)Xe)HCxpPd&eUvlWRQ^`F= zVF36NCKJOhz_BP`P$MiiB5=d8M#n?(ERQFbVhRYDcFkX z4^EG8`s;J%pznNp8=)I-1yazWT%RIs=xV^r+2{ZzWhAAeZXVVGmFJrRmTXBfC3bvF zp(4pRqv7`V$HIGyr^4Pd7lW(D+%HO7w;9b~I!pnMWyTHn0v$u0>pDq>ZQrIjtR*!r ztVjs|OU7CUzzf`(`p!HCCdx~r?%(3#pQm%2>^sPsemc0kz^5M|YG{C4><}R{02C;g zX&4ar9r-_~UG$Y5lUSyt9Gg3JW0HzyB{}bnsccR0HkeuN}cY)w; z$w>#{rFMMZu`pRce0OopcR^Z+6D_46mQuDSz>&r8$TXp=aoHt(`Ig$K6i-50C)_#W zr56sz%(?YDD>J8wC4!7>vd#A%e{jATe7Ss}>Ct3+I+(rN05IU7QIkR=-MR&~hrXOK zCdX9Oh`~_-7D4;)FvSnF$wiDOHUMN>+7w>(ZHUIbv_tz)OD7Nb2}-8PR3>&R$Dq!m zDw_G@(=*8W=dmcG3{1=OkNK~=gOe!?KntGl&xuJabxO~frW@o4q1l6E7(j!Fh>G_!lwYQ%F`-bVMs2Nmdc!YK;hb1ST zYUmJMo{(;2w%ta@fq8_)hyNL_LM)5tHhZsUn*G{PE@i>m?c(XupU0{YMl)qH*5x#%Eg?Si|S>5Tn zq{-i!uujR7!9n!VNA;Jl`tHH~%Xw;M`fapaT+$YeA4s)ZmmdHC;)QV#0w4iRQc<3I zDV79b+&0i0refO5Y1khYGi&2e^<$S)EtLoE#PF^CUkIK9BIC0nCQbf)6pIo*{uik& zX3eq4pU6B6JSL8-sLq>j6r%;j{xU6KihAa1!KvPe zs`OL!`K;+~H+whTOAW#>@xI@do+5s4JVYEl%2@pfnv;XJh%KBW!zb{x6eT7;<^4Bp zN676njY;LG>R9)?Ncv!E;grU*YaO$O+J4x^>rXNX`A@sA2mA#@@w4g4KYsS7hUaB} zxy?Bx%jV()Yr%|CoEvI&wrakn`M#igQ#HG&lDEY=nEK1-{BPXv5z1~K$1H;E60xu2 z=qsM&m$gll-l1h=x@j7L3EtPS>sQHMTUA4s$opYH$^`%XBc%-fN9uFx?_SiGVxT-` zG*AkP{Te*gpCF8Wa2q`Dh#oU?;kck*O~bTBSdTubGS>?jbe5Upa^dCZ7_1LegqJ2L z4=&vt@cg}v)9L?M3C6*Nc(X~Yj8IS@_n$t>9;nRu)OwU01vjBKI+2t)Ia9MJG&cN) zy=*+tTa;t3%?M!zF=py;m~9l9gN0DRF0MNHFPZX+JQ@ik%U=IT9>}t(v#V36fm~Mmop?CfYe4hTmhRvc6sZiQPR9AD##uCCuV7 zjnK5Kdf76MFb5EbmB$BQD}iB=2zemYhjKz*aM{P&O`jd*Os6zA zTf7)k4V*7bhc`TGnuDh9(NQ&?W4VIMVt^T5?bjNXYZl9g%!u#>> zn#GCOa@EdL%GGdF(em$h?h2p>UoQg_-7gLss-gZ$hQ5(7&R@yla%>%x(!{*yxF|Rc z5Y{8_Ro}8`spCUDS7e?tHCM*PyIi^Pj{2B^q8>KA)4CO8+Lz4sbQZAOxx$RhezM++ z&dRlipRbUf_X7ZiGss2(0t!PL<^rB$>4>)532ER8n5YnF5GEAb_f>k29jf(HD#*u< zC5HaGWWcnK@mg_`9nEy!smPyR4XQE!>-Q2t6aDy<%MppI5`HltpG3_ zSyLNDLO?wa>L=ZnXYUBpQY<077*LjDdxOX_6h>eXIbMlyzo-8q*k{T~ZG(CcvJ)~Z zmNaSd$MX2CE}@E%%!$|@@-P7euCpl;*g$)^_r-mnb7S6SicgRdq9hU)4#N6)s@X+C z?L*H#dX;3u<(%So^?-8!9MkC|fdgoB?PH^-&GD5ulH`1u@`?%D;3jcQZ~2@ zA*y9Lt7~&mfEa<&7SV_rb)kzdZ&?@4deA$i(nlO2FUKr$0mj)UafgHsj;0V|3GQ3d zvTA>~@?0<6-8FXntwE?}KYsLuULPMDE0#{2TZl9>t8%wTdUWJp=O-76aOMwhDJ;~3 zhOYoL6Hr+BNW_p$J3iJmo@tY>XBJ0z#MTe+7q6Cy>x)IH*i9?op6q zQP<&(2DNwLykmq`rDgXPWC`k}q}G7Iyot=C%2@>LlL4;Co^F5$@l-?m_;T~is(J0I z2y+Jsp#Ut&_|hyU&;97o*mV2+RhGZ@(P%Il?*EIndouvK07})sI4<6J2d4)0XHC;I zdBT)Uh5i&3QgKF+roE`YKUe{X&6Ic_#OD2U51P)=HgBsY_TFi-j76mc8%`WS z^w8K3x*OC?K4uavhCMcVVzvdpyWDV1-k!3DH~c<0^>j~!jsX;HPL;fN)l&yWP7=S! z80^2T>NUI=4Ps;m%~6b*lK+QuP*v~-20{R76@?-pqG9{uEwX}lNw`|$bU661i7gU1 zmP?V8cswZj%S>{i$@Aa1fd#FuWOO0_TBFbhJ-cS9$*{1r^-8jse8UvT0^ACPvR2`* z5_H7$lV;V}xV%-p{px`ympl@Q{JRH$0MHTOZ(Z1aI&0Tx->L|=a zX(&vsIS^+hTJI9d($|?x1a_+Tta&M_K9#4w{dlA#!37~!h&j}*+uxpY} zwm32XP(%SWOE6*1aKeT6x`BJ@lZYY&i)r4Y;Vwj3Tv+FXsUeC1^n>9aBYK zbxi-;uCObG@N@_|5n8Gi4{sdJcinz42@?n`7llunhw)bXN7i@08Sa~{swL> z`(0UA;%&O|`VDAl|)HG$*}bg_0QP zY=jqZIn|``>XX6jJc(ii?KyObViinCEMwrsQM$m>fazzV2Wf~J7pApyx@pM-NCOKK z-TWk8A&+Q}MV~?%{6*Cv?C_{&&S03Xa4~KIyBEj}R&3-P5OI-&s%`N33zL;=;nv!p zFlUcu?a(e2nYi!yI&g={QAQ_X&(cAy9Z_-$ODxYZ?l0fRc0%%Q0{+VOe^hxJvxG$6 zgiIrmM=-!Bf&54aMF7k(Z;o;(iKo)`jIy3bUsg|I`OI6(YqPbC#cDxL$Iz8MtmKMm z*Vg~!Iq}bKv2a1Weu|NxEKD*67LO+S^H}}NA%1(o)X5)b?dM^Kx+T|Qhlgv%XHTO@ z5ey6h0uUHiGoEaY*}=R~qm&^~$QTDx@Pt_ce<9ZZ7tni5h7yh?eIL8$CV?h;%FJ{fMVc zR@7cw0MMbRbJVypLRWTr+|r&Y)`Af&n#^si;IjgZ~^;4{!4U z9qeGP0CQ^_>j8nr3>}U0-^9+L;S)O9ApF{X$d@yGSiO4+&BkNb@k%S>ryAXix7sSr zZ_np?BjX}(NHIu#=5Gwlk2tDydSRdgM(x|42eGFNrYQi05-c||&hkYV(|R!Qf@>wQ zMo18J31`HWs@W(a&COks8q( z+v~E;$x6>5N(ZUeHm!G=(jn8dos)SuRL1>jZC|7^t(fK5GVx$iPRxu)0q{@z17ILy z1ICn+#zc$fqFGVehYy=aC*=84!O&cMA1oZK%t8&E(x~VC&H1byLX$p3Y&EaF)8h8l zPt-%0Oy9RCHVt)=FDi?;Ycx9-`M9OSYiDzs|0F2T7m|%LffCyA^u7vxXPF^EF~gLD zy*7nyzfeE{7v%@&DwB>16#03|n>0y_M655aG`}$V2cc|l6e%AE{%$kI0Ztxz{hjX7Pp{RAS-RYb>YRgC!?oKfE@X(5A`J7_3Xckb9gleV z-U1Sexk}AK%0oGYS!r&)#jNhlWNreY>`hjKkj!7QMeUmPl^JKDGT!%sp?y6*1v43H zZh_;%eT*eqD3c7r_sWd_(<{pRQ5yoXIZthC&tR8qFwXqT zWxH^!1XlYpVFk#vscEloH4oUIv&CS>nE^&e4W*4rDkM{&bD?dB$+hMSR1%6KjHN0q z`VjKat|=hk{Fh9PD6C{g1Alt*4X=%HaoyvGCs3^Q=&upcy2|O?IipPWB0*jKMTm1m zj0EI70VOgH|8OeIR0>Rn+Lf+zZaf;3^8M{-b;T>lDUfSk%V)8OMY`k3DS|i|eYh8| zNSe=l{Y2;p{kH(ie`YL`l1ecjf@d0~?M$B1M4)gY`^0gUjhh!o*YErFwo}erTMm;g zM4$FVVpAlpDUP^K_(hsc2%#vB9VT!Qr*jb=Aqy=F=<8&<5J4seCY)yzERE2_T zK|=8{<8znHQ@2=590CjZP@fe%$l>RgG-&)J_7ny&ohf}fnEP)^*uRAUWN^^576NXv zB`nOzm9QuzxyP!e?cb>pocelE8>{C`+ z5}V(VR;(b(Q{D0JM}HPaXRjZStjCdiX!ipKM4E6lgJ}S>{g^Sf)OYmGbWNd&+HmP8 zSoX)j^h0G@~{K0ChQf#N~^ z{+w|&>rHV~X7xb*zx6NKW>5AFP?kDB-X>6dzF^xWID3s3gPM>1Nv5-Qq~WKYweNLs z{I`J!ZBxckH#Dbt*Y2L;s?Hwd67q2X46Xsu=^)^%qwL>Ee#4T}+md2rv8!lFr(y5pEwR@^Mf?c|9ohTmIH@9K!%=bgN#K1q2~H1toy_iht$fhZ%gS5q04 z?kZb8LPPV5)pkGw^`c?PT21^VXnMm6Br>ZT9__tiCl^;dd-;3tN$k5Tmq}LZ>p^Nu zVt0iIeZnw@5WhdE6z=0I6KB6F2}L51ldrE?bs$yAY$9Emo^X!Zvsoh#!+1ln zEL}P>zaaM1od#h}Y1)4V|9^uO(30DfMNSqGZjc=Im6P_<b_~^0io|e{~Y63 zM~0=QHH)91C%ZK6+IuebEjCabA+z#Y-7i1Eh}JOIaKZxT7fDNcoCpBe1r#O_@a6kg zKL;JR@*P-){po^{Ve;?q9MC86MGCxPMsQ3l(FAs7-05ToB4!#4Xg2)V)x>10#|Abm zpu@%I=@W^PdI7>8zd5P={ktozd*fKAE#4izdi?J*&-}rhOpLCQ0QMxkxGp$SioTry z94m1wxs65r`fNNAuUzo?7Dp%J(-OzS{hL^cZzGinR<~t89pl~Rw%=$7`kQ=}%mbzw zM58ltC^0!8-UxCa58!N)G013`4;L`c!u@O2ix9(V(k5UYmBl1y+WR_3qbBwQD`r1T zmeID>ajTf8?yWd(K>1NisBKv>H{e4}V6-NyC(Pn>WQC;tQ9ku1%sR;wjY_;ou$3lTh^V!7F;Ay*H># zWFsy;)Q<+QOzaJm+RGXLYp^$~wxAv;F6@K#GHfzdYDj=L;fH#{i+@q>8vwpx8S7{j zaI+2_WAjnt#J-48u7Zx8S*Pup4U<#eJ-G{H%m=g(M7qQ{&~UYp!IZzQkN)6D(nH2= z-I;;IB{_2OKz&_QMrcHPi9IK6yj|s6EbWig`;5(Dp)N4bgz$waQRxtnQNTp=9u2GI z!sVyid7w{pScP9!Pxsmj5JLNyUrG|JNYkY_!Kfh7zi$Fz#SfK<#M6GAh+dqgPM|gm~n_u9H1AI zCW?KP{N0|0`<=&O*oBlLScU_?-afw_pT&w1y{|6ZY;85bazLlWD~V{?O$=uQ5&KDZ zP~q;`gOj4P0XP;OH^BcCf;y9P4T^8VV8sP+o%2`y@%X1W2>uFiXMYUt)xyV-KUxZC zVoGRnZc^pH(K>pI8KN!#Y8`$LO$-IEEbGb^lx2ei}O<&pnoflsPpi zR%Y^iB!I9tZKi;9H*P(@b$jbp%5!4;Upa(AEz|+-2=3NlBJ6J(Vs=VW5%|{O zr=i3A#F(*t8Js7~D@Vz7h%$xKQ2rX)rFUuMne-Z~aRWctgH$wu%d>WYtW)@GSH3j@qo{#uc;ONsGBZwZYaps49Ap}f z8qN%?r8fEH8m2`Voz}WIfL;TNq*A?BzKgws;|+i3#epp$5lK8oCR*{@cydM}V=+!X ztXY=viJ?{IY`${D-&KuTxqNgpr(ND-2E%4;NYnP{3uB_@@t;diaz(jzyIRWd)`UHjgX#1IX5#=bv zD2y9}5IbUYz>OMQl*)1yHW`Zs{lcrpQIzMRjI$MeZ;+C+r|W2KRqw5o6GokDnNFGU zrTQEd*bo0d!w5p!qwJWoi6}eL^#jh~dKu^7T+s%3$!eq2@FR z591w)Cr-}i!Z-JZJgi&GZf!~gA{qPEUovUZx|F;Lhq7xwMm5t-(lkxASTa)(!4MA016nGFkzp9 zK#?D>bRWV>--h9hv0+=yRNqF4`6y6oOogykz>0an(aL@teZPjVD7nDuqvI9Y&336! z&x$HW3yDbEUYm#MP}8_Z)4~sAbVyy6+!pX}ZTt6FYkY3ytW4!o(>+Jg*ZbjW$PbLJ zTev|w3B^;B%$q~z|C=@H3I+fk2o|;7-n!2#{D-6Hr{l((zp43MCt-#<)0BkNpzCZ9 zkssaZt~gV6%*U-eDl#5-iDx|?ADACK6=a&U%gEBLt#D;Av(O(t?@_k{TI*S#$OhpH z%ae!C?{)rJ0*HGjgJ6@wyiaZZuAiX^1YtC>auXph7_@$!gWvwZ^VMTYaSVzaC?Q({ zdCA}~wpzujOnm!Ud@77z);D>_gBCt*H>2frZ2bGhr)oX06DXIR3X}C8rDF1xwORd!1*=WgD&sP{* zvo^&tGH>|MS00G6wsuw{4{iAn(G~pf#ELgS#G*{oW~pg0=qT#*f*mh5irLjM0+bYA z95>my*?964hq^xW5tDG9E`X>LiJ>zDE$pYdX+hYZiL&wJA<5>8vtMh&@SRx z@#@0SN=u_h!@1fO7gQX|>|jE61=CQyQPTe1HkRY=5WB}tx4&j>^?mQy3zhmOqASF$ z$<%(wa4pU8_a;y^IBo5GFbaAi7WL!p3mTJqQ!{wE*jydF6%8h-?iIQLW%w%Oaw_P* zDb9@!HPs=-RthUAX4ToEA>r3tB%?sY1n*2H?;y+G(FD{wVNIN-s2T)=ZADNzG>0DW z$_xWy-7^)@#ghm9-(n1&#-STLX_sE!xoO5md??;xHB zcMkz)AS{#n83q+NKn9PR;nNc3PRd~cR{VN@tPCnd8)#CIFC}t=_4?AEwXfC%_nk6Y zMl{qh3jG}Z`ela9K#^d-ecRFS0ij(4xQS~*CxaiZwddI8_+2~}lE!#^v?misT&%pt zBHaA`@ci!xJluFF+&O{B?mL~k9H#M^38I- zTDz4FUHFquCV|zJAjxu#*BK4)=WW%$V#=+lC>ORYxiie>PoUxQJu4%NZzsQo39+Ba zZVw-#ULD`p9w5MAj^M0Vj33;ol9av*zDBxE_D}K0T;0Dr1$@pLa_^bAy)7sWi&=W%gXFg7@8me|?Nmdd!z(@!HPhmKz zJ-rCX>`mv?5M%~!I`w%o)r1G@Gv0LkW92qhEQP;yK@&}!j%wZUG8auBJTrstqidkX zLyckapO=q#&~d^XA8TP0%HI97x#q{VSpA?t2hUA#sA;fh-*!zNR#v+`jl`r~r zoe2`hok5PtD%2zK5%=gA!3d=&bA^hM|b}XG(`kj(=p4E`vWVnITIbM~Q8O{^6L2~p_ zyIA3G6>~yb!y(|{VtqqLj%yxtdhGdrUK5wAKr>V&>i!m>Cgy*ur%i{jyyP2#*A;^ITHl)D?ADFhvfA6T84{| zeu`^#rRV5m-jU|UdqRy9Pb*q8U&G+`?iSE^0)IkI;8`H)cvy<>Zud-#?2s&J{KLBW z2Q~o=paj15V*d7sZ^pZ+m7q2p#E^RdA`*un4wT90-*eb1XKD3=AA={bEBsCHgXQA< z9Y@@P;~wGpg+)v7v17rZrK+Eq+@m;O{{k~p?xqX8P)qa!0D3II^As($LtAY49L+No z|2dOTnLNS(Eed>00M-nafuLg*(#F{{UPv2)$u`q@nuXBNOP?C3Oue{enN+5c(7~gW z*D|OHD%1~Bwo~rxrFh;1V8YS+6}51dq~a)FJc%Qa)rq9%F|tdlBmT?Ql>Iz<;6u1f zpJ>BK#qNCYMR~^PwZGh@mGpJ6QJfmbYr}z%dlfQ@r-4}~)&rx@8s`3F>0XMv3Xvx? zRjyn0JRr1>0AFz_XC=r(9psJJeE7t0kTF~uaQh(bB$mrz2IRxKo<)D>&j1^0+9@pU1^%l3jIV8<5nhYhk>rx5fdxsy%t>7u>)BFU3c zFU}{~`pTAzV$7H3^V0J}&ZygkBc&76-+fXnsxnY%#SSSPe?y3Y={FYFTYi#lP2BWf zZ!p2G)WXr2qy@q1c6X1=t{1$+OGIA%4bi>vl4sER^$K=#{CBAa#e9t|Np>uLPW5t) zWW;r&^_f6^*7HE3tr#7KRqCVVW4{{0`mY5*lwB4yr=9}q83)H_+-Jbvj9KEWd&hV|^ zG)NHPDF9LxD3+l^hh#kVr-YOxB$JaHl{zp{zM{yC+YUD^54fdd%L-r#oV2mMrzEq$ zXnFD6Gqh;-#+TPAX=wYAPT}@PF8wW(_dRXlPRIn|%6i$*!?HmXY90$$6N4Y2RAz51 zrYWlJF;3U&A9D}Vz9TZ0j-{Iqm|aL;q2uD#(_ zG9WbHk`13aBP``n&HL(*w?r!pB2HlTFdutyHSC_-qlI#2|W_ zI+vc@;A^heqSt)2Q1&wXE!{j3%OZw%v-yE9e*7~5ls5n{gozXg6o-Lh)Dzagw6R%n(6U&h9idFW zTp~<>LfLSJ$ZZRonW>dr*o6IW-bTm9%El@c5txYj)BCjXOHpNsm($GVLw~Ga9ey>N z+jB;62T|tl;XpMd0~Ebo{yk-lp2`7qsRuY6&azw6yqN-}iC}TtLfe#*j(o{r65F3} z_&y1_rx@oHvSoy0Mvr~)#V7ZS4nLKTv|WiAoV8Dn&vp2>Q{W^LjgMuNq+R*csn`h3 zQ&}3XxSsiea+*fG=Qvq)(=mfm8EekEK!!h2icFS7f0LS~*b7inl4wHe7l|HE^#qHZ zWiL%NnuOt$HR2!$RWW$C*qM?%CWb~Z*{Fh++n&9SF8jpc)7JKCzUDF%qb|Oh=V)p# zmZH1GNuLha*|%o(w@ISSXLKz}!{qWe9K)q3;yKt;Gi1f?jTLD2h| zXoVPyRCn$9Y04DheO9BoT@&p$hT}WACQ=+6nfEUXjDk6i33N?ni0Bzi97Yi-VFXF+ zR_%IIZ%|7cs0U3Aw5I@9@&+YvYky_*DFLC*&=?I-7#M{2FK4O-g@or3=j)so{gk8Y z$cd`F?PBZDe*Sx64;M+7i=h=z`Ls#HkExXJWq$1ArnQlwFX7 z7%8tDLY8nkjybpxa-$Oupv_R?Y*15VA_m-W&MdZpI6)F#^CjErbWxtt*Df!q%>#tr z`@OLA;dZdqb?wL(Vz>s_G%WnzJBfuk@0WKidXT+m`zPzO1p)W5jAbE>q2{5EDxRzw zD!p;s$2KK>mT7yAlleES5s=9^m7)M;0MiBpQ=Nnpy|cWlaNxgp7!z^>!r|q*uTFVl zG^rYend5A;g-NkMYuk#n%FJ`Cms{mO8zS-!$IXNPAV_orb%KZjSa@*X)kORFu`C<(;g-|@N%$g2E1+wl2)lef!wg~_nrCk0xE@T_b!cKU{G zm%P9VAQ(4Rm_@(0jh5(%Acx`PfDT9U*6+~gk?dmd@ML;Z^N|?h^l|n_mY0lzyMJ4L zJ2PYDxmX$eIn-xlvW$fsO|(PU$6>+0i6EbX%C56nQC*omsP(`~)Q?}HNPx`78b=T< z*gB4sB!1UZm>htCsPh3gF)Al=>i$=$q@_1o2lm2nKQOB$Q{LCY>bF~@T8+#Yq?!5F+onG z-BChkWY-$B20{8|4ygCvSiP`niiuH&Oc&-#>gjfe(H*{0>85S-UhyMe!0eooXXX3JZ-Rm9$zGYzGVS)Zq53>#>l@ z-%We+f8FA&rAj(TWzC(~bE-`zpG!FJI%;BHl?s@=UfuT9D({K?^KyqKPJ5>>l{OL$ zg}S;9{PD1_5{7Yb5Zycsp+!2E`dckGl!a#ilnxLI+|Wnx5R7`K5BZw$?DJ+uYs(qW z{$L&xQ>`2xf35M@={N20xxV&8)T!}CP7ykn5SU~SCA-Cjgbt}CL4V;yt(Bl^QO6H+ zA!Yza88|5^jBEHAE%ghy^Sht|#4r~*W{|9PA2ol4!gr|3)LK(QI&Cn2K}IZJ>DMR^ zeU?TIZ7gG2;8kwbxeR_y2NpjPA0-WSwGYg$KlVTgL@acZ zabk3VD3a5B=`RsU%eo+d0uIXl0Hp*$qn-=sISCSw^S2V>5%&jWqhTTe^zq7yLgi;z zUa&ZfuPHAYv1salpF3}f3r1>{Y96Cl23Q_VU2 z4xq!)USP!oLZ(^JAW`U)iC7T{Sr`;f2qOgp%9@n2+FWXIFZ`%_q!(>mj{5d0buL(kxgAW^~s6~jqkbGs0Q3EESq-ifV2pVY&pp75{~vxzpoCl2GZ zlx4P$xyZ;BU#h@dOCRWR+j!tOE|n~#QLl-uk?8B4NW9Q`X4fNInW8ji^)&`DvdM&l zxW_UYS-Whae*(O3Kpf75*8VevU5|A9^v(VgPhj^1e=?QZRl@7iG>rxznKiov9jXBlL1!2I zy$@v-!7@fLGwNWIUyMkygVRLWJre7bTG2NvNvqjkoea`UdF!c`M|Tvez7!E?dprRz zDrSyp9wTel%DL~6@~kv1_S!)(eaD=48@kz<_9reex$Aj_y6+CVr1%UxQq~;^hcg`7 z<%8`xoh;>x146)!p~^z}bld%;peYfp0-e~uq1>*xfjA2EaS&|Q=;v=wkyLvN^&DdQT-R4%3g!z*uJNZ)KNi(AY*x*Lp?wL7yNkoKJ^Xvrmw#=PeYH^ z?pH!S-V^!s{QRTbOZ5=sc0$2OZWX}<&RyLw<8{mMh`87cM%Uf3zEI-gsder$ss-Nl z`1>Bz-7{3t4*-)M)MT(v`kDF{jvIE8q{H;{MA@8YFoY5}o-P=|FleTj(X>rKDLkn+ zHLfDoSnVsmR=SE3W@J(S02TVy=*B2yFX} z=Y19pr$4cBLXv{AWp@%7a|s;#Q<+%Sw0U;HpFyv}D_hsHZ_h?*ucVnLKe*eoTJD6t z$1Z|8`9?fKdK-tm-p$Q0Wk~xAvCJ?0v3`f1Re%DCerbXMP9EW@D%~3g(i8b-p+Zm` zL@6oYGARx^vr6(njjaYH>wb}qb57I<9RI=z)<1F z|Lu0y2y2;*r7mE4Oet;GLi<}bFk?W@*;?X+X#aFn(bfYg*30-m|H0=Jo z5gE-VtJ zkz8{y(X~Vq6PgDxt(>6ecbIfs5BGEX-<2Z1z)$#S_8Q>Idujv(Bem`hx;mfIM!dhX zxCh7w6xx`Q<2}zk?S*egPl<4Oi9O%@$K|+hLI79*T|O2bJ3xu^IHUgwwJIK!S763t z9Hxpc^a`c&zJvEGRMM;LQZFlYM48Scooz=yUl*l52HfSoVm&dsD!%x{DBIZG(>>^$ zm$2RQ@6zWWd`FCTjHZp#&0@%p>Gi&o8eSUn@$T5z>m=r>$*Rra8=S2)VHHC(*i&{j zOmu~U3m4_}^_eHPM)oi}2Bbo>2A~&wNJ!XxvFnrHmAGndu0(z_vI6g?wsC zECEC8iJ1D7PBB+*QsO6u=wQ<;9*ck?QooLL>14=)?{3AW+D`mP)@ae^$V{DFr?uYE z9W88zT!u>D*~)X;!VVf#QxEFPhQZzSg>fC=En#Zw3!ia}fsc&s<~nHl33}@DqXAJA z{GgOf@%N%i;DNt3m|bt}8(qMe8B6LiwgW7mdC>_tcI5pF$Y1b=w;rnd_?(|x=~?|9 zzPYG~;y(RCzyrK>8M)v1BNB*0eQg4;;h^8{2=KZ5X*~a~cn@p`%XV!oOnOQnAMhq64Xx!vJ%s(2xMb~lxY*eKf@r}O z=L?+G2Os(d^mlQq=(NgwHn4GI^kJ2bO{`@-w#DpABrA@aqW|u}VQc#TOu{cXcoz^V zN!A7M%(L;*Qt{5?;!J6y5v_@YfmDQqy*Aj$U&_LRfY2a zc(CM?YOYCno`ov(S+sMT{YUAMl!RUCexIy^+CcGtF~|)j4p6Sqf-}BOI(GPVH*E*G zDS1g)wP(oaLmfRZ|Kg#8CoHRLU&>qK#}ldZq~G($pZuga#QIt<66Hr1MZKHK*Dhoi zaq{hp`)x-`9 zhbNNnCeeV2DcQWk7uTolWbmv#4R^=6fRd%sB@27VLDz9klr>LIaqy#}tH8Y_pTlP@ zaq9H#^}`3%G6CCWcE@$1V6~M|2~vmN7uJ+$2-&1<{*Zoa+M>sii^SHN&cV(Xh26B} z?=^MGd(`gq0f&}h?fPZ!QpfUXs}F5M@xM^r0Rsxm<9AHR$^~j0hN9JP^*wqI2p!<< z;Ifa=WXPPF%a=3xC`jXe&ti79weK@0+nrj=KifblqI#6*B?02FQY;$0J2U*$82l=W zDO`d{LPER(l6aXaDo;DgbCuw}5RNE-ntm#Qb@N$`DC5VMwB5Qn&v+0x!cGU{9$#}a zT6hJQ(O+aYd+X<%bLC{(Ou2|0PV6$vRuj@2ald9?`SX7%R0&X7&AJ0jJcv~65N9i{ zj4O60F`0hnETv%?$;`6eS}d^>LE0xDd3dK}j{^JMq&jsy;$#P$x1cH}oaj$B-soI~ z;9_03zx=y$6uTBo+?P|7BEAwu)(Z$~Vu(K8ftP5Qo`Y`)mhhj>5F-6{u%HGt(;9dU z<{@<~H*ch+s5l+{G5BZBz6yFR#9U-ZUA{*DwvR%6&UDrrQnOI((lS1<$ol?3XVh-S zRwW^)vu@!lvVaJDRO{7&Za#*_eBQO&qeK7riYW9EFC8(4KqB$5El!GXdPKl{>O!0# zsy~{*3{ZIaLfIfyoLZt^TrD!^lf|pw4y2ZW9ga%3#y zItc=9E-cCA+!-#)_*rNu-2wI_ z#>)??CmyvfBq8Z_B|Z(CwX~k){l6SWp=JO;5+JAupk!vtD5Ih>lR!>jQMR&Ds$e7Q za4DrJxKAroBHb%VIF7lQXFu-^!Eb9mlAi0mK5~`6w7*f&S;NTAu;SBrbB&+mOvb(b z?e0&d&&b++PcDkU^7MD{s?JM*0uDA$!XSHze5>ZUE8Zz>X9gfMNYPG1c_1`QlbNgY zt@SyNQqz-?;7KSZSib)UY>rlZ6u=A=ZnrhET={_Lz3o_uxF-p^}d}=e; z2KHo^j%BWAKHeM)a{EUL{!u5fcP0WP``lf;tGz@>LxicwQZr(1ynfB(2r8&(bs?M{ zVM_4*!l9Mr_Q2qMOE5!~q8HPA;`Flfk&R6DyFzCZ5%xMK6=YW~UU*?KO*(7-!Ee1p zA2o;@qp2!+77@YX#=`**g+dv?fpUO&**!?HzIi@sqVjifG&XXJLz96@3TK_DkYiTf z1D}k)kxYpT-*zFeN@+ER$KT`&Bk|FfKoMrjSEUqLT)%Y!weoU6gI?XL&wj78ozVs) zyAb0`<8?Z{+`(})w7$V4TZ(NBd$GPI>uZShw(e7S!Ap&H5lhKJMqPrgVz z?i4N!g!qrS13!UWKuZ5{7-OZhhH=)nj=yV%i-iveeUBXgag1fPQM;LkQh_~{dGQWo z7+v=w`l6F;;wJNNy!U$Sk1{_HH-2PETv>Yku&5A)x`crNKvWZK^&YDisVGp#)`=+4eQkd+h3XA8oBFvB!@Kd%2$5Lh?NKEpHGMJ>S|N3X%t0uMAtA1 zKUQ&r(-FKEUN&>NM;+Y)6eeL26hghir-Nt6y<4S$?apDEAHt`+1Nu|Zo3TJxgM^b`;P@KB$BI;zejo<>VHNQKKAt?+Lk$c z?2k~W4XS@mioQgZrQbIj)h1y_n@(%u0CuBng~1b}(!jh7UCJZ`14{U5iH7-!Yf0Tv zO)Sdq3{(}%)On9>Mdtc74+EeVAEl6ld9l*`&eIe}=$XXGi1NU=pR`g~*eyJ(54N_5 zLW#};02a777IXP+m8XAa6bLGR!COjCpnqtTrYhM%rs+5u-i-_km2K& zlYB9i==|HfahDPGB7q=a^)dtmz- z{mY!|$&5Aj=aeZDuX;+~u;uH9o0LXnRV}YR!B#uW{}Y$>^Y1~D*xS0~ynqL)tZsZ7 zSdO!z$l$S@!kD#acq0H444#$V25S2PjbGaRo{i}I{@5q3D(o=N`N}>Ub-2D0! zdfU%7cJT%!;>X%q(*lDO8!L-ay0VWgZwjK}xglf~U7c;cV~Kztf*Z=|<9;g}gA2m7 zFSq~nF`>&boh+UI=uTG)epC7r+0>1ZQ#DFR{KnZwMq=5x=RWV+p$K&ni4)NafB*o* z1n^qzBSq-w`k!V@C?T`ms6;@NlpII`5<-ASY8GauWNEYK2ap4`k7ZVPH(nL>jtBKSBjfgkLtG{HYq2lA{~hWs5ZG=& z4nwb2M0i0qtnXL9p}87%TC5Nbo+uirg2xB=j#Vf_As$@05;W^wMpDg+8=Ub4&dEHY zS`d85jPfo@3D}lj9p9M!9e?$-nmd>VOZ)iUNwE-l>SU8vpcxQFY6{WoSoJYe4w03?0Ozqqz8JV5!y8`WRuB`>utyAd8xU&f=va^O>;Ia-ztXY z2-ITtL9$7rgBeJ>J+}@UeMie;V)qCed5-6OT4*@1ml_S6@)|n#dFz40x&Q_Yt+Q2) zPV2>Uo&;R;NgEqAB{V!-NK7~Z%%~t%^AYBg_K!Sr*18t{b;2t?)Dwljw*;nkQBOI_ z)=9gQ)9>)3Z^^kLSHi-va8kom-LER1b|aG(uuME@^^TTi{~v+6=mmqk5l7Hy$wx{-(UDpR5QDRm9wo4EixpdWq=l1`Zp&aN1}2Ghpwx6ufV>%V|DQ{6PxVL$~?w7UE)^r5B zU{K7nnad#r9<1>ESQ{T7K7t> zzEQrYtbaQNcJaVhykj)&GN-yMIO~>ssg)3%B?&pn70hdNWe9qmoaF@Ld>9S_p{r( zb@oV*2G5QEywHu$<>?WPF)WroJ4zSE9>w;%&QsIo#V`p-{prp*{{K1l0O*9caE8bj z9e(u|x}!ym|6LmAy2rds*izy+bS8dOPo zoub>GOG{+E7yEK;CW%h(?)-#TQ@#)#8E$6w(9i2*0U&Br2tc_o*)N65_o-MUxFYlP zQ=0=GJR)tIfWx1&`si+$% zAAD#(b^yYqoYf}dW*+J&=)MzTXI#vKDn2RcD$)#`~D6iIa7Pp|t#@6aafGFrNXrKL$I+n<>|tca{pwGcFFX?7fB7|My}6mYI{5S_6HJQ-gPWGn zUyljKe*hY8Uc^+(g{_PRqKZ(r`LRHGj4XWZtxMTk8|p|`M7Yzam5_dRgPjfMp@41Q zb2@|GOts&d?W*NDmw%H1=h!Dgq8&DVVOG?e?9SsnMifS_vL~N>s-1EKbIv6!Y3`Qk zzHwbeqq~nh3NU&1Fw==beFZ4!!B~c#*jRI(oCB{(JW0jnSkT~D(Kxy#>CW_qHlhcG zZ`v+lTSc8~&!{|@0!u2{)+}%K1+{#>b2|mt=i8eUuw*>S6190^-{O6lj;@a9Q1AW^ zA&Au{?y;9(>9jH+2kh=FLG4~0O0C| z>V5#yXP~+5PCL;2aR^2Ksm_6y9%#UXmBQf~&$pCXcNDPj1-5ZPi`q`nS|RqA~w!Mf7 zQ{CV%-q?^o-OUp`Qv1Pi9+=&>%*#;rj?nwpE;W0u?H`ek+odNzE%yo%KC<1ho{je? zu24b9H;$fSVN!zjbUe`>p)ug09cG-1aGk#zX#~jzQLi%m#wl%bu8S)@6!N3phH}D83l86l<_pT%uR5i#dO7;b%)e_nc1;XCJ9|Lr zN30Gn<7if!(W%38KMEgKX&mGOvEEBYF?ly?jr#}c5CAaYU~;n%?7^bgAwgqT9iV)+IGkvjuBXBkqjUBjOWtO)g%7k|^k`o0C$Yvf2O76_ zAJYU-ToT5a3~nNSJ~1fypLt%?`#Pz&zd>zX7dl%#Iu+~iX%jjeWHTYu_3>M8*k%l8 zBnH+^|A-;J)$8h7T2_1-qG^v5ii}lmkgW2m&l3{vlbx-&PpbqkzI8W6y=(bnx)@C) zbry8)OhV<+#B<^BuyY=Ty5|Qt%5bS+y?B#e&Rb)UxjC<`kD902+s#v?p`Xi@oFHl~ z&H71Rod}!!1XwHM2;c1Ow31g1OudOIEfynPpF<<{p6a{QPtApx?4mwA#-Ug`KM6euL|_(%xs*1wx<~aF zEq+jltDMlJ6%(#4>}h(Y4j_sFK_Wsz_}GEfc%juZ8;2`ucpa%FKL93~e1^SJ02$7C zseZ%BIoU5$h ztWw6@ORtuL5KBm+Q0Rh3m1>)Ph^8z#72O#<_mn=R|H<{1P*O)7tM}^wzc>v zFH)!J<)weZeKjG9iAlvU9oD+7fkqoc4`Q*XYml?GGRA*EXg3zT!Ky;jZltLb=2zkJ zT|f4)p3!x;ye~J&276so`H_H?`rN!VxAgV{b+TtdYuVG)ThU=deW5fcr6}zuMe0 z^<6EIpM4cGPnTN^-b+^y(>Rx;RjO=@&TXq(uB>?eKd5o&zmj8UnjSE-`z~D#kU>LZ`Op>Rkfr zTJJ%$5D@1=_R4U7VUn6lSsRMXzBS{%v62DgtAI)frK>Li&TMpv#Cs#e?zFxKBf9mk zNK`TM3TxYd&LXPztLsD{4sHBRH>b^V(m~_3$I`*{KZGFQ0**=51nOg5TaGcu@QAq3 z5=Pfu$G!k!+nh=I?*c9WG(CMj5$p|6t7xFB6XGqrthmBAnS4Rl8>C7H9V84?&qG%T zwwY&ZlT`0|neA;xekpV^rS!c<$?}=U<@+gNE*~DF>ceXDtG_$)NuAs+w>NgpZwi}! zuCw*L2OJj)s;ZUR{!h0}a9|g(q=6ES9?p|=l4$VoijThSQ%G$PkbTRiW}+$+Fp1@- zT1ZM*>(nW9a**agA%Ep%JCw&3G(scboKtr&cJ9-c z1Em}D?@cv@u|;ZAX!IsMRIGb(Qi+pVL*iqvCF_eeZ*%_`VnG$HArHBu)ik}9!o#Yl zwnlD?4)GF;y|8A{S2agv^1r{75h~ID%R`X1=n=XcxSp-3|0tQLZz#*5p)!#nz6hpuS1PKJt zJaWe$YvGods#S*5qG&&x>@1p!)Fs!q0*AEv#*L}aF=D>ox!5ET`BB$L_{dg$Tl@pC z!O^d@v=#Yl2qGXhl&EQ&pde@@F)uWdLvQ*$Iz=!q_12lf1foFa#k%5F*KWz}xA~5n z>jX~d{DxoLo$(#f$$KD5qONAn%bi`KPEYQGK3I$bg6t*XzWw+6Zi_1r!6zLY)_hP9 z^TP5@FrT{lodAqEl$H|w3e^j#4*s$4s+=cQN ze~dKJ@u%nNXEIP_d_d@XsO&n+SVw{o)Wp`2$%jQl4H3iaYBSasomOHtOh7rK zvJKx&ucPS|ZL$8>9Vog8G6%Q8cJM)Src?vUY-lp9DN?=sLHWI#KQ!oM=MuadP7`VC zpklismY=N;)=j|-OA;iDr?pdkACs1%v8|96!^iJ}=qcZ1(I;7s_uCMrOD^z1B@2tK zPO2057Iyzx`*qig3IIZg>jALY?uH?>{;nsYffA=20Nz^#A`v|PFyRvS<-b(H3Fft$ z^^|;8z?*(PWkj!*+3Txw$ltNhw)E}V4Qmr*3g-D$BQ5g%>xo6dmu9v<`H3mukZ27S z<|+`u$9o%7w0GzlgPtrdAF zG?bQ{wZMb?j~re`Bi){BgwFR$m%hP}R}$-YI0Ov)VsHXB1={UW2L)YL=3WOoU+^$a zp8^5KR(>`d?yXe#io}ayR7@95n>@oN(%^Yixn8^XpCRDtyZI z%`Rz6>;LSr_I4f#0NTA^WHbT;pi~$%-KUmOFsqJW#8#lkE!913P2VpG?}QNCkk{}| zqJ(zeRz zggDPj5(M*pKDN#+gESR{(93cr%v#YGgoOJtKOz@ujaPDW%}o<{L-@h`Tq|={1UFFc zN+O0~A_Dw8`{HDzT$<(j!{QTcbYHpo#N_0|h(gkRxYA@IH~%rK09Gu<;AvfHRFH^i|+;R=hKnKTTGo%#5Fw7tn`4E>aBGaa7&V zjsK6OtMH4mYogCC%Th~sN_Q-s>e7v*ga}JF(qX{TUD7GtDIF@^2+|;>gn)=BDYf4Z z-tSL%?zuB}o|$vbB*jBmia}mBTF)}+s`Y+MVp007h2Zmi{MCu3sLDojgq6`b{ghZc zbx|B2n6zht?qHjU@=>_n)ZEjy_aXB7EJHC%a$c5UsvN%Rk8sE9xDVkTBkJUAd;Rg$ zcQ^V=H9%ht)sJMU231Tkh3Z;l>IL=6WZ`DUK$B2{A*6y>v`6^wPfsOGWcMFHRYlU<1wQ?drs~aPq z63`$rkM%;vj+korroy5C4Mrj*xkenoJMATCNx##>;^N4x=5$Yn7OH%8+%+3eDqcLh zza2k)+%=dDA&r=QOOA*y@l5E5D$jnzy=K_e`z;Pz$jP?=prir~Sa*ZFzcxZXi;j={ zBN~RkmgB_1u*CzSw?6kt$s5HYBAW{2Cm3f%QanI$%^w=K%})gP1I+r!Q?v#49=nN4 zs3ttB3&{P&vaxd0(tj62zY;JgSX7eR{hKVUGFg0``C+k93fbtRHpx82T#yL&B zp=OG2j=>zA`}EtEA3M`&rW16E-;l8u3$yPJ@Tl^9OiprV7TCu-IJnQCmu}dcJZ<4L z^?d|TYBeWDWC@YzKO}il)x~$Kw9bQN4QPnz{^B2v=!XHuYre7a7cWd#thtQori5Xb zZ|%GP=KD>H5IZ05GhTK6YDA-#W&uhnu+0V|KdjT@vySitg%f@pP9zY5k(@j4Wp}M> zb*X|)%X#Sv>a%M~fU&-OAcJ@+sWRngqkR>z7;7a``TmX0%~zUR?{<@|xepv@b5g|T zt_E?_ez^60mV;qD;Tfjw5OTBWyWbCmJ6px=(p44oD89_5HPeh=QT}QFh`{uHF?0&R z62a}E05!@>Fyb#)?stdS)O?w2cqBc|noh1m21glD@=p;m!YED**u%F^8min_Qx688 zNd7H9Y%Gqi%z)(D$FvR52M&@jjcPn5))mCOpm>(CXs?NhD*94D>-nF%%gv280FE)i zXE~IZnyCX(i3Z{|J@@#Ip?6dPt&S@?`y~ftPt*^Owc@S0ImIA{edWS8M-Q$V{*YJL z6!Rvg9qC1gc^S}})FPU@$UOVE>OQv2-T%$q4lX9*WdLNuoS5*Bs6Eb*+qZF+?Bdu* zJ=RDkBKE#7^@~&rS8^d6!MqRO0dt-Bo9&ptGh=*CN^OR)UR+eH3)?^cNu`7JZ~2Eb zb69S3XU}6|wTBFgK*vZa7^E8Ki=~x`JwBPO;eo(Q~20Nk3$6&q)C=k_}XDk!TcprQrFL8TNnW+eWh3-uKUZ!Hz{|7 z#si>FmP3WM-M`yT5r@TBPK>k|(qg%iP|sqbZ#6m~Tftf2!8t|@WR&0>WotB&(6)*ArXEC|R)^%^w& zNgLinagOP4OypPhfMmcRd=fqz=X?r@qA+dp&vsRB;K#9aEMy+KLcBZ)Z^PsZkeIQ7 zmg4-s`(I+XkE~7l61iVZI5?1!>CG3WvAkV&-zPpdzo7PYB=eSsfk#9qyZ4mkdCMk2 ziAcc$ccW1+OpHm(nj^1niPsNpY5gHgxkIx5jKa@pLln-l$Uj8&weGvWnhVHVO}?g0 zf3$NbCjOFEzyK$;i@hw&NU)zQ@Fwf74)yK^ZviY0*t|&IeYeOHd#Wa@16zwHVW!mD z78`o~J?~V)9~3d0#4W-?J9(5sLEpKwUXZ0v_*WTAKpdPpw#%wc66~5?-eHsje*kN+ zJ&EoHow@sVR-0gzBGENw7*UWCu83{Z8zq|r+c(s;_XjZ9$;;-?i@>T0Jygb1-O=~a%Z_eX?b6V{G0y`cHO4#2av>)3? zeLsBoYd+vF>>U}ZpR==5KwurW3NI%j=kJpnu%Y|=s^R{0mk8&elv=?o+aQAq(F@pdUd@H9!RwrvK`Ke9DWB zCmw+eU}7O&nRuxc^xN;KXJSZ;t;MgcLa4`6H=?JmI$%d|3n%{bwkFTMPUQ7B9-F_n zuiB-fC~O9i&~{p5-e^v(1vL7`{N}+b05wOzMA`>2dD#+P-n>JjXVB<<01F8!Eh7%o@E^g=?40GQ?WvG3?qg5`R1b7EgdqW zRE!js2%5JH5MsXh!^JvhAW5oKZYx|oGF+NY;h*-gP8??ECGScC>JnO)?9k_bNjP*p z3>^$d@HW7N8k8%<`&TX90hoT;hIQkoItirRA%Nca|m3ZG_8JV6A78 zBPH{l`Co26MU(jyuRT|@&==n2c-h*f#O56oMjY^n~FCG-=F4eyAR)KBjR@V|ggdr%2$g{x+$vS1b6lChOl0jftEI`|t* zP4ZFM-@esqDVgeyJ5!8X2mRD)o*wB8o>&PGL4|E^^G8@-oQfrPI-dgi z^g1*)$8YfWeBimx*{9Pj^L7D_^p?&*udBoC@}&hUNO-b|wZuQ+*2jN+j~vPIf}K8j z-&b$9`IH;6Z0#sa#o(wsCgoRU4;ERG$6O&$B^@k2D@2iISX#?UrK;bUj?cj4rM$L)1o?hCfz8uA5_62Mb{nmFCvZKGNrurzw|Q4 zNNELTUIACLDAtWft_ZOg=Xn_5WkAZoYCL^sg5o*n08@EkF2mRxnU{HCzdlt=u6FQp zTd-SW3~XxFjJo>bVFc5>9&*3U{aD`>>8nn8@1eO5>BHt3naQAWGY!ua_T=G&0nO}( z)($}O~{)1;frr;67<`P}Fegqr*f&YVK$7fs>d`OX;YiHvbf>+cIN)k;}h5K!8 zYWflt39j=yQOO-w%SFE6h;K9iz<&)?9+A_=*3E7>ezrnv;b@fC(cT9@>gf6aHm9tNzFDw$ZGh zChz}ZK~c1EHv9dQuj4CeDPI$;d_~3!6oDcIy)gPR@Q$!wywALUr`ho-a!1iAsU2qS zsd+xsNyA{bqo=B;%i~(K9<~xEbCng)YY)yZ9>?uA`>OxeC%z}pWq#*x-Jn~g@Y!R( znSbmm=pWvh4OvdDFGWi)J%0$h-@-#)-Nn$4PRZom3YUAlVn%j0D z7n1FYagp*5DgTar4{uGiAKqELpwas%00=!?kamVE;bh>NQGb3;6~l}XKLhEu+#rV5 z1qCxs(dZ0z%kFmL+ewTLO^ilu7M`X?=Fwg>g!B2n^Y3Kze=gJq8IPtcQLxz1e!A(rokXAMp@Q>qU!cf%;oiIWZm(Y=aP>5a86UsN$18kL;1z!m za6)7ti*PH`je6SmLf0mOx1T^>GUn3#U*|bx#oR~Z=gS9=iFUtt>_|7WBHJj4?W*_? ze5K1C&#b_~9B{^I>-i+AvL8_i&xo!zZsM3Ho{65)?ugzr%fp{hZ*G#gO_P}M zxcoyEnlvOC?mT|N%`@IeSN&$L#uZOF9yhw(J<4bDIUvdik_-j3U9`u{`Z7;6?L9&5 z<1(y*R8&m@DhRi6Ic2s!EWGtneSFLsN#EAL4ni|nV>=pM#Uj|UtIEYxS`7?9!7a8b zq?kypNym2CV31NfG4Y($A7fl>&b#P8B{&L0TX;A-Q^?I`+rh+p(-ap`C!qPw4x~q3 z;T@6r=nj=BBfuJkxv!0p;cQIxkC$%TBGV`U)!T>{S)gqMv1UxM@4LPjC4=^fXCrL~a^R&~XKB;kr;He`j?DYu75Wx!xA z@;01Rj$H^E4uCLoc}R+ExdjU;PT06<^tJn>I5YT##spO?uc>VlcDz3*ybmTNNEcY} zp2#rUn;O>ErBZ4LiBr0Zq2E9sKhp?p0v+6u&6xDAwcX$Pu!hXb-Ro`f#0nE9f4ql5 z@i$Jy0Y-?_@_Uc9iQgXpmL{OS92ZV?Ellb!RQpGy#$A+(7jBfm#EL8RIP;%Sma-b~ zi2;*&z%4L(5EZ=ng*#a!!0%5!I}qq|%aBZK_#^}2&-*Ba?4h1rwG&}`R{L#$+2`Y? z?8tUe&ZYt>Kvs}Vca`-;Wre7$K_+xeH@K79^f5^MW}wQwfqe2?#F@irDi5)?p)$x> znfz$n;>YXQob%94RVtCM(LdksaY*YSe;Ovq_p_Hrx34C>`qeshH(Xmp!SZoB0pht0 z?(r8??6fFb>K#7xhL?o_;w*R`VlstuL~ zu&Yf-AtpsusTc>EGbZfaL=qO%xX2MKegVN6uo%w#_o0dR5FX#(BO9FF8pVJ89>_ee zP!3pWh(`H8nzb~v&nrDW$zpANIFh_wu_yP{m3q$oVe=5%GsY=S4X8f7Q$j^YsA~0aWkD!FOR4bVLDZwKaN`&AipWF7gwlwpo-@D5moC($I zzCBqhYh^~-sgKkDm^1?`f1t{Vk#H*4etyzPeO#?~E?}D=0a|r|%PD8jY&lKnyic}0 zpQmd>;)-OKiIctKE!X_p#)e;^>n@lfmXGyyHS^-Y=@f{Y9_TqT6MNCwmZh+{)$M?} zx-v9Ns8=~(%5fDNAytb<%`VI74mB`${bhdD`53*2f{6QqK8}Ia-K>`*^6VX^BNU## zQ-KL!v`G@7th$k&l`?HS_1iKnud)Mw?>{78ds-g}ccPj6cN+=UuvlFRCP^z}pP|PR zQ`9P7AO@2r(dECti=o5NK5|}_><&3M8_XycU-d_Kj`fE!E~;^9Z205(NB!TCu{HPO zwc9fkX%h%s?TJxf#g$?uXR(Q?d8~ywgRRo15lZ;9yKHBcn4?6>dib?9WWn~GT)`2Q z{$9V4m&t5giww$k;fdkHPM4pM$uEO{X>^-z_8WgE?8es`kntP-)h|aMCGy)C2i0N!cFcL6M<9nq!Fh@un&%3@pm;l}UrAcwI9oS=hiZ55LrR+l z#l>&4v$7ZF7_Y3nvKNT*M+mxQO1nO1to;bwM$aEN&Rqa$-H;^l{wG1PI~8FhIBylc z6@Fg|lGEvtH+((Oj7wfSw)DL8;Ib6cj(^L8&63TNUdbWQ;AaQm*o77|A13wf3sRh7 zW)N`s!sK9Bu(NsNJk%;cHyxDO8P-&Z0x)@zT#0b7MOwj*7gQ&TQeg&}??$w#F!``3 zid}0~v|IPclMqg*Td{L+S^M-iN6WPRxla1_*_Lw`@O{8^=Bc8rui-cS@JB9wF*+85 zXWCm=XW33{6!+COqz}ZSF73zdh75IJ@#U3JSBc?twb{mTzZ0iWUG&8hH|eJ6xK!L1 zEXztWP;)EW*<}1OD_TPqS2+w)xuV4Up11w|`0N>f1nW_Hf4*5&mW3X;XxYrGEqH;o zCoKq2rf#2vGF?)$Ru$DfjnsduS#I;e9spMX6ep;3 zqZrFke20;B0Bgw;)w76D#Gq4&z`^p$9NXjOwiz{a_!nF#JomKOnS~&&)%ApKpvL$* z;B=dW`DtxXnMdWimtRRG?+yWJ9+}MS$C9evV5txclpfJ6@-6n#g;&34)2N^}u5Okl zcq^#)w7mIz)-eii5SKW}dNKd2-f=3E$<@h&9LF$6OtPM2F7VT!!tXJM{J-6oUm6hY z{r0Bs=dv|1J`{2Z#&8qsa0CO813-dH15;d+m`uc~yW?2&btF^jLd6y6?mpplg;X)s1=MirD(s{0HUMe)YQri%sZ!?pc3?6hWz4_=nUfv6p)dhh;J4 zGufE7K0oWuat?B{3>3MHsPlG-8tr6enuf2&xzJV0MPMx#v_foSu=cr{N<;!<3* z+|HNC)J5eCIU%GguLcQ3y|Og|(Nw4YHs0=wdwEqP!5{Y9#$ixsg)f1KqM}RD!Ph-Y z%H6tk>D6YxwS`-)mhbL+w#pf9-l`ahHFi%E(C9hzEo(~#03i4lQRzZ`K;dU`GoeP_ zffx|6CKJCzs~OZ3tE)qX5mNy>Df6R&W!w?&4)`>ulJxm9QI8} z*X5`PFd||nJYlUl4`a=&S*AHhQXlzIC~dmTD~3qT$Z602U5HnbmU{EBIhI#3HS*a# zq46gQe9b!|-64Bq)4wO|%gYUmY~($ZnFPH~W2+cvY;v`-Hx}D4Mj!vlz6E#|5;5Bm zxVXlf5K>RcUY5kfO%|;Xa-g@n#iP(VJb$x{cG^P2Dn{xcsd&Zydw)_SiHMv~DHoSI znG|abz3cqf(Wi?abpx!otz6d3=_%E8c>~k?{D89`x9MfQf#@e{fbJCJz!V_O{G*f+ zzn)1$NhnZ?6}hSPj!Y6PJYZI|#}(t>hLd-ETFjs*k_wx*j8s>(3;%bpGa(oB2H z#qrT;_13>`XTtni?eZHumPrJNPl{VC0x4r$JE!{Ryqs;1hnXFmPIc|&jg@1>!M z+t;X`FLD^Dm}c&@#1wJ#Df*Z-^CPWbpKSY^fUo-)FAMIZEhW%wvFABZ>PKcoZY<}2Pp>d4G@9El+gwe zTf4fjgm%Oj25Ut?X-2kKUsDj{Vc=!p(1{& z7HJM?A8`qHihbR{rX}MJ3MOV%U5S#mcEr*@F+B~*N)=J~kB{>Y0e*`jDd)w9j)USJhe!qhB8W7 zmESwUHpB)-OsthOS4cst?q6m}_-c@Fdj>}=DI69r$zbXC0nb{;M@hYAaHfYTtkx}rsiHYRuhg(qB;pZsz0N4p!%W@bHOoAWfnA5gn#9(#K=kc| zcqJgtgYi~&_+Ki!65>Gyh~1Hq8gVk>S0~n@uYegx>jOOS9|ZxkW#2q@9R_j=R?e6YY3II1wbVeMiGn51ff=b)undjk#w{2t^MDGrJ zvj^!Y@v*~9S)dcd#Y!Uo=;}=^&CzEMJyL|1&}cINUbc>-u+TfU6V&B!+&wep!DJLc zz(6x%U<-=rugt2=nc^74gFk4>o+JyRJB8*4D+`{nrkKPnYSo>5a%WYXBWuQFrYFZX zeaWTmnz0}F>(0XWh7m&)fX~X&J<0kN?5`+Wu~=l?M;9Kwff2eA7fQ)zH^>#aQTSVQ zzc!_moc$BGN1Y=@*S~KVU|6=k?t4`(kza}|!sT@@8X3CQ{$d6VdF@B}HVBYhH(k#7nr6qnEkCISTp=8Ulk1Sm5>%Di`&ro?Bo)Owi<0;3` z3UT+4aNLVr0nKepS>v}Q_~T7S_pKhKd{|aAjVdR$!Y5Hh*xoliB}%1Kp2g6>tTP1sO^0(l!V4ej#p<+n10Zz(rqHZJ!kVWR?mjkBQa`jvO}#s7>4pH9)5^l)n(Pzh zKJOI*3RW{;-+q|!%{)nOB)ERY%O=c|LZpa=7Mjlrwf)-%1yMxl|6QTP9uVqFwDu&Z z0HvFG4<7HK036n>sIu!8DBjb|4^Lm@F=DzAAArNh!Nf5BQA&GBO^QgYNJ?-_`D2`? zbakBKOnmNttP6I+MZ_&C{cqZ7W}m&6jW!dvyESg~E~35P5A`clo{m{1JJa~cK`~R< zlU)n1x`F)=J_!m*v91`8ldyGqxMNYjW1#9e@%?*W_8UW=tD4J}z+3T;004l~P;>^C zaJY{ckR+nTAQ*Ru8C=R6J4g%yfpq|c9)@ylr3!rjm?(x&)$$8g0PXuDktd_CPAkd} zjGuiIlYS@o1p=|uuIUM;(5224jP#a)@8FttZ{{pzwy=5&R8)2YU}y{a8hb1$Un!hf zMeIWoO`8{vGjVR|kzJHiE9X%%tM7zhS*}pruSjm^ax9?dLu4R5`RkiP4R!zJUkNyi zh>-4|VqX$u43QPLYpX8a-Y3uP(SsLm-*8l3S*s6p~iJM{Ya&R;3YL#KK;3P zb8?6bKtXU^nk%)-0i>>&Et#pytPBzfGss|R{Kf4j<*Kv5({2<-)@Zf3AcS*5-qYO;9|RRf z(G3kEwT*EcOt3L=Ft~Hx$w>G7!402Ix+G~RVp#Q!wWWwVdfcdRG3Q)7xcZaMP-?w? zH&B@v)?q9LRX`eVuwUosR3;qUw!XTv(YzyjmGCyiQT~{J!;qq7iQUlBUy93vqUF#G z414t$Q+?2vzQ7o$POZ&7i&NVFnjPRdb0S^{kEO`*o-lM{&WL${HDMsKo5c3@X^i2V z&zr`)0Q}uomuo0SkO%9yl4UrD=DUx$NV9wo$S70OYdsrVIwC4TY_>(8b{o=LCuu^eSF^W zr?# zCIRm;ZD^IY%OAuhz4PR=^HRC#)XXcZ)J}hr&h73;`rXITBIn~#-!9*9gRUX*bkLqG z*^a)7bnKB5_|iLhi=fawd7*W7$$kdYlpAAYFy5^Za+KoNg@0a|tbn!0V2JeM=D$zL zdIE`rpp_ApYShkK>3}HZ%*z|h0XY2O)*V87Fx`JVD%tHqZZ@0$INW#q@S0LNT{nh; z>?n2|`m$(DKZUo=X6;N|pzx4gv@SJu}v{q;B0iN@RPlCYtrsw6m!dfY8xOdixe_Yn|6uij0UL8AKdeFNa(2r7D*JmE8m;fF5H#Spe$ zwe|4E(fj@Bq<8uNjj`9Wk7YQaz*PNG!B1MO5A9oQ z=R1BDmd2KpliTshS}WI6&dnuTuPTb~t+DoE5f!nA+|1JHgBYX3KND9Au82~$Y3>DGBW4Juk%-D zODTD3YzaaIDSp^C;nnuM-g((oO3YK_sc^WeOR-$%>&j_-Q`_9V$BDQ+&CW zV|BK(t$L^N!}llA;E|5U)FkkwjXh?>Z}g-JT?!%sd}gWE$Gxo_@e~Xw8s-&uQnbxL zOe93zMGp`*b}1=j;mXtEC6r|~FIAO!CDvYKp*?N+-&by)UtSsd@CV5Tr^iH|M!_K* z;u{jZKdA7iwTPVe+)*0c))#KXAkBH|h%cjQmGw`&ItUUR#$|NZ-u?*p+7>)2E_q|% zmtU|H(S@??Jh`2gihvX2^bNcUfgBaM|Kk46)}6vvdVe-oL_irQ*v1|l+-N!@zmk#6 z7doRLM~WGZ7`0X8d4^Xka%5%vak>|=%Ujb}cinJSmaK_L%r#BE^)xVC1Ryio3*+?G zG#Y1IPisuf$whor@3UN}JU*{lb`%}LOkq#h7p$eh+j#Fo>yx^@*B?)1%hZ<~^p>4H zG&LjMza9xWERtO8Y;$oNW+B1kqPAwm<%%P@pZVO%q7Pba z0M6(*$r_ZD-M4pP2=8x}IQ`=(!BOg&Av9%6SdP&%OCq>ohKcQ-#*Bj zx(N;Sd%P!|*%;ZgE-7|))%@zKh1@3WX{Y?O>YMw+yKw`oTYx^DjL=f2rGM)zgI3~I zoJ6IfRK}FN(6vcCxXesRLG}DADW{2ouIC~UX%H@W%bzaxN2?iXd}>U<$Rh0{X0-U=KMNh z4>uT79x8W7m-_+8ow2Ci35_kb{pCG5rEo7A)vI|Y+-*}z=*ROf3=gXQb3pbTLI5P4 z0AorlB&R3NbGQ>haoh_V(x-Uj94nb`fGze{ZA5lV=k;WgrB~cwUSmYC;DUR^DL<1% z8q1CP#6)pe9oKo^;r#D9hr+NY?r}2wiQcHCv|#JlNDPHhJglJ@dffZNyiHW^1n9^q z$o;ZXgDtfL3$mCQC4frgMx@8;D4d9n&L+0U`A3yC9gY7F=D&6FKJ%CSjJ)U?a_hpG ziR~a%*;@fVN;ZDzZ#obx9_!J%IFEeBqXN5{UHL!H^Z)>p50KVq@;dp}96zf4jbUPo zv40?f^f8lNw@Sh{rgpyaH>>0{;9hZoH{Y(B&7A!+4*7znnw`X7kBvPK;8CZYyhY3> z)=+L|*1Zh;z58SR8BpOIjH8U;AwfEKSj6&MS|0dKW-7DQ3P+`pk$e$mZrZ`tXLe&E zD?O1O?reXnAbG+4Pa*apNXB(>Fym-1@*QtwLe#lD?S$J4yz+iz(ulBI!H=wG<8PeK z?n3AwR$h{AIJ?7N+unLa*;ADl&$*7TqrsKzvOXJ`w}~p@zWJufr!ObV zK?#qVqrbK}p+VnA$;H61BtJ`0o{~w|^Z!ID-Qosz8H~)-FRAG^QmGx^=Q=Y{sbMn= z<|*?e6JyLOU+!g9eLl@SDEMR)Q@D_V%cpA`(NO;Q@8>rWSRVeLA5Q^S9FnD?W9X$1 zc1rp!&bB!&fzRwap9)sfTB%tm%ic?U(a%v894y)k))2gzQX)cjgpkNAWE3}uf=kf< zj2jyxPP@F<%gDmGm4;;HfoUnG^;b^8Y>Gj&UKCF4SAS5>ICV%>%C3;MH@G8#RH0?;17NT53@!BIWKysjc-M zB3P@pkdRIxbp(`;jE9&u-{1gSBxp3d7%)XaW6eBa_{T>AAx}B=sL6=h>RITtef?Rpx~DdeB_#G#$EcNvAw9vc;DA;2b; zB*FXc{)VF0&wC7$6xK+n}($;}-fEOSrOSrzDhj>PGx=Kf~LT%;$mdnSS! zdYYU1%dxq>-+*~~TgSqmtn*>x$z3WaKmj;hLYuMhA=%!U#2p!`;^%@Tgnd5lf_4B$ zC3XTQqsozcit79K45wp-AwBjSv2~yRHZxr>IN}A*$O*@*wKxcC-66CIfQOjJI%DZ1 z47TnZN!0N38yQ{q?0PcDttW=!87XSLqT2P=2B>jA{M5Bvziqy`sRzQ?kT@i5aB9{e zN{ra`=sH;*rr@z&RtWWqxFl)0Gp{LA{cu;hqnICGncnO$_8vVe0;*rr5fKru`PHle z32$EF@_Cbaf>+?*xc)u)@h@~60C;p`Oa%)=r4k~S1ZS`3gvwcJ@ZJ9Cl}5qox#$Q#(7oCPI9nhX^vQroJF zadX!kUP~0mQ1tCFx0K!1bQ-#(7j*;c0Bke`?vAX3S z*3oxcDvP1{M7T&I*_a@X<=5HZ_jV7dzsT)yXi;ecQ@XPN_&O&Z_qFFLh1l%AzG+ST z6OE=)3e(yfqh*<~WcEMRGYbZ@F9M%POo-j~Vp&)xMTcpb22D7fep&(^aocL@D}^v<)6BxjKLScF#Sxo>L0h{AZZ z_ON=K2v|qPyJkK@ns(oh+&DCMXYv8XoU9OdV!WiIyU2zj-2JO4D+#V~X=yoIH@f&hysiQhd~ zw4K+A?(A93efqux2uuJEJrz#V%vlNAQ$hgJH|4h=1Bu7L57Y?l%OT|9RH-i7fj!H9 zp{IRh8SZ%YiloM-a?GhrYH&yeAG&PE6jf=qMfJ86i?J) zmU0#?UO{lObbFijD3fKU#w^k|R+dV1UlXPa4eUaK`uLtF8FpH24_87)NMx%^>WLZr zh8#Mcu~QS_=qiQ|BZIZFHDVud+54F51hvfB=;L5B$h{gT)0*ZS(I%=e)i-R))p>nX zrIP7mac=Q!o#$NJq@=S_Aqe^Wvxsab?Hxk<5VpgSCW2)j|&N+@*9} z8@zuW_EbS!08EiNmK`)1ZQMQi4Ge{%1p1Fr{Gk>G{C#iXIXJ17NYxhZ=4Yj<&2}Ox$S{54MyPMUkq@^h)1JIX{(iO1KCE|-b2K|cb=WerUC%o*6t?l7t50-NaokGvpPE^d z{nX66ue(`!Zb7jHNT|4jSK~QooN?%(A^S>`+<4XSYE30buv(Q_q41@e+ z*9o@MU@R&QTIR5Q&-xF6Z=ch962Gj8rzMN`=>6+jpihYYTQuC z_G9VU(BHD@SF1irq+APEnP-GoANL=l|N4GlT?>**q1asd?^hvRkR0H$2dRaD3ZD0r zunkslX7)td5?KTX2hR(>31f;jQZpw0komV+Uk_({(0)4NQ9t zjV=e?7*FZ^kng;2%WVO@oorf8P9iE#tyM!j@g&B#GMQa}VT}}$Z^f+?AGPz*wsyIn zr%ZIE0N6uFuvRscR*YdK&;E=Nbm7a4FN%qg63^ohj!6y*5b`tSp0|@!6-CAhl39$8 z5QSL@RgOCNuvs04Zw?p=s!|)M5`T86`y}>~{^|QS&%5ef;%8M{2XR|5gV@Oz>C)R? zLK{+CBitI2SdF{i*z#IUMX~#GjsaUZ5))r&U6}hWj#Y-HzrhuGdyz;4qX9`MA5F32 zsHfMMOp@Nr+!Id4CuSahWq23L$`Hh9%FrstFC4m33d2S&wpgsjt|#H{xwqRr4=8A0#7{a3pB%o^x}pyn zbS*o6sPxr@r+TF=MMmZkL4jeACNVbl{7%H!z02f{Mhzg&2=b)Z>UY~bIK;>G6~gJ7 ztyW%S!%!LFYD#+k_R@{>Et#nXM6p8Hf8K`7`#DV9-b9EEf(4hK&BD^i_!d+}L&I7* zqLBNi~!e&zHQOs<4QH9JFg`l(W;uO~Mj%(;%$)d4f8l6Wg_m9_23zM5~gWQ_Cq z0rgrS55PeMdZ>f}C0xvMUiLN9X#>LlU{N7O>~!fwCXF#j-+N%W?s%DM>XWYJEm&#n zyC0;o?9e)kc|Xm&j0oA?yYaxUO#5)!N-_28BdG@l^22xkVh|CoZc}byneKl!w7*e6 z?kAK`Xu8V!&+eRx5Ma;=K!FA>1><+CLyXyL-*rXtEIK{WGjtfA-70@a9*NpZaE3AqN2*KuVCfHIX`IPW=|uOGsQqx2#Wu z7R&n-q@@TeoF+f)0|=3F4Q|*r;A9vaM2|H4vCQ_N@=XQw;;v|fV!g6r9<+|H!v(&+ z)=4EhwE1DSu%;1X=J=H;+5nJ7g0D>BRSFlpEDTE!I}DGASQ1wuh%jdyh6H!zBzHca z#44C&_R)d3G{MJT^7Yn-KS!lgwES;Cec7L7{XN4Au4J!D8_xolb?BTMbsv1*stTFzPda6FcMU@F?S=iyYQ`%IF=XZrs^otz*=Z5m49PAR$ zNgkNNtC1}WrH0qjWW5iL)x+s-aU{UALWCroS!qRz+RGQC$R=ak;WMQE=KO+Vw<~ME z>v%@#O|JUP7Z=(^kXDnqm()0x^E(MgM=+Tp0rO%= z6a2c}r*h%e!G-YgqsfI|?_d@t1W+)%;jQcFft}YjRc9Pj0LGJf}`Ua!5+t2mQdX9SIV^1 z>QAyiSQGY_wo)8jxyR>$ax+yX4$u{skMGRG8TC81C73o z?!K!O4nq4L@v72h3Le=jjIj8!YvJ0L2xuV(f}B%}C&tMo1wa{?h(;_t7J?z+Vcb5f zu1!X8zAVf#J%g27V@rdSt(qRB%n_eTg4D?QQK zbD-zg`ti~4(!eKKf$zS3otgWN*Y#!NPuxYlZYz0p&wcQmIlwQvfG}2of2tfn21nb4 z;=V}9LaH=bb&F7tYKTtke(&d`C1{5Yc(fN+g$*IR(-ar}TvYK#lm+N9UAFh@Q^CIz zYY!&8b@JQP{@&`UTX-C!ZW#PxeF!qrF~?nA@r(EVr8oshV}TAMc(;+mhgxKTmJ27l zU80;td93Azk{YSZAcJ<2F&;A%$hIpSC1DquvedSuQvLSiIo=9&67rq=Z=oWgT%<9tOk=}>O`Wn&(ls22< zysYzC^?7(jf1*onX5Wy$>D9QDEj5p2Rq*}HMoI>uGpqHDw#ah)ghpy0R$U`F2y%}Q zq<55EJv-A_+ts@KukRBHSbYiW;Wk!J3i%fPDzyZ-kR!U5UH%ho+L~S^7TOn$&UZWl z0LT&&jDcW6z0F$x7s^N@o>>@CCK&;)TlhwqREgbIexN+7dN~(58rrsM$S_Cu_vc5R z>d&sD6!g&^pcr>ifEt@fM}sjs`w6cgk&J+zncqxil>R;=0GdD&;6_kU0WV+dHR%oZ z%jYx|s-Z;kfZ@^*P#n4O4TWVhFSNUMY@6u#pUi_$XrHIFR4OQmE9kS)a_IJ-S64g_ z2OQQ$XCHe`*U-2j5WM(_q?l`n>{{mryu)^fT_BSY1P?_e<4<@peP(&5Ln@BPqKN=a z(@pnlv}{;^d|f+R!W`1|)0sPqb|su?=gY|J72nNttDdd4MG%tFx>)5}n%rXe(QXUF z$@=w7Gr^$BrD5)?UlaTi*(^`llAG^ISrGz@SgKsTkt{E{H9DFhJUIP>Ow{4nTCv?^ zG8n(*)CVEvNUJKeaKR84=9{;|Ppf z#m3Q*$F@T?m_XqQ(_ccu&Q1Gc|Hsl<_%+?NVf?o-dLYf{(WARVy1OKm?ru6n5erTj}DkJyf3Mx0={+H&ywCDjA*~pD*Vx)S0{CP`85{Q6} z2p2}n1;`KBsf$gu>8`CN)?*%;fl1RzT)7-sF+N&Fy?zQsFVtz0s!>~pOQiT%&$vxJ z?qsVh_}JkUP+0sZS;+^t>di)MY`RN(#n~8s{fp9{NF13yKz71D+Z)6N&f6g@%p;`o zgn|voO9+N#2g&%uYPg6$N=|#f=r?A}yb_29d0gp^6RR%k@*PnS6o$Wbh;&}p_kI_#pyKx#kwDa5cwWhlCYSF zX0N@&By4E>)uqlb>-3pxZ@JVOgWKFP5II5gp*2c{yNB&bbb0pdwI1v!oJ|?PKLSj zZ$(i_i(3gV`guzGua{x&ct72)!-ZoBfBfyoRY|if;|E1%IEnr&+-QeM$uNqehUc-i zjGj$354L0pwAa49Xv*C=*OAnMosE>m;auJFQ{#M&Ta-w#MG95+^RElAyax!^=H^DA zv?RjZD1l5dDyFqzwb$rNV?YcJew>HJY)_Z!>-(%Ui?QS`AqNT?JZoQ& z;;Tkg#Gq*_I^o-BZa(IC7}D9ANbY3&Wh5l}I|+*vQ#bVIy6}fiO^=ivKbe;J99x!8 zUqU4(~ z8UbXawbaBiLF`4;9)a#w({NR!F~~cK*`PbanYrLM{llMX==P+L`#balz))dTD}14e zvK}@|^x3q+HyxH}kZ!PXBzGtpPNi~f=xkbR>fB)4M)y$#B+7CD0Ko*1Bf5jfu4>1w z(x2fdD?g&)62C*iWWup3Y90Gxs1o-u@+$_>c=}t$WTMi8d`AS;fJzeb4`#mp-));0 z#kG$K*G~#6>@A7uUnA#dqO3dE7~U%lIE!}X%7Hilarx-jj+dVV^0qr7Kc42RuQayiSxSUv0e1lvD~Ey zqM_YREOXen=r>9T&g}5(H_~fm`^h0|1=Mcx;UJ;njl#*I(%zn`N4UMg0^Gg>a=*ak%X?<-Yib@D{q;9grJJp!fJ(G^4?a1 z9b!S6oCNSXCeQ8H{hlfIQ!COnqCVR*bb`_w|15TIOhMuWN)udwRDGW9<_6gpyhan2 zhjeCB5&jTht51{uoBVOkYMv8r&&TEBLl<8U^zn|!n~9x;?~oEdv*2Ol+UOvz*2hJG zI1cH4A0eS7nkb%l2qAxUi{vtoP)nqMb(s)K01xU2#l@n-RWGp#BpWuF)7xMoM3m^o zk2OS^i^ArpHJLRDuvHsJWuF)vgvfg-HJs)2eIS;$w5lAbZWy>*2zOLAWbi)TPbYGo zXPtY4uk<3q@Lxyy_vUE{0Dv@qO>Zg_`Mn^>6mxc;M`p5-O3pbW%@#nkBJP+^R`+^eB!ZNn57SHo5UPUz0! zLo`|g31B-|zsZ{8ha>*H%9NX)jNunj7>gw)kskca5hDo}f@RPqstNWJL*Gd>*0~`&pUt?8~r!R&Dzr?Y9ICj0q88BfmL{AYdD+<{}{= zcr>0f(vgV;!N3`hGdSI?LA35_sI78h+CkRWaZ;tBKx z!wO4;6{^k#27i*klS1))g$^wpC2 zBN5jZ;FimFaa(-TU!2&rmwo50?rOhW+@iV*GZcVotTuTeB)|46$Ox0tMHDb8%h-kX$(;vtRh4|Fg~X}7&dd*UX?4P z<+Pms*4fY)eeI35csQdTlWp?CcyBsXst?2M_X$X&HexYAu_Bs_u1U&E`h}EiC-KLA zNQ0L$H91Ji`hNQO9I~9u{7aZX4dpecf|tyK{iNU8Tns*jL$5c!Tgd1{^O{*OZzA}* zcmByQfO$;EyO*~cae>$+D)p!}th>E&fYpBgM<^M?ibIX2)9l0rQ=K_TSw|CS;`VgG zl(#K%Vttn&8~inaF4UNQ|2?i4>zCud;Ce)|xx={mdQTd}Wji#K>Ipl~SNX@6{q-qr zR&7_le>~O&VMO`nN2mG%thAZ~$VuL~?BnP}k+PHoOyp~A&#nfm+|hq)n^FNT zH-H3y;Sl5{=F+}%ho$+);_jGY@8A#+uAfXo+?y%s8vUucig{tI9*djHw}T!UhaDm= zaZlf|pO(=ozqo0y|F}nSY|qU6_j+{>Kgz9;>P7GVT^1HOy`E^bhRqm6In7Ps7RhT4 z)|`dGaHIriqA!T-;LqgL6-i?NrU*S6#t3?ZY@oAVMYj?^Ys<502A5retRIMkAAMDy zg)(7+BX!1u_-wL69EB>LwRuEJB;Z%xSVq@ zMcqOiVj*c#n@^G>;bn^-+c-dNFa43!#%$C?U`9?g1Gr#f;cg zrf^U<9lt~CqAy%g5cA_TnrbABcgL?fcGpx*i_p{5A4vggF7z0Gq@QiW;zabB+(xaS zMVJdhn)Tf%aJpPwPy0R3DQ0+DTVAi%_22$Lsm%hJgF*ppj`+j7@>Q>`f|BuJ`Qy&ZW8SdAwj*>SPtYvG49i}xMn?x{?snbX-pzeP4bMo zj}3IM?8()%*a{hUaj|Z~!~f>i0yW&Y^h$8#HnnqsqgU&1yFF0=f@C&}L?%Qrapsur^I8czrfxnt7;o*@exZDKsTM9TIZ%d}RoKVqv23OF(zg(xrn%b+~+HJeN3g(q^J# zJ@#L7Oqp`K%0G?8;tYY6L{giCI_q>vL){(ITZ}|riOqfdvQ+#|4Ct~3*(kvoqaWbT z=q@!l;6^HUG)Yr1IgJ+Dc-G`Zd(#XyFMgmRqaMyx@xqPghKa9nX6=J#6`1 z^!AI~A)f^g3$JKrO}ffI1cOu#Cb4(XzI&}~KqR|I8hw*A5&#VsI#zl<_VZRSl2GqW9`=pTQWm=NJl7#;04s{t1C z^u|KeC$IC-#mPwtHr@zs90k(eS~R zjG{4YzK6dm9=`vsYAB@4@*(o$0e0pAwW%ku8b032@17L9Y$O2Y$=XbTEGA)dj@f)l zncI8Jt2pYeC8l!OMJam?juAXt~*WjMGWBxiIp+B?mh_R zngBpuIHSfat~x41h4;jE6Ip7cXrVCN7C#O&mMO-=BfnNqQ_!b)-(II0ZWCUBqbGOa=1AOTVy@8Qg?Ay&Ka!hZcIrLB+8 z#rR8_IVGTm14CF*?y;(h>hGpSZArMZA#SjVK|UA0PRL=`$kMOxm~z5#uPiLggBQ0~ z1Ya)RG+pL=Ef>d&ij0k5lo$xym}W}=dzaiX?B9RmU9D&%A(-6Of<}@2YI}n62ciZ- z{5=gKLq3EA+9K0YmO_qg#_@#y>g*&8jG)9#b?*0L5(J|DOI5F$I_Cav;as}y8;R$SK13&=X>EYJm+7l~QC^`ChCxc%#e)PK9@EAQP&fWK(qCq4ir)E0_A znJAgZ)mNHOd4NqRLWD@h6Tl>Odfbd;+pO0PKNC$K{sPA&#$XcrNb_4|>)u68j|ZHmAD zv`Q{^vFs~9?O;6PtgRH}kRgt>A!vR%g4phwwT&Q-^X((CLi50%^0#6 zsag?hi0La&sw^DJU$9cPU(>U$iaX7c$`jBp0%87l!&ZpLmfI_Tw%=~~O5L%qmq9y` zY>!YBSoUPZ%@J%mAw^r#2yMJ*EnhlY6$EKinGlh>m6Bi#r{wuNa_R*Ir=xJE?Ozn> z%t^&bKMzi(JEm&er2c#hTwnIte7+SB-smB66atVMJ@hk*clElW8aF1w%6H(1E9gbd zl)UE%GSH#tb)}dn`BK;A&3^JaM^`_HBFPP3kZww$gPPoJ4#hxmrw!PUYgjQ=`?RQ8 z_I-}a%@sPTdEfR(Q1IJVF^WH#Vc{08jOxyn{fg^}OPy&|=gOFIj#Ff?G%QC&25&q; z+T;~T&M<1=wV^jE+Es)unIXr6A_!OR^(d9PJq4E%fC%VHeW&2(H(IPKrLDlRv)aRT zC>cO{^j-6*5fwI&ns|OYry?Pb2!)IMEv;y@UCyEAVK*s8EZ< zt5Tz(`kWI{c7oi4f1wNlpEXuQOV~?kiLFCy^qd}ET%U7SX6jCma$Yh^Bjkf$riiNv zeT!h`IQd%`DLkjqtgZXU6?yNc@UR{SwUKoYZT&fL7v*FBsRU;zEo%@6rj@NY%}Wz$ zHD4(*i$hIAw4lqsfQ6dcen^%6M_hZq17+XX@j=vu*M9L6j8HT69I*wC_svXy8S4MF zLr4rwma)PDvo*WuYjdBdGJLgBDIU2%0kyo^fasLtzv=N5IEXI#cZp36;AQ}P*!uc< zn2PY_(ld|Qj+oL>6+9Trb1Hr*F=rd!0EWK=#CYm*BI^0?JY7D-N$@zIg~!G)wdi4B z=DQjQyz?@CKozxq)pP7{TwPe4_ex|^^mlrQqipQ@Nn?rqwzUOqX;f<&Vuk+w#mf1RzH{tM6R25t&*iCI z6-My!v*HW8-mnfN0DuolY7xPFg8 z+Vn6f!hq=QoGbmv!R7HG309LpwyxQP)OPwu`rfsx`4^YYe->r)|09IK!KOan;d0^> zl}{R^E)6AW;`W3Gg3PjCU%bob`xEmq{E_Yt8zPR8VrfgxiX!8k`(bT(?dP&SIh1W` ztazi(=*~2p5Svv1S*C|)U6mXOCE;(TXPFZmPdXgF!5tOQ^bycDW;K!D{&D2{$V|dw zH8(|fyzh)5t=D?3bv#mCd<7ax%HSQMb3=nhqqnT(_3(0#R77jkprqt45>t)BP*<8{ zK58ujl3CO^=z{~6eRICPfNum1+ljmHbE;gq5D{_&rFros_%8#uq4gWK&&?NkjnlBD zZ?!#L8$Z^69Z$X!9{SMIX381H5lrv`g+Y_?HSlx%RZa?H&U@@}+|9NWROp9>wZ7wm zw650;ltocj2!eT1XKM|GZ$}Qnr5zYr=<&J5yVqCESq#s=dgaGYeX00`o%g1CQE=$E zg^_$YMB)V+jsA%hur%8T7;p1gU1Q zDW*Mjd6GLSkznz*uP=iB{v@Ra`)hHzsyk5!$qaq#|7kpXYOH%t=tnY!2)D+)yWEp) zvl_Jc-OCf74#`>_ZjzJkif;g{EB1B3K(S2DQq4sE^;pfI0JbYYREAgAovR7D!X;4+3 zNPr-)h6B8NIc)WJ<0HYK2#DQ(Q_708|KXPa1+wE2^xtT!MGyviL>!~p{J~z);0#%j zpb;T4zQ!*VVyTj-g!*SS*)%(H9~QMWTjP5y3{zpSLYm4PKdVL-QjO3`Fgj?r4s`{*jWI%ro0^>Zq#6Q-S;vZFXwRE0T71? zX%K7ULhNo*Rdr-tp10qO&cug*I7s+u|679GcAU-5eWK$wsA>F&Wt7}w zaqRB+iOjVEV9UXiJ>NWtTl7%UkcmHG^h+j<6*M5+VB%Z$T4 z^^PMePseAr<|NSqht%`8jF(^EX5Lu;uxrz;ka2Z9o@HM)b^1pLs;JL1PSas_Vug`U z>ZdNPc~!{ixivQSh}@CmbtNh50I}w0)nZVlCC&4A#bModwIS3V{z2q@UFe$<0Q9v% zz$DfNHiEz5VDK9B*cV|7ciacRVsekgqbFO@^b;&cvgeO>l1(eiFtD>qj6aC{8P)TG ze?S_xx;`!Y3|(hzfWtr3!Jb)i7z-V=`B|uHQd9eI6^*_Fa|HroB!GNGXjP?0UPZ0L z#Z2=R%J%#LbTEbt5%09)ZLkriLWb`*_&Vlix&QGHalvs2(b4&^lQ4Aq6WQ9?Z~visC)np`H&z#fLp5ejWcGB zZL|MO&p-DZGx0MwekZyjgJp?E;zUSxuGi^>u1`_>`K;*9R%CJ4dFV#vXYw z^WwMXa(h~j^~+}baCi^fDdeSJxhNrG@z(6@Y6NKDU}|lmdP|o-ukgtyBkZ(tC^h#f zsN_{UUr=9x77bYc3fdglD6wfKCN@!eEvsm+$T93HMGp-CFze^iqZ&fqjON9}Tjx_j zt1m_Ouc=52sVx>S#giipo0sS&O&xwB@v`thFz@lAZ_olPoxB-`yhg%2GS>Wd_CGXE z{2QbNtinWu0FT*wrq?j-wog8+Z{WffDC{W{!&B?#9^9xQdW>OE*Q(ve*k}i28cvVt}KYUOrNO4mx=FrIhceu9? zdP8E**euz}sC|NaZ81UtW|zZY6U7)P1E^H_;Rpe1K|OmuXKwLO^Riqu&_iXdKQ@5| z{a3}oNbvYYStH{FhIZI-!}-rnw18~!uj9Dd#US23kDz~fD0vXhJAVJP{iHI4Gzdon zI@yHq+`}I8%&sID%Axwom;l91 zEj>NW<$CgY7c7F6>_7LunBrOO`bPiYcm6H@K~V638y`7}MQd9DiNc1mEPyxGm|j08 zG`Z#G?}rpk{L&v8Kx@x&OX{E5bOtFklkQ(WQ~3RJ17^A%W?sF-+1I4=8`ez`>S`mj zk6%$vp#@#NyI)wL2kMZ6)(C46a}r*9Bc~FgYy}o`VC$#Rb+o8RL``iK(TivjmZX!o z{rs|-p@PYT#nuxc3ik<1yJmIPDh`l_XUBfN@zd0&UMpvQ>MaXc9XZJDSqub3SlHW$ zfDYtwwEyEsq)fGN6$MXdlpQitx=;qz>blO>Agpni=0 zWRT|T8oh!^3ck0sL_Dt_Wu!!xdL>N^QH_pLiGzP@HqEn8&dq&)b?{^8(^sdb`zbF3 z8MLn&oBaCvUM-2ep*F;vPmFjBA&c`-gMR?3C$kB}EX~t~Mn-9;wYu}5qw`bMWO@{A zFWUuXaV7h{F`bm)TU0WxnSgRENo)2J^3R+f{xl|cpU(JN{nkn>pynWdA%bP__Yd-q zf_=x0Vh+TPwX{Z>T1`mscAC@#LcfTFKsaezV3a(RZe^zCL}q)`@5FNVA0ep1372MJftmLcwtQDVO-`s<975+v zaVdzq++IGiZa_eNH;JSYxTDs72i;)BNv zQu~0loRxO_m@$v7I6)4IpH7hx-_?$c_Q8-w{HaqnsZ(1 z<N3|?mtN5sdK?;OB7hp3uT7)iuSjriuBrHh zx`&jE$pX`}c8dk$c$JK>L$K1#HKR=rWufp}h{SjJBUi9PrY8S`cw#1UoAvJubfkpK zDJ?Oep~ZGJWZN{4Yb_o{U{ELGA0Ys|BA{g$A|LozNqx+PIU#wnQb>CpJ3TzrK4G%Q zpi)cTLQ}m+*s4#gkc;8w8vx+JD_N|uw+#tJLs-aVyi&T@$!4YceK?@=&THRT#wS~? z%w*S`H!lvG|3FgCqQ-9?Xn6)!Jq`TuonFbpxGh=Ig#IR@j&X4X|4k@MYyVfVXo_+z z-ZCU^6_q)dn7_5;bmyE~dycIEz0ah{*tMJCkwHd48PeAm zdZI06iQA5atg+VVZ?IP5mAfajl^pqrb8_B`)5LJtoWNmMYBDQ;)6>CWy)0uprJDz# za*m5-VngV8+z5amZpg%*-N*p`JkkveOkyA^e})62lOI?7laBrKfM#lq2(Ut&5FU%N zd80={RGN%u@mKXocwPP=kz<_2RHKr!qQqaTnLsTGcs!VjrT&)9{|O7o&lG$pFAxcK zl4hjhh5u|M>(bZ1jiq|T+%Nt z7}xU7ztRf0*gCpCg_HVe*Nbzn>kEM~Oq3b>g&i-$?`1as#)c?)+9Af^2pl9K&ekNk z!syzO|Gc5m{#;!H_>6n6M^5u|aof8`RwaSm^Vzon5tiM(f1iJuaK|Xg)tp4%2+Cl2 zI93$|k8V@S%FfOOg6;`zBO~9knz-zVaGS82b+|;iO&)Zxc{(`tr;}`_bi+>!S?i-P zx_X?81D$*5!Vkp(0K+mlg#k;hhzZ{9n7>G{l!#~P>)?G=-R`5Q)TYze@BTLGce148 zvjBa2_p41ctXj(O8$x1jZW2WC_~gRSU`9sJq)SUipupMF^qXkaEc^~!xCo8^{r}Cf zFcMey3amATii^r&)8d^J!YKK&!H84IGmbLa0kGqss%mQS$=0?cO2hMxPyL}YtXNC6 z-+ylElQ1K32#GOlSC}PR>!zZtvh*AtaHs@wa38#*n^Jt@ZcAukg4kaJz*Q*hr3+fD zmo-YxCmdOgv3)E!lo-*;II*I3@%d#+%?UC2$dLMsq{+~Uh1QwbY+Y3>P36=0%r}G# zWD)Ov3cUVh*)GT+MdX1N_&c7|n0t4n=3|r!bI1vNaKk{yeE~={gT+hNaBY@eI)*)c zNlW4^@2!VtJ%dP7c8OGZvGhX2W+2TQcPk|eWVCS}|JJz7w(Qp6bMN>0f_WYybF9@) z``gD2v**9`KlUmY-4oiyQN-udF?1KEc;Z;o;ldo3G&9DhBdraJ&)DXe;y%^$dQ0%| z9RsI$G;_?KVt-}{N*+*@p!ogEDHU4!t;KGd}hkhQZg{eD7jtHm6ljPyI2w@j%wF8v?ig+90wnd#3Sw_ zPg7_6Ueo<&!JlR_WS@SAAQpzQW9;WBtvJ(F?O^`z1AelL2 zt!~xf?0ww0k9Fx=UvV1PK$xpsRODvQE^a7^kO+JJdgS5LyZH}pyZYt;B%6d+6Y!4K zRV=M%dYfS`ow$_Nee-x9ME03BihP)TfjF^&XyrPFel*nl0U0?CvyAr-XVDYlEUsf? zr&3Y;a$$MzO$~q$5)1x_z*PAB`9FDQDrmx+z$VGpCP=?+NTu!pt$Z?wB~yjJ)xSp1 z^5j%eb1XyR(RhjGD){#EoSnDZ^S5OEl7h9cwHCcB8t$Zo$^#=S#_F#HI`QF!tD_2^ z2fy>NQfiM9{g;O@n8n9wlIe)-l$S!iSf%lvISJ@EuWUaxn#HvYBrf0qbzG_H{QjGK@1oKpQNg^_@aID+X2m!+EnB`>cP6_N85(xIB~rl! z0Cz=(!32N{wWg`SHe?e>3nj=_Wi${2({VERu9>a(8fEHOM;G&D)J$E_$wWtvd=5B? z!|oaWN${v+@=e%9_V)M;2 zrS#?Px6(ydhN%6Fl)w*Vb4_*X^iJL{4@c}*HP@a|bJbgCKMRr}E?8!lK6-7}A|`%X zZcy^CQrJf-GP8~s7RY-$v2|#8*j1^wf6}andz@O4Iu=!?G9C5x4&Vdo2<&tsH`H3Y z5cW&(O`C7^hitpNcv!|~;$gG&lb23{4?|kih$G6A zuN>IIvzNjPk%Mwfa@M9ys5K_%IgE{x8&evQ_xs@Oo_~_}3psG69;7lFXYB`O4E4#0 z$H&r7z8t2H^iEyCrvhu*UdC_)lXEmj4yJY}VuCtT`jA`P%0<-a;jP@z{1XY~#`8Wp z({Og~VHZJbzAlrBzx$Stt9+TTcg)ZE@4j#%+3k^vlM4gRo5ibB%Y$O}$VsMCTY2kh zJ0t{x0RVUw;?b@3b7j`m8#hr{Y~D2<6>ITTl}uSWesAuEpS=H8*Ebsbfb1{Q$HRaC5b z*Qc}X)Co0j3Mr5vFudx!v`+s#p*Ru~M46lAlD?ldQ1tS=716v5CW@XqCH!d6QgHB8 z@BR**LI&0N)aUa^^&FTJg#0vl@jv(>j$C5KGD+;GCiBHLe`hTIpb+>Hc{4ZKqlN^q zSpaiq(12Z4#Z;reSTcOi!H^^cstkWTnzc~=KsN8VA299}hw^g#-IB{Dy|!7fQB)%Se54__>Z-l|3dozSe%8VVWvk!L0uvu1L$_DZ(_8LlgA4 zQk8-Ey=mhT`u9;WP3UounrN4`7)II0y9otJOyK5#K+d+~~u9Y3qdeMuzXCtply zeoyEJpm4%9PE$bQZNr)%=BFu(-(IPx&EnCLM8$Hy^`Qh_qOCT6cRx+900#h&x;GxZ z2e`)1tRGrMDctsJT*ZzOE0;>hmmfgnEA*zMcx%{XM#Crqez6k!x88c~M(_@wyxIu} zosB?v{qdzWA2*mOoiQtSDOsG0Jl!tv=lq1P0Ti%t@vr~q9<(&0B6-*regTs9e1){p zP9QPuPYqL+A<*Y+&syq?T-~;4yO~q~2Ozed6qM?-hc-1CU0ASj`P5bQTJ6dvnZEjA z^7r6V?$n3Ufj(c_@)$D=u7Cs-K#xW;e2OG<;(u3eNG;%RBc0J~fZA<4fYn3{5@F^j z@|j_N&-$FxTor_P^;*8egO$HWW3BA@3n;Ug(52qmWwCGcj)U!L4*C0?7Ey~-i+#q` zbB{kL0Kjhioe=BD^h|^sFf;4A%)g2!#u=QQv`*=#z!!pJ;35q{#eU6QS*`4ujWmhN zICI{bLX6uypei%R!iRa-@j^}qDpg|n4Oo^l(b0qvBm}t>Zo0A2;A8;1PW!(+q%bF} z=cY#E?oi%g?uRhPiypIhT^}G|O}@j?nwn6I{t1zQgJMh5qhLF&8i2}VXEds;jF47htJ~!FZ}3TMp`f`B=5AXbw93TMbtfh<(EMg)!9@xy&+Bfh z>*s%_d^Kw9d@0ge?UF?)2HyDPz|0?UUo$9-E=NZ)&*eD#H3Eq6vX zr~<2J0_2eJ98{o<8RqalQdPO4y{M;-O1oLWW^^aL$frUju)IpUp()!E*{5ZE z@uL&9>{DI8(ly4;2^OPkrf=`VzB(@oZ2SML{zv9Kw|ywnPAjcp3UMFMybip^x;tYtgR&YSQg%V+J& zyq&^9o^M7dLq9ZzA_9E9HTAVUFq7QMxiJnYS)0sG(!=b)adss5*5Zc*{iv>#`#{2UHX|O#!{f8+ z#?>~t=ft%NN4C|8DU$Kyz&N#q9ONMZpC8n%R;FQ?(@jwj8hOTMNivoqWaGH)B3 zl8D#*CV}W$0L!uiSyK6yzf|vwmQW>Ji-jKK)n6*_AdlpqafjD@fPr8Z`My$dM#vAm zcwrW1yKml9Y1i88{-}Fcumn<4Kg5VSedgfrN&O4Z*x(h)xuoe>FEaxit4Em)a2YdfEs_ zOWYi<3S!AM(VHqrvutG&KM;(v_3_7A9s;QkLpyI5^zR96K)}tcT86>$-bOLz1P*py z$%ill51;62?DoS^{KWpwSmd`}aTz>omW3wD66SfT=wWH)`rS#@1Cd z+KCU)@QY{GBd}(1)0c;5>==p>n>tZjDNK@J+J6xd)Sn-~JfwL0W&fPG>8!5AV%^f2 z@9ZoHFqfUtIV`SDzNT}EPy+yvjs#Pv5`dX2W8Rz)MFhtb0fE21$7y_q6yeKSjJNSJ z%`VNZDXe}mJUOIaVL+FkI;Ld#Ipm{djd)#`y9~|YO*tzI(#d(mhLh-EJeIW(gn1pW z|E6`>>(mR4zOV+SJx*Mg6~JUFv<06%gv~Mz%IEp{h2N~PshEwK)(n?4pQt4RejCV86CrfR=hJMghkOHV(;ReMi=$Wmp;8S4^H z4x`6aTvGdk+kRB{tDy(YNuB+&AoQDr|7=iMpWr{a9H1}@)0xm!pL}eWuNcH3ttDJ4 zq+RbrT%78#H_3nU;e~~?sZofY+==S*Co5bHC_o94qjKmMF}X;GP4Z*a8XKZ zpG=f%`U8zLaKdTV)V;ttTE!*S>9_Dxkr5-C7gd&q*MI_Ge!CC^lR7vq1@<-GMTC&$Dl3E*#I>OTY>OAX=vC%WB zQG-dyguR&t%yJ_YNBKqp$D-NOmrKx?TIM#jOf39vr!HEz)hm{6s>zVKB|#yP7$r}X z^UZ~IoD=Q?Cyua^Q1?Doa(ixlb0Rl@?XX5C1SEr(j29wWG!}d zQ-RB;pNMt-Qa){itYjpyRn2TmSkH3J*uz?9<D>ln>H17#`)+iwL8u zCN*(cCO`2h3UD}a(*^5uINI!Nt)bf935ebYxn01HgzMwHuA-Lpwu_w~vn;-vAD@nc zQ-T5c_DXlwl>*-@DuODs0|0CR0dM%j;>GDw#7Y8%7(E4)p72lB2<*xWX<;ebkfgis zd@s}H7Sbe&#GqCcHza=PRj{cOi&8g`i0>B3+F;i9uwF&Kj;-5#S^<=c#`~BY|6nAa z-2Pn2M<|p+#(zbh)KG-;PwI0C{JsKCS;YlTd~txZgTWWf<8pX%WJF*l>CdM66`n`c zLmp}D7-r*%vI|vZY0IK-oL5c~R|s(AW$bE(gm|5Au8BDbtU8XP*uG@)b#=cDc4Vi7 z*%!@YKXpZd@DXpgOD&T>Y^IqG)Ow}yM)-i&;@^WW@X80k>aD7T<<$>3{?4>6g+pUi zJ{-f;A|2?zZj62tmU{P0aKeu1#U5(Dy%@jgIho!@ip%^gpvePDCjqPYs+hFVH=UQv)(fd+rR0uuX~l!j;gSxcsxHy>k|tLHCE zR-62OGCum7{wmI*;&$u8?Ohm824~eb!h1qTP*Nd2b<$31fA`@K(jWyLe9JaoPZ!L7 z#q{cnSSqowqH`%-V;My5z0B}t4FG_zJ%qSwC`3qYW1+8p_HlW?X+)oQ@9|G=txfGJ z$)e_OZ*n_CoR^iEBSa@*Wd-u5UsAsx+dv8Zl3MWbgUTN!HX8FLmv60i$Stb5U`Fe_ zM2Avl{hza}yBH$z9wV9ctLrLU0WLi-)8%g&go#mC`KpVD&9ZwQu zRh24Xl6i)_w;MW6mAIk%f-9c=ssGLd#~9618>u8&V7Y1D$-LpWj`sK3kR{8;!F0;# zpfXqpCIJc{@+x80wX*}0(#C(t7%%3Aak2G8VWh?5hg3XD6u*4ee8QAsJBVE9UY$=D zkfC(f4I0SsS_$iDlbq^s;~}ct{Q3$#ZP={AyxJ06i`|VmX*rKGV-5Qr z?0w*9m}RZEU@g~djpVeq_K#Odsfq1=_7-Cz5AZnY4}kFf_4MHYM>M#?yrbLehj#NV zvw|OGdzJXfx%`8OkZn2ceL7^f*T^LE&BarI)A#qVyrk8=H{89!LN zS(@;*j>qE9>%TDcq2HOY@PoN#|5LOrl;Ha|msGwbqsC!~Q?dk}f-s`n`PI|DfQQE^ zyfEr<@P)xL5yU=`7fy`ra;lW?*1QhaOU5=#cIhy1P@l8$l42 zp}V`gy9E>}>6Da^PEiRJL~-7W-~as#_qF!f=h^F7tA}1U%3j@=5))fe&*S`;QbH%iXzg&K9~WA^wc1srQ-d@0J6*P8=% zqsm#)ROLrX?f#2$tv;jp?`@~R4*o;W%dPfRgv_e@a(*{dpVi1KPIJjWkFZj`^ov@* zLw_j&$GJ#ww*iwnml88yS)q0MR_c(N4`ZNBEtMBaTqF{bKQnf{lP)6l$PM$RUNT%+ z!daZX-Q|9n8%{qRVEsuu_c+!6IXHIU&L`-)=V zlr&3Ygy%BXjMwO$@KrBl5(U1bLU+HJS$SKS`rE^CimaJ(LLtPhz7|6vaKe~67`L9t zZ{YAM;9Y~oao?|g283OCL&1Ob4`_5f03a>;%n@hKS$m_2yZu|7JII&AS zQXr%S*Ft%-$m(KwI~x6WYXEFWL9K$VkV3=-BOuiTdZ}C>0?XmXxZx?LVS%K{Nf#6L zOL6*!(mT(?xA4_%vdQfI*y5%a1-4&3oN|XK3@rAKUmnT5HnVq0^kU=DGMHlVbio;n z&aSSl&gHmHIx>&%3ad1j?c~g;YsmVOZ!pr$w@xODdFg`wF2dgiX^V>Or9uFr2W}n*ugH|R1Gz$ zZWiz5M7|2PEJ{_IxC1!cp5rqBO6|!DyFg4&NcPyHkjTr^nlI1HXQ@+FqP*|I)w_rr z%SFa{+jBTZnWG0@1+a<#432E-A+K&KzcCbSio$!UpCQP@JKnOO>i4K$Hm3aHc+slR zSjuQb0RUYJ6W6$|dwk2}z;tX)rDWGWFdBiFRaF^%OHeXjFtJ(*`d*}4;BZauR>|p9 zedd{&3;|f~Tw5Ex%uqyEy5Tl0|J#v#MS zKeBZ6-~NREB~ScU?4?%v6vo}rlxKrZKt^7;zx})$iz>&)T@(@|1?;#V@#ICo4ajpdf-xOFy#^bp1K~ zaffE?syAcc_$~zf+Z6}ewdrSRO_(Im0YTaT7LeL(TO&KeUB@iJs~)nr2C=S6vQ`Vt zE4tG=b6+vt98ehxp|XQ9#r6?1Yhc~BHqv&LaArDh6**`ctbC=_KND?#3lj$iea>`C z8-M{kC-69V7!I*@2rc##=nn-kK4$&#Uoi)vIGKbr?_g7QdNo=8#*>RpHs}Lz#zONU z_q~xzLuFrlgi5B@vGhNh5bue%@_QMZ~6=PlX5XZ?lBPJM11ea@Nx- z$|f3KFt0T)V@>G7&{+fj8~xY+P4au`0b(1R2~KMy2V4Bn5JCb2i-{;lnE@_%s*kEK zkwICi&}}*^@yfr8%EYbYAp(6Gw^Z{u3DPawC3exKYF;Qxxdl$g<{m>7kc7NNDK)At3+&6F zPf#!rkX9ZG#Et?~^*I~igQ&=liMtNa%GQCQG@#`j$41IeHSBLwv5oj8q&aJ^=mdGz z)vMlrmzifN9Nqpo)Aap;&@KwR$Fk&dF0Wze&@1KO7@zp8o3rs0GMG&4EL6pi^|OaVr$?vUlD(O`-R(*JXkTz z0>j@0B$P{X7Ut7Pw|Q*y8AWQx(Z(7YF1VZLl|5;C zQ_Z_eU~UcIZlBPU(Ao*nhKov*;|kCpDH&p7tMD(_T+uU;AocL#8#O&8y3b5sb18M? zUtso&9vC=~Vz3xLf2xFSY4{fppQBj3_tW4zT8C02cA-SyqF_3~AF1sthCg*cvIv8& zHNb&Qa8z~O4#}Ia&cNd;Y-a!G17mnF+#u+mAHvR)pgH+vJLXW8#qh{>=PTjQg+E6q z3NpHdEFwQL`Hi*wj1jAuA2i;JKY(=tJsps!G8l=$N}^Vs#`KLA8`lUH4vxl~S54HT zIRBiulsHPl)X%&plZ@=#Y^F81)q%<==se?Py~vrttL*6`qtiaSd6MOOwlk95#~d4Y zRj;H})gYKp;O;n~BPf>#LOTHXoLk#XoB!N!IFH-moHPDqBhS|cgHYKlm(GYJmXvV9 z&YA3oR5|pOHRgdeAE_R14yBZhjG#1*K%o+E3pO9VIR^D3SAKwPZ9fT0r?D2{C&6EZ zFZXD|9Cji}#8&jX&0$Y*idp(j-$oXJ9DMaLf;TKUQ?PqR+1un-e_j5J(QIjQPq!4s zls~!Shgv*R_28#%9oFF;1#P34{L|?j2Z95#AD;MrrXXK-8mp21ZYT4vyA_-m0b4)L*xw%)oS~RQwYYf7&Jn1270Dql>A7}I04C>L zZQG;92q-ur8Kc$8&0)_!Z3P6h?>~9@PXO;D-wj!6-hkNVhq?hqKEaN;|?sd%wgHZUEUeS=lq&F zVsva?yg3J?j~(~b4T^bfxP9|5{|XJBv$E>LGs1Mc)$#Fdxbxg8Y<)+{{?I>fnf;J| zccIx({R;1iOl02p`V+s~2SVQg5<4z6>L4m__YV(sxze~T&8iykXO}j)CqK&xlSH%F zq~E#Z&=1{I^?jaEXUk*+VIFj${&2+X`)u>l1je+1J_9V63JPaKO4+jDN1Ivia9@u3 z<$6x7bjG)u(j0#N#~T`=DEK5?dzrGaL#^*mbl`-x%rC9Zt{&y0EuMq-mRN57;xoIy z0xn2iv2JK@X-|di>20w5ImcwsAEUxM$@B7!LWpaAZML44c=WjxGxNnhkt#E_wG2~g zB7cJ$-Q*58(|0%ozhHHUE7BpZjn+UVVwCb^3Cysk#T$>)+fdB^!^b94KKI=@INhhG zxp$sfTMYum+*2%6POK5c3T-)ptY$K>C)z%H^KO)Jf$lwXRG+>2vjSC?1-s(BJ0T%a zg%)nDXhe{qJjw89f>5h;g9SDY*H;#%p6+4Hh5$ta=%eaF%#JGWzN??Mt895VPE`UN zgH8Kl_Yp`yqS}CliyN&Kdf@uaYN1%x22aW4XFR`7jzXETS%t0tVel8TdbY~uox*M= z{zv8C)iUz~zi(MaQo=fy#2yHpp)i#A)bw+xb!;InV_qygQ>3lt4U#Rb`pIXVQ4G>y z<>Lsp-P+!-K1M7zm>vI6yG(3Yq)vUZNl1+NbUbWi1aF(V@ZMX_h7Z*}QT=ra1z#Hx zt?uud|I&PfB750bcTA_rB&-1v_yLCe2-VkC&b(bLXjGO zu^7|w8AnTldNbPqY$S~j?6E^A!y6qY*P{xl7^_pq9RaPfzAAKAGp?i*DjUi&79l7ZS2xNJFUDJ+#_BldXSuS1y`h0E?BO{UW!cUG8h&lV z(Pzf}+L!y9{MsK3>c%WA9teE}FjSwY=I0T6J5~nS`9Y=epzozLcE)_H3!Eye2%7#K zy)Gh~fF-QQ0ZP$(zR5&mdH4-l;dFxKVYlIMKe1=$HH zvSAU4Nu7pmi84am>r!uRAH9v++Q^vSITn+}q}y@%^cy45vYR<>y>M({(nhVr>+|Qa zo&xV06?;SRyKZY@Md6L2FGyyjYg1&5?W6FDIvL#YNKRA#_550b;}^A#9b0Yq1g&f5 z{q{@Gm!*^A~0 zM-8qURnt+S#>J?o9ciIQo2s5|tposthJ^OFr{7+OSaWPP89X2Joywjw&D|~pk$@Gs ztHXHS>-I+_JZR_(3WJG#axqWV+Y!<8!V6miH|4!*12bWdEUB|lWa2)p zJWT~(F3#TXNb55hF9szp0K5E?jv`dnuM|X>vSxEnYw}2>WOC8bC>DBWh&Br|QL&nQ z?l)rWak%VF%2X?TfmVy15EVC|W{1sF&#$&5*lJ`1$ess>pwZ``PyJw<)EJ^9%x6oS z8Y6F^zCk(0#pfSV;K`0p+OeP>iCZeBu{o4KpRFbHV?f(01&69)08{$6weiZnjFS%Ui=c54U`^UT|lH}<#|`uzJUzBW;} zwB$|l4+mV<t*%$^Mxc?4@Wm@ZKT@h+28C%fdo?Y}qBPuY3DmCwq0eMWg>6uH*p0A*doJ zuuyGFT6A_^1TzLj39L3Dkd0!E3x3CvbtWs~@;HLH*8h#mnSI?Ci8%6eI}erii%_yf zbs1mWAp5PFk6UN9c0Z>184oF{3tpoAU3&G3vAtAP%#6ec?oe6g3Za>HO`eDO9o9fh zOS4g`KXWBA(TDDI?D!GJs7NUj{U4w=+72ercZF0U}6l8iW!sBxl z{OYwX=@a!OReY{rh;`*oTw^pE{ae$^x^4i3^gOgOSRtayA6Y0PgZH4;k`5fQY!dQ0}!Y!QNAw8mE=r=kr#g5HF>xV3+JFKF%f zMd_FIU#A<(wD*wimQAJ4acpquW0uRe%95}Dq);LHC<=8! z+BwS9&74G6VCikPD7dn=Pv)u&Fl6IBmi+UTfPr)NV-}bBy*B{%9mn`F20mqJ7xk!7 zLIC1~APN{}9$cLTE4rEi4d{cr>6NOoNgJH#vgT?VMnGd1*3id`Ru#~R-W!IGsbREs`)>v{$qbhl{ z-haIur-1xp?pgIfzL!>r+;x`|wFIto4lNJJLI5dCZ5Kn?ES;%!&go7N1mktRISK?2 zs=GhaV;oL5y7;7wr6abh&XF`~AnP!0_2EtYfQhK=OMaCDC5-6Pi#qf`Dp&o%Flc-K zk7(QXoSkywgZRH)@l6UugOEsV@D4NH<4rcPyJLr&!kZIsjC}=}pUg&jiB_CX zy1;1|=m0)@?YdIL4V))c+`nD1+tcF0olwN@Z)^P6FH>FLo4*AL%}kA|&~%9WD3{3! z91k?_ET=+>4G9ZS|M&gwJNnCg`6vfStYs>5M3bqXI*Bz3);788P)*B5fT7B_h%w@5 zU-i9no%;pW37uDv%GQ z2LEXY1^&*gxfm#WV11Cg?go{NU&|KLte7`_kyhIo#lTada6`B8;M#SQL6-plK;p|w zsmcbN{|@^UZlb}ea5WqqJ&2v()jhiALM4X#ftc|fqshA)deyk++36fWa&t$~y&P#8 z$$jDP3iE$?i}H@|+MiDJ5jfc}+N3)B3DyK%C*}=0kHKF?3hw~Ka|6k843X5>xUhVT zQTy_qt^pKMP&oPxj<=@mwG-2Lf{ME98}ESA$83cNtV5|p-?X5U;?MjSH@rN)p^%7D zJ{QH9M7EB}>c<^+Zk9##_B3*K@+%T%23HRb_&@h&WdMFLKu@Ma7Mn|j>u`mWTI5(w z2!|zETkke1epnPCk<|+YQ_)Hq%CGx7XKCeJl52B^PK^|tulPPfm+D_e21T8jZvWny zOm2+~CCJm`Eq@Pj>?A%UJP!omv70{RHF2B|_Y%y#1XQmFx&>Cz;wwR`QpJL)B5s^Zi~5km8aP%<3vfzS!2 z+%INz>Ri*Erw*!lUYwE%8TC)JtZPFP$s7u2a72r86Yd%}-go)#bo6-x^uT5p2&mXK z4qDRlUssIERt{Z#WLqRBUo=qDEK%)WFu$}q=2XM=(;(}oo5yQ9UXoihqUASd{GNK! z**})^zAhs2e>XPmNw#rsOFMMmh0HnMheuYU{{bXjz)D&Lc$JzK`@qkB#PyoQnTqc|m&LokjS8mTci48^8Y4AEn60r&IRV8W! zyvb90bg^NgYKB?$J%o9KuGAi0^c{a>{$~66`k&9QjKr(COP_Wf&F+)#QfB|R`fvb* zwR38J@sgc2`(Qfh#>O*!(#Ya@3K>L^I3!LdiqQq;9GCgbP@>WQ=FsSe*=BbLpeN%9 za1~K*I_VF_lkFj*R45azT-55WEv9KrB>7+?MFpNiWrxBHT7Y$ct|Nk%`SbG*MsFH8 zoahLyZCrfPcwcFJ^UrYL(*(=UA1Zxjs<$r()A3aLMA=F(@=#PCg>#g2?0amgtw!B+ zv3)gIoL*^`WaCI{ILot!PAg;e7EQcPT;!%=T==+>d8vlw9(5@oOCtA-aBkCjv3~S( z-1eaU=HlgZTQ@Q{U2@0!GZ#ezsl7#ek*|IO9PU6^`sUz>-GxJf=VM+Rjf1cq&_G|6 zDi%*UNoFYW8bN6>2;1jL;hS?XDH*`9FZ5kqvSs>=a{ahp9jA%>5L|CLCPM_xNPazu z{;2C<7WT-(0FggBK6BM>M`N$il0(b{V-FfSije!t zsim(WAZUGf`r=`>ecQ<9>4F`aK*Fp!O;(!`qsSgCe(cri<+1}jM9N3hg9T}q2FEer zFFp<9gvt>NaN>JIKORt->?0SyQdal>dQ+aL=J?LgvpnA0Up|)+@g=1EZu?ftw)m2; zyuE77h#?h6((Y(ET2meGYhU`ymtB9@xd0&38g!={PFy%gAtwckAXwmQaUPkrc`S?T zv^miMMfu^Unv*^TFz|1Ag!|C#}^+4oYPquz-9RvRJKYpEu)Hj z`7WoCm{mJa!)1<-$>Iw)x#g$=HC)r(HI2)8sF7f8Vt-08gYos0WyqhIhK@z8YRlhO zXWRLtEi6RQa1KKZQ~6zi8oTSK2o!)Mqd`F-v=9@G4(DAakZ`yT78cBYev=g&C_Vbv z!3NGPB92WmjtzK9U?Z8_VsWUmY2&WKno-|V!%^gT9P7a5;J*Gt74<;q043MP{qYNq zfTy`bY_=Cva(ccQ@y+>oFrVyD^KHciMR*#$e(27RLHswB&O=-p002XD(^%s$YGqmw z{qx2WPvE z`rCvOIcp*qF*RP$Ba3K(>DZr7*LvD}(s;@%GK1^>h1s`PT+igJw`W$e1v34LeNt4*Utn32 zOFv1W7xN9xG)emg`fJ_Yx1HvF3}?I^ctwqVWyuh}xq2#n|0sJ5tNFpucmM^@Flp)s z${tuKnGrZJRVM)L;vTz*g*4Jb%jt$U8i27u4}@Ek7_5-V+y&ik(Pa-S=QanyPZ6On zdNR0N!kl##6ru{3xMW!yxEq)FW4XTw9vG~f7|S%EKYEXcTyJ-dCFFW+rp%I>?H=^JlMD*~$VSr;ZQ+41sFGAtOk8dmT^_-G zZCCJdjZ>)!KV=0GP*~VoFJ&`(HNfdrVYb@>Ts*4uYabBzAw5#c#W<7Pg5d67qI~0| z{(O{X<2Y=sO(DIq{YRS#^zytzjqY6Vyoy%X<~+R{ zmZY(r==E}mE##MBZEgHpPufs{sYgn3rug}lmkcrOT7Q2ix|o`UKS<~h0GD&lPPxlV znjqFKZBE$}19MqCzv2uwXCn&V;fT3?Pj8BA-}>hBG;QN4j}r<2wkE_l3FxI1K2zl6 zY49k7wIfrjXTuf(K5l9<8KqQP_SIH&lg}=yL9-}0<|n^Bmf*UhkP9IK!yL&}|&!{@egbA1M z$D@ZrRd6~iV+nxLxnL6v$?yf$8jew-z@ZVZ?o1h(B6vKbJTu<&JEyehs*4KrWKpcz zc7mzK>NRJNg=s~|J$VQk{f8c?cf#RF0hJR%fC(@#LYo(9Mhn7eNRh>| zkPOHyZbex~TrCM(=P$cy`o7wLD#DR_)2CK9_OLDc6i;(U0jKA4`{?&07zooDf4pjp z7#riV2!_tKA-wAcQ}DeZ3Q$vE=4wUtrBHPY+i~CSB!x>w7tWcexW*CV=`yfQ#|l2H z(v5%4XVI{(EPDUuUY?_G{1v;=@~uGEX*lL+A>_AO>4@!>%mbm_2)SYwtv+peVO@pV zQ=3E6#EN~khs}qAbO+kExz1U$<}TtL;(2=_a1nG9003^NSV27~R!Z&+VGOPXFQqf9 zP(<}>Q$8Zu-pMK|cUZ%^UU4b|r%}oZQ+*2zA3j~4Tt;ZtZYMNuN{s73TM7??%e~7o zYu~1D`bbCR>HVq}2}(2D#3Ck9pOebZ zee*@37uJ~@C+I^}1F3aS^n-#DcKAe+sIW*U24&l1cU-_bY`XY}0r?-_c3~O4iPP`l z51*iu zZ{~h`i{*S}0IUZ1s-Ig6*4bC!JNE3UINUBckE$^PlQ;RCMv7 z*w_Qw2QBy=sG7DVB7*nKKq9$_fZ>?Q0C}4pF_pasz5xF-p7N)Bs(P&VNzc%@`ye0Z<&7e%{v29?0C%bgf z*JJhOSS#t{*bRwQiorE^Ss;^D2kMh6!991UV|CD zY~Vrktjq0J+8xI2d$uZqq**r0{4=cP)6H3oYL*cQUWew{jk zv91NKDPd0Gx9XDxVMA#Wg^!%i^+oJs-v{feS-j>57Rt2HZl92)UnD@N6d9(DOxSZQ zPV;B{P+1JHhJG#UUKYXhn)|+(Lpst34#Q<7zkpdrmp6P@C?;mb>76q7Gb6(D&HWT4 z~t%`UMTPN@~TU2z6kEhgToHJf)qY zjQ$si0+0YU8WZ>ZO6=P(d>&rJ3;TeJl-pj3itSe2WR<;F-!necd&EPdw{H5MAw~R- z#1v;&ecx;Z86d^EgmQtfE(6`~TZb&~w~34x|2ySSVVNTH`n+-$hKSrcFNoxf#umcU zzEL(Y+ljWiDp`aX=VCGA* zbQ5we$j)_7`CJ*h{Qx(xgfV=};%d}Tx$)9+MYUofV6+ashWalx4O#-rvk#mZ72^@jiRm;xH zAR9~=PlTGwmhB!Ks7jV@G|V54NG;SGZf}YS&HY7u8Ep|2#O+dvpK?`O^u_7A`}A)T zjsD^9i~^Jc0Nn<9ax?4Y65yYGs7|gRl>x5~bZ`grFRvJ8g9#~v_~AlwwRZ|Z*x<7h zA6*B!)Q{{psy%XeG5dNJTIt;y6`_wy=FfS zQoQj+0YWKes;ZxtWvG@Ej%W|`iv9P&T(T~^S)6d7W}pQ}~V zP`5ENoev}JL`OZjW`d2AaIkb|35K|e()T|6cTBFq;q`T zbpz#PI#`Y*rRHi`IkftU-FSOM>N5z`b(d=bqVbA9^W_MD$JWB&SL#K+HY)+M6mgX?u@nVND#u>R!!Qp)PPu|zA0 z$Boy1&G-!=dy!2G005gESbw<52(f;MaA^=)dw-OLt#P+5Y|C_8rQ*T*|1v#@TSSJsXSRHmSNPgUWO;9*N?so)z(;HohNE+ zb!SKEaZ|IDdm{q%wEL$?UQGtX_v0vn$|`mixoG=p3lzig(j%880lMEEI7rx-pG#HP*(6outp4QnG(UhEC zC@OHknNE~YufC%Eef7eb?@x9p4%?oeM{@5{zfQ3Y>L{?9S#cQnc zveeOaZ^@AL6BqS8RO`(n7VbT+3L>5_0iWVYrL=p~@Q1$;u-UGJYcW2?wf!!<+bT~N z*8ya#04sWHY;6IQj(NAPCfF-rE@x}V4)5{#NLe^zf^|Y^V!gi^1UFRTbSb|&h~hEJ z8QW5Bg@>JCwS(^3n+qS>$Zj#JiRn$KHwf+u}wGmXD7Ax&(k*)E2 zF|YkF4iV!{{YArU_98FmLIDD)SC=gt%WRD$LDAST($Rt?OH!?Z(=%(^SPuFTZt{EH zB7gbNOrAeicj_Gu^X5%r@Q@Y4G(BTI?~5~4aAxYdC$R(%(zV0Bz=wL6sUv)XoaIXEhS9_uzEUFr=v5Ea5@YVuG*5SW+2eV~( zeL?ikeG?1Dn|GR-F_ln{(Lcemdi##^f3+N#;A)ZCDHWPcBZay~sTik3-g*{~T@O%R zLDgw>F2_84uwXdY*Z=@PK-Q)+_V28;C~w}tu*ChT_AyNbKV?IU68$vubgc%bjxq1X z1EZHZlkrCBPZQcNe%6}4mL6OD!L@!p5ng=g)|y_(o+^D7x(Y|`yfo{2U^tKsqt~KEDKohGy;vZ~*4d%nI5@4S~Z>Ql2Ro_zz&j=_d!eAeYV9c%2 zS30y#LU8J2SV<7}2)b!Db#2qm^)EzB&g>Wa|M@NL{zj?xFiVb3PX zRt%pT$%4PXv+43<-~epulIjS1lNN?ka+&#A_uAJ=%{k^Iy~^Xj%n_>t=W{4{ll)B# zRfl;-E%^yDsqV?1=x=J@uR_HCuc1OwJvS`sX~Sw<0tcqp#E!~`RG-j9;={xl`ZwH> zTAFX;&Z%4+8P9o+^n$R~2D@el0AgQ&)oO=c?82UMy(bPQ91}Co1`!hR2NcRJYjdos zWFOl!w-VCyYxHb@G=rVxn{gLSEd8bYrPGKdK8G=#9vII!PTQKlj!vj+&%t*1gME3s z+4!*`+2g#!wdcG5%V7(YU1XdNyOg&UFN>M2e=5USK(s02L-~R0!c{c)E+77q#)>tQ z=kK}8hRr~?u~DJ22n2O~XWp0cs~A7hW0+FwuGTn={l-nC*$+lLTv|pomo)$aSXoLS zXF4=&I`?$aV<6dxu%?eCY*Y^PtLyC(MU<9Q=-LFaxMCla4sP9cOjZ39R-T)r*wm#@ z!)DUQX`C-1E9gxVF9&Yqx~;_F@2mG!H4|L~Qa?ezboHJxWi&}VTg2Jt3$tbm6y+U8 zmC0I{j3GHvQc#H0GFUV%j;^w*rUe;4U#BH=zwAB=qA_8t|>3f97O^ zZSr}x>k((^>bVy<>8GejgIzGr$*PaM5Vw2~PxKH(kY_DqBFif?3;#voP*!b^LkeCp zB@-x=yg`?bcA2=rE|F{R56rZCUuoS z!;qcbQhP_t+7bAH1KZ|^B^|1d&z`}7WB?F{X(Bk>a<$Rwn9j%}u)QWHM4FkPazq;& zzA1|Hu~b?sA-`V#Nd1oP5Vy66!912X2Na+?yZYq09s6&e4P`u-85Mo<>mxk1+o%_c ze1jRq(y11U3Xwz#4}`t~7#5t9`GN8}Zmg67e#~aL8vE)E^ja1i}NA3r7DE zx6x;NYDIEd>-ehfaYu-P69S|ND6;xu-)2n<@c+=kY!fXMTa^Mg=(SU5jNM7#ig{# zQ+Iy&s{eflkftJPX%etq$n0!>VmaqISi4koo9ZKroP6Afp}hV%vSs$2F0khXhq<51 zzW4ThR9sZ5zgm?3N`JMfbJQ`qTy@}h_eX3JzmkcQjUT{^7`{(+`dtSFV6wjI0O1v3 z%G&W|#Ar(6lsz9kRTc8wS82?Y|MdJyU@i)a^VdH%I$B#^XWy%?WQ95DADI-UjG^L< zugn%Zb(?c^&U&)`e0|Xw{jA8Wq4Uuw3SbTZM2JvC;XB9IIR!yjxGL~9VGc50B-a!s zmM_n|MY;LTU7~ zS2=|wOz;pr=SX2PX3k~32R^Mg@2mmQ20f~@HqKXx|-Vp3af z;U8q(rg7PyIgGo#JqU(I49K$b)_$-gsw6j7l{otxEM9&i+On}Iw*!FHAWHix$cY;9 zp}fc(gnWi6%P4P24b}XrDV!N5`Q;zup|RH7URym zpa_&BvqJ9MDl>*%Z%TW5I;KH^-a5qtY$I5D0;Y)%+{`S}d-J#kJVxglh@-IVME)N6 zdn=)4@7MeFKM#b?6EKWLCna*JY@J4X1pFYbYDg=}B8t~ba0 z`2OO8K|f|AGiI9>+w~s-+_uxnRo2RyT@y7o+X{t~{SWRSxgPYZyR+ z2^IqPzLDtn+Eedn&9LaH5!m`NiVr0z;Kf+R3FS~|a*=HmSt`T9jG{0-mJI_vI2-C4 zjMqzsVdrJAN#Mt@ktFZjw^wT$Vyw)+0|pCXHcP*p6rfC^_lLsfa`mO6VWzZB8=_C= z{?&YAAc=b*bd16fWmnD5G3Rwq4082i0AEdEsbS`mvd5#`kp<~%_Bc<%xk+f-u-Beltixma#Hv0_zqC1;#J2P)pj;i?r+ujzh=kdD76baKPULOo_9sp!r4w%MYBiSl& zvaIa@B0LThGZj|X;S)XU1mdD9f>;mnf(~3BJJHD@tyJD!T*h$xCB~}z1ENObqqGgO z$!G}?>ihpR6d}(os`W*K+QI>=R>_Z(XIgTB&C>xVHjm1IeVVLh8QRnzTMTB`jxp$tpCJ~M=ER#7ecB%QDTMIooQzA-D=~Nq zZtyCM($SJM6yGB_IPUZIxAm}IiDUm`G4AZNBZ2S9?>ci!r@7<9U+6Sc-2I6FKz;X( z>v}1*gIWBTsg%sEZJ{wp^%B<#{PPjRI6@6XL_gnqfWTQ_cLzmFG)AV?W^ZXNk{;g0}+k|+DH&21FG54nM(CB3}8XW)tpv(Y3 zF%w0_#G7BTc2LgGN~pxm+hRXp!^mXrSqsfz#KAmWIx9uYbwOIH%CY&$-?0>Oa1}vT z!`K?P^J;$Z-gZigFS^g#7h)+GmEO$6wWF|n5|J9Ok8ni{-~JxGi~fbj2o9+bs4D@~ z*|HtlfW{$=MaZa+n2)-P8Wxn&bA81B zDp&qjg0}~NyG3TzRY|q1#>(SfXcuEuOD$J!G4ynAxWjeGFuBsTx9s`d`#09=SU%p%Dvzi!zw`h?UYqL}irA6$b*HtR)X=g+Md(NeR%a*l_B{KR7T)@5(!4hJmXP2q9Mok z#T2zxj1SZ5n71H?P(4@9=|LLBUcc?(D<@}(@MhWBE#Eh7ZA|?AA?th*>63ZAaUUxb z$AO@?z~mYJ_JSlQ-P)mgUbbAUY=z;)!_&je!!HrmVF~uKK|F2lzml9$OoxMU*Mm)bMDvuPqmQZw%)Lp6H^jD!%&5HcyA7T;&gQvtQtctA?giy@5i?K)zFO zLfm-1bghRVnlnrI9@mwm=T*N;|!s;#uqdI091axnisBVmL5GlOqOOC2xo$3 zl^L}7;s3y+AvTG}7TX_^=NBTRBSY+tw=uEIdkg5s8pJng+`T&c`NCPoO15OqzL#%X zsU{~at|1f6t)i?ve_og=;xC3n!$W@Mr2}AAE#Y~1a|nrbG<@5=l!H?h!0@oalT=*K zMrC*SA``g68cYqmds};z+{sCK%aZado2cywPuUX(mM{DB0&UwF{3;+PzVF$K@d^A=eM%7jcDyK!!BWyJ6 zlJXFu6EHB#2{Oz~?1QjL%{%#1o$3%7(UMQIQ8kr2P)Je5e?lsfI_fSh%2=Y?boCzy z9b$qzL}vB5xDW;`XPOQeL5c8f7Ek;7*rxnL+Gz%B?nlI!7$oEHRrAc9jztf#t|h>N ziSaHaLdlRfjuO$3H$;72?kI}oeA^zN#HdMlLX$my=gEqH>*il*8Mjq1UeR!wANePe z8XuOGsBdn(C~j=yiEcQQy2&8Hu`B^+4LBAFVBkI4j1VZm93LG{!%Tn?6Ls<*nGqET z9_807qA&&vo=L*^6eSMlD20<7K6PpiF#6kA%>A7l)Ykl40qLdp@nr5}S|*@|$I5dY z6DX^cT$6MCP@`L^bQAJl`CJ(PDBNyk7gF2&P!)Gl)`GLrLctpY;{KhwY?E7F8W!U+ zqTobLW$yS>{8daH4uU^ARQ*GwlDe=W%UfrB5ZYFro*FS(!UE0Wjv%qPD3KxzixN(f zX$tJT`?mHyYQs`>l3P3iX1j+PMqrHTotDnRWt@8#Yf&J4m2B!3;1cnho=`&;Lkzc11Mv2+$}QFUJ%KQj!SLrIBr zgF{Kf07G}T0@5L!217T}DM)vBD&5_nNJxW#ASK|u7tjCw3g>s$UVE>&Z-)=e8T)~o zsq6-=y$LMW^8=x@)y5ziPzCGSmT)Vg!6Tm-<$SIDT`c(@gN zB=oa;E-ZH=M8%1+CrmFSpEiUJQ?Zxbx*>i#zBxy7K?bejxiPkINr{bp`8__556Jwg zeK#!a^xb%Z*^kRAMXNEex2*J>Be*Q)3-WI$#Xx~Uk#ZratdfxP!tiWDaye+8e5I9B z4@+EoW5bD|09|b&CiPag`oRy2CHN2hr|P<_BdIGnvy}4c_qGoQevKfA>F3nWzjqaW zQ?dUIV{IU1j>Tpi@n|abZK?3>BUHu>>x9%K@MeOc*7lvbH+NOfo_#&NKxOXs5;Cu$ z9!<86y8+F??{*keuPnh6Qxmcat+(Xf(jSA)T)AhY&<&%^`wBA?Oq}C~Uh(ogSHKKX zLFzM!$S3ccy-JyS!dP5m0o286@}agYi@#OB&;M6ly=UFR0RUVLs7C=}_4n}i3G+Oj z0%N`p(+y7&Li&h7czDigeKw(yNnnQqY#dUo^(k;j8d&!*E=98$Q;CPz7#~gIXQlc4 z4>=Z{v531S$3u(HBm?JFyc71(*|gqYPW5JH>3{NAZCVvNnpzzythNWqJA=avrs@2B z*bY9bKp2Qx0Y-id*PRfH-FEAo$$3I5-kJr`aytFK@qAe7&hQvWDm5Qd{FdQ%$n$6sd5C@N!Fq<0b!-;eSZ!1_07z1SU3z z&hmP>U}qRO3By0D)~&3G%NZxwB{%{uq7w$iMH}EcfD&$ANNys0946n6^oU(5@2@__ z&`%_IUqt5!kN4Op>=4I#|M`S)4X3)jbx&ymv)Tk#tFU=5uGKRYL=;g4tvlahA8@tX5WZppzgvq<@;5P!~($pNNE`;)AeNG z28-ytKj6i&2RzXqf5eY3LK-mr)F9VzgLAkX|>f3e(O9Cr9) zZ;E7)7F0JQ5Bk}c;nu43YW?PKb>bHp0Q}57q5m2xE;>f6DXayPj)#J+__5<|`(R=& zL?0_icRlM`lvSLa_~nkL*>In+Jm;cW`F^N6`1*)h$Z4W%oOoQ3K+)`vH34@Qfp(&K zAV%zEz~2QVAaK>hqf14jlzACt{oEq3&%BULlXMo?8&cPb$T8%5U}kq#EP7=hJZMW$ zWrH_V5W%ER@xia!>=$i; z_-Ep~;rt315W3ge@n9~zEMD_rD{C7!N4!_LBo$Q2aC-JUw~e+P0&FjqpS7kau}}E| zm`z*@XF4i1A6OjQZtG|4$|FDhkmUWT#^j+ZVU^$;0yo9rSPVezqfo``Q5zg?El>F|7{g%GKE!hN-R(ai@pOkSHV8zPY?f zB&Y`mPwPGR*x&GVe$xaE9u65^a z7E$iVU_Wz47m@W~lf=>eX;f0{?5CGtcNs?3Od_HG^IxTv-Rv3oWiFPVnkv7fh+52O zSuik*PbWprmrjH88ZIj!-UM2~(_$lyp;l6=H5Uv`%VTELtQ4lU4Zy*xZo1!mnV`Z< z*hsPF@k{yL^;wu@4S{L91ek3L!`6|n>-7T8iNBR@zzH_Dt5FO9RJe4joD^)>3TMM2 z=_D>0RIHf5+DK47RuY#hOU`aI^u}5~%lLCKh87Rp@%61{x6EHjopzsswTw?}b^29L z59RNRDTDJX{IefF7D1BVW4OYoUO3AIJhIg z)IQj69aW~#Wu4K&9(Fi8IL!6;<9FRVmDK}>fR9&TVqSBZG%+7MO8(o=^*-TLpZ+-P zJT~hvY!_KqOcWF_XtXmh;$S@dM2H?aY4*6n?{H;IQ1QpWd7O`k?C74sq`tmf#Wg=0 z=0(lqg?ff)L~O1{0{+X+k&sdjW61|e-~Pc%yF0Jt)yTU1hHVwXcQUk(<8mh+Lg!JW~s9Cx-zT{$g*Y{yL!$quG>%wsL?_vTFEvodd z(}QLq&c0e#LCeobolbg(j4>O9k_U*k=P}uB(26Hb&>-E&+N9$49c-Y8j8sFeEpXDd zsAae)F5C7zS)O6Sl+`?9=adDV?QEv1w>Z;cKD2(W4<> z|LBhv597KJ_6CPxP_X{|H9`5g+M~f>{w&##SbS^06D#m7L+|i^Aq2?kaE{JpNN?D( zQp$QTs;eOOq|_)Hf19S*mX)Mfn&kCF78HVMYq7>8p+gf}QZSnF{?`TiNJV8pGcp0) zNfWvH9GV9-f^@9qVY$0$g`{<%I#k>RaCM^?SE_{kjHj(TO0rgIg=<~z-p!rVNY91{ zo!hgFa6BMH@E2-HY-;lxcAnKUHaRw$_eoOwfq=FM!3SyFEduG*@IC! zRT@JV-Fv1S9+@(;Ak@#U+}>|H2Zn1gUU9=s*BSVxRx zr$Ok=c#1c~0ya>AdPwO301=*CbY5tXW}mR<*mqKK>hWDy(WeC6SyGv5{*tW2-@j@X zXI%&T*Y6PL(uD)uQqfv|7PSx z9{p!A0R9#W*{^ppxe!3|9qm%r(by-|D)mLHMu8i5-E*_)%kp1 zd2_JseRL{y_y%bY#W1f6U;dn8l;h26TU_xk$8f^}tj8GIhRD=&PU&b!dmFHm;j!!JIAJ5~(9uMy6-+v6|?+FL&0D$CEv-9BW_K84Z*To=-Z5aU^Bvy!_ z@{(6JD@4JHZ{KMlk$l{CXtVXl%v;_bsg=(&xWS_nrsUo&zJojEE(Croh~mMA^OJ06 zHX1D{)7eZi+z4i)$zoU-HEI;J97XKVJw;ynY z3g!B4t1IbhnmWy5O#ix!Ovm9K%g`QY&mK*U8qdlN@+4$cN5GU;arBmJ?A)(t3V&~P z8SUoVt4+po_sh;A>YBfeKhM^5Lc2*|Gbp?34^8J-AMVah2~vOh_%u{?1aN@Lnm}!2 z@Ym#<_==kv4b1W3!Sa|4oYOx{NeFqx5MqV(1@A!ld4o#NopS`k^;b-KVMJ~^#G|-^ zdzOj0hSvB#pZJV2Gm6a)+NpF^b$zG^XUq6T{d3YD2^~O!35BMPG^D>T%uNz!MU{bV5mhcRL^l<`XS{Rd@afWdg9qonVz|7 zoB~Eb5FP8K_e_(?Lfp~&sQf6U0`vsSvFaHxIWwIpI&J9k@*p>1dBybgOEG6VDS?+q z!b9-u=*%?K`zQ83mHG-bzFPc9^6YrA*kpa-&4E~VN+B@W!iuwIH7&Q@8P0+;#e+B( zNQ=*N)0Jz27$4DLEq?F@Qcyf_H(h|BUp&7@4NVp%DM)H&-h3?A*P251Xqtce>szn< zJ%QHaaZ;fmR(S(7qT4;(a84r%gA)DD3u&n39(&C}9z?|Kuwg-5JfHXGT`J`qj}@P- zdcl`|?pO^R1WdA{P{>~+W54=zi~Cwa5YZ<}qawBRraZUeVQKf_Tg7+NsV~Qm#W(=G zvxppt>PMfZy*0=%vyHMcMxXt@2-xQM znsvfSB^7smMbZs_VS)B~wZFjTz1OR}x=eh6*Nps7C6%u5kUx+O7jUkZ$zM8en zL`%PM>lFU+^w96VUW@vt^| zoHvd@Ay8)g@lR*H{R2cu4lF@VCRjc=nDgN?>VM0|>SA`d`{hjWyp`|R8)f?O**~T> zeM=z#V_{^?J~7sx2t&tCqJt7;%IiQ!!gsB^x9}b$!A|ii-VzLKxtk&T&rz8<>0T(5 zhf&Zo@M4SBh_s!IJvFAn5-$u>Nbb1&YCeSIXf8`AfTc`VCUX4*KUZPcF8b8P&llxm@~ z+biPYkxq&)DAeI^qNq;*0D6P!e4n9W%!`ZEumfTk8vF4PgOiL5Kzw7E~71@&=ST60Qqcn}egLUI{ zj6F(ThA--ncb9a(PD-@asg|PrSW)r7`n|=f`F1pSTlm*ew@aG3cT1;Y9U{COO(#!X z7XV071`LN+Ekk&_Gov~oln~sINqljzHI`y5nYPlkI+83rMc-{L6{mJupe?`XE@gW1 z&K9yeto-fIm$uK>RlmOae8_;jSl2Bo+NEIgj2=Aoip`-*Txd-#!cqDHy3*HDEQr$P zog_jAJ3wIcuhA6f@Xh6onU`_ZpN^LChkA3#bjFLT;#Zd!_`xxxr&?Z~9t>59f|Gk0 zC)|+L`;rR`*Pp_rPsHtdZ1b-C?oa+ZjX!PX*xH?w50>%R}0|0f^(w zOW}(;_1mq}+g5-GCw4F>ML=6G=EX`g`I7yuaf}m*^npE%oP^<^u~lM+bBFQsYQCrN-f z+1#_d4{bcpNj0YFVL8aM7xTYU4(Q_&R>|_?Ft>n=L_O**?e9pd7HZYmlCX?)(tWYZ zG+NcDU^rOeEK(q6s{0s1+Ys<5senIG?L(W0*%vn>E|@F~F$f_G4V;L_KZn8&EUQ#`vRj|8Af36@?mS|y)UOZ`EDnHO&|2(R$S9y^-yKEOJ^*R zKxT9-8Fiubrlf1KkuX|*gwQt0wJh=I^xv=FNI>KTz-hM+F{Dzh2pv!0dg3Yo#SPsc zsvIRUOQK1qP_L@H@4*zTJ%YSuBeQyvu3X}eVQiD(=E=3qAQBnO z{-o%T9D*$g2%}Ky*GqXMbN~b^2#$(naG0By_&Gdk#%y=VRlywH43pT*!qZQk1&`n7 zppM35px-?z9!nex@#q{xxRmM6YBXm6qPQC@pbYcMYpmgEuhnKdw#MHixNJeUZ`A#g zQyS)qhH`@Q(S!Q8=coR_p$w|VY{%_`(+OU$Vz2)?$V3!Vp?Ng>Ij6ou&+=g3@RFL3 z`?o%i1V8}B&q&}40u0H^vx}!!7H+ZC69`ZG92haogZyZ(Phmwj&@OWrYrn~6&~pD7 z^IQD47t1(-HMyy}U&G+(iJd2z(bl`qmGV62K3=R8r)>N0qLndWb`^D=S!KyLExuX@ z0Ahf7hzcC_3?5jV&ShA~kALy^=zxR1&?f%H3*CDzT`w!)`1q0Gv7^u7wy9HhlTCRV z3I6}W<_(G3g45*49YkVXak@~bg`eSTk+L4`4@-~YOW@sF49`ls_~W78{*7sh5K?|k z0Sp3UuW*m%2(4;!LnaMQx>KF+n_S_c=;GKSBdnD2)Cr?z(~+(0j5!s48}3U^oyE)N z>rzIz{AWj>maiTO9V5};!lNB+9In;zQg9tzi7 z`jb~rPvKz7AGopftf)hj07P_7Jn0Ho1a?1Nbg^6e*m-?Lr8Kukjk4>p-7McU9q&eOY2}T5gZ9yo!23yqYPYrh5ZNhk)ZHML}g+r4_BG>T|5mrnGs(m z4XjzmHyDr8FZ*Nj=M*D4ib2Ai4@4|#u`Q}x67`EouIRsCI7_T<6GkZ$Ldq08*pzV^H$}K%wSET4WlbR|Z(KO)_dG&tFrF(M zoBYcQ;9NN^fzd&7YB6s;)5YVQ475I4h2dmTol(6`GQ^Vg)D;7BQ-zSQDahxlo{a+t zZC3dtsE0U?%+EJ^^pWH{-dUAZp|Rah&t@IppDkru|0QYJt@{1KnM)!g|6B9Ko$*}j zL2=pDkc-F7d8(^&D}X~j8wnCdr2v67Pj}2yR0R{1hoYwf!AaP%{aeVcbR>M(f1T)K z)(v(j`h~~XcS$iK$tTg)FR!@fuJ6QB|6Xxq;I_tGjrzjgYzW&!5PqQ=(Qd;q_Wj3D z1+PW|o9vO;wom0NiejiZ6x$HeOBy~i?owL5V1`Af2jStPzxi55r1e)O^a@vNAMjlF z&#@Xe-%qu+(|^Ff{t_c2FD~I^G_&O+XDM0N}>LS?*qkb?{UtQqOwo43i>jHe5 z4$pkpL0tDnm6QaNriFGvv7nO5I)0E?f`La?dpP=_TqFhvVO-k`m?!3ZSvNMZcKvU} z!KC8+zuWdP672)0Qb(q;wtbNwzXzwdif)aB8cd5-<1D>~|Nw-xoT9iS^z z_8w!3aDAY(WO`C`rYCQTs~)=(>}KjR2TAzY$p-mA>c5Nkw$wb<-tPXJ;L9u{5z~`T zQDD^%^w*3G^s&f`Q=~m>lY`}{n)-toC+gwVw$KJhF_9$I2vF55g?fSPste@2vfe#g z6Pf&JR*L7kye&qqc=}`ZrRNQa^a{CB%Q;=1H&=dPy@&0yv7QF>M)n3_3-qKej>;!W zP@KI_^ktC6MJfl)0VKdwh?G^`?*7C7r5!&CotHfqKa2}1gQV-tEP*B9-OZigZWHg> z$?I^dZusJ!k<-T>jt{&J+#z4&d{f{af5`uw@Jaov{WIv>Iprd=I|)G_W0+kb69xq# zu<}34X(Spor^=ik@AfNZzg7Evqo~FrwUTrE6-7JAqb|8ZR^xOo||m zpOZ`YU|^HZ-R*STjnL;CAj_a^YDd+S2^;3*y^7FljtrcqSl_`LB=Y-IPQ|rQeELYL z7zu(P@#2~xast@igs$w#_O`kq40eX<*lcpmBqjs9IG>!%1U25}u0NoT*X1{@)BH?O zzkd^Ri%z^Rc&oMj9q$unaghF!iOq-l#DHt=_e?!Cs5eM}327SF3^7B;*1;_%q--zW z&l2~RC~S9{O>g(slalP!CP__(od^}Db*UAKG|x&+b|Y!R{CCP)P;Y%FzcX)@NJ=P%* z+jL8(HT_8;1q(9>m3lHFXR_p9Lz}9Fl$B)sT46${!rOm;-$lRg8fGd@{bJu|`SHKH z+zEiD&oCmE&NuxA-Ja2dO%*4%My?VzBw|gr-;_#OT`SQZI!a+|`gycb&+yTYyg8gf zyT>SZb2rpKJ(|<8T$f9KIEK!Qa7TvyZxgQb!}%TaF3&HG4-NZApH_;G0;U~0oWT+_ zhZ7+<1TtkfZm^&9c5HJW)>4}c99|VQcSx~?CBQCqP$<;eG+4t4?XWqVgM|Y^)lJGr zPW22NX5JH-4t^t@Uz%k3R6Z|6+R=-Du`ppuaeMUrt58v{-+VZUXe9-86d^<4ro8g( zUM3IEnYD{&-2=(j3?JSKIM36!WjzT0P}y^)l)L_V@;4J&n+pI~jMoDsxM2Dqa#g2! z$zbWy*}?2c2PATSH$&6CYS3A(MdTJIY-y&i!|IG+To4r$<{vbIp`UL>N`}?_3!Y2I zjDB;xdDcoJ+!Lml`b1w3TjA0AN3ygdvnHP7;L3ALPi2-l8vfF$Q!nHpVNT>KHwRB~L*WSWG+%8I>)&iRC`Cu!c;e|abd z4VP&&D}&G6Jc4r7e#<17m3a^q-z#S3JS zqh%KKdMSi3xZe-i@+bJzV9)hAQDAEjDGY-l#KQZd#r#{R%eC97K37WZZ?h^ma}axD zt<#>`3fd+(B|l7qYd#DMOqDA883fH$W87EdzPA4+NF)8%xMU_GOtWhE27UC0?+i4l zI28*7dTSWWSm$5;Zd7vARK+qnJkd^0cBJf<+(}(@(I-yw0}r-)b?e(Chk0q#9qlilFiwCWEuL4N|>0z9Knxu3kC zLECG1xd;*FQp#McWYNeCX;l~!?59J)YqyM&-JDMlmywtXO*N zX{shInHmmXS*2qNmSbkAhn|t3X|R!OGA*cy{JX=+#T18jLlO=s>XZBPX1mEt02lbB zM}x1Szo>9g?q>oUjBVWT%i16X(l>0DDx_sSy-(X`Yzr^uM5#3w?LLaYsQDfV?cm8& z3s0%2a;h5IvOn0Py^UFJ;d9c&(+u8i+ zGgAIlcFXkCVcWng`>ONgVf869?cMWVK74uZr-P4WKOJv4eeY2)R%~tQMG8C*Su+-6 zPR?DKKpv7!)->Q)d#Rvo@F8^0+xq!?25Y(BHEIJ~7tK&g)RBv=#wp?Im(x#SxGvfa5@nJ z?avcj6k6TRE&jUOkKRA_NFsoIr#VvQZ433p0FW2J1 zF*VoAQ2P<5va)f_CPB6Bftg^a4Sr{CYpf&b6RoG_9le-aBj$^+6m^k>2jIm?t|o6? zm_;I^O&k$={~kq;5B^AgS_KorRn$&v%eQIZI65e#FZjD`Zpb^~*-S9aa2~vDPk@W3 zTTvAw`l=604bL-;+E{$*PQ%F_^-kifBiy@xX>$`UyhMo+s=0Y3*{i$XO;iydl$czu zXm!L21o1IoNWSFcW@li7*GR+9n(Ii@H63GCir?~%4Ph!x0q?-spe}03jwlCo7c%$@bp)WM4Qj@}!xNr^M0y&M%lHrF_ zK@j*3Pf4@WB6Y8oGne+YMKoC+ubiyr_S2so&_xmY79C zb9;L@nj5@c{rdfhpw0ha*pm+w+4}cm4=ri#C+pqV^)kynvN_y3KOrmI2d&osUy^R2|z%iN?v7hdnB~?c$XV{)S)IlG|4`F$rk2Rn9&MASXE8V)nAVf^?hyT|A z7f1T{|6WZh1VpYY$%<{0TxYXNl_M-^)ki+Zat(g?`uFR?Ft0!v%ew71la`Z#N=W{HE8a0acnWNSZ4M1HK9*y|C5ZB zV`zv3Z87-Ceb$Cy+*NQ$DY`Q0I(9N=9CTW21vB{*CYv-W*)c^+7h+Gf4d*zU@9!+S zq+v7seQXc`h3g=`oDfzPSMm3ka5;FwLNOdR6%hvS4b)PFz+5%iaC9M|feof6gTB*C zg8u#8#wL5zl+CIE1hY!tP@X6ovEqMTpPRqvB!64!aP}5^By@($ zhL7>MuuE|qcp;pZA|httik(Aka+=LvT73bc-1zMjQwJT}z6O#~*!e#R z+lpQKG{KjVBEj7DCfi(7t6#IqY)E(^>ch*Io$hW5!fJS9la#sp41QI(Dsy-SY60qf z>nPNny?#_T6vXp4o2e*^VhszHKfmL{5hd5H{?{;H32rIF2S>ZFV27DS4eeGJE2XhA zSV6g_l1sGfH3a{POc%of<8;j^mc*uUcqx;OzSW7QhddHGK%$v(sW|%cZaG+IWID3& zL@obRtAthYnvfm*xgdx`a7M&a(GVW#ek@FT)FHjv001=i8#HAFElSYPJr&fKO>tfh zFAwOjT#r)Hr>bDI_%D_#SE~|fFa;bk zOtFJX#fkY^W7Lbx$9I++9d48W{t^ZeNnCPM9aUjOs?79SzNq4? ziD3;_>(>!v0H6BU?oTqz#y+{Ri9gf6+L^3HA1^<(VC(4{26X657#DKiR}%95VYT+E zF2eyLIl-5|_1OoFShnz~u;C8)6a#XG7%%v!vNqCn%w z%IRfntVVtP=E2QrWu`RDgxAhG19Rsw^06GD0Ykt4%R|UukkEg3Ia^z7V`qkZTtuB- zN$rH2ZPs2{cpeW{YVqZd*DvonthB8iU8<^GE{DWXr@?VbEniWp006M^w24*uYC3IM zO)!khr^>x~9lJ|hZDGlNWy-fhP0Uk&Cv9&!V4{pE@-&Q&PlN^opQ=tn;7R==N&NUU zUXo_uwIB}hi;>kSWincR(IwH;cTPq*7!uzETE#8U*M9xsBavyF8W*k%|;6M%lut5PgWE}MDE8%5Gcb(jgO&r>awm(5Qiv7w> zx#0u7!I{o;{ZSQNyI#>Fq*nvKrS5}qo~%lPQuKCq;7_M|YI3mBdIO=8PupzdC(7t` zei1w)qV_nhcx8qJ;F#QvSclzsL!@6E)&)1;;ql7i;IsDF$UP5DZsU){_HYlm|I2q7 z)Yp57z5na(L%E84SRQQ?Pu2+yIU`|A4+iH!$yoxeZ;Q?>t=O%mFUT_&NX;gtc;S%5NERbT(5QMk9uF*X0Am^zPvZO= z15W9|^rl;0Ls*pqD@BzkbQ$plgCx@N<0TnI_kt$v}!}lPO%qLapwn7eEqRVEj z_!dF{4nAHFSwo981polY>)7PRfp6y=XvE)n;Gsz&uT?1nJNqKM4CO`77%4S3!XmZD zE55{r#x$+^a$C1M)6&+djT))ZNw=6!aD;3p`vJfP3FNpidi{2x1u&@cx9I6xsqW9hDg9>>2v0`^*f%ynY%x z-Y=Z2B}HeyT(S@i+;;1x6h>h4eYZSV<5%tRzzNpP&o2Z zl(BD@7x3;KN79?Jg}o4#t3P%xF{clyT9lC=@qlf~Wr{8}Tcb1RWH+7ug(s>Z1Bto1 zl`hm|@tWy*8C`pNha>Uum?l*8MZ%~8!RY45Zkb-<`fj>-N1}M4$oA{6uAZt$sBChv zlV(!71X{0(BGb`eWT+fN&1kUoNWHTQlLJKL(dH^R}ap@BikObAu`YquR+jF!B_ zqC++{v-BpOn0gbl{N$q?OG5#+7(wV~5@8BeG=_=m2NGc_YxQOH>CM;dE&o2MDI)<8 zgp4s3d0w(AngC`l9f!!)v(ni)jHMi_mAXF3pm6)JvPkShXHiCQ3?b{^^=iI_E4l0x z9-?@^HFj=k=#EWgNTl(J7oO@rhR}8dxSCUa%usqzk<$8Yd_-xhfUT%#BPnFUk80q3QyOKQoPRPifqleF2DnT!jn4t_QZDZWdf2artD?pYKvbMJlsA)!fR;U2=77 zbEg-8@a#H$-;rF@)rtFVXx;fM6y}c|Q@4ZLr7#hi0YRW)wQWTa#Zho{eGAY)T^^y2 ze86ES2UC@4207_Yk4(>P>Tc^=4D8bK88GP1tc?*z5r64cFKZGn`=K^u$(Cik>h=Au z=a`L48p<$5pwr>q=2?y~F4@YS*}qW5t%8kcQ5t0IaWsPMyPt4Osl-1nI~;XCiUl`f z7yB;{7~qm8Z~tQAkP}#rn(xFW6Mo%NRDJKkWgbCAFXfiBUYGX`3eJ(CxTkIPe2bP- zw4M+SmgbI@YGZfE4=*4H`;QP3yvVJ2#Llm(TV!l(5g`#PP$QyRJFS%rwT>OnGsjQY zck9>+Wu}R(9VGz(fP)E1oEoFl_(M{l_M3?5OdsCNpZE--QtBDerFoF5e?X zmJaKodQMScadxu3{(GMA3=%ghxNF#}fYJNk5`K)H6<;3?i$6S3_kk-ZcV~J^m)fQo z+ijWyn$KK~ruzrbQdm`W@%j|hB8u7@!tHI)GU&((ctP6H8{teU6j)nqbl30QMTslF z|D_0#x-LKYVFttL=7{ilL)m*{(=d*X`g?-FXKlW2(+m&p>1TSL*cE{U0Oc{bRZ%re zz-ix*k$VIJZm_F3JTV^ zN1=>{c4l8W&e!tG*Y7?@)MY67=Uj{osS>3^rW*4o@<7(!-s_LTWoX=}KtkT>in$++ z%iRHJL+Fa+GisKj<^dP{dqrst-UuorfQl5AV0H(F4_sbx+wD=ufrdaiW$WYq6v%K z;y3GShAXZRsWaA0^OJ4|iS3ZXzto12fzc+bSpKkxCs4-GjQWoe_D0_RkbR-F2k^e8 z`=2@H+d@WSB^$2a%1=(!E`fMmSVG3?`7K#3K!iQQUDc#0c;kahJs^{>j8Ra&D9>ed zR6Xtzm@e$c4m_6{uST16a`#z-^J;}=Q-NIAgiw7(YJL} zeRzNm3Z?zHqFS`bb|{L|+*2pZ&5Z|0Bc$Q&pP=f7bbxvk(3`I>&j2RP=1D&ZqH^ma1qZsBC&@vczi=UI`}*zQ za~BlqJD@g*fSLZ+9@>MT;qi<+YEZ+?2bE+!&{dV_(xkYUoNm<;Y|DztGO>w&24j#= zew^OaTn1EY?UtsA*zM9O+;So*DmAf)WFVptqw~OgqzE@X$TC@;3LK70fk{{Fg@v0) ziyb~KS(TQ|o07alrFAG(=$uBjI)BqvI0c1f7Z+gw-5%DPDallF8r8pE{q5FPgZpTg zc1JaM=*9S3<6sS@1_N;g7|d_Sed~W`(J&0aWix|C1aqH_;G9^H&-=g9Bm(90atAqR z#)!+mFhiKy!anILB-`TpTsCpeIKh{1MazRl4Z=oix|qgB4Aw#k_Q-N^DpF zR-bwXDV-G|rh#uRuYx(mwYIGn>K9kge{oXz9L?>BwJG}H5AwO)@B_UF^-lTQfPSQ9 zA3wHCiDKG+DyLU5eNxP$bLrG@SF1J>A4eWM-6W1jQ}B=k=84Ya8+O9y&wsgke|Ffr z*u)0_fY14cu--p(J??4pgq}2A{|LPn9TLNgkm0-)hldn{gydciEB7wJP+;8sfHFP` zKdFiwk9g0WvGa%2al(=u(E?=YTD-4~NdV7$->PkZlZx5xLSBK|5&!bX!dnc3rT9tMc!8sC+b)JX4&-yZRZnaPfj1u0dq>h0Ov%Uc^wzG zXFQ^{wG0xK4$}=Vlkwmsd;i{Nrw;2>^f-lByey4yUjs#vg+g7wMEkMM4sA&%c75>@EY>m)(+V-|LVmbKe(tfE5wb`( zr4VEy{I3bV7t(twrb3-Td1h{96y>~cgQvSArCLW+7fkY~jMe5x{W!#gy3>QRffad7 z=s?#Hs5P<}QyeJSSX>CdTcUFye|nH^sUTEd+%7T-?M!%O6j%=o<#nX~@gw-U<-zmw z&l;S8q>S4(`62qN>{q3zCAXZ5_$WW5GYb+RqeUtt%!4PWe8e-6Pq7JYgwW^!yAW;y z;qP^^u^wtv@*N6xn>kST&7p+qlG2296_R&LY8s7upa2!lL58@os;e5qK@s2KJWC|6=_YFl zmd7D#jeY3hM1m@CumDRCdqXImn3EwJdB!vk6@W;T58~uEZ{-2KJRlbQ_oaz=;qG&p z-ukFD6$azi-BZ;4`M>1GXr<^r)^$Ykt6&>Mnn*~=Q&0W{WjMdY+d&pE4jE_s?7r#c!w!2S&o_2znR1* z9&ZXeNN^3Wwj&qiu`Y|Tnt5;R1age!WV@^ntZskioImi5h?JKL zks}(8EwuW4|5eN-?+UAjkuQsLz6@SsH!=hOK-&x+oO&A+Vyh}i_4k9NyEN%pUyr7{ z%zP)e)~V1stGRu#CMDga6QA}NvnKIeYTpq$IzBK_xs;pZ{o4GtxtH>bp#4t#(QWAU ziC>mFVyWTYrHyA^?u{q%hNe+hoye+^eAZ;*3p$tM*+g zg$;n5ctT2NE(~GEi(^Mz7Y{sL3O0{YP{l2V(pzf1I~>Xr1cKx9G5iMgW?l+=&wnU4 z{*O=$c!YVZBb~!iePDIXye~4=bLi1hpr?>x$9_EbHlx?ttLsG@PHN`$V7hhM>EUp9 z*g+)(06xA1{B%13QK7orUR`**umcu7lfdBCIaj{9;TG2yF6%1>9Ysx(n(r7b$=aP& z7HBa9o%P;n^*3*G>o@X8&$sRRX7tKYKX0~2+R~`sJFQRMwLMS*))USKN8>o%z)o{F zW9hzZ*$FZvzNzO&!O!X`&;FkvS2HD-KD@A9ed2c8UnCPR3+6ifu=hwWYZz_9!&BnK zl1n1Xd4o)07_Mt2K78v~b6a5ZS6%P)CRVFA@EioP*B#+fA)&DMPu$J5(>xjp6QAxH zicMQ8HEP-=cbYz{RpKXUV9YCvJ;RE-$i025F{#(t^=gW9mbEa$0<;8$)MLh2=^{zK z(Z43vn$Y(H2|0mAG|ZW}dW(W#fvTyw0@UI*uo&3tIA!U1c+byXP);c$o#u8zFl{BE1^|H59MFz48OYE`(S`bS zC|mi7d3VrQ1V+$Y<>fPAfEqJHrm1MuY=me+ZsUSy!Q5>Ae4F)8<0O=Ost6f70-t}> z$i_Z~`BfD`jk7v-jnC`X*1|bVu@!eM#}oA&>tD+O0R3Atesw8~w{a;K&o#YgT3XjK zsFF?Dk`f~!0YZUo0d?TDk_Mg>KS}Te35RO{C|m((i%Zk|abQG2Fm7;O{gfeV{Y@oR z7;+)55M=-5K(V7n0zK%Q+zN}FyMsse z$S2HaA0#sGm9&V1)E0HGhCZzjiO6as4HQnB)@#zfYqKQOwHP(h|GYx#fH-GL$Vg4` z@xGSz$eFB4v+QqjFh&9Zrq2x)FLw{P*eu=Rrw!5K3majsS8f!?tq?^5Qve@V9zr~u z*WFZkT@I-X&aE@->V}DlrYxD~cK1Y+mktW~hD>0OS!Our{SiK~O4Hlt#eO7o5+X~< ztW2FD?fSaN-oZsiQsvm|c~z$xD2$33e}d+QPBW?~OXvzst=9HP^Z!UX>$j-BE{fk7 z1{l%-h6ZUEx{*}6yHmQmL=kl8?(RB#Qjw~y6;r<6*KYUBhaJa+m0?izE@0ArUeX*1|rGSyW6gh8m-lBr4V-!F$1BT9{ zzN-OV;b3*_LlCGqC^}3WHH$)~FbGmul00ccg@qDD8~9%9?`tdIa+CN`b%|FqEMvVv zU;IZV$(IYkB;G_r^!Ac}Aq74z-c{B33#XD$J&I#O>nf(_%btJz`fQoe|har{n{av6>U^!>Uj2^Bu^L zK~t!;{bvS((1$>iSO(FBg;K8Y$paJsVA}u2NvI4B{p+R*LXZVp(~TJF!k+;Bin2g* zc1&5`OTrt1sq*+?b6tLyXUqU-p3MLrr!zOVT+TuogSUAm*=t^C_3?(KYvO0J_X5_` ze}oRgLUf*u>)25f8kI*=+cLa~g>G`IW%?*NQ`q5sg#IpV5B>6%I+jeC;!@HKX9lf5 zIBB8>006u7pO)#B%Ko+_cIX2n%AluUEW=x=owLf!p^@WmYh{W7TR)yx-}iqq_ZbT* zUJlp#rM6apVh~&6yp?nQ*X;}I`G!Nl#_C8$rdSuD?h}g8(P#$wliH8)?4-_T z>i|Pk@Uhz9{7=GIJ$5H{^7Mq^rD&w?gEo)NjSTkmuj84)!oKu}QGBvU<a zl~*h#^=dihgZI@;4ey!-?k5$|LzC3M+9cJpr;&0x&O2|$f1yp9K_&BO{%*)bH9Z{o zvsfsRpuSbRFf@nAdru@eV0O!xv$j9nw9ua$Rel5y>yBrBAp0^R+5g00{o&#A{q4Ih zkeCZtIU55nBbVdaZs|PC+=AEA!j4`Pf?eCEC}TuPZ(}-3w`eA>h;204!I}tzf8so4 zT{B)IIMBd{o5+9Q>eq&EW8p4XD)H#%1jI!(9EFkveW0!dj@ z_I$H{2}O4Md{xg@>RpXcXBmncwJgE zX~qZgPs!;Q^W*-^tdkSTOR=l4zgwrSP8ukVHYn}@U>Fa@6poBc6B^d#nNUtz98I%< z=Y3PQ!K$)q&xVrBa43_}!kduD!;JAYc`a|(&!J8B{M}FQT)+MvA~>g)cRDR=eQ2r+-jw|L2q;I%PKOjtz5KFp$EJy58e`E3z__V z)<1xD7jQHeA~>r!k&3DyQ`iIqG-D4H5{=-Aj6sPAv3TC-GRH?K^=)Da2+$Y_t?$&T zbq<|^DGl$G42N_$y~IEu;51&ECgq5Zo|%)`)-BK~LHbMO!vj5ce|x)^$YpZCv9xXfyGp5SSJ>R-VPSPD=-2t(xa7=R42x+5kH&=mWBQI%g7=Hl z#+Yv#e<-Ubq}4_n^-B=HU(==4l6{~G!vSGWMkN566rQ!dO7O)>0KFXsT6rl;5oQFO z(KU#iQ#d(@3)+G-7|-8^Btaqwf5)a82t&hMEKZc4SuQ$bEPR2rkfzW)+*>GgwE;;(C;i)AL>Mg_bOopN!P7Z->0?o?? z1UHtQ%E$5Gu(2#UT3ccN4}3sxbWq{*;6nj?oG37MDi_yA&Nr_6@m1@N$WX5I-Cx}= z9|`?H${++)bR4N+4z{fPeh$x7WE{Lx0D0hB3*dH4^>(LyJW?=X3WBTl_rfTsIWqN#- zb(~*-d;0$BT;M~<12z(1^(_>w_Od`aV3?whqk;NTs3p&6y)2M+^b!c1=nRj3dq`ZS zHAiw;@OrdZVd4d?!M938bztwI&BG_^`lnT<0elp5143jEv8<*aD@2Qb3a;c^N%9~euiQ%JKPWL%U2a4t3heFd=bU1Q?j;W=|=X@{b)oQ?T zVbUx{&3j|%M&ht&U(5&{YkF^T(ORryMf+*~Y-cx%<7i!_4sFR#032IR#5NH#O>jYy zLo-K6R-GdE^(X$qRL`8l1l`Yr6Qtz1Z@==hpdQ?}{$43I=`hk*_I_f@T8BQf;wSwSi;p9ELQkkGhs+!Sej z!H@wL2@oo=D|{~_ocgBIphb`a%tw*?ZT-_jxHnlxQjAl*nFpmZ)~3nxhtj3u@A^(4 zCN?l(*96Qs2wt@xS`|*8m9j;@K?_%PNB?FG0b7Gm{ct=fO{lFEZy`KeQ8k>JX)4Y3 z+O7_%-|NX0ah1CtVp7EvWt`?^Ud6fRp-gF=Tr_-Z<^{|DLu&6j&HuOS&Hf{yBP5z4 zt8#ZY`I%J(d9`Cmc(jkFT8#w5xfgeDPdUf9 z?HTnQA7NM2tL2gpyvq+m9g}ctYz1d$a}T{7Y!{Y#-no}*x65E!&cAs4Mod1oZwpAF z#R9x#&G5y_!ULfcrv^6_%)#MHm+)Ziqcqo70}+QwXy7rO_U|S~`uVnN=2y-(tZ0nk zHM&Jf=nL3YB6cY_T-J}me<%(H;SuV#ji7;;;Hd;&TAbj{7m1ju^ft~f`;)fF7-spp zwPer9W-~5SMtXA$o>EiOk1pklsU5V9cZtB6a0oBrFY=vkoJWnA+E`TI;-vmO$)B|q zE_-}RZM$o0$tWbItKYF>E{K$*i<&76#r9>7{K({tY2hAqu3m~+p=9+YvxrJ07Afp{ zcXpctM<4Aqs007c9K?~Lbbr<07OZ+w_ zn0Xo^WvMzViP2DsiDo%G-;9k3)aU?lbb~ zk3g!6tO8HO&BEJr&s-N=F4kH@Q!$mVvO0~?*_Q5j zWhC3FZIeo&{JZOCmaP*h%^TtSkiC(8^1X48{f+lIcRg@);fheWrJiBNAy@P?x zw;&(#(?x&P?XAP$2C=2R^E_hAy3G1}%QG_RveOe?LTO#wc(s+PVkQoI6s?;)(fSPYFo7Hiq3%$>)-lYv@T8@wzkQpdE1dv28LFd7UW+7~|Y)~vcGi~<5 zWI8Pg9Ro@VTWeW)sp$=Ew5xLP%F66JCsns8ci-Nhh~=$6JZz8VBtB~B6hKpDAD_#V z+OP{(>T>*{9P_$euSNp%-1cdeU^x5%AsH^X_w3Rp1%EJjYpRy?{lmjQNyrIOQkfYM zDm-toR?>`Fg+~D=z?}e#3wQ+cy~k*p<>1_igz)7^+BPgOjO0XdRdD4(lPsUiypq2@ z&5L@LIq*UMlXLrhED|z0A~N*+ubrF*?i+Tc1eENc?hCKRW8O}MSrr;NfQ`~tq&5-i z*sAm^4l_90-MPHQy7^>>Iq9O4($^wuVw)Gk-8{DR{q?9s;nMER1&uY$VmVxw)O5Z2 z173SiXOUMnI?S%*-WJE7# zME!-S&g)U6+K(z&ZU883MTHgP7GT!YN+wdB=c z=%EgmqeRtPJU1L(lP|#@Z{E(y$=yw2T<4h7C*p6*KTO7m4&ed#>T<3H3A_{ZOjkTuJ$AgFj`J^Sx9gjR zHS2f=Bns!44+;K|-xA?lX!~aYMdHK@K;~>YAaGL!3S)L@m2txAuN(US!%+8-puG=Xo;Rd3miRa_sCU*JE5)kISuNbQC3xgQ`-|zw#&6r(zF5Ae%x(qAY z8Nr1e*)6QLi7sM8&2o?$cRDDi$|mJfcs{#x;G9MMEF3Z)(11zhxh53|+?F(+TD5mzh}9$>+)%PHi-qy+_xb|ktM{uZ3#v5bqHWA~ zj$`U?ATP7T22NkvZ);0-qPwbwj7gP3MoXInETAZ&WFfS^HXEJ?@>T2B!`ic`;BBW z!oqV9N>N2{g?jXp)EYus_Dbp+u5iTqjyw;hPW6Gm-FzttK0tW%1t)~~qsD3WV$hEN z7CX?830}tIy?AErfRtD3p4Xn|9Xaq~kYo;!vlMjiE>+tx zf9rA|Z2@AoL*j!V3i3e>F9026U9Y25mcFhj?Rh-*MA?Nzn_+UcencPga!$LhQJ60EQlp5(^gAz&luhYvp^?anG-&-7$uZWqK`Yy~&-63tn7KPn1=K%^P$^C;%NeMnlXgF4#PA+Ox9MMKO zjqMpMZNx6hK{WH!{JiRzYN8-et~9{vEyd5=7V4)%3t#>i{e9&inPPLC@cUugJyeaL z#T7e1tc7+^U(umgN`+xF8h|89a&;`@Q9-q!n{=5hFaZG^%hw}Cp@Qf>7sAEwg1ky8 zSqtjCx7HUPoMxYNb^pb#;rN#T4|VHOy({2sZnN7qbLVeLPR=z1MN$2#s*IXPWTyS} zOaqQu!Q#^>yg56cc+qcs7eBNfx>{~{Gn?2nh zlI){pn5c~e`*i*1co?n8{{|Hp_i*s550&M#w9@`dPB{h8(QLY#o)o8BB3_ zYQ;DkCeCe$9emTt^K+*j2J?h6{B8rx0^H99%*0uTDSp0d005lM%s+21%!5mw&TkVC zkI{y`a&{3AQxsq`Wcr&i*>=ixK<~D*M+6P7fj@|FYPttT{7URMH(XdC5h=G;I?=u=Sn>TcJ?j37b}l<_J>vPI^`{Cxygl65l^4H360V@S zx)L-NH7Qm;9ShoHHy#B1mFVnC_exGpx_z*fH^Lh=ExE>Xm#{Pb#BFAW1E%w?i2Yyz z`HbST02r;Z#r?LxJLfp#)zrg|lijq_2fOQ=wCqBgM-Baeph-U)%gL7VvM3!Lb;jV1 zUTc4$#)~Inn)*TDn!G^Z7b}<|V(^4Nq~4=lc|1&0NZ|Wc67Ygmba#_ zBuKi{+CS?Y*9i;ZbcYs=hTakDLFHPf!&eNP@lW-xvQ;vC%aW})^RYZ)rByoKS=;ot zvtIXf{TcAxJ&oW-jjF=+4^vUp#*`S1*~QDrUu%9PbnEre9{_Nn_SZ_9C<~+L9;@gu z%m^X-s)_tJd}YhJbX=5Yunmz;voiI`cFhUDchLnR^G=5j;Sx4+hq~FUSq|y7H)P(@ zcV5x0t(l`Qjxt?d$oj`V`P??JTL19*9~^)whUTU7&|#nCm(tR@2$#9AX41=snSEyc zD!V^aC{5TFkoa2peewH5?GuG+(Y-%zt7FWEqBxQBv?kpq^;(L5MURC_3(7=N-txin zP2#Q;v$R=r-~asg?Vlc!Hbe=&QybK2^*-9C)mcpj6ltSFFrOM-m3x%9S$Od&YQOv~ zWsu7l#lQ=vWe?&cyN2@}F`lGlg{G*Gk$UB_V)X7fe?RBPak(QTnijhVpMiOt@X$5i%n}8$*w$?L9J-iQe0Md%}X*T&5j}uZ-MbK$LW0lgH!SCbiNxoIKx!5wVY*Az?M$zWnKNWuykMiN&4^CMFMiE6+5G30C7_;-O3J zE4n~P%O@cwUr#&BxQm5L8j@e8*uA3vA^a(s0!xaPeqhRR(b1(ByY^}2rn)u1Q!NXb zNr#j{17rPrcEY+0rAvkeUj#zw?%2(yjG@tovX!vN0go}CV&6t*>E+ckC%_V%Z`SL`y}%Q|2okz66eT% zf(R0P>Wbu3La=f+fS%f)zz{`Dd@x#V!6O6T%<W=0E%p?aQ&O*>>4uZyv8B z0adYoe_l{h&6-fwT^)`YtH$thrlrPHPVXOPlzez3bR3EHl0kVcTgk)8_6>tKy(#wP zzR(FzJ-J`n58sT;`?nO{Hvj|xeYi(Q=kziT$ra`6+y*RH=$V$^VN+Kq-Yd0m>w4!VL??!pk5TfU<#z zwf2*lzv+n)$ElRwdLUXzhAJq$vYONC??wVD)|(hj2R{ z?(HGAQJN^?>w6VlZj{bInw#GY06=cr47dC!wg|QObK3(54^FD)GlfL7J{gN$Fj7jl z%@~T?$@97~dVli3a^Y4^)q1C00#YFk3in#N3ET<#@Yk#MUdgY+Q3c%hb^f`L!99I- z^iygG4}d1LI#z2bKE9&_Yc8iO-5_ z)P9F{9RednzIW&m*Hcj+WGar(-^lj8Nc@oX>Atn;otxm|`+axw9#01AvN#1x-p^nE zdtrJgM*<+{4QhBQL27oMQ62#(q#r)(D>_%LjDRNkv#UcH8LfVD7Z{1`QoBrCuxVdg zMqdtvISJcwWn-khxiX|QyXkq)q1gARp&f{HIkSefD!;f_!4yU>$W>9&C z%|RPQq#dPHbO}@)yhaG!hyaZD9&kP{3_zrr(oeoXKWfI&XM%&uW#!FaYZ)0fPV}}_ zPc+>ndT_rXQ^b{c60fpknphVec9)!X8fTYU&0LZkc-{Od(gtF}nzgWSkcNa!&&(GL zyKf&uyOPT!xm|#?GO3WSd#nL@F8H*bnG+5c#hx;fyXG^M8&;~3;fvCNJcpZfJW6U| zV$#AN^|}Pp^LJG8jdMzJD==j`5+i$F*^>%Jf*hN=g`HU77V^Aatj|)Fx ze|$sx`8zoSX+t|?G^yr-=pt0)!Jg_9IspuGhW=KQz^?>vdY-7!!&<}LcB#;XK{mbV(&K< z`QE)D3w}Wv38e$Gw$=4MmTTawOn@DcZS zBf6{gH?%O6yvA~p{^I)a0$4I!KHAIu9z{B1<8??G%d(g>7?T2V3Ywf|<8S4>lsDBp zv5j+6b$quJC+PfX%AdgvcKEGy;`47Ywt=tA*d{3oaN)UNGnJNCi>&p|U0)~|({PYf zi6E)WZ7WB?Tx-^Ni$r4=n7XuLmbO$CJ!?_{8Kq%Tz5|@4V!&`XN{xyjc3L#-nirHz zC#<>|lZy9*`O8S7RETtOI&zq{S?*G|_=7>u+8XnhZlSmvwd1MPIXeOlxe#-%D4Ao% zn9%8R0Dva~4}3Eni7Swh#)8;^-_L0k`@#_jILIcP8mqmWffbW>bc+o>{w!#|G7IaT zAX4V=o{U2CLaVC&qqc~5Gm~9Y{VnFr3$s3x?o9>7gDv+~!7S`;Ey1T5*0RTAz1&;?Sw>`BaoHf&De=N_>XF%p3ThzgwZ`!D=6o7*2}k1ATu zFJL@_=O_;Zv;hJxY;5@YNN5`X5A$d{<5G(2$vLfy_czD2q|v*bMD*qmm*!2-RrbZc z)^gVQ*7`*5V!$69aCQv9Fz$oCD9uCB2+3uv>mWlJ`L+o=KM;n6%p7D-8i=%$I*4nK zeZU1Xo4}tG-4n+LwchN|hUzV)T6W8La2z-}N0HE#ztjg92HC zCO;J<#fu&*>_WiGBWx51mTDP-$Y)u^J%3S(u@!=DG1AN(oURz_i(M}ZVf26Hv8i%! zN1oiPX5X>9f^SqyAt`60%nSP_$;(GUx6?IbeK5LMCKp&afM(4a*vB_Vtj$sxD^@067~t@Ciuv# zbv-ualmZ%IEB(`V1&pP?>PXG$D?O}a`%#+>BjtNl<#P|qi;ou^vtDp2dD}83I$so4 zuG|(V1|yy;w=dhZtkE$F`(|xUFQ`+j&FH)8_ZVg8Ut+=PfAX2|KXGJ^0`D0Da>MEM z^0`PABe>GwSfWdX@yt~ZRrbP5H@CSAj*tMRE65%f9ZFOe7GkDEgdM^U zhkyVwBi5xSWtJtv;Cc`T;t4J;a<;)?WjOXlk`5)Ft-x40BZjy`+|+h)-~jqQ$T-a# zz5RW5_xk78dK2W`DK9uXO-x>TP*K^iSx`=YVy?r@@G$J&{jWS6%E15-_p-P9;Zw8DJMgO^frlgy`<5FBAAit z*cj_)g>{MZ&8UWXb6}L++uX^(KYz_rK^GP1pr|X69IzodZkeb2e^*g96sR=A)&+{C zyVN^nLyUzVMOGuwhyPo8P;$*n=J=8*s+)Cg6Qb9tdY0Y0>i@cU!~OyKlO(-3Oub>I zimC#v!RYizXcqvtGic693&J&-SpV9F*vGr1F`u4<_0|)Yf0iGq@ELXpFMXO?Z{09p)6bz~Qye(2Y z!Ec)ukVGGMEvC)3VjM2!Eq-opSF*?69Wgp6qvp0zd$^mk=^8 z9c0F^t=LIE1O-(fzPJc- z28a~F#mIpVS{j)B*3K08=h>9e_c4zz9gigv!jX8sih;MJH(Z}`MXHwRB*=04vrhRa zqc!Qjk<$$%8VQf)oCfc)L0Hw8Ga6S+K!%u_1eTU7+(9rSi)(?;?kcXN>6giZ_$4tQ z8etRCF&9Vk{h1zsp#!wL#)@1HFjcvS=Mkuo*?zH`R^F@bOsgtYUeT6T@GfIzIc_HJ zKOy%&kVP|E(ZBlCBSpczZ~$-~2ge9sKg zS0r=RkJ`ozi!4$_##U?y+OCvk73m0S7!nmrT*~7D1r5QnT};02NOE!%=l8wnG*Zyh zS)+d~1AjKI{YpBs!UnALz~o&tWn6wEZ{wz+tNnv`lf7p53<_X%JD`07*YPtiPsJHA zPH0M!!ZL8>{M$FAac(hFQEF)Ti&LcECxv0vrj!C*CkvP=J871TWq4le&7b%6=$=hX}f zJPP>HKBl`J{GytJ_d-R|3M(8Q05+!g;Ilwy2IwGQ@h6ZXj)j1;rdq2Sxvtg&vk&e# zXera7Vzkln+ajWF+VAnjdPGJud`?!2_h>xFJt}QVo5HukCZb6>B*$Jn9!NKGNR4;E z`>fI6L~6&N>4i)mIzbeVES?Z4DJ1Qq;KB18BU7b>k?N7LLc})-{&J-Ghd7~t2+rZz zIKsJDx8BIq56kcW8h|C`$+m^K#HIt}i+mRe>ARDb4v_#fQN@i1jL2lbx3ZyndC?b@ z-;~Zs6ZjA;9o2cfaHFP4hBHj8hQ9H8@$&88BE0ZEt}84yS35cpq-g61!!SEx?1LWr zmNv2dx;MJN%AazB!T-Gft}pRBkc1*1moSBN$>L;v!!yeyt#^U%DOAX*AoA!$8_7K3 zfRAImdMivYMx*7?fB5hIZrZ+5RJUnaIwEy8w`tVN>+#F4D|?w&KXEeKsg(OWTEP=~)!+uno+(9rk>Zcq1YuVXTT5cVXhn%ODPwoB7dMUz_ zGmoQ}Jj#j2pcyFHpF;WVr@GVh{IVrwxg4|MKVU{orFb9yz{d_}Og(lLK`S;3v^=;^ zm;Hs?LZE^lf{Ww~2fc>}az#Jy>x2t>P@S!dUdd!{^|pUh1wXq?dU)K?uOwzFgxVI6 zkYvBYgdFH5vM$Fqr54Hjn4Z;aiuQ^PcjMYT1?eVueW6XJ;_dvvXdA7o^t*ijj{JS3 zR6#eR{FF0N4&FoF)>uqy#AKpk z;(wV}-QIiJRlbRC=F8pTZ-8CqW6GSuQato(|NFvywg*7ClL!%n;%g9TGk~~>F_p%` z!Gl7yM82d>)l4#)8K_3A;he@)f6chnUclr08lVAQYcT}8CyH%tg=N8X zb`NiHe6mpjuagA1K63>d`yZb49X<1+BM#dT!K01f7g;j68k6yH6vijh?4CroH4K#s zV>HdQbdwq3&=8&4^?l40fYhO&%8i+16Lvq0F2Wf~Qh_($Xy4Qp=h>)`rY^519hpTDtm`t-B~ z5K{7qnsg1ZfLx?IB>8~6zMOY0^ZKZ~yOlV}%K$)Wo@#0V*v9>PL2p)5qFVLSdsNrH z9g)!Z1(;8@ck32=DTGfXrgn<#f(XP>dN(m>*5&cos(@SPNmkYZfk0 z$ct9?CHRz&W-QH#+_R|N;MDl>FZ+Q6qyW4CnIEASvSDx{UX@-${OFWY^QLQ9v-SZ= z_VK6fmy10e-25Icm3=HxIq5C2SrI0iPq4M<**Vc%KGPo+lBzCCt|Zz5v?5^x5ZyA8 zFJAQzKTPD34*xTK+>VRTH`IgC1rzF)v2dh`X^+=@(GKBy4W@@h7M!0{%rUqL-L*8* zJNz8Kv%P5J&&Z6_!D=?Z9g*O}z{6-NmCL8Mz;eh(`nC2Q{&$z#hoF%>E_g{>nUooM zyCcMLb>q0z9qfBu2=_fxE6Rg<=(A)e>+b9vlEvk-6zS6k}gBP}W~l0M~WQ=qF$H^4+illKt6q6@{O9wm(;wuq~$Ev;Cva0wyQk3$dwRn%Xf>Oir zdup2hr&{lX&goyx4nF7ir!EG!k(v%1ebE#o#;1G#HNk&C(Fo`@j|>$jtOq+?T^Wj% z8@k0^#Ya6g(h*nT#QDY~Y%w|Z4-XH&1Ax+4Y|H^|xYG~E@DpZsVhRDc6@gX|-ViPh z2Lh(N7)w}+P8Eti+3u;B{nN~+oCj$_sNgc3$1@hx67f_F?K4bwgiRWsltJlx(UiK8 zb{5aA_}z`1IlFHi0K!?$P7Vt;tC?xQ$CF7!Xd&5A zn1wtga>zz9Hv%f#V@p`mkmXH4r_&@$bKTce{x zWI(hr)2*H7VuKuZ>*Yv*laMHx2aO&sq8B|vJMUW6ra#aww7lFjoUc_q&I@_=?xJs~ z5!uBnee?Gyng+jhuQE~69uG@4F6q=u9!|Cs_M|2Hs|D}1q{82;UhX{s@h?KOgq@xWiGX9<^{-Pr78V+| z*SGg>__&V)6~rXI3D3?2){6^eYks|7%<`#v+WlXE*G~XVpH;=#h}Xj{EQ`UL-2wZo zUZI9j%~O&D(Ha{6^vCtX!^2mdx0!&tA)2y+W({ohgZ0a%3C27MBQoJ!VtgEBaz|uh zHpcw+0{_iyMi|t$8(GQ8CH}V^4RjHlgr5GqX~M}URq{N5w~D1|4$V)D86l!Z=|q0G7x(nw%<52j##9g(tN* zm{jF05J%8dePtcX9y`=lsbdHQWGZ0>r=h#D^GuWy)}*b+R^P_g-`DDWO%!;0MB#~! zVn7uSQ~dldG5NL{MaY=B>jJgyB%#2Hv_j|0PLRVXdxJqA!}ODU#z9o!qakOba8H?9 z&v>@+bpj#!014|9-*Q?(%uZ>$>0YM46%W`K~SYk zd&Ch@P^i}cX5BdBN4b(!HkT4Y2-(NrH8YM5GIO0b1~(tIg-ovFd?yA&hYDv0Vb$m4 zKZ-AuKBHm-006|oiImloObbAznBxc zgAC>byt{+ zp<0`S_FmbwkYEKmFu6cnd-uxKps10kYkj7*TRVSDZFd{3THP+8v+g@7ZYTF|NB|~u z(jt=nL(k5Eph(1U_(!Ydj3w6YK~0h7m?oOZhbK!@e2hIe#7`oYX8-M|;^Eoie5Wp) zGf{RT^!@ehHeTculdLZqGagSncaJ<(MM?qZ@h-Osfb%(Kb#P_G^>!T(9V1m@P3r~K zGIeJo)5`EBIsRPYFjXuuYF{=#_S7~2T0t*Oz8Zo!5!=Z-so)?c1XTI;KM;)A3Z)wN zvEHuxZCFmyM62(PQPbDXJNn>|1C_K^YYob3rujLQC%5IwuiFy-H;FjIV4Sz@DFMg^v&5c02 zkby9bm+%qtxqynV0D!&<_@z6Bezgx$AU{#Ij`p02x9Tmy#O+mX8?zP0m{=uinj9?4 zpxS^dcFi(j!oZ*E2PZs`KN=hcdYrwwPE|?uuMI@JtK+K`Jwy%qL)nw4*xt$%cyXhF zidpYo^wOp_fBExy5Cy>CI}XraFBqv-&*b807wRBs9%Dr38L+37-oxkMzkOun5w8_o ziY6MG^Cn_71l~rZPHv(IDsoc0vI>vKvgIEM{fI;ZF{{t1$_QB6b*?x&aL2}$vAH>7 z1{J66e4B_9;d?opNDR&;s4ZRNsC0voU;x+pcVZp@fYFItD3#6fF{l$xoEU@UF94lE zcG&`A0dJJeWO>I2#PrlULTRIh&t=~ZBp?a9s@1B()rOV&3{wMjz8?^s^c&3eZ&{b* zb!O%>xqUC3n{x$}{!1>!ikF8pnhavA-e!)pglPa;a+YxQ&AK<(uIYmWmg5I!!eWNX z8|h!@UII$`E9~3QoQ7|&Kh+DTI`XmIoFx(@pxqIKP8I6+kBP*dRG;I;F617~4Tdc) z3>13CF;ae5nFRpGV@pwl2kdL^NLCzim?~gui1T0EdGAhz6_baGd$#XBL}0epq3u=*I%x4WK@4N*rK|x$x(PnBEI{-{M9?X z#}*#(KQt>Q3`2~P{su)5{g-nug(#hjutj6vt&2CnT;aC)nj~~|(%^jE(`oyI{P(#Y ztkAXBTxsOI?ti6lqjs#L$(M&B9tmv#(tRw`a~v|_hI>_`jz73#GaGr;Hf?8v(#jn_ z(spqy*tR$bAK1DaP)3Hx}#DY(2@E%U4~v8T*c1b))>FHj|8jnb<5(q!X+2&cL*P*1Ry%E*m zq8wT`$=ob8fuQmFxup+{A)CM(8Hy_CGL!D1`KM5SCjK) z6!4ZFyM-OV2M7G>Kr+MV+)xyGjdvz4VP-fr8JIa!6M{jNf~gX*WxFCLukw@%Se6+6 z{3bDhte)?6|2K;yHl=LkS!k`mTk(Cb>8FLi@>04eE~_s46a;#ei_p20s$oLQ0P?X&pV`4KUfO@^eQQ$0N&Pc5}7ut)VTQT#FKh zo$@jSi`%U45(}ECe^1l+9cv%6)=3GUiwx)X$!Y+TElVakMsXP48=U z=|HMKd7URbiM0CC_54;6esQyUs;s}s>n-ep&^$c+6WsuSAkgS5l$6p>xUgcRtuPmw zjZT%kYT6JuV*8&RCiJsKfIyzsyw&?aw@ardwVZ1j`c}W|xvRX%4!X)ny1diHe@TSm zK6_^?s`=m@P@S#vPX0lpkbj?__&|08KoEK@qZxujLkRH`Grth_6&{o`VB!R)z)fw~ zpKE@$HtgVsLS0n2?`V9p_!RH+?jz!@fy_K={uOsD}^apz2+to^GaaaLI z+6~Vrc-6n+q~~Kr1YP%7Nw0T468Z*!|FVyZWlMS3jwsza{*Z{tqm!!Ta8Jsh-8*O| zDZW~Lq9hyL1}V#&wxbu#D7sD?5!ft=$95W&v@0q6xsjIZ>deS{KqN<)k)s<{wGoceECNii4d=x-2w8 zk3q7GrHMZFDWISQmnYwi&shjPr$0bXHG9^2AB6PGy|yT%=6D%3dewXx(=Dj@T$t0> zjNUcu8hWQ0eN+=a`Ph;>?iZD(-*v^Mzo46i*SsSey=#0FP!bOVY-XHcx}@j78}by%|t| zZ8%0Ga!p2tWDZ$F2XhXfNpJ|sJ3BAlC0W&5drB%@6!O$fBp2NY(Lbr}e6!!I($Z`q z^!^}+mXX$GWViKi#{BKco9F_hH1KdHR{OdVVr$I;<&h<+f#deE(3op_1}0hYQrdrc z>4!aCu>Esk)D4W8(p(fxE-XigFzO700i(_5c9s0r2DGw`dZ*{yv(LBk&tniHvaQ-( z=CgW%yz9snt#*zjW6&yiQds(t&*zy+2NhIW!vVf>6XcgAb0NLOmF+1VUZa`0zmB+#^rl;Ln> ziEWdWjs!4qf?FB3{qa~PRYTf5^1&qfd$p4Y!q-ahTK>lc?{%s zp6ox9z7IXEQaQO-F}1vUw~qsj+W>fnLq#l0YQxqxO2(Vn7$=bW1!t@tuYHk|&n0$w z^I>@I9wu1@-Q=sQm#toPC&3*6fbsGHOS~7<8vBudPO{ek(-a4byMfA+L1UJY3hQm_ zV7V;$M@~@^8T&i}%Aj z1GBg}<`bI7kS(x@XOIdZ8wOcIRDnyT%7N_bSY2E2d=6KVfPAI=@(zs3%jRX3Adw5) zxUS)BwdF281di;ACzluWMTpoh8;Wxn`bYBpgo+}3@w2w}dpe`x_uCIDhshO1w998> zpFO930FVksaMo{P4vv|4h^?-2&=V{fhy~05rtsa?2s?*Ck_iWrT1T| zvLY1&+Bhb5`zU9u;+Owv<`CC1XkD`0?@4qDfesD?d?kg#4s^p6Vh=mHZQD9!r zh%M&O0+7Ytb4D_cP&`_6b~-)?DP$5=5UN$JO4oz)$*8t6)4NWNL&`Xt2s0xB+178Z zDl+ma`9@-hU6-sPbA9gWe=OYvQ(R560N}HWYjAf9?k)-LZo%E%HNfKT?iSpF2MDgg z-QArKBrg!yTYPnY!l|cvW={9?^m_;$#YwfmbHibC5(*EsLAgBjn7AG(bnOg}T@VFU$~w`tD5E}s z%SoPZ0uh;*X|_3ExusJVy=pp?N{z?eo>}}O53l7> zdb6#6-QSxzO|Ztxgroi(yzplQ{d=0(%XGo`q-6|+LT~zjyo_ZAlM*rM?+k-_6l6Rp zBmuK4@@PneNa(6tPRR^GPJL+pADSl5KR>=P(%_K&X8Ka~oqjpq0t?ThrRG1<6GMnz zrnv1ht%8x}Wt)uWY)Hbdae-g?5n6pPV$T1OTb6#pQX3l^7vqi!wm{9v>P`AxB2Q5? z%YlSkr!X&7LP&;CQ+=@EGabi!t#5$V5GnSK?Kuj1@& z4riAy%=A;?qNSF;Sc}>kUTU_2xO$BelX5=HD|H>iMOf>%-j)d?h=Nm0d4j+RxsRpE zIZH>2%IqiVFKu1Rc^-6)$&}=RAHpl>;?PdweLo%5Ybck{f8C?c6t)rZ)=Ijw9P-(V zLT{6s9pkEqDAJMY8&lqX0=1ubx5Qly&NX^K#5?O+o2@!iZ41(44g;9y3I!N3MfYA{FgcQb4x+pP){ zhNOP2+HAvL>g!9R7ZjSu_u3V}?^ay|3uEzqmm5BlJ#CjEOM3ujhDp85vU*dGq{?M8 zi49?yORRteCja#G_(j7>a$WEBU1$w}0gKNl(Bu3wmWe*MG>?sQM3Si`xNUT$p`B_$ z&U*Z;qF2|NO#8bkuJBZURZEfk*eaR0SOcOHB}LRedsBzFRJB~?@g_EA3sgOT=H=#A zII{I4PSdj-JZzLxVL9mgUB1|+sv?6iHe<|S^dywYOu?HG4iyuP9E_H-QFur`Nv)86 zSBgNbx%rAz0fF&iY-%lRx{(ccYP~ftVLRROE~*mSwcKT^8kDMfN9-49;(L|KJ}E+v zgOMLrG>m&tc2IMH!GSKkAH&a+4NQU8TMdDSa$EBR+C6QOJnm=G}^c8jE4^kf>T7Kw&_@^=@gO?5! zR<@Co4@qdZPUus0Ys)<(*$04DaWg3eQ(n012AxDTaYxrRi+K7bGL*f zOxp~5>txku@n95%+a!HS?f?IS!l~GtJ-qOy?bw-yy9_HT{Td%jwiZX)H+qI|>2H7A zV}5DoHs0K2GhgylRKx{@K9uR(wrL~^!2<5 z7?4f7oQpVb@QUe=PogDw7n%jcZ$+mUWN|gEPtRi9X<1{-KvK13Dvfp2)(k)CJ4w{l zkPn;wgehytY+!Cn+~`rBAuP7#kmVj-+7_ID4nk{p%1%dUY9|$P)pP9d_4_;g@K*@t z6dis;k8IqcLRrtVctPG@%bJzzJ*`~o0RTV+3q*bqU^R1TIGz_bL7)*YHqpW-qlTnj z)*>O@TvX+lQ7zv1!b|h~YUTATVEi>PiD1~wIlE-rdh=bcTT0&a?kR;#%O$Z;PmT4 z_|Dw>2dwY_0LCnX95LF6V*ldU2z|Vk1WgsIc2!$GdL!O*$|3s{P5TXZd)xIYv#&F< zkHJ)O1fy&DI&WYWx3)xOv+wVr<=HanYnl2<-)e2=(YR&1>TDIm$o?%fi-sB~ccAWP zDAd!V#UOMf500_#U1$^n1IM9m|3TNm@}q92BXV_YSe+1aH$r1wsf}kUqZ0t&TzyB% z0EBE8viAf+>GKoSHE|eN*hc~}0`yF3jVs>pH|a?)>#mZ29SZ8sH{vxX#_YBR9-Z(d zcJ7`Iss7O6Y4IoTc6RZYHj<;nwgP`F7*Dqzs{jCye^p=_q*f!;WIF~EjQ;}>wloPJ z2HirI^4azdRp9EzsdCF-XpsXy%Eizm%Pv~3ajA=bdVd6AiN;9LM5nWIsCxg?kBY^I zm~%!7lN&=TmhO8jaRf`})0rK2nL(tAUMTb}38r_JP8X7phx*CsPSjGv247K=lzHOR zGK6Yy_Ye$O`p{Vpp@MI;`xUU$ZEAu6yRzEah`xzWA#Y*EARkn*XuOq+JDtYus9}u>JiZ!8H+pVPH)eriPz2|P<&rUkWw3ZZNXiU9)KGXoBfM$j$MhYAO z!_XJ85lQ?$7nxv{EOF3CUfYlKMYSyKMYNpZKpK(w8a>KR7|Yy-rp@M-9rU_s|LSK= zLbL=@g@9i_78h;}=T+L&Z|m^9RU8e*dDV}hFaLdyf`5y7hSsI&eg~CND{Xi-kx|vj zgGF0XinGILbgG%LQ&UxYd5Ul%!`V%C2#cZvLMP`ggdVSImTeM38R9%O>xJ`1<$qN5 z)U@}`_M=2dW=4k(f)ys3mVoF4-X*ZBY2(fc6uO{E`H=7U=N-hPIM^j0v&}$ zSbvzo!damaESS8+q%8P(fa=0sM*^Lh%jaTuq8u-7z+U4~u+)U#Gl(>cYDu}3T2Xet z)0Q}f*+8nA@?GdBAOX*$py8{#YZdj>X=|Afd)c5?QJm@;TY9|p4Y}z)cTxDh_pdtE zIOy|Z#YPK#Odr^!jK&6ui%mho4~9yL9?RPKy(3%=Q%{d_`Z*JUg)srfL2jUX*BNLsBk*S!D&x)wPRJjN}X^pwKU2;Uc^WnfkL<-0v%G8I8$j zxItJpLdxuQ*(_?YqR4VewF;L$%N0}VGA2zC(`;_!pw3NO_P^V3p+!?TUgEL|)9e@i z(F(Lg--4}{%#FKTZm-r;?EN}y^FUKGR+6`OZdF|ueV~TpJj;Cux~qEFso4tNegFcf zc51nlWh^Yh4}HEpeISgqJXr+X*aRx}$En?2Wsn0(aA&<}c-jJ-O_JWygx`7_i*#g-O3Frk!ZpkJ&BC^vBa z$3#arOznYTnP~(UWV>@{)BXxtAVD}BInrhJO;(;dLXM7n(2~yLtZDdL5Gh$BZtOqm zI4h%i!*VyplAVE0NE4CyfXHo9GNA6pD+PnXsPo;mhNb;i1(&AeLQa2CXUi8#*0@CE zX71i6e)Na#D$Z~8)jP8P;>Z6>fPoC(Vzf)9kk9|oPjMM-P5owb3v=WdXlco%j6F}oXbwMDMC-rwkL!|*CZ;H`>)7}*M%#$fTf zXkoN(U;|jipI26qOfnDu{m~CCyU~%YP$XwJvc)Qq*~=RFwt;r903{ZYPIllGOC|^> zb=NDQ{CA`yhX*Yh}oYSGAgJJ}LS&aLu)wSbAl#2`r1Rg9Ht#yOO zRLx2^#85M2{^3mbEIc?Rew*^;+8+M~pXP9Co2V=I|`|^T_>|&D7~= zS-QIx#yPponJbURpSoK;0G3M5Nw(>y@*csolh618NEoxaR%{AVCy{o0ngNxU79vWb zb=sQ)72J{Y3Jo-n)6at%IR1)zcEk&))hLIHwrVfiK75&yk!DYxsc~A^SZb;%!ibTq zJZu8d%0zs{m$hh{*!@JC_k7BY&rFSvVs=uFBEiKQzhjoY_=ENN6szoM^KOm08FpZE zxLY+vMN;!db8A!3y@^G0LhG}0zRf{WZV4gwm55n21iZjpe$*I@#*=QPZ0@(vl0C=E zK1_>RRrfB`9Sq(Qp0{TZ+SECfd$RoKACrtAS4*hIVMxi~8A0l`p>fSUVHj`&m4d|6 zaUx}xVJMi7k#@K0esP2MrZLO46oTZq8XFOwFIYu5amr(46Z(EcZkK zh+uKs)m5z4Fi3{%`RqTxTUp`xtX0!OZzRgu|dVUizU&b2mz+Qoit2}0`r9q4;iF9EPMmK`if@S4eW1dNn~ zxz(+@x@Qndr>Ex6)2r%(DXkZZsECkbZ8$Y4JVHE%NeNJBx^_44b zKS|$=WmCLP3-FzKg7wfP=K|Xa`Orh)^1zjYVWgx{yQXXnXhDHM0u6V3 zIs{O(N!x6M>@n7O@vVBEvBoOJT#z2_A6VR^ zA$Ya_uRJsffG36LFA#CQ>d_0bn9>G~=RK&@QmAq07ufPdAbk>AtgTHpB(4GvIGktH zX-G3%y7Cw`F&`Ap)^-Cp@)Wep4v4OXb)jO2>hi8f?0lx7gVT9(yUBw%97juEbiRvo zReX^7_ixt!b@-0g+nBS&U2o_5P(oyWYjClDDSXEhMJ32DFq!+=XusxB=z|G>#N)B_ zjha$ccLUUtqye05@>&y%PCBH)1^>tCbmz|6x z{_cy2gHQqIneBBrVgA`$&lQbXcTEBk^V8M8`}YTf(x{jLyIlYfg{`0#ZNZTt(7~hZ z2ly7BZBQt6%2zg#(`K@A($GI7|C^1w>?4XZXc3tx_;Ky6VNAcSSd%S}hQ2x2V;N1+ z% zO#ff6{ssWf7g4uIVkXgu%n>n#i;si76JcS$YAhuC_>jR;Ui1fFzE@-{wG2SJAb#N6vvrMU#z|CB!%)!kwL zbJujsR^Jsb}+dA(}-r@qBKA3mKyWH(BAxS zG*r9wO!i32i+1$V9Ak!|4^9Oa^wVFZ`0LO=2`izIee(GJfz>wi)??wzYO`-T|6J2^ zsa@ji`zp5&0pH6iH%p5!>yE}C+l+a}no>$BW~<+d<(#!fkn#?FR#G3_#S)Ck_qKl4 zN-OXIM8SC(BT$B@b5KfUj!hATg9uQKXzX#6qa_tXiD_epK0ru8Y;JF2X>=ECv%Td1 z0rL1V@{BU^r+JkLSLTTAa?RVP4nMZx_Z7m9@N2;Htu8{$)y|Y|_2TQ-HT!^oXcGDs z=rt7DmIDA3q70Qu$ad(cE5N-nVmTlpb*{Wzn6i`1-D)&$TNau7$R6!1fgWger#sGZ{O;Hm@Z zeC7wYWyPVJ6-~N0q5Exl46xVSv-%&qyPR7NoAw*Xv12B${=;%NawqqQ(CA-Y{;KJC zp>+`8&Fe!p30mN#F7ur0@osSNTnicsfh1oHayg&I6ubBp_wy zM=Fbe6_aFVhZMbyQB4H|w5{rD+srU9EZIF|m8-2&*$wu#-hEyTJ@eV9r$a0CFYkD3 zG-7-gx`DvN%E%Mv2>h|9uNrrZ@`zcy5v!xQX>`fA^$cY!$C{2K{gb9~a^bsj_VaNm z05I8kgX$F)bN2zTdZ(m^OPEpoRLY$YQNSDLGTR_I9n(c&vNsQvrRi7T^{S25UAdLK z@`nl>;-V*4IefOdxXjQ>Z;QIe!q&y~wrs8&{aOGpem=MIaC(85ty}Br_H89wthdsa0Mc#2?^Q3-aka>M?S9ItHu7<1HDcGTn#VCLxO-^2L6WbI~R4T zV&O@Z85IRw^x)ueYG-nxV*_`bXYcXrD`vnLAwH~0Z7~{Kl6~=A4^i-{s;xD1QQK(! zOb*WgI@`rx>^cdkmv-3aFBux$ZQok!9p}r&Vj9Ejxn=!RIV;`fyiykjHhu`#FIwO+ zasBV4a14PFl~Krz6Ja-rs#>#!tCQph86ODA~D!!=yix=xlSzr4|cl} zby1~IsPq}hy_HrLhxtL0w%*UxFTUj}VL|b4G&KeWMDYu?M@6dHz|Gi}s&sLu^{byb z&7Tw7Sf@`<*{bKUB9${E_Kf5_KxQ|hrKD5T5wgR7eFxi6e}?waw>SU%UfaJ~eP+G_ zhH;4J)#%k;6^zyG3=mR~)7(@s_M2=ts3N}=z0a_z^2NK+t*n{w24<1Igb z7&N`xnD&3@g3dvqXMGqZ-xeL<_xTAT7?K(F>6#RY=;n+RP?YJ;`}ea$24Fk;itsFZ zuvG;7Ti7S26P^5oHG~%R>?kYMiZOm3%U`eXR?3}BQ_RgFfA`% z|7xQ%T2wN7DeuHa=kd~ZydIAOY7KQt>|6yychS22%o7AwpQ`I1O$c4v{M6D+xhcG!Yi4PYb83#YihysV#yzDvG{k)8Ezk z6IHYCb!JIPVo9X+8@1Zfk3EK#Y$4i8qN&JTEGWC$gH)kCXY(}5$h-+6cp^D(SLZD<2K)p~_nC1v_|oqk0;T!;OA#ZE8F0K4$u;X^ zgf+w8V`visLn5QLz%1}jm%duW6uBx6j#8wK{Yot;=hQQ@s)zpt4F$(@X(W_8I%OZc;;rUSP8~u>)j%i^}g&tgKpswd)Mww004~}UA`R)Od*cn zV|*)khRDfX9G_9Jo)jU?7h5j`-#gy87W8h=lb=>Ebu$p! ze)%r61%L;I)$Q3BF4d*a>qOuJ;#}?+uPrM214}>Bej}@RgqEV=Z6vLV@x#hv9XLWR z8!Czrqu5OeX}RhT1-}a%VePMmZtH13BCPy%;YoBsGB-amvn?}PYtHK6!=~zV#@r<1 zh+kA&#-sfbFn*;C>KB2QX?$l`YkU1t9wooSj|NcG&|_SZ!4{Gr2ihXe1>;2oBO=0; zaJV3E#)zhgYLIfF!8Wfg(9;>xy9Dj>!=2S};^3Yu60XWzJ$x-4QVA<{wRu(KKCx{^ zL^9ia3mqx2=bj*OE?vu0U0Yuwe?ED!5!Rvg+L+EiQEgRy&&lP+qQg z{l9$(7F;7dtsuuVsS`C$WHFo_-$+V-ZE+Tykz01wOqON1eSGEi_KJoPcph#7VA;|N z&_E=VV>LNd!>Ku>D`$7n1%*fu5I5&@5IHDfC|;gXNfd0DJ(}5xCC!8w>M5uY&uOfx z5?He&xjZ#JBrfn*UdqR&m$*C@u&kClmvcJ8H^E9t@UZkS?C85+0DC!(uvLk^1=uJ^ z=`OE_4hBInhs9^qk8vh8mfXR~+!rE>83II&49N7{u?xhZb_Oj|uAtTqg8q*T^bc7J zXgou%5)8EKuIv|H7dI_YG5-0Vy9J**9@kW(;@Y1gYCcP`0RT9Pww(x!ZgdEWdw>xD zst+lGDfoqD>*+n6bu|;@;t+4QM_~8`K3HgzqxcD%XKX(UO`-ad{!2Bd5;w@F{L^dT zHs$bZdecXqggaRkLzlby&X91k4!K}`P6+xEps)nN5|_<5Y=rF5Rx!q1+Mg42ZxiG! zq_aMXT7mFBAgTn7Uvg(>`R^dfvH0l}JWcaCgjmB-EmR^B{WaB~)s^p-HyJ6ZbK|^t z7dn7|tA*z;V1$2aN1Yp*h6}|X5z{$YDu_i>+whc<@m6C^1T zD>E3v+%D^YnN?gPFwUE z9u+CcWJq{{JQEi|lCI8({VWzO(46LwY>ulejeqAqHwy%Ne%RknQfX$TNy57#OZb|f zwql{tKFqL#hyvsNGo!(_VhQuZbkgx2TRPpS~f{rrO_o6d3|+ z{fn710o*VePqJxNqej0DR~jR*A3UW2P(vq_ORG!Rc=ojiVfUxIcV7|v!W0#$Mc{t zse1Sf@m!fj79D{1OO}cwSj|7n)wTq%6cycl_MTs^qS*y$rl#=1A^)aCYA?d?31{@y z#gt-hh>|@$WUp)UTEB6PsumhhFPRfLbLjrVc66*ZV`Wq(ih;jfJ!zHqLEUi`;;Xax zhsTR4jfP%pd_8(&C4W0Fg`&7dhkH(I>}NF+K7mS8Ov*oE?&Q9kELX%7*C+@EHld4z z5-dzjxne0Z-9d!Db)kkc%E!_hHMA|yKjPu2a};pbM>fu7iX$4muw$8>G0MGi(rI4C z_lYJN(7LXkyNZ8WpIb`alRZ5#NKDu0gw}(xrN~8?yTOevrbs50x@B+!Vnen)c=qTp z$~;M8>vGK^*yn4%St_6QtX}>7VJR_?*CSVdNl*fPqesW2+zcCBEEjIEAY1eI>vAUm z_*Pm)L6+IU2yvXq@`No8<5o(Mrm-=yzzVG;Bd=>{toLH)4^Z({xR#{q{Z*mLu-y2n z@@ma>?icFLBsy{gt`NBSAAaSY4lMIU=Q|;F#bX>=@kw)d#vbtDvFL=K_wJ#1dRBE` z5AMC_8M02XkfN<~?rB2JsY<8w@=x0`C|`rJdwRWaV5{H0|MBmILjRsa05DDaPN=Tz z(H~Nqf*~MH`scFpmrd;_NUN8e?>~%qui0#rKDoAFg`#l+yYrMUe_A;=I@2lhU2S$6 z$?WhtH(ue(uf40!9yU62Bcim&uJma(H1#>hSPdR&jffbG6S<;%q@;4q{2TqR{wfv`$QD$q)Wb{J{WJDUiYQF z&cDbXhqoZ{g~2ih0$_mC{zJUu0<)6y%!Pf=GRx}nK}^zu-A2f)#Hsh0Nvlk%n|mUg zlwW3Fh*n8A+5h>SWtH_yY=BfKEll=b^NtlPB_RPlb&G-cg&UncxP_Czpwq4Q5IP26 z(1j+6otZ?bCISj0C5&)fXwjY%g;Y&8}SRLd!;~6AnxLsDry)`ABE!#MgrAcGsy4no0e?cfMYi;!~ znzU)7WBh%tYM}0NLEW>}67QCn%UbWsb7r6_7?HN%&0y+F@;L#&&BdSX{yOCSh2&vU zCx;-eTdX?txa3!+A7+gw{uKntx@r~tj2wz0sWM&~>7E;N!=8Mt)I7Z9hFl*SjznuE zb)gS;6A>%`hzXR81?*S%S2ckKL@C1=vNKFoA!zKZ%{y%9RG5+p@Sqya5TlQ(glPE# zm)+xsQE0_2bQsh`VH0J^tAXb|FqaOWE8U1q~DX0yLoySI)`9m zN-1dg2%6d2R1?|8dQXU8E7S^K2q>4>a%3>@o}Us##|Cv35R+Vc`Tux(5+&rh+UpQT zqY_hV3rS`ZBhc8_oJJr-rXej(h?M^o*)PXudy4$&k92L|an67GZc#g?^s6+H%eZMW?jF8UfM^mS)k z6rdKIh3h_EUB|Ds&o~9YOZU~MD)qd(~Y{2T>nsK{zAcAuq~gw`RyV~Lz9N6 znZ4;zOmR(_Do(y<+(902C)T<0CaqTf+IqcZGkQI8m>Q%-D;HX1(%YK?7KSgP3}vpzmZs* z9p|^8EAn}2R<@os0rE*XVHYazOJ}D6&f|ZGu|yPESr)VQ-`VMrZ6VK0G!S}hG&ICu z2<7Z^0OL2MC+Q!b3DXZ}AI6jU-ry#WzRztrXJY4+%%A4;{>CXd(;w%~%d@j<%;fv| zLZtY3YPuFHX_&C)kPK^leeu9StLPC^1c2!ajls8=V(}>+>c>GN0O7#{8)> zN6ECWGR(^(>hnU=DKeVk!Ev!?2)>c!zR)|cFKipqJe|6k|B0R&tr zIB!ocbg5V2=V%(Q6gNyN!oqyxnp0vqHOQDe^6+Pz4Z3ncZDs-hh~OoPtRgiS+$nId zMWvTz<1$5^pZbV;3;p&5S6$ubSycFs1Hj*0(@+gGC(jB?l%?L05Q6X-H5?i)DW7;+ zz6XKR%D{)>PcNrl;pe$9)V*)%hb<-mfXo*No38*aS|FoMcrIrb@9Qt-NE-Wvit`qn z)Qunwl$q*aeohJh+Vh&@^xLzVLC3@m^9$I0*14wrBfQZ})p&B!_$%VaRPQ7R5Pc_D zoaI*;Mer$&{mRO|Ve8p<5uWO@P_Ugs_RtIk1GcUQR9>0{gL*=R=Fp!ShDso}22v$J zHZa{U5~bQ(W4V$VXGg=>5+X3UMVaG)F-g(GQr@LHs>i4dT< zXHcE@QAR%Y=sO^f4L1EGepL_nGOTjLwRM@>>U~>Eg^E`}sEvGF*cq9iQgUrutVk#!DkX zF#r|A=3SP0ZW~SU0%g3UDai4IIPOy502*`&8XinA^hN77G${~bD)B){6v$XmYNrNu zt9yjy5z8}nh^gY>EtRB8{L9~x{OJU@*c$iK$LKp^hIhf>9mtmYZ2CzMsm~Dx%h^C{&?<6vqeeNLElR$XP3>0 zx@d3Qcd*EzE3cALOAJ9?1Y0nSMF2>)b*EH~j>`pyv!x^k!B|bZJ(buwJ5h!2Dm%XgtY0DLJl zt>G+iXl%2tWEvhh(M>2<`}6G9it3~+4KIVVx5T=+u|%;`87wzy(C%=Fnf}1pt7}|F zV%at7{G(PbPcQrk%bm9KKScH$9$&0xzEYA4gv5&YdL7C{zlTdP*gx_DEXN#3GmI5o zI89X7rrKS8xXw^0H!CW1b@^0vrROUcfN`8KF$yWNM>QRyF;1v6)V?_fhvjP&fw@mesRN1jSNH#C|pL=TuzLF|uF^8<=PWeXqf> zeg`JCTD6}q@_9pt@ongpP-M6$1}d848!tB#-jm?V_TnzG!CM+IdK-jgV#NX0XRAfaR|Uwa->7*orXetb9G=mU;6X?#tK zXqQw@|BgQfgSLzcRYa|K0r|@vuS|yc%j42p9@)~yO?RHp4_Xa+&CKD!@biYNv}iNZ)=@_2vsjfUxHOZPjB3DMD*Mt91g zADAi5wwUoE&3y~5O0gkeY^5lLzfkwCF!ZJO_tQ#=hBD*buiMLPHPd{m@Ll#0T6#ug z`tqWC&trE{r`24(1BISZHDyAx;Z1;&Jh@-Y5Cz?Nbr8JySAh9l=okY36egYE2LU!& z$L42L0hD-RmF@@#hh1qUaZ?cj{yvQ94~qQ75TY=YDhlNU8Y4XM+nUA&OnOe;rRKa( z;~om@3f%ev3x0INBj@+?i$=mbhBIoqE-s&1pbwYl&M#TL-RGWP7Nfs06TcyY5P{`{ zQFJ(IM49v|CZi@x`2%*#7qGP=+`+v#`%gjhM7YytU7e(pOrq8)Qr7G97@tWj9xcYp z3Jil}FUJpa+TKsq$7Qt^>04f2A```7J6HFI8RrlXCa%RtLS1-(ihVgEP0Uk)IfiEC zQ0_M~t>RdgG+j<5H}iD7&G28FAePwkf6iTX^!ej>R-RWqX2xZ+N^4Km7bcC)&kv-5 zz4Pa%ejPWtPN9kCq~m=cJ6?6S)jl0fZ2ID{lHU@0OoKNu0qs_B_&7v$1{`<;Tlw~B z4KxAl9leZxw#L&nk-vCIP3`N&IM>+ zTp!4UM?C%){sUZ`GRnH6im^9fP2X3big793r{O33T>Ln zsHiixf)Sdokd3XtbBGWOE&hyin^7fwMupmttd_$!)p%)3vXiF>lzjNo!Z2vL6_J0WCV{QU2Yq7jY3q}h<1eR(u z)LXC5wZE1*l8rrmKSAs1EOGsF^l+;)jpjE*t&aA8ekvaSRr^0eiTqI*tkLd zLLOw<{?^=WPNB+t;%CBLyW7jWI@5bA)Zxh z&=C~VA_|-s(`4Y+JF=GLI>$2oXcL}3&n!0n(R9y$L^A)Q6Y3 zTp_#NRNYI?#H@)C07AmjOld=4!D0>qbIR){PJQ5&4)^>qVB^s5xfP82dz>u0TTia} z!Aonw{%6L4#Kp$$y(&t)aGmjo=DQrN#^2ZHx|@_VDr@eV4QG}rKWi8h_%1F^`F{dn zOtFtNaj4sJcZ`UWlB!gu%G|{XyEk7muzYq|k=p-7-Uw`(cRR-Oz6-fQVBn?aG~_fk zbx+5YEUkkk&o>UzT{ug#jyv(YNNN*h_Xo@SkWkjUCHuvuVbU7C*KT=i#$TX0Ny< z((jM<4RK@|7zsK8nZ4Rh@U4MHVgt+2mB8rUd1No+OZBg+GZg?~+P4%@>Ivo)2|2iZ z9qud#GPsY{@eu`9t>&K)nm+ zm8a#@|EZyWcevSkm<9IjAg~fPZHG?4TLV?!(%OGEC<{fCX$Q`i_k?~zuDw1-rHqH& z6rlzQf*sStM%?-VpPvRkSCsO#e)xyoZuQ;i;KnqySG@PFcdqcQ^j*jw0QX3%D5x+7 zs>`0+S%L#%q6w91X;f}iOBs10NFF^FGFp--jw4K9X;PFX93b-8im7N8@i0|}YBUrf zCCxTc={7QA33kT%w(}PZUmfP;r>+|$RJ+>Bfu#Jn9K&=a(>u8g0VRLd^21H@odE5^ z_D7bThmG68m@3ac>a+6AI0Q>eESlDGL;l(V77XKLc*!EgC~Yj09|Y<{E6 ztF^av)n)LMi@OZu(zDTwvDg}Z)TX8g(0`l0h{vslq9LQu-V?k|MHoAE&tE|8s(dp6 zXKV4GWZbvTc6&ar2r#T390ctVp$M80vMB8?Y~dQyHGbn>jR3Y{%r_%B8V!2Uq(Ghv z@-O8fB0qB#o15&s_f)bgNgh=Y1>vm8382?o@-ma~>oV%~c~R_F}3P5=P_ zqC?UnIOKlf*XT@~QaJ+q&Pa8N>`E-c$i-XcS_`-tJ1UZRD!2a)1y2eF$4SjEpfYZ2 z*sR-GQt6IMUMbYF*WN0npOm%cmka6BFETW5FMZTfkOy4mdZluP6@`qYIejV}3`bQF z6Mp-JB!Wqxj3USo03_SK1ylVa4?7F*8#0R$1>&0m zw9p6ahX!Z6tJ;R{+F5Bstv0iaEG>P$hoU_dXXhu4&j&Y9sJeb3FcbmZbxk>xHaRTi zr^@IPh!dRITod~mhXe-`^3TFIw;nIkkuJWG*>Se{ie-1bqMsul4yJQd_$&hX+2?e|7>^S?{_B@-U`6&w0oAMclj zSW9Q$`=%ni6=Lc=0_;ay+!sDj=;*)pl?Jl$J_`Nh3Z8nPOyWI;ssV6>$f1&Mf>p@V8F6kab$S}=?s6vj( zSaZUU*;*xdDe>FsC+1mys;+SqoNVQo&0k}R7Woyha-*T)e?NZKUgZSNxA>ZJWPNT7B@`Tb_y4(gVUcx^V`36wi4R5@^8AD`DXiEEIZ6mka?Q+KaR- zsu5LxznO5yB~c;gU=Vc`21o^$tB^6k51d%kOGJGQC43Os`6{3U0Wgqe$t}?+b?irg4FWR>IbBl*&aj*C7XJQR z$%E2tg5gIR#2c%zDuS+nf;}G?e?{}kyy+0EIKQTB`qAbD$9JJ209+=$c!9_wtR5L7 zXd9yvosa#Q`FriHbk0dxIc`1IvLS|>{Z&aelo+~y^GxHSRp~fk4(8m9iB+b8;kMa8 zw*T;`3hUZw8RnKoN*YHSr$b^rN~1_I%1Av_us-Y};Yl|LbrKt_^M53gKQzfnZGBLv zAzW9jt@dOn{3Y85Aa{vUDbO&g^RND#o#Qy<=!}kW(Qpx-`^{?>jEiU-J{YO<%jMe5 zRiXoaJ333Wvv%W)3m1KGnriJL2!&FgfJ}(S)5^kR*GKiFcp%j*ttUAp~`LKG6JnQiO$IvnUR1#$P?+6 zy*~tqh;tARqLzbmW=cS+jKpQNwdqwFxjhLf81?w6dDg!-pk*iBHu2OpzxSv_eiXne zlf3Qz8=;|o{={L*ACHNEO-#W*Q^!D7%bVxa7QU-sB>}tjU1%2orwC3@s0cc!9C!Me zA_v7Ok+NP}l=UZ+SWY#QC6O2JFG8UbakDzB0Dz)d`rHvglJ{{yAj*p#Pf*JNleFKe z=_B}_3(Gaik{_3Qr-I6t`|Ovp=9*h#JQnUNicOrT6FTM@R>$;w6mOD{j7uTFSrm{x$wXQMdpd4!?UBI)_N0$jn|S3Vk&!?^v_7 z_K!oN6jCbEQVT0NX81`;PoNP*4Sj=JzW3aKkv4vl4y=`!M)Q%n&1#xt-x4EQu{mVR z${`SlkDxEIFhj(#-6(3tg=K?r3icH4+JIUj65PC zLO3$m{?}sTNDHbT^|xp~=pO_HuV?2%>HXZ|?3M=mgPk7RET>Y}2uu51?qqLX; zbLn#8Dt+$7b^&_A-*}{)Ao%(pVB4rk6jo|7#yHfnb>wg&ej4UtN&ebKx9k5S=`7fy z>c1|0W@wP^lI|Li?ie~Ax*O?kDH*!EySqD-ZUm$oL8Vg=L~`EC|N9y4Ywh#f`|Q2e z;8Pn`NA<_;;&61wVyk1fd$;IJlQ1xc9ot#lPLk&dLrxsoG$oB=me1dsWA6Rg$-(E zSJU9iz^TGPqgqd@(Gpa8$Wl&}!t#`X@cd&IhLnM;_{qtA$OzxUEuBy{EfK;OH!~VJ zL4f^UOjLMvPbR?K{_*9Ds~+aTu3%8@yMoqBvz22;BvO_x@%qs-jXg5@m6zJ%jN)_M z+%~Mm{lUZ;#NPjJ>-atOs~#DNcE`h|6sE@<@T%O*5!(M+0|HG$S zX1T4KAkK??+opdhl6h`#({T`VRK9&aR16PGYUdq;hQnl~nR7L>xPMsa^xy z^51*0Xu<#~$?~lP>gSg0A*|_1UJAs7>T+=vFK447hOz=G(^g5Uxi8ZyABR(lm(=(1 zRkXm?m71R2hFYCnD;AMokEQhRg%h>qs0?bQF#)oD+4^d^%Ks69fp-`0IdJ)9B#6T!VSjuO=IyhXG zB3XQ&FeWyP6x)Mmd0HEa)!BB>P)lFTLk@W}gu~nQIn0VgL0$-!;6BulZR-Bc3-S_J zD!;bk-vfW`FF6Q4;5F?myU!fOP*aglKrT?OPctT&>T{r|)J zYXRmRxaE@NGxjsKs<_J+y{k-7S-lj|Z%~_%KG}u#2WLAO`1eVF_0Hq(=zIx*FXt|< z(3~7Eroe~bI&J`fX^7a9k9zhsK>g$?-ct0tW(XKqO5?#uquFyy)q9VriVh|{_k$-ZoWWA+ zCQ&Zc?puH6SVf6;KOW)+zAcNEhQzP#J&r=EekHBJcE9?kE0(4HJlc&B!gq}b2ERIY zNDBwA+r8}lo3=f<(c_yWOjsw=%~SuvjsN1kkG8yI{x#o)$`#*&P80pFv)lv(>5P{? zS|I^3;D%vSP#Z1F*0b#}wIyJ?44#3LCneR8n2`VY92dNv>8Y`J>8-+lSX|NP7uDrV z+A>ta^ZFM+q?ZZaVb=y>p>rx`|M*Kzxb4J%vSl@D-rR|z^j$qQB)KAv$t6Z$i9vbm zZE&;r9~I77OM(ewvfM}X6cp^`2Te7Jl}h50QrOD{{7~W%i(GNXpF}Z>0gQ76hr_pv z0YnW#ij?MJ+HzgbX_?K=w4q`3aaoJ2FHCc9{mZ!(LtIZyKs>#jj{7&{rKAI-H zASL25=W)OB|MV4p3o2p{@UxLK0a}(1~Y7l^)%s zzHVT#;x%;rV3+g#+}>k_=`U5(VwQkKk?o>Kc(Dt6am&RwcV9@b^$Y20nTV~QzlDD6 z8)SG@`ULU)8L@|5ExZ<;ScBe8iyEvYLfXTjYFr5!&v*jBpKV! zbSMbl%#GF=cqWsKq?8jR?6?ark5fnFmu5H(a1F>k?XF5Ob=n6+;BbY%DsJ$zrlWzB zNot zp;i`oCH@i=lgR5W(Z})ziETIYBm_St&mgh{N)x7^o26xuviZobxq8=jD`P+^TF=}< zj?3)2^oOcpCz@Q;B)m~V1_FUQP^8$tr(pQAV*=IV|5rH#h@z$=GKY#ebIgOiwQDRP zouW$dMT-=roqHvrO>k38NHwSt1Ir*xA!RSjz4`l4G^nR|?;!-50{fYg0pR*b$!YwZ z8Yjx9Ci*-Bv6M+6d7|^lwB|!N*Fm1l5)tp=HrWbh{1$Y;V4W;*Jd(3LB!WxHC}?W; zqcqokHfXu3X1^u0Bq{jCLH@Oz_|~bz3qVyU&2w*OsNH^y+Vovyc9*=IcOu6fLkm+^uVvInlZ%+!FPyPMO>eCP_nUX6PSHij zMQ*scad=b**Gr>Ly&u9^F(A#txSayq0UuGDtKlf^pmt#@zcotI=SIobhuDg%O9e|w zpBf!ZJMnX5fzn+U11NIp=B8S%O47CBjlIvLmRikKj5Y>pPFiX@h^pxel}~@_{X3Q~ z*od*Lf=CPgP9DKnkFRVb^2PyV(O`_H7s$v_u%XZv8sV^B*6Pos*3-Jc?p=PmE)a4hQC2A459_(Uhk@rgdm`3|HarwaRwW(ZvylB^jb zKa4SzP`@iR;!4o%mOv$B&tTRZ{vBPHflC0%vN*5VW2Y3z*O?zlh@%%TjQbgtcEtIq zrZ*>La=bB*(Y9t#kVbYKKJdGagfbkAUgv9p$(3q`hl-5~Tz$OvU18|E=%O1h833c9{DH0uCO~hr7{atf1U#mJtd^b5Ndv=g z1E^+B7v7z%M?@N9yyGoPGuXt1`hnMNSTu?Mi<~|M5I)K)YlZ2wS}@mGd7_9aKTsM~ zmFW1Xm)TioQVPJ#qkJc@Y$0w}w{m0bR=U%*%O~(V;DEDe<;u9uDy+5>0Dv_9){fFY zFn4UHz@?B7oXT|i%(`kywdG_5C~ZyplUtn3)y1~GIcm~+^^y*+oYSB5=*Ek|5pDZ5 zD_s8jA1G(%BA@GFfb-|*@#EWNS-4$t($rnIPLVwPwV^TthCrj(z3AE+o7!!6>=2&( zV?iqJ`_!3nNMY6|9jetlzO52b!BZl~k(T$k3&f#n{KxjLqigYC7GtQmyXbc!E50D> zxBP<3HhI<;&?lGbV{K|WSNP5665n@l_{D|HrYQ3c(N;ppM?{t4ysU@s|7DwVA}=GM zcsTIvYrrsgK1ef4K_U*Fd77|F!04RvRt8^L36?43jXK@G9~pqa+W6#Xu-?`hEh?Fbtxnjz2EGF0ay%Rx6B``lBj@07((XjegH~1*IP) zNzVSF9qJRu)Pce6F+-f89K1Lf!|peJHA?tTLx-@i2ze#)UZEQcd4pJ|vC=mdq_Qd{ z8txipEG&@(_YTOAUo9EG6;p7b&^7CV^wU2@vjwJ-6e26jt>e@HfJ?hNco&8kK`{Q# zuI~8l0b(~^i*(;FXJ_J^K|&VxF`Km&XHJInF1n%33-vH;i|B`hVq8x7AIwi>I%REj zrQwJZb$y8H&Jt&@p&g7@3SS=*xlQ*#)E&>HvL+>N_Ty%FtSqN_V>UIK!q_U+#jNyb zmfZX2*h3I#sh@STSYu*^_1vzGZ8|w)qyY*p#Tsp|1Z|b}lA2%AO6Lyc5q~D{5TJGd zhfm~O>klFVgyoz~$VBFQB!4Br$BgKJ4zex(5Zy*!BSY5$aHQ?*rJqeCf!43Zv!9cH zMDeY!kyCidrMPy%p%WKm6lpC#85WXXV^h)P zX23?NI=Otc&0q9x-i!>VRF%?+vCsYQDF;LNAg` zgg2XuKfl?WW>+ih^2GS74)6pR6iC>i+c#~?FCAYtzFTrS9S$cvvEe58>gu{#k3;u{ zYGoe;Lntz5SqH)FD(OCu2PV$}4{xZ4t*o|b>#K2a}5klGW ze7t1xzlTUO%Av+p(wbgKo=l$e#<(Lw$%O9zMu&E1lL&bz&`6i~UrcE)VFRvRi)Q1g z0su&|(4r11&FcuU7^JZnPnSk*ONvBfGtNy4?3tZbKodzSre}sMl;CWe%Gj^eEov2(B3v1W-8e&ctyNaPKdfSTj zOqce9j!e(UohnDTH|>)p3`4QlxRVjlKo^?8qJ8z7AfrD4i*wPeGya>fX!?}xCzauG zMpeDXI*iRJrEUHAKLJ?3TqsfRXfGB3^Y9{^nFWf^bt6h&tKP%mCsG!{_nAPH4@fh! zdKCZquT)Kz9GMZ?dwtyK$?Gc|KxSV{59fFoxmY&BF3n1#mNU{S9;e}JI|2?Rr+dA* zTstAfV%T_HzALZeVSjA2refL&Q|epRFMZxCMbP|=fe+Q{TX!?7PB-DlAl9F>#(zkJ zgbwS-&S2Lx#6#f-si`2TcXD{JRt+e7;L%Zt=@k3fDZT02B#*&RqyE}nZJCgyzW<(# z7g*k|be0wzu=Uw=tz9pilHd|p_tX3PI6+p-uDSKfo;wztqBB({f5s!w!RIJ3SBjBi z6$L0#0hrbsYfQm3Db3>PPRt;kxJ`Wiu>4=PAyz^^YZ~s5I2`Jb^Vp5xHU*0%35b8p zlV+ECLem@Qw~$_9~5 zXeMvJYCmT(zg*I*&F)l?fx#>2cRC-X{a>*9a+5cep2L9AkDszVIy&$uW-xoFSvJ&v z)ZvRq$C)R>+_G^whRN0qAdgG=FjrA%N|tZNT;Rcka==O-A(8o)ztz%F0)bO+UsJsI zC1kpzzm-Vs-Y^nlu2LU`>6YC&s>qYfieY01VVlOJrH@VlowWF*m zc%?WKk=MPysrdN-XSjmVXjC23WbSMC@r*0s?qelX;33nK2NvJ>AkgwrPeBgaHDVsZ zQpM0~*wOY9`Nq_v_^|Z69Wuz`?N8OwNeN2d)n@rrT14^b_!xHiJ_TzsC}hD|&v~K& zG_}>G_S(``lyj2s;o5Pvk zYf}DO)>$G_M8;a?YD+Aq|GI-0KX?8%fK?#2@m|N8;5<$z13nr&#{s9ggx^IXRPe8A zF>6*jppp)imUwIYl&Zv}ix3if44QR9ouE|jnq*!lZEI7;jVA*9S_gpa{VK!VoHXMW z21i1uCf(0LGHlt?IuWZC6zI@)*lNji#ms|m8&4vh=;N96G8}#uE54Jp@gxC184Sl@ z;oWWprX#W91}%Zy{+OiBQ}#qmIhu`Y-a*v7^J(0^Dq``t*JN(%gNMYW--hCM?>VZ^ z&e-d}OMkqZ>Zx}ts8#oIYUTkq zyX&RB3ZY1zjaD(SWXz^CZ=iEikX}S2YX6$?C2ftZ=g0+bZvvJ?+s>D1!_2a#3o3GL zluyZ{H|8K`bAxK0Ly?~fv%O)As!9I%00OcaZMp9;9s&0tphIRvlT6W#NQMQ-vYZs$ z3Hmw-@NPv+7I%O9`Q|inV`;is8o_Z)IPHCzZy)~?X>j`my%$PDi=s_(dDfyHD`M$gHge7Q2z;;`_8WvJl^)3&?j1oSJIb=V6?T?r4r*;`piP z&D$@+Xnk5CX$Q&@m5L;)Noq^Z?W;n#ShFO!UuVx)$yo&zO6naD@t5Zq68S+qmf72LgBBHMR~RU>Qy&~5O$=a^2pw-Kt{QtJr8+f4MFhSW|@t!{N8{fJJ+L<{1NzLWv*<5d&6+!5{*f z875Sp%|2na_gea_43ZHT!T96Eyxk{HC43-jRor>VRhif{FX|b7*(EE?ZK1;ddRX%| z5n?Vr^1U%rOU-s?B^e1bUU{{OLkJaWrb`XOTYbf=*%2l zavgZ(+lfefB8NYWGphtns=W62yMd<6y!9{*c=0mnPRT&8T>FSh&8I}@7$qEysQj<~qNNNKtfatXn+5$m65hrT3DAs2nWd`Z1E!=|C`Ma}Xp+U!W0X z-eE~I#)YJav{B$5GMq{{RcX|IT7#f$|4oX$g@i&)v6jFdYWkS4o_2_4@#~_YCFDA3 zSA$=47w11YY5*bXT^$~dCmjAqi!>PiPDqPv5`;DY`lF9a1xm%r0qcVhcTwLkS0hYD z`m|f2hD&p2Y57Js9D%f2QqQOI5eQ6hLj|e=V~R7~3x$kB>-0{|Oo?91Q;{*@6wf z;Zv9TtpfvaNUKRaz)-$K@-XpJZT=3Skg(~kPbwElu`$DXrifNu14!+sOD?K0dU)+o zHrQI!D}CIBcWOI(ng0lFzz`s^3umZerut}Tr}hjV;xz~%&ih9(x@9$Y;Tc^~N=3VB zRc-O4x51vPoMgmGVvC!bo>16!hSQdrXPQW=SZ~DN=0Mh~$7%pCg@N%qwNgfVtvR?& z6q-OgE?H|YZ()q%n=)UCrgYGEU`hBWB{))86Z@5pCS8{eS!@9AZ8{1afcMnmt-hy{ zh$0X2?+Wr%-V1@@lE}O=(!2)68v}9BDmqDtFX?jT=k+9v*|2>U)^=!ycf#aGMfDg@ zg=oHex`y9}&UFiJo=&-M=mvGjJ*iv$T_%2Sb#r@E1I)O8SS>o$nj*VJZyGC#ad$E! zH+m($HDT0kCw<}Tvowy_sHrFrfYb(|KksN9IbCjN1HTIk!-=5(LLu|JX;;XdCz-t>-ZlKL1tssiB&Ye^IlA2gBiL z3B+ql`Z-mjO2yz<7NYZ(kT+e5`pz-gt66<=AD4<&ksrnWn4O~Wnibs=tTZjP>0>1| z-2^HY@D@LvW_bPY&Ugnv;E`4PqO3imZyn2FeWH`FfiIyd*7TtNmhrAC10qmcAw^?L zYv&3%)A&=}*M=2)l2-UWcY8;7C$axefDdw;gz37@nNV>0D-FLEq5*8neV(HHfKs1R zV@nf>jm!lnGLNhFos?@CKUFSC9FEVlxwKB15w6SgfbNd|euhzSXjsFi)C!bWP8PO; z9@YD58nY^r3pl(cV?(?!CVbVKiWA774CMKmOzW$Jj~^KGAC6{@8#0ZBkP8gTWI*Aw zUb!mTs6CET4Ik?c+Kk!-2&JFAP_cW|l|1$v%?np4d zcx_!anL`u&WkvpmHhV+0Hdx*>BvGiO>5+de#31Or zj@zl)tOFTjpr>Cv>}EuMyg|SoaHR^ZiuVAEi@5$0YDd zn+u_zfOtT*{iV|)s6pk<-gGm9Sj!xU)xIIFnD&zY&4dDo3S4U|IdgHPee5Y>JW~~C zZ35gdm_7{Z|02lu$?igZN~6Y?8N$3uc*$lIeY%O_zkCu{NqWmD=viAcEo`FY8vKZ? z026*JT?TMe((`;I-*!?_tHJ^eh@w+21DF)R?N`0tEvBY#g5>D44`%;-M#{bsA@?R( zf3QBaTXu;o5Ut>QOl*Ppd%nH!f~bFUb>};D?(Q3K__FeQOe*Fu69#~g5Pv$u_8Zt$ z{OAoAbu0Iu>uT%qH_p!qx2*bH)xC73kP=k2e4Dx#&izDx^AY#036!cZHh4^Cw_GQb z$&&H9@>&>N0Tux3B1$%Pme4ts39D8*yTbD9Bts`l6eUuW7y zO>2-)O>1t@{Eg(m4@d23&**&r$NQ;C9`U%E_9g2EJH}PX0MffYGr&i za?9p8)}70#`h!AI6^tm~|HRn{M8=F~V$$Pq#@AmP^ArjzaJ_Qnw6_W0SVLJ=$=t{P z{+K&vevC?s+Z?HK`(>rETkalhEBsh*FpytbRAI>o-#QPyW;6$DLWSs)!CoXZ@rc=I z_VZBkx|X6gAyK769_wm{4tDRC!~1Iul^mwEsThST!Fc+ts~Ym=o0hzBLE6HjHK2lce4TGfkWjg5QIy!BWU;;n3>1$@ouw!^mb6m;;S(J7E&UU ziQ6pX`kGhN4xrQ`V>v{WBn{J+!R4*gN$lEKnQkaT3!J;)uWfIs;X4v|)(}94s54Pa zR{AGv{EV)p3Qg+>i@{iYgHwcbfsq>dv&Y+c3&5*fksLCVzUFQ4NEGSchSR z8>52;hUy-T43+QE+YHSbkIxlTZXIk|tcm9fyfLF;(%KUWcOur5GVH zaXh@L*!f{vY@K>|j^e+^G;p`VXJBx+5)a0z!|v@KDzscPRexJfy^A1_9FjqA$sqAH zf=^CQ_(>r7u*^*q&aJYOg4j-!*}rLxv8HWcde?qOY*n1U5eqMWI!|5m6&+=9sf>K% zNO8D)Ath|s`T8jj&UH)(fB6r7tG^YZU0V!Blf~OkDel!!EF_)JmK%#JIp{XY$z!w`rR79CmG0?n#>Hk>V!W=KgzRM{I+qUr2AB4WigHAu}mG|Y}_w4SOZ zacD;or~hIU%}{)Ym{5$H|6~i116gu&r@)lK*-EGhAqbK`KZwL=5ipP}2_zS^h7xv! zGPs_*Z=bkD=ubU*&VJKi9?@JDSrK&)o-EIUxk=3M-wFCI2@CGN7k4OtZyjZD!2rpw zT9aV(DlG8t)FGzH^a@@)WRP%2EVx3OlKKFc{A$mRfnOxI`jn{74gJ_8`SrHwSr4I@ z#qfKJv2;|9wJb_z*602Hl~fJjs_|GC4*$V~@$eSc+ItY7=Z|$(fQ6|`DjDQkeERv0 zIjB^NR^`X2&_<|x;rVEKbOr_GeyidhvkvyGSrNH7Rl2S8ggBw3*of-%4%S58Q+?2^eF)A zqa_W+>Ho5Aj$zj1LjyQ~JA(iz_2l6sf!iT|ua z%K-p`p}(8hV}&(|Uk8)L-J*t~(wS0s^ZmjXR>%r{LqtyXXs7~DVUl%DR;MqSUR>g!H+MurUmy(!s&Co{hz&kUyCw||0P#9DCxkYZ|HJsKj5 zsEo&p%nt_^EoZiqd_|)c32f)TUL3ApVvC+Q_&N?#gE(Fof)7kTKjz#u>q$Hnt>AP{ z6OAqva4JO1f3z4I=*yeSo;ZEcZm%|af$lseu>)@a-hy~(oqA?|cbxHR7b|Lj6Ko-s z+LXN^B_CQ8ZpRf@8X9rt?I1pnh(`0J@ymm<-4vv>GM=L@nSonpbOgL)ePIY4Xo{%? z?Y{P3G729xRB*>U`oAAtR_9y&7uF6=cTS=9-7C|RDH&Qvx=U=p`C1iP!G3%B*)eUr zEp-@i+ecT?o4=L=nQ+@7OTvNgzuH|^koVuip0c5F&nS9|di>m<8k|NXuFJt=U;o@W z;?JI)bpO-P2?zm6UQ-K$-N|HflfphKCQgxDxxW0mC5X0)SQ)X^GW9 z!4_$7JruGhl-^ANI5;wgWU~8|mHuAX7|VMpWg_E*Ki#_5y_^@_zGM)Bk%)@{bo%t| zmSV%?hGceICZr5)-mFq;9EIsGhO;cyk{Mg=0TN?)?y9^+0 z1}ut%cMs^7OJuqX?otY)8ZlTD?~U?$YjjR!F={jPRQCt#Ss1_7_7|3k%9;W|ocG{s z$M;Z_VFQ^m0*D=pxs#%%iJZMBEgO!Iq$NqFdnYdA7o>t|HlnMsd9mNn7TJ8feE z(nCmtla=ry*ww&VxH#${#~oGa$b3XD3=xpF%d*sJnL2qEjk|J!{L zbSAy-u4>_q(k#zyA)ICcF+UvRdw;s6uv7Ru=EG54!7VSoKi$KReaNtq(}B0l?6+O` z?fcim{~f2C0|?@B^NwsXA1xG4JUwMa-`tVQ*K@R7JJ2#SR1!9#h1rnXEOb7iMu|*N z12||%f@zYHifRfi=iRg|+QeX(*a3K?zKUON%<;x_(RM&8la+_G?_H7t#fS`sSYMF0 z<`~5Y&5?&dDe|Y?j%yt}33wMX%~m;PaLHaWz|M?a8-vC2)mm2IZ~XWYU2^0Mjus@~ z&5Sk9P@;EYDdy(n3r9M*Qq~Ta#?MX?b#tu?)ImZz)5LL+71NoBnINSg?fzG)#`3ZX z;ktjRh=$ApusgROv*#@1zDG0C;x-? zMVpn)jINxVEGsQ$Ox>*)cRoMjSeYte-Q#tf&u=*VCg=Si0HiM~pgBI5Rx#IPVbQ7R z_I+TD`B2xf8XDhZ7Z6I#>GBi2Fpn27*^TAHwI5q=?s;ct`u&%@Cj-MT%D48X?JZso z#!p%A{%Pm{01qo@ouP>stJ~CyStG?ItWYt#ygv!4qP4-CrRZr>p1_4y?M?zMj$H5u zCKX9kfKU@_u{OS)!1%p=L^i@$onUc$OQ9wkYAEuDK94O z_f9nj?TVFMba^}k&ojQm&c8JQtciRA?+p96AG{9^2ap{H;|GQT5b6G^?a&V#+rVMV zVRM+LMrxsMc*dLxvKpMP+{eK`+|be$o16|Y>frvkqwDk={o*Kis{*a?@D4%Y-i$pw zDtlGtOHoM`{k3zaA^X$7+0({6dx8?hOi#R|)oKcRLWvImu*^!9`wyR%QM4uMAoKWT zSC)e@qOnWm6%`|htbq&(a5yMx0BzJ9q^2bv24sJ}<+KQ8xRqleP1nmD?@H|Gs$EG! zLwW1l7LA%xjk=X;1?)*)7`EeUwnkvTeQapzoD!5e+tAe&Dulm=5aiSrgOxa0=vzT} zc1@DPpa}~GtC$5TA+cs&d|8iKw|sQQ}%R8feOGt@%!s7XDJK3ty z^=u>?q*c2C=L5gyjDSr^x`qBi``xvS0Q_V3deN4%1bo@=TRAm=L)?#gvqfRivcg#j7Q-0~c=HUxldMbtf{_VPc z3kgg4ErVP`rBWGsMsaI09_Z(f6mBPGB9OV)GXWb)5m=diL;;jFGv!4w(u8UVi<`3B4Hw1f6U5a60u%-lp-euHG?oMBYIF! zwo99(l?(y-T?!;77jbgN5}w^t_&F8(4e-O{rD0B=YAR#Ni@;%d^(=7hb~cqN}1bMr$TT<@mc z1l|URpTKZH045yqtY4v|1XVq?_=a#lsy@vj7y5HMc)!R;cb!qADas`WrSrB}M8Q6Pd8>%- zQ>XTZ@ZQ}OhyE?vr@vMmqI8jb`IU##%S;g-c>IT-a+N$UgT*W382fiK`C!(OviVt0 zaPi1@U1qLZ4>A+hMRz&?(;aA73N73)>YhA%>e7;jS%li$yqZwd`nsOL3O6QhXb?1n zbK%ql4O3Xznx4m~pXgw9alfZ6?>g3LeHmIpr*)U&>$2rzk7LTv@Zv)A%E?dJ#mN0f z=rB4=5UTb`(eb_EWGscXWpLaHuIL4gMv6))b4_?nBfTuvMB=-$>d^(Kta~Gl1tSeX z-MFh0P*JedMEa=>p2{Ci390k`a1JW!i=Da|aIEWs-;?ND zvPgpH^v3xm0nQHZ>CrM`+P!?NfnjOZEMvHOd)4x_V~&)Y}Dh%9F2w+J;8v4 zdQdnTf-JrK_d{*?iuc$MkN*nRm!Az2$EBE0lqRCSW~8qT_X7$wWsOfbr~N)6@afOd z!eoa{@5OgFG-EFU$3?8)wf4J9n`ouJu)+W(h4lTF$a`W*Ysf=Fw}Ddox{`%Ovt1+J zeBkDOtp;JwEiJQm+#n+xWk9A*>&(kH?e&wZKH#-VMM6&&vNN5EZBig~S=qA4+~o3) z&>4V`2c6D~c5b$a&hd3+P{NoZ{ja|H;z5I!rTzC?rY)m{`cGA=_4m1#NrKcUTUTdK z*n)BYtknM96KJMRGW>UfA%8K|ZWMYFy{n2_$pEAbNkO_Urt&HYlDwJ4M;Ylclb7I1 zHSGD=F0b{Xfq5s?zSAAQpQ}u1X?CtZum5op9iCk~d;W7)wqgsPmPp7?_`Ty=3CNYE zX+Ri_O0XEW#~0#qayyaUc5qlt&@A#2;Xor+xES7SwYpPkBH<%aNI4e78y2{Jgj|$# z$ClRMi+uc`kCRZPLEHs@A{D+LP_U~UCO!2rJo$aMq6$!Ro0tDBgoV>xkM(Q`&a3I3-c z06`AA=*T7-Xo{M%Y0dO8R+d;^rMXT>rG&Np_gl`F9W!{74k`=)GR>6?V7!w1&vOFb z*;I64d}J+Ag%@Z5|Kv5oS495i#M2hGsetJfGkIjKP%=@6jL*DcQ%K=M(nru@#UldS znu9yJW$#-}a;#R)omxX@5 z{G!XpZa&uO?nKu(w>b!cc2UG5DH+k=zHCI z{{(P0j>1nfaP{te0}ky_qrOgtQj+&$B(`enR>k}%cJk8DdeVL27?{a%TC;<9{9Bj1 z0uTfhG_+XQW=s$4^<0@a-^kMNy1cJg@TaeCNk?~KMbU2h9Xa1L?+O73S!)&Xx=FF< zyew@|VQ9(4P8DD-2IZSk-3N;bOJ0xW@^^YMbBNMT62G))t)R+y&VaMF7<*B5x8FD}8t_B*{GH>q)d1w(S-1-g~vVj#|%g}jv-E+UY~s75ru zoNJ)Gyv&3Y8Pz+sHo9FA&EMfZ$#$$t($MV1F3DV~zlZW4?~nHr^#&sWT#(!auo8Si z3=83Hcu?6m-dE{KxbW1c&rg_7inUS+I zZT9g&gmR!^D30&&=&iZG+c&7s2*(9=S`e1#7&HX|JeS6SF7GTywcDmY68zGek!jK) zSItAWyQtO)Nz+z?0RX|}UsN=falFL!ne+BpM7#CKVHg4v^46Oa))G0yFde4&QX?#_ zfQLf2OWyP8ea%L79c+x`C~P!G@44})kpik(Y$+a;kZ;bpJ?ZvhtxFpieJh>+ByVB@oV|p;Qbi&;N5Wz8+<6EE|eM>17 zg)n_eb7LKd`k$VOQop^3#OC7gg{|cJSVKYY!kk<_=?YWxnZS*sZ(9qSsyi48Pb%V# z2DBwk^#DiJ@7&nXoVtgi0?OLT|9ZkngBwoHYoa8zGUr+HaTjC&afp}&@B^iLs=N56 zBAp(Y4(tpQM?4_dvs7%){eX@l+%QSE-; zTd9?5q2BmGZN7?qB*hw7|4wSt4l6W>Vr64eRfj5TX+1yZ4Kj-CiRhYaz<`NUy|dp4 zO6I)6m;9*|h9Qt7{Y5(x52wQ-Di!Yr1fuAWu9A%Fr)NJrsLDx9QM=!ZS6ATHB`f&b zu#CoB_XVk{8=vXU7%|OX_Jx4SMhogJ>XXIY&XNz^HZB+O*8W*1J1dBO-beJm&(IqL zZ0VULKbAeS8gw!1P}evRxu|n-!$NeS?MXQ+C->ZLB8(cSJEqJ;6ny`~fS+La9<)0S z&IEPbxf-4&%sXhRSYT(J{(4V{^v4m$v2mPvtmO?7LiC;9hqge%uo4%vwF&G>dC%{d z74T9ebS>C-Ga2HN~d@QWtuI&%Pq>4An$gUA|}?e}nqn1c$!8A+BV5p^de zz*OWXYp|M)uz1X`=V05B=1A+P{dts(*D-mn?oArYBM%+jW%guC&kX?mS?#Ini9(Br z6r)(_75ZA@Z>(<BweX z0gatf_)d?NA$ZJnW_b*T&{;PAWk5yXB?z7f0{{eNT2Y(asTo*oO1AdoCd*Ed`S_??-{leCnJNO}j8Jsi9$a|dQsW^L@a)NT>s@L+kJI}0N z%03vG%+16p35|tmCmur-YpE8QKPC9~-bA1B2|w%U5R$-OBtREW#Zqhk3p73khIeHC zQ??@rG_~8#Dc+s3PA%1dGTk2y;t?d7@BlhBXu~2o)Bf z%qNuv^&iG3N~}bo&cP`eDEx(V_nQI6ie=;#iZLomtjds2f!GA$9SS5`^{8mWAP>7g z$zfcpC{tgT*#$&)Y-V+ficQ0>?1K_|$tcsV&9*j0hiC482ghU08}^g-RJ7y?98ZX~ z&rZ>RlIWFG~+f?ye=F6pR3Plxb&{H zs#Z3On`!mbiG7r7{00^KzoAbQoo!7;iN|a3-}xKt*V6N%Q+6|aC374A-)9q%=Mk@* zj1dhDtd(V8*~Al}!LJ%IA&q0(BsA z&ig)@G1c&pOt`UYhqnF_JFNO<xz%X55jzflE(UB51#bLy+=-U=p#60Epjh^){mhl*YwonG0 zQgjLEaKRtZlT!CghYidCDn!UZkaQlDNmLQEaW?+**>*SJX~Wu{x1AL`%WU){5y zB-SY29X53j6hbS7dIzv{048hD9|L+p>L3TYknk!Vr&R953+eV&+_F* zhF@AZeKKiM%bcgBcJqk?f)+~xsWg%+mZ!F*~fs^Y((VCP2&RrS-+j9JVOw7OVP8CBuo0HhrxRg zJ;6`&8|3ID!ew-K4T6G_(K7RwGR{I7#_$(Qmy@>Hv8WeLddpw+o_kHc%bQ=bl2;iz zagtSz4yo)ig=$`RZ+WX8vt)uNvR4)DU^rlCdRh!@FcQc_B5{xzJ=_2%(LW8W5=j&l zvnz-Z%icnn2EGyDH`*@%%Ho0pIYoZ(+f?I(c- z<_8tTi18Cj5>Pg_%`>8;2%@Z2?A((K%Z}rYowWPF4KMWXUqzlv>==P2FfNZbE6rlx z40hJj+4B0Rwn_6`kvCBnq$l?|cw+VM!^8ivbQWw?cHI_!fK7LIcXxMpcXxLQ2*{?p zJEgl*KxG5cA&r0tO1E^g&*gi*-*8`Z&9&AXa}4OgSf96EmszS_6+mRFUPtDIKo`;e zcRuc^S7nvblskY}hz8X_frJAam$rP&`qG<;KvXVYN>lFZe%nA#%U_VisuGrZ?`6Fi zBIVZfdN`&374*+{DE{L5H$wX_3go5W=Obbk?YM4wz6W~!?H*@g)%l2GL7}Web{Hlv zOpn97{xB2gXGhyM+_%GJOvjcWvo^ev8+HHTp-#)Lwd{E_pA9|zNwZD#2=xNzyu?Cd zB>;#C~UW!)$BQEDNnU9rY zioY|fh>Fw9^JQ*{b}_Ano4}_~h)FVWU4%>w8z`ByvEaS)^PYEj!1#{7wH$`Z8&nxCFC=eF_q}Frh z$yYx`czyS>AX?it_$Et`!dhoa>!)>gP+U6twWD*!N@9Xf=FKdNU8o7pe_EpChSF9Y zzh&_ok)o1B??))zlZf=rIco1jmdXRPnYDlWuYt5ZQqA<|@#2fdr!6QHddmR7P}3D= zD8Y9VsbtKf{|skpV~+X3NjxI5F*Y6D=18Vlue40t|1x1yXK(cOFwNL{;?*Yb#iD8x z6IQzXyjsK_(*!?2g`df?Ifv=&;**|r-Who@yaKNuDSy@=Y@s(Ry`z${fkO^ocWV~D z72$dQnRbstATRw#3ibDtaB?lmnOj4O^FKYrdV@vRaJ0-=!%EMH&4P-t@$33XMl-t) z>Iy$}_S8|&Ni`(ZFULIX;X?p`xXuhmOJ0+Kf%|TOU>2o*y;{ij{)jtbo2ynsMw;AU z(zS$R6>~&;?fYEDK_fTZZPR8Z_Lxe}oPITn4!r zS0{WaC&c_?S0swWczpFnwzD98iu&8=;%3q>8mHYIeF>o=qPR*ezpL(M@<?cBWaUr0Q5veY7{Oo}f6YoomIGjS2VgJkf#4Z$3ulA2i-x*Zl z(H{F}ZSmcs%xe`FG9#iZu3!t@uQ9*9V!1L zsF<>;r`_c zCxmJtM~heoSHY0t$2c9f93G-s6iC`sQL0B@JDp?Pj`ZUYRRY_B&OY?bm1)Hm7Ki=5 z?z2xo)r4)d5ucFJ~daF*Sg3OV) z9?GG5=_GmICHMI_u4hb%1hnTy%SIA#VGS$F>lO3tY#&`@>Q|lQyl0C4M`#}`>nX3H zs^;?I;bg4q%czFhi*H<0yC!U2dT>;kUBCME{T_+`gx1{n*TP=aPvdy3e=J|Z($oE~ z$q~YdS8sUTQnY`OLLGnzpo#@7+0uGwRS;B#kVjH_mBnPoUAhSa%l<;ijUgAV+-)p$ zVP40)hbEwP0(YL+c;mAy3#JShjZ$Idfo-VN*LKnoT83jz&#*}rghCq-`l~{C1gdwv z!4aj*)ZZ-WIF)?TmTR299yaWrB@Bn;e(ALIR;Xw`7-G{{zW&BKrshiF+hyGL`0Pg% zlOpOZID=iZOSe*VrzwJ7pDR;f1VX){L)8i5zh*3i9qft>J z5>FJJP`F5o6`A3Q zLbMLiEBo_)qRsgYQyYd@OYqoqPbfDLA0*##A&YEw3mjuKv$BIDGj< zS5&qTL&f}c_h^SidqQP3whnJM3dXvs>?g^Vw*u`}3tGK$XPnF2R0bE^^K^}aH3n0E8IKLiF!>+T_P&@CgTLhHYXbQA^L zK1OiQCBd$mM0%QMVtz+~^^3bSm1+p(w|ScMkqUTQThk))wWDG2vyfe~_@2{%q!Eps zM~$NA4aYxEq^p1h^BT+a4aii2yeQKOnr2oM)5)h25)?HERcfs6l#}j-83~lrkqx6x8;qzj``PJt^70a(6KjFYHtM`HO$86j$kjw zbR^;$f6~B`(~Cr-iN#A*{s>&v6X&G~BV78Z>$TWkb+6b91pa1VLkGI1Sj&!b%XV#7 zmV1ej{Zc2)E8m@u&3TC2bu~eu*NDTM52DOLn#WM+G8h0DieJ#SWJ>>@Bk>Y3K&ZF# ze`}-r3G4_VkRtNYnS9HX2uCxvZD7fE1`qfJy|7ES2^$aoIO4g^qWo7wJ7Cxyg?Uvb z(Vymw1?cwhsma*nY#vT~gC9$(nLjf#b(H>$N35T_#N)1ipR^G4A$cwogQb!jx&=Ke zl=vg;0(q^v z;fN>pv%Yj+E7x?04-PI$?IGv;g4b~7uA$GTrR1#6x%I>f4Wb3^L^U|@`SP#egw4$ zTq2>8_ntRM)>4ditFJ?15i%lR7^~4DH>6BODluOcLU?wCDBcx9*6z}TMfhKgKZEb~ zB^Y}0yM+pW6{v#KgWEr2tQ70H?oTnR3HkLL$N3UP8(&2kV=K2lQ7!!=vWYqU-oP_x3KebYs+!e@|5zR7DB`?Z5CKb1W0s-- zcbqs9*Tlz7PxOOLk>FO8)zgZ!^~-PRw~rp*-LUT;8Ft@Dl-R#w(SGuHH;1)3e>}=l zH@OrRNmk3jl6@BZDj#Yn^o@z}{Gbf7v1mnyf?~5KeV_#XH40z)kqY($ZS+aHKxp=Iru^~y)0l^OZs*;mwss0XVrl<_| zYo=LqsM%^Ah{s>S2OV1XHqo;Rk;Z)=+D|7>_0NR~-@cp+drk%=Q%;^io0t{m^E3`7 zChZs?gAWhMA0CE8`Pb|ib8WQJI$cn#1x{^G_c{s~=cVi|C@k|^Ck#bQ1hD59HJ4*o zd|`gQ;V3qN(%; zsf`4)OQPrxMH86#5X992Ii89RwMG6$eXZ}Gq@q6}Go60IDxB=+^N-Lj81_z4TUSlg zO?#|9)M_X=xtEl~gR2WBwcL@v8{e;wH+16^D(Wgr8*kX>c|2Ruunnr;`h_+5L>EPY za;F&XY=xI&v+l5Lx`IJcK2))(^oL9<)4KPWLAU-9d+JEgD?HC$D%yQ^AoDG?WY$A=ig37?c?eKZnSYqTl0d#Si-iT6bJb4=UhS|2qD$gpY7Lsd*3m^N-LUAYxF#(0N}^ zLz^i<*`D5g#+Z;@2`0|ADVbAGym&C_&Mcg3@giQ7=k zssq=##?pvWjt#cY88h$VyBCUJ21}33AEysZvPV|QGU|wS4draEzaAySajQ}j#$672 zUevr&zLo7VH{8DQWO4xjpu4Knm&P|lYm4^<)5eEOX?g;S*lNFcNT=16mjal`HLhAp z|3ZJE8_d33-Yycn^!Mk}Xq9*ZNY++pLebM z6n2P6F{ai7m7YtGd=7(ZI-CLkux@~rZfzYo$%G-w?Pn`Z&Jz0>34Z%8mKC;>!`3lD z2`c;5G|nmf9Ci=N41tI$4^xuX3ev$=f@vzu1C{Z_z-`@P zWhYrl#q|Lu_T0vBtKuplv@C_kXkjVt0$-AC6VDnkf|X5)^gn8G7~0Va@23>hw``10 z?kF}Ko8-Mq4+gc0(If6XilBSs%&ojKHG~~T9QD8^}zR@uTu;DR-m8I6( zQhZSNsa8q&Yn*FXTgyu+TFd}+*F*#Wq;OLpr0GoDGm13c(@EBhl(>_>CLxQ=(xnTB zQv3~Fd7eYLk$*ju>8|$#^u7o6ocabS-`CSq7OjxFt?*^9FN%6h<;>GN)S9!t z`q_W^(e-1DR~n0e|3lWBs5%4|)~XgRG%l=)u`fnitWzE3IZy6GPl zFgquhCsX5dYFHf%CYC!gR~0{P-CJJB&2RC8AO03XYkFBKIv_B0M2l|7)%8Z50#bUI z##yQeawP&u{)g#8`sad3ch9w>B&8M@@a`WQ8=^n#t^P?+>#I@!1`hAMVOYml_$Fy| zTOAjjq8i+s@b>v}pSZ*O0Rd7TWOL${z$cqkryAo&;aK>MW;hV% z@xRg(v6*j1QTj&hzYhyB7Y}TgbeZSEByJv8p>I*cPrgVOyrIeG*1y({f?N=exPIPc zva7WxLc`uF69|uW=5^HivMOkn$0tjrVCV^U+Q5)#j9UaFcXnX59GjPaz`LaD$5&+m z4)IXBEK?mRiamlUNl&BAX3S3K>uOvwwtkfQUJ{MOS3qb0egB z5v#GPWM@)t^)_vo>&3;PKF!{U6$)xvN`+KpF4K{F55fdj5Pv zzWC-I3bt&rO36^+yJM)ILzy4($5-BfkJJ(XP|z2CBb9zMJLAxfN{lCkCCtUt2w~#2 zjQt^uT^tNVHWqOFTrH(S8S#nPMqsu{FlZxI>-luTm(f!oTWh{Zb^TeBoQZ-k+g{!& zoPj#e_p9%F=2}Uive5pnnOB2jcOP&>@W9iX+=b4207M6){OJd#PFN0-5UuUtcl*@# z`4$dOLjl~RD;p)~=Ux?k%{|{m4-=bQj_Nd@13jMCzz6c3saw_uyZ-khgfk;18INAZ z(Qf%W4@;W02d3Jd%#7&4WOd#y)61n&+3DgQSK-)BsR?7=#t-xGxRkn&-O0#pMt(G0 zu3-cH=C$eFRYhov3)J06E6By#9W<19`l!aqXWVAy@p1y^KUGXtc_ zQ0oNaLZvT`LS{_AGBbW ztZiZau7Ds_g>J;BLGxmEwl)h7RR8hzmYQ+yGXO?bc=}9_s+JDk$%BnZ&8YHF-)9?f zmF3f@@u1<&gpoT91!*%b``026vOj96Q$8{@Z@BWx&xIYUYQ?@ajSJ^O8AHMgG~9VU zeS}uwg$yr4005Sv55*vaDoChuN75b`Ag%pT))uS8Waz&|=_H4YL;pMMu8-9DokzlE z;*ZXxD@8+Y*VOP*$ludJjwQCqxF$+i8|b0@ewqI26+V~N{|G(Gt+=AHothr5hgu&< zCh?Ptv$d(WsZ=;~M`fRnuCCm?eta|1Au9Q`XKWOjmL*Iej4 z9k6!cT0pIqbczZEA5V80xJ}H$S>}xB)SQJzAlcD5!9v_C>~rjZ2+>4|4cQ?D?GhK4 zp#AS_ehk`I6th8oUg*?rJXk6&VT_e_3Bp{T^VDUFOsWT%sPpY+LvYGhhG4AnU>L+w ztgm$YITUE5zL{Cn-6BTEHuYBAa1bu{cHFcT%EmT^u!~Gz?zUuq>spZ_=5#~S}nwUUqDo=A_%q6?f7%sMWhx7N}1Edo{Y zn72>jba)-q5CFid>PC|BkBdTP13+A4>R4r3EOJISMOKA%gf6gw5Ixa-0T0q~3RQ}c zRAi4J+gq+*Gg7r6;*BDW5iLIJ`1fKv)i^ju9|Ql3Z9n+8Xso&zB)sk5)Z6R6=Q{P9 zTC*nUv$uIg-EDO72Zbcnnsc1E8_KnK0b;jlZ7#bPx@$n#o_ZlcXNMvc)HzaC{Y=c&0 z<32vT@3j5TW*_+=`^qYYOYe2xwY_F?78aY_#C{^&C1rTm%|c1yTp{}`;=HVSj6$##_v2(0ier&(KVrxdt{*}^F);9m z+Cvz>tNtrjn1Wn7#XFkF7oJ1#MOLeC)ALcxw}6$=qf#zO$OuN-DnKKyJNVogXbani zAN;UN9MRM&@jJM@g>hc_i?W# z&xpI`{$#!{!J>*gY}e=ifUlp3mrR6!PL5K~EAd;g-|vsJV=`g0(7)7z^r!Bh{|fEvg|BNJ z6aLC%?U6e)IGG+QUfp+t0EtXBZ0$gOWs2sdb4IcX!jyG=%fq5jFa<^5(mdDLoP-z>!wL?W%NQ30qTRqQOUbaseY|yK(8I9iW zU4W)em^%v^k|D}LRb0Yf!oTt8V^E5O7JmVLWYp?1%jN2oIEh2TI4MRyy)S2(l26nBN9Z>IyCXJ#!tCm1fwUfK&*+}$ zcdhv^m&2a6o~p`QLt*;#uA`V z)zUs|{3`%UM6Zp;WXcOtWmt%??>GOkeU5`WD^Wi^2^NmMTOkNwqKaeiYizHbTgB9t z?8;gF76~0qY+W9I<39_ms4a&NgF>HOs{t+KtitwD(4wt+scQ^T=ClE06mcR-a4k1e zxln7YJhJFv#<=bGoFPRMNm){r7nymn5Q0*|X3OKXxyAG$nb(%Jp?SmXv#qYd zFE{#lVJU~mB6qMl4>*(2#4~49W8NuZ=rt96m+#aJ%qMosP%(8#9ImF@1BI27mG{~D z-H26{__ZRn33?2l z$|8{m-*))}T6DfqaxRgI87xSQh6t58MAt#v|Fm|cy@e^AU#QBZmk!&`d%NrH)WSnn zkeE-R@~N29k(@V6F!+YP=j3_8IpA?k#}XssAE844j$Lf3(3M@vn(65AU+Tm7wPqt* z)1_8Lb@FGtze`7$P$;wkfYElXjF+r3*DXb5(&q}DIGvX5X`EZ>BR_Izj~`h0U>fnU z;7i|%mE-XaW87oT9VsRw(PW!e8$DI>Jv6-mJ-m)gzdQT}kxffZBIs$~;Lfy!%P3%a zF{Z|1R4}1bDoEoAOs_8W&M@te>R0D=Ced`xH$Eyph!u<#M6PZYAzHBjg2Vj5F5O=# zyER`fI*QBG=TUe3`deeCm0V6tC7*a-1krh!*!FN3G^;a7fAoBJGD`BcXZC~$CRD4& zYT$KTav3B=P{B$=qMT|NV@;m@dpk%5}{Tg#?yqiB~w&NSCYgEjax9D zOHHgRN3!H+pbF5f{W8R{inmIaCYrAAN{E^WhY{J?_qeR})1QVi?{N)pUCm5RK;<$Q zATT*^53~)y^dNVBD-s$ES;?<2eY9L0nDOL7d7iF8|RUz9Gq)n9YN# zb1)`*4|#@~>#F7EiYE_LmfS^`A;m})uCc2c0k%z)X6d8LW7V(a52^g5D7hdPMJT;< z>Hi_UnXU$}(3H1=Xr5fQgx|(Lo3Qd($3mZ_rnM&2FiKRHSxS z*LlVJrSG=sjB#uPo8!7MsOOq}H>dWG@HOZi!DJwryUk#plX}@Yb6CJHL4()1Xuh*L z`1fVpKL8yc@M&%92;TQWUvr)r1UMROqL3S_?ZH6OIFOEcIJQxo##)DA6Ak$(ymwAp zp-$zv+PgxH;VdA$GODL8O@*l8N7UyYUFGYwbLiAJmHJ$f{)@ohOJ`8eYX4BtQhm$S zXAu+~8U0cKMSc9N9mW7DQf7$Ame#!)U)dsxs4C0$cz*8r)o4BSye1Tv&Ci8tE;p`r zK~tLSxz1pTk1*SJs_}>>p*Cg;FGJ`2KVAJE7`9MMdx}luMK3O)Slm8n)|rs)TwHUa zly)!qTSDF%X}MCa$rn%4TmMoe!c5OYQs|1qR=!OdNSe94(7%;rk{XAtk_H!H(^Cs)^;hF6!gwC4NPuj{zbt%Pceqn$#x!Zd=AN z=3m=}3{Ie_F!d0?>gj=sVRTwuUe@Jw7#)ciz#)^$<_XnqeooWrQ!yR*2Dovc zp-^ZY1VHg&bZZ9YU{vF%u=vs9<@g$t_cxJb&4ze+vxHwtjByO_mQhZk%!fXqyLVji z6`11<`Xn1|iNq1{do=Z~yS8sj5Q^XZJLLX=VV~ufTrr(rbZQgatr^vmt#KvUxpf}H z$oDrtXZ5A7OzCb*@hQK>@`p)FPrOCnysCJ*#o+u)w9Hyh2fipKTET`S#YNc7q<%d+ z|D|~Dkb*FoEf74F{!ISR8mjMg*jgHFEi#1QJf13rzOmlyP)13z$(|Pd%vp7{h1NEk z?#D*JFfc32QkpiI%*@sf*ri>8x%8vrNVpUral}BOVd{aB89xd^V$9DzcG~YSGZ)`N zeLf1HN@onyG8Lha+;k6#mBNd6@*MxPQ5Jz1!(F7jj&+-7Ca=$QsO5(J9o30k&xXDb zZ9*ln3g!mdv)J1n2#!+}*$1q!r8#-ws4;mpcpiM~^~MJItlkBhaOZyXOH|;GiFx?> z@DRf79jBx<>$3BGqn*O20?HkBsb8AJSN$Seq4eBaOrxq$!fXo$ida>}6#d6npP1xu zi@1N+mjWuvjIB>$a%3E#At_k+v{QG4Tgi}vDrBn#Z( zomRDXjo%6g+gAU1UNnmr4rpuTB=~VtrH6-6;L>-HqkF@t!|=Sjk2|m*(8cPvI2r$K zhyT6Q5%ZvU%%{{huiK4_IbaPS+Jx_k%X0@x9YXTyiot+qtviL{kNQ z@^qaL57un5&}-6k_X1Yy*12-=D?!m!suUf+wRP{8Bru6pk+MVMKQQ>1}`5bqhcVc|2uR&xh;>5}C z&eFhI9fF!C;~0PP007V!0M0n{mtf$)6=>z7gm?U_tb_3fFOv;DW2@mhNq)4eIH#PlJ(}h-AdODgWkR5KWhM})b zGcx{C01$!Vt^kQKhsBKEVFoM{OOI^m}Eymen3>M*;Sv68xy)Uc^_(vjbig<)}=@l z000$l2>t?1LVEra%#Wiel!#claGbF}>EE;r2-{0g=h`sO?LA>xsejcnJmv!-e(NlWVwF~rr8jlOWpN8$PjRq&(2>@D|98M6z)k)hJfg2`$>J9Hk(!ip2-)<3pf z3c$EC(xt%Qe<)-TM9v_%?zGE;i;@+Z+KT_oq={V^s8W>{M%dy1wN6Tm&MQN zGVX@&Oiv}&^+Is+2VX>gMU7d${r@Qk`&D%Dgjv)~AMt&%HHv%EJfWa^nKKK-gB zoB#j-nUo9bga(f_Pakj$go%j)nDg3@hG8Rh;qVV@YpC`SB2?*4xq5yzcJw>fsI=B( zkJByh)Pvw6qLz_Si#C!d#rA%dHXTRZu&kF(LNmu`3r&w+?Ipk0`GsvfQ(dJ20Duwz zgA<`Ft#N<&V>DUz$M>P65GkfSRBYQ){B30tJ!YDN&$HT88#*JO;~E2_joM%3tJD)I z(`d2rq;xkJ(rYs;gQ@?3kl}GLkKJ>s7-pd3TQ3z3tH+kNOad^1?HU3$JHsD^4A@Em zIQJ^D4B)r{*I)UXKwhOxouWSdUM4@;sLdGy{~I+sM^GC*zR=CBV`IEC6lOZ3LMV#$ z$#c_X0<0I*v9*#nuGk*d6Zk(uE%HmQXu>ZhNE65QRn^nlgxnf-N6O8#^v+%NahDP3 zPxT3XoVL&}Fit?4jEFuIhy$H;^5m%Ilw13Pf9s%l!_m%_4)bWTK0yRbcc%`wtNHc?^C0M)?RwlyOpy$}uy;u1#bbYWybq%D+tymptLNvTh5^d(9x1 znTsGnsxjn|g`~~rh#+(<5jdrjZ^Qd*cQZuM*?nD6=?`&l9*!BqJ2LAH$0DZqB+}0} zY;pK(0x+h4iFUCxh?&2u%N%W*_9?tUNg(ddt)3u*k-o%`S zq2{Kg50{=t#8Bv=M(w{Te;hbGqZMLzO(ZdEbHYbmgpPs`mqs85j+6uMj0dXRnqaQ0 z#K~m(B~>xbGizR+aF9Sq@>s{V#7c=l!|{T`raSPXlhzjU7jbjG_RpKPsa9{f>Af>hWB|>RfXS4E)!S5%MWF8?)^W1dS0W9hM$Jg% zIX3W)UAF{dA$oI`-SSzVi;Ynr*s^!={IJY(WF_#)i`sTDj{F`#)L!96`xe38ni;mm zmD%&!Vx1+Ei1J_f&;bC4r?7lR9^K466 zAPfj{0sx+P^Be$|3qnGx z6xzFKb~z;Pct0Hp6kwqRe9${6`kk{eKThHov__ufIG;+XsGZvK+TNi9q$m}(8v_Yn z-EO%v1`%{)syTwsznhraS%a%5NQnUTDZ|~S12@Omm+ zv?JXbfhN;A4{1ccCJU|IElO9xyP>GJnD(&L2{0_|zvl#*a zknSMs!`TM{b9C>l3*b5SBj@;>xZA%D0g_2Do_Ck;$% zD!)3tj%FVF-?!W`0EernMq0r9*O4(_*H=XXRi9Y7(QZ%Jm6pXnio()(86EkZO)1zD z>b9u+q6pUt06>2uOiYDWxp2Z;err&Y5Hc8$8v()0X{2K#yA*G8Q8A>$og#FhO=W)3 zs_-i3>eTwm>vgR_w3wJR7j-mwgIM~BZu&S@gM{pW*Hxe!+qRMz_lx;}E3|wr@%V`f zfUL8R?t+DwUzQaNcw>w-j5yqb=RaZEgLYbYMF^Xn`lg;0V+Oub5E|DSaxn(lN-*vkUCbMxPR61ps6p7Yl^7#g_oC3RKukDRnuX# z-qz)W0SbEzp|OAyxJ^N5T5bJ(P*_Ac_^Dwu@s}Pjw+co;IzY<(stAjiVOuRZWvO85HZzXPBD2X^3)WicK+{@QB^ zg)WVRnXfMY>qAH2NEQWcS54`k)`)cl?gtJiUH<}&FT7kdz8CjVTOo-W6^YJ>9krDT0fh(^jr39 zWdwiuXkhs1{-|j010!*@wocqG;kKp=36kj+BWAZ>l5pWriJ-5a*0=x|fRF;aiT<#* zb6R5utt82wMF57(NWep=&lsGaCW8i=RN)nxS{Rq`-_BO{YW|vM>1uH?yje!zlO<+Q zk}m{#z}#qP*YK-&ddN3#I1z+GgQAMd&Q?;PP$>H<7=U5I+={A()qv=wWgL}~Q6M<8 z1t5AK2X=2V(=<602FPX5rnGjw&c4egU&kpi{8PT2fcS*$annLB5Y* zP)>o0ARWxMG#QChG$M9B-3IG$um{|X;(NKLE{F+dD;~Z}xJ}R_S5IFo#cD#aE!CZp z^*vNkt4(2RagL}-W1{`*n&sjMQpU;hS#|j^pe!jH&-BCO@QyHIvl>WaY~-;(DhgFE zWF-^NYai3m_oEjgb{zV?T39(dwcC2ypKrVlIdICe_IgyQp+QLQ?*}LKoga*n^$@q5 zLCGKH-$S8cDRuV2TSuOyAxu8J_)Ex8DD>ua?w>CwLSG*^W)Y4SQ_h0xLG9uTbVD>)N z?YFJ}!OjbQ>z_Hrr*+SuQYnd)+q1E`DbfsgT$vpy6Lr%Cl?p5Xp=(PwtEs#aJ){)r zoeP_C2~Qj-hDl1Thj)3h&N)CA?U&=C)5)^vIvmchcC?W%21)E`HVz*QyzSUak<<4K zXy>ykcO?#kCp4n>n6fUrzNP-B6v7{r5RIp`fbp=m??(^-K*HBU#5X5s-H6Rhg_~wE zp2R+CMuSgq{X5KTLS3rFb~AW88;rF)F8u8ij-{uNQ~ z)6%PG)x)ZvfyiUC=jr_NA~ExTFsty*@jD$B7~H~M5;Gz&fJJXdm%t&6HOMR)H)l_$ z>*r;{yvhnXS8mS(7XX7TbBFOG^_G!WvRxsqvPA<43J zoFBzdoO!IaW=RoyeC?|{S1=p( z=Sq;0h~961;p5!3DE4vX9syE#`OkQf?5tGIdwsX9zoUlODl@v&o_h!M&~e`+t`eH@ zY(F$S98>*$_;?von_4v`u6Ydw%S+c~(}$eJh1f}1vxxy9l!CQ4bDB!Z^GITRVeR#* z5A|`g2JbH&!YvkhKb{*Xzl^KUJ|CwJ{*t9VBsuco1ipPUElgVx{T`_?R~NvoLu z6V@LAaP%Ux9cp40ma+Lq_9ya*XXJ)8jlR9_X`Se^N*GW%q6Jhn93AK4x)91Wq0o87 zc>n;wBpym+cre1l%7wghKNk3UG80iDU&~_hpI?Z{;&-<-X2vgva#J};{qm*vRN6`KXe8lKd zEG%uaAKw_RxVUdLmNnVghm9_Kdd~TJmhZ{D`0In?-j$}%STM%KFYOT1j}%HiP$)l6 zC(d_+u`sUqic?g~7!SMPW>r(D$5%aqtATvZ8uGG67=SdlLO@Ft_aW(7wvM2{mPfUA zJ;Sn%7%#w+37w<3N=!+ItN5T8lS`G=OoFQ=99Olt8z*j!uzfZB;gPuv(x$s3Ta= z*%lgb3&`9139Y8DyxHQDw&DuPBx4#ebJ1B_eL8Y<>KWcgcI z80#E?h%WWy(V)qdKi6>PLuw?4VYiz%+)NN{Q*us?5owt36*J;)0h`l>hG&DdHXZ;Q zyl8Q*@AB#6)5nL{q)6Ad(Dv_jTVXPTFjiSvZgIyD>c@sUDVu~g1(H2knhVL@WCL2s^)>p0aHh=_Pk-@%o_~Gl z5DW{I|E?=5PG=pjB5v(ynRrXA*odxk8B@l58$&J8{nGcfVOb{vi z1^~dZQj-KJndR9+qJRV{7GxS8=6C853DnM3w1R<7l*h;t0SvSR{7GLjAf#{e$hfs` z@sr$_xauvSTj!#gNv9fzbi`_jK3!j#(Xi8f{oY+@mFnv=P43%(8UTistD$!WDanAy zG&Oa-7AfIhyte7fZ?Vc%{)p_Y(B>Wg#8dWs{Fb4cCy{-CE!*$d#U1pnRdLuLQ0A`!4`!( z@Z8DubWqv;wY7>M^KJHUh?V((NCYQcC(7JKf8}(Ol!=wbjtLPvsCkP2(;v>alm2`1 zu(jAV$qQ=Be|t!DyKMJ9cSeQa*uecrLl@m-vvE1 zeeRk0{PTxe`94_$n>^LsQbk(?bvG6A_-*F=$N==!^fqPS)t3C3bXf|3B}Sewg?W&E zuo{7jnJ^jr-EFgW&N|BaAhITZDEWGrrk~~gHmYsAIe&cGdYrufAc&$Ayt(?hgpd2X zR*1!~CeP=nnKWceRzt35d(BJ9O*7mNbNbg>?*Vyw6!-25G=BC(Wa&?;$87nsyf%Tt zZ0aQ(0>5IYRE_)6#JkC;gg9N|I_9NENa9Xd5~!z(qt^3WX=qq8!00rYjJP$zk3e@ zyLeBtyBkCccCt>oqSR0-E!rYl$c(Jm;f%_1wDrqotG}$bS9`C#j5s6@RrnC|kI)$a zr>Zck>&oWm5NB)L4}Nb_Sd^Z@v? zeYdd!s%{&Q^%nzpG&BYQ5(M6T5CUvcav)Br6TA#^R7H}*h_I2c`GZ@QFE;1yBT6zd za`Zh5BPgS>hCk6pz1N^31+8(=YpaIiQBLS1^zItT_B~pZr(0mVM$k1 zgy+66Qp67qxVYev1 z+_x0{rA5?XN&i*_#)4sjqwij;^~Zn^4YDBltI>ya66$}$!sRHL%S|Qr#fS=^^;5Vd zZGjehK?gif6O^c8ETeg@_riYuG5*B%Iaq{MNG=~d@s>2*d5<&j*ZsNa{XNR*O1@Ob})zi^JE0D{#1qpGWSb>q=u@wI;Lr) zKx}zzB76-N3h;1wcwSci)?h%X0V_8Vh3M0YY3U`y&cZbt;U6N+d-0A~A4B5TX_1EO z5;3(*veM7-lWP7>e-Kl+j)fJRW7bEsCc^WSDAlWvpgC`*3g@xLsW>Aon*MWD-^H36|AKvq34MpUOBj^*6u{rd+znLz$xgM(iI7?M9ysu_tViEq3dbhcs z@urpOdK;=VRv!fbAkfcMX*fnmt?d{^GfEEIaDDnX)XZCp4L3*TqXpW`z7hsg9tK=X zM>0mfxqKp#KqXZJ8;Dcpi$F#a;>FzE;VCe3Uly^?qfuVacx6J#=oMZ?N1&i%)m&K-8wS3@FMECe8Q^vSeN9Eo z`6c&MpvuQGyGVTb&&s3cv+T?R=4Sn=plQQNn4l|Al54NP0_BE) z&0WFsKJg{cGL+OG*zOst?p~#)bp{*MDg-mcbUOYIrfK1<5vUQrJy@mQN5dIZCCWDF z5j6_D&&kBkr+u4EMQsm({r>yQ7VCGB8|IXso^HlSN;2oHu%sxeTSJnzC(CjkJQtTMUjsyXFID@D{ALiv|R$Fd=o zD#)l;Y=z2fzd;3ySrX({we}QwnRNed= zFP&>^*bIH&^^>G~;3W)pt22xsp@R3T0XnN)DW@%u9hHn{n?dY4jF*fd9C|$V*Ji^* zRF~D}&2S}HuxDS<<%EZ%$6#666)*Edu-2y(1z*5%xnYxv@s7lwUgGi9E_VurXOvNP z_{d~#V#~y2W$zT{hO1CRQ-2j!V0~9Z)*b{)bI^gJ5|XHNkO7_j@?+5~Su-p)}m(I9NG@iMNhnWcfGD zS)QjRaSWcvuPsctJPRGn@+@MUR~VC8sg%NTxnjgP;B-N|b&XQ644J}>F9DFM@Rp{- z=Fq!YgmcRkJ{7#8Z2wh3DTB6(NLc=m#eyYkLL+33$>mUzlRhdupHq2Op7Y#>==BN9U28+Y`aUcWgM5|<6pe)y^rs9@} zZlF9#5eE0cjd?{!?%z|94lkhPHD~ZaD=sCF0+m)zPQs#t+6t|nAexoJc_?m*?lJ$k z5;vCi#hteYQ|_f{Mnnuo(Hjm0`We!;5`bTIC+Zx^syOp@&SYF&S+P;qeZ3yt{(K<^ z)Vh=JtR8R{qt2=feiTNg`<(3Wt9B3Fgd=gU^jZjI&p6w~Z63@R-b$CB)dW?t*+svYsg+aCZ;MvS{05dk)B47LZ{ zsCx@{QA#NemC2wOOQ=|p*AA0*)bmT%-ep7eu3DeZ2^fyN``LX$vtz;j=z2n92z^WK zeEKo4miFQXL zaYzHsVHB{Yz`I{nm2lhn3$NdJKLCd{PGm2Kf=-9_Ifo(4EIBKUkBFzgOHkgxEJfTd zybT3ki53(BrO6IZd>`>e#az_Laya66Rui)4`}VW^Q*~k%qtrP=k=b3>V%}QN9hW5R zY4q$)>;}1Up#GPp8zD@0zW@}YQBJc8?IgA|eZU*l$OEz}{%ADkT(ECzPcna6x&7!j zadgR^=9a2H)-FRq&?1BSt&41&;9{w5iyoc|=iOxM`e(9;=~qHOp`ZcjMF&(N%WViLwnQXITM^Kj{#X}RHv=+VQ|lecRnag_A@ltB zOdu4hcQol=1PufX`Syi!VkZtUKcFwLmB0Vf_m4J5cip8Y@#DVo+_iUaz=irek42H% z%k8E0LulXMg-5^oC3QR#80>j>y#fG0;wlrCamRwX8{?W&WQHOj^}PkV`XYu!U7O`4 z2F9`E>&y{RJ-1*nF$;9MGkdRp6+fR+L+c2>x&>RnG*aP{#*0Qutp`dg62zDjdlecbvv-G^PjQQbz*lUkN zde;>{4P#O`O3%0SQ~$&h{+iSW=$TeWar5ekkdV15XastZ6xX4gwaBEjAjb&WY8%#j zX{3<*!S5)+#5cF&&;As5W($HVH;(T=l~ZNS+oN&pJQP&5Qzz2~WZqk$KFjCB9u--X zU|U7fsOUitVz90TC;%{kwxZ#TG%+Gc`D{?Q_6OJZWBeU%eJ9msI)nxa^ezz7F7`9t zJs(<>zHAm}5DCO6wCOqG@LKbdN9JBb)QdY>Te1-Ur=b;@#U&IWLpAvmWwUUh1WZCE zXFHjJ6siNS^5_Z>gUlhhpr}NULJ8?{q1fqxPkdYfwv=)>joa5k$DWx9R~YFQ?Vj?f?1VS49$})8f4DM{4PKFJZEel4+nw4 z9QdUu zY3e!bcf^JbS@b;nsx2#(94?thWG#4TV`Y8*pN95i)lX%Gm359bokYTw;sr=WSZV97 z3XADm!YWUO=l!znxM}J&VuM!HtS+)R9s`=F(sx_SK5UkfeFA_GMb&rIh<{1xJG@a8 zRyil(Gqaa_%VG-;nzBhVUMe@HNE`+mH0pHU(@*`~RPRnO=7{3fA)EI;npM2ncytbX z{|r7!IH}T2+{lnuLMJ$Dn&DP#&>F#r2>RU%!mJcvHkCHuG%rql+pXv9b~ti*&&ZNP zOM1THTF_&O)8Jqx>HdMrIB@nuEAjJ0j$nRDLBEiOk6}wWBvX_4tSBNdzWr*krB#U-%IKOv1qykU4m9vBLm)D>wIzbuVs%%Y59kI+57t)6wQ58Uk&%YLA)iufZlfRG3`%366VXTYHO0a^sUcX=q?p5k{`vgXv^Zl2Y2y?PW@vX40apomVfUzG(q>d6 z#*}>GNf)Uqm4UiSKT>=) zhkq_*gN<@RLt!hufreAC}2L zD=}{~dvH1^i3F!m^r;Ia2l18Cv63Q6(chIPpuP$ETDZo49;O<2bT`d1p+}y8EBZ@$ zv0?VfB3P)cc1iSmi@r4G#ME=z(wy1pe?K371;ZoAE}fzae=@S&%oDK+h|MEatKn3* zvioAwQWC?$htqIQYgj~EbMUtKyzhvW`)s90M9CBY0EKmg>vo_{#ki0AShzo3D+vKn zc9dBEJIyJ{A|=;1=#QTT`Emf+iuZT9JiMZjJ0ABBC(o+B3I)0Z@PAqFo9d4lDMcPsTx$}Z=mk7&nCbLj4!zcqi5NpZ6f zP*_*K|9f}59sdzw;-GjU(+{WLOKtCX^#D*AtTHf_F zI3x-+E)3lxw*dtODt;4grCa9g2|jx%suMaWyCGAy~A`{i>lOD7=E~tdgV9lRe6Wy@#TxiUcXQig5ja zJ+*CV8bR5IHPkEbuKb#3W3ecpwCU~P`U9EkGk;EW2X zM)MgPu2yO_(LE_DY?gLxV#o~DU@()%g8nyS^O1J442wYebRn&D!6o6f;k1`Q;p#Wh z>rsp2iGJt+m2pM9QIhk7#YW4GUUJa*%nR&MJM;2V2S5NT@SnRPhZEyIp@JQjcves0 z_!j@tu4FZjKT=|*cYOSSh(Kv<11C*nwHX6>U~MP)b6qTX4r7s51WVXf3NR%nI=jtC za<=DYJID1k)yyqH_*eZ(rs_QlJvaaq)Ctxi;t$d>5(|>Qkm2Ifh!GA1Hh9Xj%4efc zq-$V};7;1~+TE#Su_0ae2c{(x!cXYYBP&<|P7g5#63no-pJ7aWXF1P^re%n*ClzTY z-SZyLl>z`@X$t_sK}S;(a3GFHAxT!ZvR0Igp~$zKu6LIZvQyc2)$!5Gt-sun_shAa z6YcGG3=S*%W(hG{OmysO_RL9FL%Eq=32i|^cQT8o>{@|_$XTH-Orq$oRv$_=w+}_O zcBSc^UZT;#S`Xh!VFnqAkCk|R8wh>;xZ)^bSrqrl1N}CU> zZnkvkohn;y@2+*?{474@$$2^KE6}V?!WA&H`P+_HF+_gde8!%5L7jQ-yD9E+f$7A8 zSmz9QeC#y=DB=!LM7Dg~gN^c4-6#lBf#7J~q%o8H7j2y*fg79aD@nV5EA$}^d$M*V z#U$ubrQj)r*`sMoOX>c-L}ZM0+#xfU!W}_r?t6`sWmAUFnG*rYex(Xu?mPn4PXtq1 zPMubLV47;TbADn=001D0%HeMJBQAYaiPwN|NEM6>af88wlqA6{CBE%YaVlU>*0NkF zZje%@-t8>`u9RPdaJfcq)$WgVr+f*0?N2-!cZk-NlPLvZbS9%5gs+Qva8-?1vr_UV*hVQ`F*s`Wcv0e^E#Q&+|fWsjiP>FDso53WTw; z`w9*8*;)AainouPxKDa7+oBzXyb}5W1?|eH|75X$(M73JHj~VXFC=8Tuqq$OEwZwj zVfc%6jifUOO@w?c%B<>U5Uf2l{$NDCnxMT}(RXEkD*he{VAJe>MJ)@$AgPcI;I9}p zty135yOA_E`7HXg-aRYY=Nb=jkOh-tw-4){k3cQ;9Qpm5+d@SXt6Bwnzl^Lq?XudM zY0od_0YaXtX_yo`XHBweCnRxkTd=1UPe7kGZQGfTk|n?~yo4uu*f;G?cc0%=V?LGm z>`y|ko7{7{_QTgC@Yc{b#p(${W1P_~(e&9ug$`o4=^FPrk96qzN`?8Y&=(_Ey)4Im zcdf2VcC^~$(vP1aY|f;|{dmz%@cHZ?{E}qdXyS0sqA^f z7+b$#>a#!zQ|aK{};0DL4qeNem~8R@UQPf8hqHdYOG7~x+mvm&oayWI=gTcJnAX|N=)L;|- zlc*{4$Eh#NzYBP~w=2%HNwe*i+tec!Kju%#=V{Y+1CAu!Hx5m(aMao4l7> zU(h>E-ah;6+1mr(=Nr5DKjo}K_k_;3y2jr2wKBULIDFU_Z2V*wNqBPFh6l5n><7O4 zRZ>L;c>ZmE?z(AGntT#* zU4j_;7WA4GeP67pG6_h72V9$^A7|6b??+S7-DwF29WOOrF7A$CuxF9`beVoQY^zd| zMN}E(x9=K?Q)M%g2(#VR}*uF?EXBvHAa;ulXagMmlib<5;p>BoCz6-by z_N&qu=Pw)-ol`pB z3DHTtsPB(1$)2W0W^Idf@#P~*ca922Ez2eb0|vu5%-`WXSgh^8PBnLNh^{k&Ch@|F zovlF3H>x#iAWfMA%WRulf;ya%I=ob3?W$RdsO4B&fYh5irN)}=+=Ye1vuZX(mNP^@ zYyDQd33X};{UUC?r>T+YAzMAld)C{SBlHgK{D4Em`w&Mp}*5-r0HFYU;=JSjXaG$;8;#BTZAfT%`Q zFB1XO7=`Ckub2F$os$0%N)?=&QeZaJMK%yIi!q8j$LCZvtqe{twmP29{i{H$GHvOz zBNvAcM^V1JBn|)oCTbsODMs>_Whz~&tEnEH0iJm%A|NS)%aqw#0;)94!^OG=TFjY{ zC31=j7ci}Un#(5rh%Lj-5ER7gVvG@bxke6Gflc%9$d$jBda(RhyBO}qdp_MKK@WKX zK$7nIwoz5kvs7;m>~!HF{W{;+o1B`TLzog?hbjbZe;|xo>rQ(e>{B;{C>CWi3!BP| z6}HN5Yka#6+%iOTF|bv(ivHN0rtqLkST{%GRTkQBRr9P{i}^Q*EljHx+w^0Djd!%Jf^kMr_9+ChTfd9fnN*({@^f z!cxEQHXPDH|7iZwA^%`rpTEI~sZuX4dQNZ0;&QhBNOHK2op<*5)y@!LdE$-hY_ z%+GZGdCSJaI0Rd-gpQ!_Z(h$K!A~X?8?{cf5$N%R5-JcK9G7BStKzqGmnNdNO&e=a zl#)$VqnhLoAtM$t2RLvt+iSIHRl+&U!z-3ff6hBIcr~_J&q7!Xc4!E-q|s-iUAYM= zn}X#N-w^sqL$c?Qj=uA0uf${0lF;73@e-UHIftO+f2eDzM3@#06;$Hl;)PYh+z;|$ zFjyIY&CqO_JU(odk53YU=dYX|puA0;PvJ)8T(N-~+~q@Ay(}O>Ag1vfU(eQilpU*5 zeGq+X-!D3xqF5+k1@>@z=WV1IE)Xgb6j+q}PD2*R$cgW$ZHDVch&dt~-jv~VbqqTo6*YYC0~qgmAPbH52!_$l~gL-R*gRZgXDk~rDP;Z>%0Y5#Cpsb#XWL*7z&q?GpsNi&?v0M-7f#J&;9&{2 zrVvo|F@8_|RbJbR$8zo7!F8Lq6M83}l(JnXZE0mD)p|NVN0s$0()_Yu4J)X5_tN`h zg$f?F5PM+upN3ALpi5bGB?X~rhoj9rGtAiNXJU;SNNs~QHPh{F<(He6izomAEFLs3 zfySO}$Xyc`7A0KdEhX7c75BZ+#NcE?SA2!OLo|j-R)XN(O;us$c}xV+z}c%-Ke_;e zom@xBa;0t@r`y5&dfn|*Q_l-zyXJPky7929;c@a0WB2CWyt0fh07BSqm7h&T@DUh| z(s**Sy&tAgTK$8V6ItNRi1;0jYm()+mV%cy92^^LSu_c!aSYX?*aSTH!8?iKOG)oT zT28LY(MNN~k1et|pvzJoxszwh+iyQ@OJTP``Y!KN?t>n^C@qcvK$Dh#%c^@@e#AhbDz?~U?M zig#byRy5JaOyi&Ly_YIG9p!lYYHKUgN9#|YE@3Y%1qg5ogrvy@{gLo2ObE$!B2`0n z?cJG@@ZzIO#S5HHA&!=Dg8H}__ZDFEdL#O2JLgH`(wLIDU`GYqAA zo1lktH27jbr&tLU@`fB4g|Ljn6{iA$JbA!Vjb9y0qf?+Y3sN@WDA@So1^S6f2g@+_ zonNugQ}LmeEqlZwEqF=39-Dq7fbA}HoS!OdlGwipMh5QxhEF~}?FX>Q+TUSeRr3{3 z+iQW6kXU^g3Zvn4IG2)_wa&3xO_94l;!$)VNG{zV#vAmKWuF}R#03nCM=8%~T{|zQ zb3bc-v>4qj>`}Ndi_oheNa9}a0G@Bv48xtW*H4-H0h+R6DNLwVWw$>xdDTjKcR zq2+QRPfh$?yPfLT_f>ea25%5_A0N03h_2+5xKS*)B@OugR)^obe7;+lRBz`CwFOm( z8)H>RU_Q{k5;_DxyON8i%uG+l^nQ6FprE)ya$c31+hE_~^8FIVKNOq}S_=BQO|wO9 zFqfrq%%ne_;sB5aIt{6or9~u559=fa-H)=0sxg;-6zMJ(2 zH|W_SXi~X0w-~30>d#`o)H7>#xZ4_jb%2f_Yklk)VJNDx-It?6Hk6TUfl@H((g`wWsb zc{0rUEJ@$C`gE~Of-tn7h=z&~lpKRc;`eG*z;#2WjU*yL`&2Gat=vC9?o-iU%1NxZ z6oP=yQ7$Qw2?sR(lHPJ{kWz+Z)*cR=YHDpLDoFLrS2g)h%or>TT3PfnoCtRuwC`*y zD;wv2{?ABq2;dScs4p>UJ$;CpB(tM;n(9H9sVUTGa3ei(OMA;laFDU7J>={oiumDK z?7HuqjPUl(0{4XdX9%kv003k`C>LLUVBq?5ibgik=SvktL&dcYP;)-lDvcXciW4JL zc9knWZNIoxImvBYLq*c+8d%7Q*eXMCQ+b_ORxmMlZ`d`|We?ld|BO%7!(NJhF}e6~ z)x&o4HBVntCH(+|V9{xLJ(|b+giWHdK<`>lhj5FKfT}PH&aqtoCIio3$BKrDMc+t* zdNQ^m)QW3JPKtQ0z!vQMyg_!SlHR1d0=J*iaiH!6gFVE@!lr{Jb6aQ}LxM8;u`ScV zjwrDxOp};+JLBr!45wemVg^f;+O@O#Ms*PrR&1qgNJ84)@^1x&qSsCSi!@BK<$Ba< zXDSMnLfCyY{((&YQe_f0qRQf;vvmx6k7JMxkZOZKs=tAX@5%6Bw87|PD)qv300SkF zexXxiF}jx$BIg@lKtp*vpSSC1zk-|Vim_5XWq+9`N9qb_Wt+L_(sd8b59y@+&h@t( zEyd&F1jt$+2QmICG|4Nmr;y4Zee%LbPW+Gi*y|GfDJXZt( z@xvdULzCN}zrQ=n|A{^%CtQO8q{~|!KXl9`T%y5-c}ih~UmU=;7eaUxCXvK|Vx>X1 z--fw`=u;*X;!kj-3->qLTzP^s&Bsj7F&Qn}Ig&RRbdj%r&+t~PZ^!MsAL(;XCuF!a z?q8I9YEhqy^0l~6pgkZy@*ydjGesgei};@Iw7D%=0=`C6g>t36$8nlD`;&(kG4~Kr z&6HL5lR%4k>L!9BJa#(wG+C{BeQH6iJdayL=%1tj_Rj6_MvevSGTwo)4 z#_<5B0n^puhL1b{H0p&$fZCEhTicycAg)b!kaJu70X9qu1+eLIzoM28f5je4;!_`{ z?_Whnv?v}vhyW^vu;N7im|A{{wFo8Yk(xqyJQ0hvuP6pp(cftiyXV*2%M>~&@`Y=3 zJ&pbUl!H%|QzXsOG&i@cO!Yt(MlZw_VM)6e(4n%WElJ;Xi{`C7^m`k#Q01_F1E<9P z9K1D7L!m_9gR-rL0!8i1Zv1@n*it8_6tC5soTIv}866{l>;73`q;RK3kkcjYy(o-qmB& zk3p0(g$f6nid-MriwxmmOD&P*k}Ij4P70fMyt7I&FV-ng3Y`bo@C+do03b|##ZmgL z5Px!jNYGpn7vd`U(WzH?xFxx=htFZ+{>)0?Q9j9>Z)5>{F6bH4N+unIZK3`V_U()X zvb)9!sE0PSO)h)7FNatKXmBTd&6|!deBjqYjpr?U-Kkr>g22_yOxX@jxdttmfUt+O zX3%sOz2(uv%zj1ME?qBwSzjL6pKL^A(Espo^UmQqN1!7&LXt(f{5UeDVg2uq=@^$+ zLO-HFhjOY*3fj&34B19jHXq|DahaTL4+ngxDmY6r-pX*s&G~y7kM(ID`mJHc((JHI zsYNx8A&7Z`v^3w*WULd08CHPc`bHg+Iot<+_@#vW36%Ym$4-z8X=Ey^4EpuI}Mnf8FTqqwDEf|)s+_x%G# z7;RA$uX>T9FDvq!3c31c(}S4H7m6ycgneGy=nFg&=ZPTL?hPEjUa2X={a;2CUj!B zmex0Y8?bG&ZaJ}-Ju+91=;;^b%p#}%xTdQ`$;zy>)0KcWAQ8} z9@a`v!{Eaj%|~?S1zZ2eUlb&e;7ExHgGaC2K=n!t2qaXc@FG&CQ{32lajq0YOcn8e%rc5^O?9D&-@OZx>i_Bs ztN!xwEYsY)^O`d!fNy-^h+bPjj(b{i-I;FbY-}S?!sUXp0_0N z0v4^O;%qb>5G(+L6oerj&Lgf97Pl(0SRLQ3s&ehhA7q%jLE>7{3K+oLk`&u+`82C?O$eGyzKy6`k&IE|P19|yq_)n(ZP zC*fl~Vr}<)q4*Q&U~?avZTD(#4XS3cHk~N&e~W*)W4F(HS`T=Pilg(yY|>s^p884eUQd zBLMF!Guf3b@Y|f$&&iC{DYl33eR;uchdib2@7phQhWlIBdCx3P3=IpAq4jD>><0D#0E8;-@Ul@wJqA$Da$QfL z(t8o-RC+~mt|s$eTu1@~lc@++rpC-JI1y&mlqXi;Np~G7co&nDitl^^4suN^VMc9i zlhOiwD{9Y(C|Kh!e$VO=cz(Mu*yA5lf~x`twl2Mm<$=)q{k$sP)Km|7F8w`4f&wqq(Qv$ zQn8Uh=M+?WQeBDf(rQ1~aPU#1u^*5&sK6UR%G`*`&pi9BC(rH3gb`VhDww%|w`g6} zyN20W&A*ccRSz`kQ)TswSIKrVQF_C?U1yG{>@QYk5gyy5l_|W;6Pi&GZ(GgWJwG%l!Zil&mAd zeKra_D!`YiSz-#B7?(IeL9T}wy02)7U>S`t5StJ0St8YcD)8JiyK;CV&cSmW$ZA*{ zUj}c9XgkkIx?sqfm#H=)(rQL&;Z#cE^@h8Uyr=I9_O1SXEvxu(#a>${6rjZeyN$ww z<%lpOC#oF;7*(=DtDi_HJ}XekjWR{(uXDM%8It;lq`64r;uzoM3x6DT_Y1Um6CE@h$v$v=E)tk(tT43gW(l2GitTWRjGOSLB{Y-bTZWJ!9K ztgyzNMQZf{(@|32*F#Io>RQsNIyn@JWusGVbdr}X5|BqxPjWA)o?n(^Y|nH!V05~( z09I@3f4z6SPfCKsjNDvWq5`}fxH~BNp@q~k*IXOazYD+uUdcu&=mv zlY$h;ISd(&L*H%;!sqj!x1+=mO9;yNqSh)C>*qUr-iwC!>3ASbccRf`yb?P2uP#@~ zQE1kBT*2OhRupZASfS?YVS^(632$WD3)`!$XaNdDDF`^Yml<$DvIJ_H&0e6poQ2|p zqnSf`xhPC&jnRM$TMnr~f{1gluR=!o{bTy+u5fi(W_HxZf|0VYUI0S=L%;j+J*x`g zWDmEKw*;ql&&6OIj$bUJ`1#;aRNd<$f@(k-slLE1!*-mX=4k$V9Df?*U$_$f?N;~f z`cQ9wIiq?sSIT){B}Y}yb%-`i&=26Il{}+3D{T@7>#$tbPO|?Wd*gOgk5a+yM1*MO z$^B-2ZSHq}eGL&Xyl3Me&2r%rec}!U2M>}p=oP2R3Q^A z43v!VK3vG`V2-bPnT>JXqs9|BJ3Id@t-yWpsK!>tePF9{{~;OumC!kqnovY}$x*1l z>d@u~AGQF4RRJK3PB@<1n5^8%!V9rX zEL)2H(U<``zy^&{o`d&3=0#tLGu@OqPiFzj@ehH>KG)}o$un2V$LJSln@}}4pqhwXdYJGmQ zeW|Ho$w4H`*{jH|Vog}&&gH&f3?OX<5qrkels%kS=*WUzJmJm55l0cBj05qu^q?Kv zcR~%+2KqJ2jUc(tIYgKp4MF*W{D3g|95#YvK-!%t!+iCFelRPMt&>8ULGd&>=e6nzL*qkz z9~VSr3|3O9+E+6cF&$<%J_Ch?Vr&9ztbA~$)MC-PGh+k!%w)zC0Kl5^@F6*24AydN zmQ|(*$&?w}VJxG;P6^P@|E#yw+MQ(cFnHD8!8v61&Q}ty@)kiqaG_Scv+vGhudxNg z5?@-UDJ-f`o}OD8B6@T!E_XY1my=YM{^r1Le`^C^&Wx=T4vGxJw4$0^+&)WL30K1} zSsBbA%(VbfX#32X+O3B4^yVYe2=<1=x7h_cm{YdX<5a@a-J{<*$MWr!P@Q3GguT{e zP*tmWUnM>={mY-G<2SOjUHQ0tzLTPlCVS7fLqZ7X`2?mK!VbOcBV2u}%RN&GgEkDhcK ztcrOiRH>=a=#as;<@{W5Fe&U2^vI?$j{2b$WO$v4*^!k)5uF@LnOA^KqX$VVxB#Ju z!V!uw?emf$FY_-rJ5{tRa@#ba2@X&i&V8&W_L`}-kGd^ic+iji^nlaOWq5F4?3b&{ zIJvmhX^ysPUMzxu$>kw>c}wx`CAQ$5I#3S5vHso9q6PHKHmPwp@~?n2223Xq_z(LF ze;lXs7{G2n4vJ9suOHCi%$$XI%d3!EBu?}vf#knY>O@6(4}c13qo7F zlC)*$(Y@Jygk8{)-kq{TQ3~u7Y%xFT$=YQJjpi!yMSP?s$P689REzQ&TDl?gRr)~D zVNSPHST`R|uNIWmOS{O6McZSPhlnnS9KW;GeI^k%7Z@{1;Nhi3ON@395LO}6c!JmJ za=zGbF&NVi;OJIxx}w;`#ql3b*HA4rdD5f_#iN9B3@p2O%H={cPt!AG4jwNcydSHO z;ikx|A0<_Pc_gZSxCZc;XKpU{iI71H_2Q`$D@4h#MQk%nu1W)K7ljh^b!c; z@PM6Ed{b>b4(U@|{iJCTO|yH4`FqA>m2C!}G(l2S#Iq<0Sv$U8jh80-96U|t%{dRv z9?E`wiZf(v!>0y~IcCSjHtE)iOl*W+b-VSt48PRjXa%$5*ZZSt!%(7V%)*G$+_-6c<{-a67^6SDUSzyIB?@^8>;FwK*zY)`LpI5J)!06qRw%&72cj{NiC9YV$ zUkX$z+D-JF{wPFkfA?+?LN?t6YLgJ{y0I1<`OQeKN15%D$ZT7{5iwk2$dl_!Ai7bz zsGqea87|-YmTb9thIaFx+RsQhpH{w+!gXRpRxOM< zzlXO{8Hg1)pA9gwZiy`ps6UwQ5XTQp>}9xXNZM8zQU3E^zUvTxmw2uElomBc$wsxa ziH-SvrBWk&c%?&L)qESX`U^Yh zUcEv%#@G7l*GvamNi%=!*Lq>iEz|U77cvZojvanXUQfoF+pBq3^y%zkY0GRQoS4J5 z4~<}el19NI)7RJDwB1=2R{!DvyRaKyTK4*x151Lz)>Y!5fCNqLZs>Z%Z?~-PPgI{P zO9m?$3~w0K_#6Uy@*~CZq`v!?{WQC~&M|W;$VW>|yon9?!~BMJ)VNmXAV1Lgv?d|- z{HR*8#iU?um%1n;aXP!FOh+6*06V}2GKy0$lJ{spYNg;dx@Zm&BT1migEDxnuKA61QB6zmXg|Y`3L67YKZ@WXBwYutU|+y z8^*tlxShP^Rje|_BVFD#q0#QDy(l5opz=0pzhQaR&>0k-N_uw5QOeV8yz;^B zP%UNzS0=8up3{Pw;VKPn?N51C*F|H|1N(LF9EI)?00PtUZ=1e@eqVzIFi7T9%Ui6?@NVu);U% z27$I1!<}xQp82gx8pW;?vWdXWzIm_Lggt(VS;$RPA;p3DngOIM6oZFNTacAARB-Cs z`dhNNuTVC@^*x8mLqku`!t@Cy*1e&sya^ok4?KlGbZ`Uc=c9gB{G@9>#HP2J6PRTx zXD0WbB)X`r;xC15J=k}nv@}wjH8MaHV(DVzkEeF~{~1XHc(C+zm!mM9Nz|l>6>V(1 z0uGxq%k4l4H9c(^`8N&VU?08bjUQfWb>;D~CKk&$Z04T?RXZa=uCtMM0b8i$Iz0=L zX+*+Mk5=l}l((~zt9G>DzXwIwSVc7T`KI7xjT|*Q#$?p80 zip-nOs^^{a(`&|0KZpEax*Ij`L15?BUu}+{%unfZ#~1qOe~;$_#p!6bFW<*{(%O1@ z7be%9cuHl>@<8y1G^q8f^TZO}d~pcd`{YZOdzHvd$la@xXN+{3`}O{T3jVe;~5B>h=%ioVb4cXFuGhywqQeuSMVyJ+$gw~f0`)Sd3*UenG>xV#-YcH%_@t_V zzUMA<$R)6zr*h{YaPK(LI9ZC`v>j!qY~c!%YjssspoJr zmDkQ?b+3 z=g8q=4RwW{&RF=5`?_vcimNGN z$3yvx`B$&`m)j~RAdZl_7W%Yg3b`xHmSpr8Qr$c*YvvJBJdzzeo7J%SEx)IV|oXCA|sGV-*(?!`6ORyd&4Lej4T)CN7wF!TrL*yj(Ab zK})Vj*szaU=X0fAY2ytQEuBKnt)7{j^qXGU- zGd;})x?=Z}`6Z*an1z6iJfYxHIY_<9MLFFpf4Z!I6@QbEDrftkYQI#|oC1?ni%`;o6LV7 z?RgT75u73)Q`EaE*j=*p0{=BEK!n$Unc2K>+45`tX3hgR4;FM7BV|~5_*vabr__NO zMXdh74<_G7(0mH(f<5E^BG7W{vTB8v(rQUmSZAr=^fk}ov489EL|C?dIB)md_1#NL z{fMCSUiK(M8J3z3p2N+>O|U6-Sf{Ag!x?$i&<_CABdh)smD$+fXj4T5R33Ln%&PLW zS)!8KrXjMB%?9b{j#P$pY56F<`Rn+uCl&JD<*^?U4*-{ydE9vFs4@}@I!58aJHXrUgH`VD4PM#m_s#3?#cRk-Hooz)NHHGQuPO!l^p!fmdP0_2n(( z{eAt(I6D9UjPD@W=2nH@hpzq&AeinI(7V2Aeye}5L%&q2T4oX+jnl!-fQUZBeH5zf z_`8^f?w++Nzd!-C5fL6>i#bWNQ^l+}5sPcVe)o4$u=CwcWoHL~F?0HILQeaCES&{c zRo~Zz4}IxwknWJ~?(PQZ?naQfba!`mcXy{CDS~tg2m(^V{SSWc`w`A~##wvqx#wDQ z9}GZ|lLmz?kE}{rgTNR~BBYcq$WUg&dofzqLCnhVBPg!l7M2iXsf@G)gm4HdVJ-a? zm8JUGzBxRQJuV5-r;(DH&>8$$h{OvDXpmQl)+P^lC-f5x-7d5G3lT?2cWQ>jEK+{9 znOIbrpjq9A^7t$*hS+zl90!k-=~cG&7h@>idwiH-5{O^yh%V#o*#fQ39gv_9qez^c zd4>ZsjC`IY$J<}-rR;h+T9Yf%^6i1OLc1VEa6!C0&?p}nGt?OL)_@Xco?-kKAC(SC z?N?9FDBLiu&UPFaC>MWUo`-XIhXkLHP3z-n&kTb;B-~734jnh;f&sFvr7ZkB+MTE>*wx;Hp zNcF0q|2`}N0C5wH(9qNDTm>Q`{*8F4M(TXpB~2<)S7Iveek3=qb>`WuSNW+%(9g_*Sez1jU52 z7djEH-yUaPXDW$f4u0rDNd`@GpJdX{O@BVJtbL)C&!Tu1erOF7J!{>}+@P4bY}etr zQx@Ijd5?PqU_J)I5i{c^xPB^J4nZh8$yyBmtVUAZ14}|d&$SBe-pR0Fn2KqOsIL`C z=gKRUWk67XOcWB?Lr_y?O}_Z@8PYc{+8M_#zK~d6=UwmIQBI$xwub-7pbB5^)l&4f5;WJpP~AK6SIMUUtT)}7b$ zIBQ|Zgr#@fW-eAZ)8ATuV^;E&L*xUNVgK6{d;&mk$!a)!WVkRq%|Wq?w2O-;=2kA# zT8f~o@T$PWkPb>x41yF%QuhvI0Z=-h=mg}sex~p;&7ooB*2;whu=UL*@E|BA*$Oj2wbmM$$tkbk!qG zHzq-vV}0GrW`XELR@-GJH={Q|F>}m7nrAgz%wdxd6$)>=y)j8oALP@Vxta@kzO8># z_f1F)TKL?LRelO!05m)YW|M?DE!E^$QO;p%I7n5sLzv#0R*TzqXri&M8@^YAn8V{V zhH%hm1DeF5N`ch276-At3MJ?p=g$YC4%0*qbL}uO3(x0~1kMed*|7Jk`jajN2QLbXf9C*t4PW z2@5&JDh})09Q7f$+{LhhrVum=JQCaW5WmtM-aSnm60JK9;gh2Id>5fu=B2XywT9~G z{I-P~9DP5^v!y|17H`t(aoZydR?8z}!DZ?%sM_d7`3}k(RmkCML+%;-B~E}_TvpTXj-dU2Tv+uP8#hQ%z>A#NI<)tOo+ zy1B%V5lFe^pHoTDHl^;r2vCQRxrGe+tq>{l-t6m*`y(bXG=g+@` zMgZ2FG2{NqOt*{!v{%oY<)z&GcM zKfe&cVO@o*F?mhfV|VZM&h%!QArrgMe1$Q76-1B!9{Vtn?J!?#L{vL#_kLmo_{ADHi zH)k49%x|H{Fv*@FzO>hSLi%&+uy9cFozMXY`b=7V`J?!ov0M(Sm3iVU-!ECEesuQa zk}8G_(gOURooenH^21_ngP=EBLI8jU!Fwa4VM5(%6Ra<5Lx)=e<;5WmRMTOE)%!9H z5o{XYV+CeXJJObpM+>g5?x+%4MFK2-kA}Ewm8cVLN$B;vtev@wPjH^GBSU^b%u#|~ zg{hkJ!2p0u`-o7L2FqgUrd07ENlS{yGumbOe zcpfktCS{tA^@H&DZaFs!F&b}pU%R|U*MHvL^c_h8F+R1PUbaB9%+BC|)0sXqI5NER zM|mt504OWAK)A&OP>2gwB=G7Ok1MVOw)R+O0z-K4+u=B{)SirtO1E&GD58FPf*+xk z6{~LBzc?olf}%0f@Q8Dc>G$%!5viQs!=MowwEFN{luKp?Xw*LMSaJJUAQ2gU4KVD{ zvcIYU12{F0WJe{OVqRA2G8h3nPIa{O1W8TS-y=$y3sSoj??2RP$TGC|pZr?S!p_fS z=UfpZ(`-NQ3~PMr?;H&l)H*qj`A^$E0-)EWG#r=(jrHieFYHbX=YdOhs_$dgY!Bix9~5Z!i!H$3Tu|<~JOcXkHq`EY&9X#L!`z>YE9MV~NOA zB0-u_WVXf?hov#nj$&pN<#iS)Iixz(injVE@4anM?R<~aE$d^|xOmOiS?iO38c}oR zuu`Pgs(H`*ZRB1I1b`JF^U5J`xWI2c2`JNj+Mj$V#d-L8Nle|2i>_2Gt@5kGq>;;X zK9`{)fe%Zlxas(>q~~#B!J-4gDl|tQ2Q3=72`}U%{KESMo1+nNLqnEYFrQq zWY$leTrE;uATLF^aRXigO$Lqz`loRgQG~x4uOt@w;#MPmxC%#?S!k7v65@*zQeX0y z7}XzQOT*^&psMHdH<_NL5xv}l1N3x6@nV&?@&KRhkGytuk!%@ftEm68s$9rhq!lG{}= z*!5E{ft*k6)(hsgZZM#;rJeuMhxP&J!T0eY+&W{q=^DGpu(+gJ$+%_*H+w3FwmYPT z%kR%5(s57IJB{yet?%6saSt7Xn4Il6x&Q%zD}j3ldSR`dFlV~-g)vT!6*6!Ef?#kB+0=p2kJKV zB)m&m@uO^T2I}3G&>ANZlPHLZ%p#hi8U|9lWN?_rO=AkJi*=F$^lL}SyL z!e)w80d#2)_ z>a#>hi|%vz>Psl@kiUFu1FfJ;o7p+Gnne3RHaRd9Eo4`;HVZW11`nSV1LENdfwhE3 zP4gRrCJKw49T^_sT4>^eV;YmLOi%bLp+og$xH?Q$jKqZ+m(;rSOOHi$Q4_&AQR4La z-~GW@$X1ZWq*Y;eY}bDq>yH8G9YNJ)c|l7H+wcF(%&TLqa21u9>u=0I*l~wd2ht6a zJq8VgtS8JqDdKXWCfd1*g3%Trr-e^hOLeS`@viEv-%zyrtY@l<@Cl_qD=?k=}h3 zkomOpc31wK=ss8`g)(<}LH)dH28;XdO#uvdpl}`bLuFAFtn4C4jVux(2$3c^7<4tB z*@uSer!(6KaAy<=1llC2Us9H|k|g6VVSmicDWR17^laVT{5`M(KSbTBmiVl+8A~$z ze1+l3P8A&QC=b_7`=68K7Z^sEdA8TZP{MlZ%+CQ%1RMlR}@03iTqmW7EJ7`{ZUj_&i`kw zLvH66#8{!{0#%v_o6ng$ZR-*U{E&)bDLkk4YU9bDEz(hOU@kQ%AO+f879*+aw}eO* z-Y?7qxQLB=UldX}V=o_Tnm+b2lSvPe-s~CSX`58(kF*)!*SdxQ8z0g)KmHT{YZ zdfSj}V~7`|+8I)m()`p`%9HeBt1yItL0eYG2gi?7H+sa)4b;OO^*Rh~z#BIJN#caT zyTe|DB5>SY_D8~K>pmisJ*jE<=xn8jks<6ap~G1m<6cCrpu*q0Pf%tVZWWTtKhP9; zW5vm0BYqu1qUCj&-aCcgAQ(QT*83vR_of;<5~IduHO%lI(YZlNoRhQ^c^Uq;ajS zC7G04lEvL8a#a!+9C>{wAe7X{++O|kE-r_esCADILcjjSeOgGGI0Ib_{@A%9Zgrx# zxcSQ12tWO1*pIWxj$e;M&eS>hs$^JIy855XD}D#)P8yR5v@(zph=508PYMxp7r)ho zGqT15yk@a_Z1ZvTcCB%+?)tyEm-7!fX+`?NiN&dZdngH5u~hE#X1^{8yjkd`>^OYg>@Fp&y$e$ z_lZ*gsqp$k2?j^Ccug_MN^#s~zU^WZd0*H8ctQN50dq@xYpAeF)ZCa=Q)|=?b=+mB zf^EIBeI0x94aF!{oD+4ClEAZx=jSB}n0AeLrV*+IUQPdj9tN^sk%_!cnvY9MXaEvQ z7ZgC(71dz&i#_8*NO4J1=w>7gTs%oC-t~Pjxqa&h{7|)q`6xW$AYdf^J?3ggG!4%| zJ+E9Rb2Po{o`+8iD^`eK+{fAiLBgf;Ldqke+~4FV9-ejZfCQ=8TgPU$K?6 ze*>E_Hm28L{hlX!=?{4hTMzecEA5ZzrtX!nco*Qnp(+K=exj(peR+csMuBymr>bzr4`Vg^K0x7L8itU3Af`D z0Fbh>nE|1`&3wh8Rg?nkr`l8B8J)40*>ip#XW`YC`IYAGwk50U3kx2Ga5Tb!>m{zH zBS+;63gu;;Pilm{e(K3V5Xc)tUHp06)z>hR(Bgj>PO}|R0tVE=@Ly@q@nO#gt<;?3 zD9dap(-5~$gH(@OypH^6eqM5w+$v!J!%>G=a2AJEVBf!@Cpsi|-OLhPa^u$S)!dqd zKNAugI%RB}W8%bv;k#HU?fQctkPQk}e^@636r@Cc#eXxFW>729A&k%nuyc#Jmhja1 zWrx_HD*{JP6tg&mQZ5W6>DdE@Ll!&7%FGwO)0utu7q$!UTtR3ySzRqRD7A`Tf1^+_ zNPh0%t5-wX&syvZ2#Tb%Klu;3R&*5s!Q^4f@!%2hVC+6``BnmQ*l-7E4pQVD%Rb{+ z;18&c1o)aPoMTc0h1k;9?Z%z7^0^vTB*3`(6bDuH5;C5|=Jb_reu>il^luJHQn{ON zF*4-p{+-YsC?uSH(E^?EO4D|R(2BBjb{k6UQm9(Jh>~W04ws)vxxn%Q`OX9~1ppX8 zyBhZ`3cW1JNjazK2v|O?Od`6kV}(KGfN5*X*q^6SXgOLG3-9dvIxkYyk^f;Yo(IZb z7oRy47GqvNmN?Lgy*^!DB(Arl)*qFfwTGWOj}x(_6=}+ffqXP}Vd+Ak&=Y?b{RH{QiTKd5#82F#5(e8==i{ zRMTVsPUsvQ3d^GQOJ36W(^T|@U8Ga&T)k+VodvraB|Tw!`oQ2{`n5sbe(dv3M{MJN zpjicY_v}BWjJ~-vPJOc;#LXC|kqsU@3XG6F=sn`xzT6Lt;C zvzAcx42o-1$=17=2FJz3j*@$PavvzJGA$PGZPVw-k}lU?_tuuaaxlwudGs82)2$kN z4+viun{C=HkI9BM4Ud6k@aj^Cty`Wf_lVGT#iiQ}Qw+uUq^m3=$-go$ebtFM(vFlVbO30tw7mDqFL)sPLmXe?8c^t@HKyMaYl z006-Y+AKK>I(kuq5O4*USK-n&X2qkTj;#5ilnDR)!xX_NIn{`I(P#&Qv$Y?@6iIB< zU{y2M>SN3eSul56Qn&bN(!BC(g ztri|wq}i+~ovB?pw^DgSDv508uY#c}@Ffy(apt2esm;ZtZx537YhFv-w=*euy7Mc+ zm}sUX0_~EJ1GMm<)qMa~{9q)6$@<3g?U_frItdV?PA=U2oZ>B@7ve`%qLaz)AaPX} z;XdOLaQ2(2Be(!#mE{Fyw+6j<9hJy*!T%vKmMm{h%LVd{76enhWcymdC<==oefA8S zfJ|x{{9X7t9Si`ld2xsi0+^V=>IrrvvP8}@>cPooMGR(kY9zSy+DFrc3FNm7eH*3- zzTgpDWmmbT>61ecm1?TQ!$j@oMp$Jq2d>Pl;`iInQrmw63+*@O+lkuh>ELIP;8W5o z7~S3p{RUv11QnOFHO#GOvUOdMm5|ypG2v_&5pg)|(pWD6#}SW&4M%IWk0<`h!89 z9cnL4)eZ`UJ1B34a(`mF(?*o^fZmZqq)!(4<>oUI5MBsm`@1sg5EQ8F(2=&KTzMP> zwTG^uK$7_x&%{5yfT>k+R#*0$GFAq&;z5lCm>GFrZTF#_< zBuRR~a8{0!A~^eeWS?>hC->RYRpE;l@y*~8RB-3t zpmlBdYL4`($50jJtSUsBC>FUhO6J6>w0VB+B0z$H_OJz5)2IPh?1ve|TCJ^f24F#w zT2WhVxnvip+}>h;`I$JUb?&hfMNR(7=Ji!YQGK3kA1E@KToNQHQ?jREr&k`5tSuIN zCv*)+3Np7Eromt1tHA*TAGu(VmKW3<7zAh{_MG=#<2Xm?c02G&oTNBSS z-OCj|5elZbXCnibvQoLA#5QBz*HH|swzd%%${B1@-JZmX{EtJ&e@Xp~e(sl>+(Kpz zU@L|dM}3#65;FeiHOC;@DUkk(ROPqnz8eAnWEDf=Pdeny&9xtMwWFg7U}^kgqG070 zF=j_3zpo$9M!YGs7A6=+FJ0Is8=I0ZYE<<}k$l-6J)ZB+j zOXge0&y*tGlZH;sPWO4)*+;ZzQPE2!D_l--sndGk*W#fi{pu$^f6&gUuw&KBndLj7 zBLKRVS*G_Ed_UwOBDDKFj1u8-7~ugh@?ICG^xK;&uMh;f6$Nv2h1 zX%elURyjb)L7^h;EDyY_<5RbtUTy!r-jy8cu|^h z{&Gj64IVd5GG7v3dKF`k%UleC4uQ^~apJyu@7Mf{FTEj)qD37?MI2>>6nq3~99Dc; zQLbrjX|qy~b`tPCi5a9Ko!qL#GnyJ`QI>a^x2?G@XehxST(i#lE$>U>^F#NwW5gll z@-$GT;~@}H>ktp`J)K`dE1%Bcnx=I*tGUUl1~OL!Mj&?`ErY_C#K8U(tUw z3(pKkV^QZ`_f1~3FE=OfAPYyFt(J*ili&l#$PeGtC;{aJA=U_zd~ZVy&D=X}R@jL- zzHc0pjMoyc~;O0j1@J+QB0)ey>0MPDl&{wQyV6|WZ zs`$1!Ju$32C?c~0Vva-t4EHE#&Kfw5j!=x}Vm8o57R0JQr*IJuk=(j%Ep#E2Aej9It@ zYF==BHOGKX3h`WpcvrIX!mtCb@vLQxT36B2ugF}4_=P39f`|5})BdL{qQ@GD$$p~X zz-v1%z15-abiW-=Zk%CWY(F;`0I*V%kaSHM$v3pkwSVWkk|bo+)0dxvkan zGv7w8K&9RmyiHTNk7-MHMeL>_NPOo~-#^HVB_)ZIE~@&1UzK?>7>@AaB{7yz`9DHo zlJrb-f>{P?M%Ij|W>NZaKq|Y69o(=Z?t7K%qURrP`jc)jgi|bI;Jy@xEId{O0I(Go z&=}QoP*I0O=uLbR+6#9|!}&wyGqQEJ78vuPn!LMNsbxh)we=#~DDZ;^ddq^-hiT{m zJf6*A>>2SNTYTB>9JTI0t#rdfG41QW&?Kvk;M&*P{G2LxT)nm7I{lke`&I>o!Xd8Q zwSlgu2RjSJ>d}+~tst2At^6}aIQ`;BI(Xj{ln*&)f0a?D=B_l2+u-CpcY823#N+mX z*bR$^4|=drFdk?&awTP@qa%@OAJ6HOFMPE5|F{BTK)W(?DdMJ1Og_)qK=6 zTNe+1en#@zX5tP37&v83Jb4iz<@;e1RM4idLymBko-jGvM;0u}^uJzFbpn61Za2n% zTlk)(iML5KK5e5q7l1VtR5|!Vd4-pTb5He+C|j?iJNBOR^aq9}s2)ht`r)NrGkZp$8?J?ObxnwJtZZwP@vT8#ii8tM?C(nB27 z&yvHIN%~_LYDyj9P@8J!SRpq$OBFM@S*BGk9^H)3MyySQI3XEzV)$%M7Li5ZGmV_q znsnmugNs3yBr1r8cNGCbD5?X&3R7;{fTjAa185TcyyL$#Yn?1|42lp<<~Vk#B4i9f zTtOKED{l3OM9E-+FHTl-O|a$x4sJ0`)gKj3(uY6sk?aU`6~lOSc>;fzY{i<(_`lQ4nB@o zm0?^li9V*8)vE*Ydo~AkUK7I67xJ`SId&PSJQNI{CB(6BKb#_4LusQXnXqQ@&Cm4cDR1E;iEo>gVO2JJ8BG-4K`Ae^i#2_S$Lj1f6clBN(?kjMkvd>zUu`2VO(RDQmZD zsl`Uh;*HY(bo5#K)i6DIDUR>%YYX5=&`dL%N~b&We2MjtHR~VSZSM_{Zqd=i!_kdxxl0ziATFA@*$ zt2P*b&IiQmlrUO2C#W~%M#;L-oDqSTVAwR&Vkl;FE+u)wVPG_>3e%i=zCs9_&^7<$ z&c69tuMBzP!kE~joQhjhZOq^4B%%4Qy05tX{TR`X4@x*g`g8x})9uMSp)&ybMNmak zRZz>JJX_X-K?%vW^*t2)Nh|%#E3Cf3vK8To{WOIJ005S0OP6nP(5j;v^ElZ`Q`@B@ z>}k>R(GrWlKUfxkj&M?LsZM1zh3vD>i-Vmx+t<6+b1vmnW@3}mpyzM;&@L|n>^>zV zM3o1)N4Z|B8uRyFSw+y%QsGp|-~;Ml6fr+ttKj5wJACswNj3%S@s3Z8h4v~JW|77v z)a%m{qNqwNFJ%d&aM_Y1m^>~oVI(5e@dJZvM0}xBsMOu!xQw44(drA=0Qg@}KKRt* z=A*xQl`^&2iDCCFL(#Dm@bgQND&Rb&jHD62(~3YU!TS@tFJmU~lB~taM7wq?F&v-q z?uq$7lct1|sELOEdsF1;gqv{VSiQ^(3m#$CPUz`n6w=bz7qKX;ReUFO1;Bh{oDs}6 zFtwNUopCxl=l&s?+e*-Bqo^HG zF{l-^a+$PoE;__56USN{(aQWnU`n;$PCnH?Dh8;OP;hZ`gqEQ7?|?L{WY$H}z4L@E zU7TEd$9}S>xATg*^%CEv6tJ@;7njkwPVDSR#1cdWfUXbPL#evaxmLh<@;yR!Jartg zjNvxWyf(zKb1`Ur;Kyd(;ok<4R&iC;@f`C{y1ke;*T+#OYL#ig{jB)D=58hSxcI9E zBc|lKy83Q|=b$`1!Bw(B??oaMpj5YTk_m|mRxewDi_}zwv#LVyV@=d z%8KHy1UdOBv^fERF+R2V*s!!=#jbd<(BNp;#2A>G=2<2sCHi=K;|2VUqT^#+rH^iE zArr?jaF5YmJG@gZ@DnOl(gAWyr5vZ@BHtYhsJgX=3}tmw@c;Wy^|=AVxN*#C`s!cX zhGlDdC<-Ap)k-q=o!lr8GI{qO4)w?6lwqA^J})2YA1p-Bx&ZN}{3 z!)S4GoHeAeFudT(5G_n9IGSROU0^byjpJ%SZ6#e8PLs7W`5uQv4U~W(eVnIocuUqM zQxb8*z22}$_F>0*maL3ABU*KyoxYv%^8FR+xe6eUw&Q08bFZjKRtdyho;&$+opfeW zGY=<}j0!++j(XL04V9bAs6P}}-*uR8RbLj^pdcw~DrkeEZk4K8+w(+(2J9z_i zGl}tIBe|sO3fF)%;;a``FII%u-l{9D7!#7M02Y|p!GU4K6mt(Z0A!W;*^jwCrAeZ? zKWFDuC7|^-C}5JBYU2S{SKWWnT(Tg?O_tvt*Q99s&%#?ffv`(=($t;yKOG?49x9`9 zDF&hPn;vnqhUF?l($JweTQPxwQi-fQ|4$`zHXJe>bQlb%bPC!W1Xar+J%W-a>&w1N zIpw_Z@bjtKY9x^DQmi^vnHWaTYk_NMf3Hf-9! zLyi@xXi&LM=&aeC;FKYQCC7VDy*7aCd5C3OEFiTlyv~e|qD;qHy*XMKg~vAW!$>sP z0*J6@`_wt*;A1Z+ugaAbT8wm}0v%ZGYMSAUGH!xc2!O;#gPp zl7YB_1zv!K4Nqg|mtesctE%yOlL3Gs6lEMcg3tQu&vMr8gyzzD>VJl)1DC|P&*x7R z;s}ooY5!7;j*SZ6sca9oz8>ZX^!xl%IyoME9Q<31YPnrYw6s4f|HlRULgL|V{E&Re zj**R!ZPVadyBQ}|^x~oWQv>8}rxbv~=sh)?LL_8!mL-;wB`PJugmI*lan*RM>Lulg zT1+5umzUd$8Ls`kxbIHK`EAfGb6(XF1az&&;*_6+6VVmc)nqNyIEKDBTHkgRdt{hSiPNfPYD0)2Zr2^-tM<=|9=iKew-oBVKlW2^R@6$ zWJLuU1(k*7Z#)TYjw&b$_81F}+HPer8*)$@@pyy8AyohnBCp)FNbEy#&2@_fyR<+{ z|I+4Nm;>f%sHF+|Q|4MkmUZF1W3A849Yc!Wqo6V9i3u)8E_3>+dUKA_fb{g7O#Oi5ZUprk{%;pE>Q ze#BgcATG<4AOtp(W4C1}(DPZvDfl`-tnFevnDZPvUtf4@^v8pMFGQ>LWqJTu^ z)GLb8)kZ?{m7_zUi;LByeqejR?7UaGR~!IQoOUaNjm)y&MEn50D0x&#ofc#{YU60h zjw|h6{_tmE6%Q&vahYpA-V$ZplMKu9PkKMgAI1+RU;TdwEm~n$6x_?C;bN{x8Lh;8 zWk&Ewu!B?rV$e0<3{JD)=;UyC4S0f#DRWcGjzMXl29)quPYGmNtPTO^0_Eb69UK z$?pEC1^ndQUw+>6rG?bQV?yvv#E{w&nZ%0)1`)>m{GHGl0K@)KfjCFs)l~ZF%!#r* zrmj)3t{>q>Ew!?IGP?qhdaiS*0r2Ue#1+fTsjGh>OzcUKr62p66DF%g1WQN7rdVx! z=|d)W3EBdpo-o8d4?XS+DUfQ2E+~554*zxg*cFB1uS_H9(q1VJi8Pf%)lXa zDF8_l1+N3m#sT!I9Y4z8nQu@59~CCKguGhA)!EYp?5)6J??J~Qb;0n_q6U6_@>J`8 zI=a$gBSf3B9Z#{tS7894<%0lz7JdX&OBT-kwh?1&{lqyUVwHPMZxD3(26AX*DC`>} zb79_(F>4)q)=)YG{=3Z}N@*^(agB1&?t zjLx<;!}fTz)sxXxrL8{mIr%Z>6Gjh2QJ%=hRDm z%`^vdu=FB|YOc$RChYBZ_L=0*uW!URr<>u z1$r^&4+k)sUB5ID8K3P5-D8U_XqF?_s+4^3p-kEAP4OFQu z^xZX7kIkOh&=kBZPVo3bb%^++5mSj6TxnOUV3DRqwm#M|pkbH(11&@%`9=gz>Q@L& z@PWoKwD#w&^3?Qe^u}S|&z+7HVt1EW zooy?pU6YUIw@{uoGJ+tG*Yo?f=DPhhG7_Cd43QIKDcyJAY0KI>znpT1t z8)nEki#|BZA{>PO+bIqXXy&@yq#yy4$A(d3w=9V;d>pY`&mb^wQq*oZAaBO4{|T{% zY$rBrmq)*vB#&~^RzoVg-sI2qwp;-uB))G+cS-s&ho(Cy=pZL5`9&<=Y*^6VT+K%V zA=qJy#Tv=YhDuEHF$ZBtb>YnD@9e~#AI61?4)zOAN_!c{_^*+7wh3M`ZPUxs&$-5; zdwh+3=Ho^-QUD03xCGd!2`MOB$HVPBbviu$ZM7!y32w=G9PD)^rVQ959x47B4(L~+x zmyjy^RW$ruX%kl8iIlrd3A7F5)gYMmMf~zXFVHR%F_&oY05VtAw}q13LsE|`r3)WH z=OwVM$gen9_%ie!BvOQMOhyCpGYF&O8vMGe2d||hF~f^xF)bFhq!8+pc^{GQnwKRV ziQX-POYJJnn_i2h)Ac2}?qObX2E*F7W?-r$c~x6Oh}bu7kQ_2U^uB}^r>q5GRVGS7 z1s-QvHBP^71xS-?TAjp_YI0BE`pnT=r=2hQ_l%(=>59$X>f`wrVf|`!)q-gRmz0$6 zl?+ELbRX5-{=FzG2iFw*Hcxo$tOjkWQ2a*-3?s?>ehyt*)A(xo(2FQs*DBZbTRmyu zo>W)nTj0ZMpy%@l4MX}Z+kVCY02HG>jC1*lB&TV)k{&x-*a{Y|WXT{1G@b39)|skk zyv0CNRl7iCB?%gq9-YbwlZrGAC;1lRal1B~w%?=D2R;}s(x8azm04gbo%hk^ORB|` zyTuuPD%I%PfU&`gWe!shHN2Yb2Xzn6dG(E_mt zb@kJb!T8O=cdSoKGRq?++-|~d{r`2_zu;h)nU$A6>mQnhKlQlLM8x?w(mR{k1%JV{ z{?vzS+dj^O-UM|1oY2sbN7m*8$dQd;;<7`y*srK4h}lTQ%PdDEp^D}*QVDGmW*B6h zSfV@5)>YJm+3UuqrM=+VjTu_ehj{^HyDbwFCy$F`f3EiNgna)$ z8A8Au7y%AHq_1^uC4m!%uZwTPJQcHERt8*s@CO;^5KKXZ!8zqs(j&ei_s0qbjtc8F zi5Wm6k4_&zGkDlbpE*$(>S&IWxb45mAuqTG)B`=zKPKCiv?`c9&Dz)mE~xL|$%=lp zO&o<+e1v(_`VfpOQ;$n%zVC5O&&TB`!!>$MjzR;rJ@>;pW_!plb2hZbHDtWWD76;~ zbcCPMYWNdkH*uD(;(5M4v}LD@_4VD*cmzN%Gc7wX1PJL$`+j!{kDQ0Cr@gYX87?QF zj-SitKer#QAkVWY6AcmNw^4OCKkyhdmgiLAjd0uDi{_Go)}hc zpn_K6to)Ge*gCm0XF%OY{yss1C6>JBT8FYkqE5ZlTWg1^+xSh4?RKqoJtwwU$Kkm5 zb>J%;C!(vtJ-NMifh7Py0S>#!btnjo!3hmm2!wZgB~ci$;ov>Iw6my2MWA0ro@zeM z)CAtncCAIZ&TXlbB79k2p23OQX9d0TbxZe8ucSL%q4%2N>L9mivu84waARXm2{;&3 zqSZ1%{HHn^$yn+$j#^8afXr&E8n7ov47Q|=9p{Yh3WfgFKI^JUSZ-KZfLG%yadC*K zfZvmG&r!PRpNzb!>vePIH$0I_gAy-LAp{6**x7YH2SFg`06sZ%3_2{@L5q0GZuVNW zZwr)IR5g45Lerr(yTWSKaUYs{m@=rFR)~PW>b`7GA@=Q|F4)#Wf^+&PU5)A`h?U81 z_b)#^cLztHzPG7Cz7;n2L)U+^DfNW9bL zl_`sQvdy45UvT^ZNw@9Ogbx_W6a24@3XyI8^D`U!;o(KAe##s{ASr%?ydBgRp17If z-Oz+15Q>v4eVfZlJ%oh#54l{x@&c%2)+-ftw&wKb5!EHbj*)-%nB@<+ve#uQ3Nkl#yOlCCSiqoGLd+yI5C`TCygnMdg5j&-iiX<04qC| zJNSpzWGvPu=~@&+HRW;Lk$tDcA!jzeY#uh1@DIPlf|75`RRUa_xDF(QlmW5tm#rt2 zAL#%T^&z0d%O1>$#=%2DCG!;_8<0mKr%^ds^?P=RuXQc*$F%zQd+g*0yRFub9r}k0 zUqB5EC|6r@ipx$??+a}xO#0#zB@1$XeW}VBUva|iWh-glY13vhafS4bc7@?;Mn%8jU(ZGp44Oufz*HUVUcuk`ioYSfNQcT`lWN9y0 zzmcu^(s-mb@yDNN*3ruMW9S?#DbF}f+@pVO1!u?UfuMr?xl#LF^s!Gru5QbSolh31 zAQaal*M0v!$Qc+wqE0J`C(j!puP5b8E}QdYABm7`)R|^gg>&-#xyZqX7aa2zoi^EK zDuJh~7t_EDhCKPTniM$D(l-v$NyaU`3VfffrDa5_ercV+P%boQq1N$ctef&+k&AJw z(O3^Wm$?!TDO+UAl3zI{`wH(!6^^&UMpEu1uw%8^IdmH{xsuhU>QvQ-S-D*cU=p>* z#JZQN=7pa}fZ5#FxTFjc4$PZZ=SFmDF~rWlwVd*V&fqWOx>Oni&`|s=fM{bgeKuxu zH9X9~EaH!wH@yBa5K7 z&g}vy6fr%2YXt4T5WEu<=tm}H&1`;83t2x@yKtA7xmp3`W|SL;3>&*v%3P47g#;_1 zP7GoY1Oj;x;V1%h08$)J*gW!mg5mwX<$10#Nt~g*uAwC<{&-4DWzGIuJirZD*8SpP zJL}HcGYz<5*3Y{tZmBUhKYgLWW9?K#wjZ%0&O0nrfB75j^5*g2;OFKQpLp^xRW-Dk z()iJHhOTQ4;E!>ZfpJi%QJ~l3Mn=iz9-^;d+v96WNG<-eX@GfNSE#xAFP|JEPN@0@ z>Kl2KBpT6ZxB4b`{ZKJJ*ROvTs<6S|SdRr`<@>|jwa=@gr9V&ppJ2f?1jx%1U}fNl zDYG@hE{I|OkEN^di|T#4cY|KKd+CNHq=lusLqKBblx{_r?(UQ>QRxstx=Ttx_=+?N zh=L#>?EAsr`ybrGIWzZ+svV?ZizIKVJg}YpvjHpRt_wJOt}W=-+vVihTN%Jj zEMTS=?ls5J=&oGgSJ>tD`uRCb=;rOp-~LV%aq8pNqhVBDmp(}U*|PQ_U_ z)$q&jk(-$}6_!@KHR1NtQh>EExr?7fiVxPV05M@JE(joyPpBFHc>b8VakunE**n!Y zQ)&x^+p2U^CY`=>^(o7j)3lHNs*`FvF7}Cjd!hJI(JpSESMVKvZPn3$wotHc#M>au zk1_-RVJgL4OGB_=u~F!gAquH7vCh8>mtZ%d*$}{VXJCX~2DYVWLfkaVJTg1MO_>14 zuHLC3-+j(l4>DSX(F$b4%@f$sRn9!4<5k(LC3W{PEjjqMtgI@H=Jt*YNyc&an}50o zXfCh1^YX(Or{k2Svd#NHjF*(wqN^F$)S|`Dnfm8wT#vFQ$l`ej?DQY7y(xExkhEZ> zE!KX$OmXkvRit|Uir(}0;YXYH8k#ite}WeObvV<${c<_A@TUHGcG~Nt{ReFwofaih zU6qNKxAND_Hvh991$Z~k{MsHZR~Fpljk?_QJS1)Njo%e?;PX9~}`}BGb z_tfp9tBGmb<5|q^xd11{AiLm(#LG|nDuoyAq{y2eJC z=*uQX=EdtAC|yeu4i^sY<>#}f&Y3cQT8M6ak?J2qA}#lP@Lt(TSKGJ|G5_rR(9ml* zcf5a+_~GtWvCq&A$d!qKlr~1fKfISQ|C!W90Vpais4g{I9nX z@hN7D|XdDgd z*V&@Eic{Y+HK6T9lZt^O!pn(7XtZg>8Te20NlCOcVq{5Q|H!p9AP^{`5q9o2+Ah?f z(e65l{}Rt$6`GhxG`_!jdp`PaYs3wS*{(~g|wcy~~@xUb|AH#*B^>jkmC}r5bEhXv(EH&c`{`gwm&sTmFc!j6xD+l~m=%^KSWKw`0RyZ;=RrQNi{1505>lxh}os$VXT_c8&8QS04ts9Y;?JiSzAIaKF+fYgXV09PGGefi> zhB=M&IEccqh2iUxF{n*chEz0JC*k(10yDdZExEc@)>Axme$O3i1!WtGU;TZHrNTX$ zaN7TQx}KqKDNXMhb9V~iCXM5!A^px^M6UirSD`!xb7a*A0PNXl@Si+0 z5>|!_GP2g~;7DanP=Ar=x7s%*E{REBWjWG9E210&ONb-i>`f&+!qT1O#@B36s>SZu z(B0i%eLaYgzxe(4@aiQ7Bm0j-FmM&SR$+ngrpa#TLwio1WLOis=YdB5OUf#%DHcSO z$)VW#nM+={N&DDpRUd%gjo_ip$9MVU-{QcnlesA3qRdY(EKMAb>f=MVfO&@}Kq(zq z&p#k4V^P#4kJ-UMxP&6hXX-k8!;GadF~>=Q`L|vqoG%ORF6R&oN#WgALl$cZyiSMW z3Eb@boy};_8emrIVar121%lZF84j{<0-uzdMTdM70*CjB%v8@Z{(Gf`M9&)Wgq$wNR%%3CBjT z5=8u?9}JsK_^YVgz!^>-i!LD~(}Z|HkV8W!K76>I&q2Q9Igr~M{N9yrv?+}Y9%+!a z@Q#uY?-!QCJBI$l&)4`ggGa}YnNB!me+o=VoC`|)E2G-($K{GLxG25Q;Fxc`dCJ-@ zVAsC>O%y1g5BVDot?<`OR3yq$;gy|sbN*EiUe+n(C=9WCJxXLO$*137v{cJUqN4np zdVtl)qE)fy~%o5S&#UxiC&+N>#bWioR=$%)^0j7Of>g$2N!{?VK{gseW6mGfCy|Ov z)0p4XdxZZGLJxdJ=(YqaJaejYeH5f63*)Zh_B^QRPtC5@nX3F1{F7(ywTanlEI0tg z>YRfxwv+TVgEBQQnV59X*F)U+*t8TL*nXLt{)JK!f2^t?X8buU_XDBx-RohVY8j4= z8d;0iJ|)*Po`_@oK_6Rl#4yDty<&?`_^W4+lu=j$0Q^)E53x=!O-LrZC?XSqE8&k6 z#zoQ4_{U^}Y0IE2Q)Xpu$DkY}R!#iV%g~B86)2&gDQ&zMv-Nk24~?D=79|l!Ue9`F zZExl`s(vnZI^OpFou1EJ4#!#yNtFJ43qV_e06sZhbE zbmIJiP2p=0EfWG06#ApJ;p|GBqy*tEDU^VR#zBBF!m`Yj6fe!U6;XfC94EdUgQ*t6 z6k;&BSb2`WR~sfv95g$6(sMpjttVf-lgMvG16)0TYN--P>&$7uo)=3==@~>83xYFv z8ia#}b&{>1lt;33OMS5P{f(WDvM+Zmh;p$FxIWB|dSFlc;0H4bD5*T>DPP=vF8hMI z3w`i!Eyp$*oWb|eT?V;s%;9?dB-tw|IGNk?5NG@?<<9ha<<*HB*GQhYItA;`8rt9Y ztF(!{(1ebTBfLzYEeV_M&(V=B($c~as=g@UucHC{Ikf? z*A|$ggHydJOUvW_WuC>}-DZ9k+TW`{=i=?YL1ug8``mPJODPVojOLYu2eZ`ctRKo^ z-|I6w_Fs9Mf2M!;@{-4tWcJFsnjf6 zt?xjTWHe((g%7w4vRP6BS>jl3T=J%B#fId~LPJsN1h4lFwHjUx^~08hQS@_r?&6F`<~nXnRh|wglq0wGYk#`>v$=Vq z0i0d{u^UZEK4l0ZWR{$EJ4m{sfnmcyv3!do9PB1+#OKebz^Ux5stlIAKg#E#k#+-1 zF{}~aHaWSdiGM1pH_JNz{)^+3(xG)H-B`p0=6G~>+w?6!j#LHW)iw>JdAl23! zt7}x=F!c4Be+nH`<0(SjyrtjCi**>*w96 zne#apa?a2E?@QX|@sj6Q>%D%AKHj@U0dV&dPKM^CuBM!;!+_X!FW2ymAmN+O8>5m7wn>rv~7c#`Y_2i;Q@% zsBH;){)TsU_4B@_ZJbi$106jpC%QiFDFMS`slOJ_Z68rLV|;&3AIm@B%4dS57^E54 z&9;2Q+()^65cq&ct1#c(+o3ANIfz?UsV`;&Z+i!) z*}mr6jUSe^Cw8rO6lI|ng*5-`qNllNFx&*q)AU?LVGwtKK=;p%`@sN|gy``F>ktdv zrF3pNGx+b53v~py^E0=Z(FB(u8ap3V!6@k@>K4}tI+@;@3q494HDm23TUnvq)B(Il z0Z?irpa?(#MO+?xtkNySJeDYSm6!-Mge6S^0pOB^pE1R4qJJiJdx>}t3Ila)${&?r zJPkdwY5mTfZ$<9>H|bG9hB}!vCGyT`4%HkFkcm56Y_z zM?M>dio;~*8wb4MFhV4953%c`I1K_;nYgJ-TdDEDAjLMmHgkiZtpAv^^K3~4yO~X1 zJoqCv_DA9=0@p<>1Xcau4*Nv7so`WgF8Ntbq(LIYao`?wWEXcFdSYp@DV(o4JpRvoZhx677IWHYjQkr-iGa&(e{P z~%Ud%;Y$KLa+q6$ob;+glz6O-Ikvyh6K0Pk%?w0 z20+KYI&}6amGgMctYyaH6D(;UEw<#^4%VH; zR(HoYgFUlm(-BRkJsattIyJ<$iJ|r?Wr9{K5LMR9E`hAK=&abUI-OO-7&l~I*G!tBOvO=A9PlNE27Pn7U8jbk63dcBhznSoER>gayGTd0CAbnP~O)Jl9AN>cH!+6KTEyfc$} znp3FZ_z5pn!6Z}{ujj6-!iyY^;*G37T0Na1>QO+nYYj&uaLkWq?9Lr!kE@$`TybnE z=O#v6Hj<{e!Hd{jVpK{&nTWc#)xjA|` zO*qOv{EN>A0FD_*GYXjrbfw7ii_w}L{8cKdO(GH!sbG^vsOI0 z$bMGzLuX(hFX*A;)bMcKhLUg(+BM?Pa7cii17kQHc8s~R_>#i2@nL2m-&)53Qh}QM zaDSP{U>5p7Ah$iMf$eB?ayY61)vuM(M@pCR;Kj!@2u`|AwiAoCcZIA_HG{Fdj-@X< zlAsCv!yFVfO3?U@{GHqs!!8<}#yP7cqdsLEz4pX$ z?_n|`lhgAMr@tbHeR?|YPfNDwkGmk1$QPJf5&#a)Ohevq)F#Qg`!%Itb3S|(v8aEc z%K2)on0KD-Ky$fEv&S-Sbm+fspZ^Av1%X_v+a&&9-n*m@;_4msH;uTadLOgjK}Tm*sPSCQCX3)M*TqFKVLLDR@iK#jr}Fcyb4c9ba$ zMY`%uB(t%8%`r>RpsiuaxUGt&h^ItQ==R%w*YD4j*cyG_z3AZGotf(S$j|FDiS(ar zA;JB+prTpLY7}5n%J-umJ-WZon(0rhpCO#5JfYynXe2zpm_kV^oW4LdfIw3Q5W$t> zy39OAVv#mZd&{K@{Nyl?U{|NcoKGRYjJ2`IQ%nuZg*W0xk3()AUy|q55yNPFVLZpK zyPp4Gy46I3LEJg(4Zwe;o!i4veGz$0ksEFq?KlbZ=D#}4Q+ec#(sHQ;l;IVMm{uKRE+;WhrYAjE2mRd4)OJVoPtU4RNj-_ z*zl6{D#Gc!KVrLC58k}4{_zpXo^PjXjsiP6@JS`oNs`pLxRWfBB?T=H9!YCM3PUQ0 z1#_D;#BKy*OP3(#ohENA5}Ap2LJK{NIm(oy*Vvy~JP5*OpHjY)kRT!m6Q^o_iRleG z+XSii5uYdoj?G?cf3&L{<+YqnUxd=rIE*aW4T)&z(L|bcK9&BE2t?vT%@GS^?$d66 z(GDOIWuIrf!`+IDm11le&_$i;7LJ73RrH~+Pt#v}KG^z)Z;b8oOQXYCh6z0$KphE4 zNEroBHh4g|A*SmtD;_I;ss^G^CB?#4h#u#)9SPSepi?bnNyfpZPSXb=d7e7cQnW9V z$<@IQTKSwMvs5kl>3LiH(O1qMYoXc>Rr6d$&sLnSPus}Bqkf{Iehp2}m0DajpVy;- zjvhdA+}Bo|v;(!zra@;eNoJ2gd-P{f(+$F;L|K9UxflzkWq1@lQ+2|)m2=7wkqyOc zWhl=kP!Y*)*0MGDF4|S_$T4oZ)l*cP}&x6ta zItqv64Gx}(QhI4tTaJwPXfCHu?e~u28a92z_%K`zKfFFRocKXW4x`Txgu!KKxUk(c z!aHs2jQUBeVZE>vmJ-dpYE?#|5E_o3Vf9Q-nLDjCY%%;RXl9)E;rv6X+&@$aoHhjB z?o9peUu)d&mY-h$hI8N(6>^-BK{JI&nVC%TvO&oK1r&T1iU0YTogmnl?4YUkO0p|) zY_eKFUxM2%pgBcZzxSorkFL-4bVi3;&%ZqMT2K2vdMR}4!5#DIk(Wr`(&cv7k6Vl9 zf&@XE;6a|+qYr!{hwjzgz zE-^3j>E#=JNt{-o2mK-1Q)1foNe(#4$URp+V#=Qr#7+jg`z)T0+f3%YK~=mQA5+Gi zpN!73xV-;f4aU9X%r>>MtH;W7|E#1Se2>sRbl{S0Ql>y*(;DiU5acMRRhr7{xidN* zKv6Zlk#q`170z?J2rW)1c^Se1A}b_O{S1QDV^q^t?CaT+*elz@cO(v_BA*}-5kQB6 z<*hpEM$$!p&zU#-oya%mzvFlzsPR$)Ho>2ChYjsVc@eWub2Pi6Y3}BwqYxh(`|51z zbNHLfmWKmw0dg@wnnw~Or311e`pr>JtoBX$>MBzeY z$(nZahcQ7`4RF~!3IeU-2Kx6q&%}O~UcWCJe|d|aP3PZs3$Ls1nqn0`=aRU2+w)=F|5GeS<;BI40=xHN36aU7h{Z1!2(Cd)22uoN}LI zioLhJyPNqtn=t?YlmUQ9RX9HIl@PBIJEU9xXW^!8IMCTrCMTqy#G^_gjgB09DVK8S z@am2z(vigY2^5>VAL(-bmofT}%6EPP+r~8ChF<3t6g}_~}kE zKY%V5upJHpnfbtcL{vamgpXT%Dh`4B6p;rGKW#S3gB@8(`cRH6Q*!@ zNH4M<+9?%L!>}Q&Ecvx`@29jORJ<@D<;cdc^D(va(uz3C7q%9U9hXfDMK?Y7T)eMo zn;6p7exZ(M5-F=L--{Tb?f>PWsDWuAodqXG3Y24rMUaXdtgnXC^JujH1?g_`h5&as z));-;SVyZ>fXH@4o`4v;=zAcE2FHvp8LPpogG5XXm5QzE|1AuNjRZq@eXKDcf;j}Otbgz zc%Y9pZr70q7}BJ*(U}#|Xcec1gxjLVn*ajW5o@H3R3$FIBz))>mq43Idhdkf#M)4P zcbsZ3Mw{Jw5W+DScNj00`kiTMEKHBI+wa)}lo6i3Z0HT_m0Ew>Dj)N)?pYSGk&=`|`%(1N zlk$a)dIncc^jaqO|2{!fJZnVOi@WH1AGs<3o56AppJt_cyIfG`@ToN1xF1s5 zSliMv#0}W8?AcH$7lFm?>sCm}Ywb03YV73sLcM`H4S6x@%eU@+$ZVp~^9HiS-`q=*4aD1Jt=ivUm8EHcWaSRbt|UA9x6UcJtt^5r*SELD0fe-Q;K-pRTL zIW^u^vLW@(@k#0nMcmYgm8(0)^b!kP8VFoWtPxCrQ+suHa4-2<0NP|Zvw_4wkOw;pkH-`5UeG|MZ(mi)}NL_>hq49waKaEWR+&p@9cF3@B{07GAy z6h5kNQ)vkOA$%=9a+bFVXt%OxAZG~K)$2;8GsXAiWbI|ZDMy;Ia}@n_k65#Ceb9ve z{o98}lzTb7{~Sq(JE7=I?Gr^{%6T)HFAH2bOWn2X4A6?+hy`=(~@9ejH{C`oppYP?5}MKpS8{W0k498=#o z#S`25IYr8KIuo|z_o0SAQr=xZgvSw%cMSpK#(o|c%uc%}26JmuOR<}Q>-sJ&9|o5r z;w+Fx1hj%H>seSxu24Ru`Z=<%>`nr=9=K_6A9gF35bn%m9#aa@n%P+|(5nA_gFU&DTNSz)tSgkFhGto9x3Lkkir;;8`f76h2MM@C)#& z4cV{Tk68{~yAN`Quz^FK@xlTHZ)>b^qaYXIyXAsb;9*iE1jsBD}V%cP?WrlEVaH}l<;iPP3^ z@zhcs%PQe*U9R+GLOmcs0=l-Z23hAZ@%kHp_F~AdEAWg4R91+2D9bVT!XE~6{StuQuUNx- zW|4+Rg{ozB)TlQrPF5sKB4pOUCal#i!!2(u=PavU1bwg_D$RW(_;jFcT+0MEr^7wm+(Y_5n$c)?}5YU34yhHH(JfN!XkrMl0xW zGU+hek8y!;np!-XF^EdCC#+GM7kZ{>%9qvl=3%U%6I0m-GeB7B%@J7c2wOkUHfDEt zmoVEsm0ruTbiX>_Wti70vlR3-+-L3Pz-ITb8=y6V;Op1T+ZS3oY*V--7WwO2&=}IS zIQPblkqp~@i3^T$kEp`ywXSY6`CDoD#Md`zlAmewS06=TeqOa_!O{@Rw|8+TCx$&? zKQTxDJTy-MB`J0d`a*MWmmycSAXPaSGMz{Fz`6e!ah2728BZNvv4`E|3p67m>?_P2s9aw0cs$KhW2t3K@Tfm6Ff6+kaehf{%Q=fyEdQiazy6!5K=bJgL?zhZ?RC2BwmW8W`l=M^ zcw0r$#ENGmvnDZ@K6{oRBisf@PrW= z5|htsCawyJncI&&7uA?r;%@*o08-F$K+Tic+M0TaQQ6MRxprEk4hB%0&+Kq<-@_R3 zl_GqND~HqAEn|%x6w)eI94Vh{{}^-@DL_7*I#pU9;WKDs<26ph7aZQ0>$=FKDt_wK zbkCtZv=Tjs2K_tXQ`?;or63+nt;Lu8$A>uKYGlri?X)kQ>2iy!ji2Zdu2TIEAIu~` zJ_j5lsO0PekxtACboQH3F>WXl6a((&#B6g`=ZU2}O4R-mIRlnvsZglh>C_c*(|-%m z9+lSp)UUw0`KMUnl|`Le_nX1Ok5I$scHe)*S`*HoJP-_EffxnFA=)(`_h>==HyTTviL`@DO>~@ z;53Gvb*ag;c8RmXPPs`oze5zFOd9*5h@_rb900(VQ?T)7q@tguJ~^E>o7%p;fU+IR z$}lFwnx$3>JBgy$W`fD-HTmm6NFd_4(kj$Knt813t#RdS;C~uZwM~KM4X)Gz29wN;$}eQIk$-lZe%BD+BlImv z$$)E|zDovaH{2!qlv|d7BAMIM6(`&&!|`yP=JZRxf^!SYO>cqN(EIr$;}-VOCsw8_b=VwCzfCIS~ zZzZ>0Mbjmx!4~OYrhg7dpwW7LWD;E~oFa~uq-0vS1F;XxAI;2}CIm{BUafVXu)H_R z73#7YD_^pI8H#F>b!JsA6WCD3)otA-;{H!6am9e}gz#Gp=0%Mc=B@|;fpskaD-{3~ z&{C#K0B2zBrz#+W8@qkW>$59ENS2$#E$;b zi?rpA19!iBqEkLSnJM_3y(n;v1uDgjV2&=dCm&YwQne5Lef9|Yv(GGj<(pdOGYSeD zrR@KZ1;CeqySEW=23n)PB?QGa)3jt#BunhkA8t-|HC$fWYqLujKAC$rnhg54IOPxx zk>t~qDNs0duHs?4ujQV_KX`q}@l~C2pQoMhm0vn>aRCmYbP=5ae3jzU|3qBOwSw*u zkkrCzvAX&(<^{m`d&Sd~zEQQ*wvuLLG?|nUL8+1s1;vdV>`-!ZxruXo(lW7Y^CqIn z%+s|f1PE!`c=%htd$TH4saXC!$?+fD5ET9P_>jf@(Q|LXZ-rGZK48LtX3Qnd! zG#m}i8{1O<8YSn$nrIhoY#rdaEv@?|!S=mg6(zl1>U+(jz})H65|7J1QX)nimbdJ} z4vROYecfq-)?5sN`h7bFZ2$g}y+9J8_#Z$O*$)$jowV8Wa7-E4c%#?J;2v85Ev4yu zvA2qdlBCX?*<2`k=0sk!`IlgaHnBRVB)0xxGJ!bfeD4T97S*$p_d(b}#hU+w^Xs}B zZa@YJ5>8A&=`xLIpxA8%d9W1{iX03HqmTortnge&t?pxxw?TD1y0%%i73#{3FQ5^9 z*KDp#HJP&-2~r%uvzl`no@J6qbK`$FJp`%7dsB}45HG>&d$`fRCE1X+H z^Nsw}1vUSMX}uOEYI>BzxjI7lM(I!M!);TRUXLexssDWIF_^njzz0o>hs%JWuCj?YCFKiot-%yMBvy=s)+IyNbDn3Y#t%CkMQbOj-f(agfBi!HB840hnF0o zg==c_0)j=YYLe{6DYp1z&X;R$-d=K9Amfsi@XF=#TzZzjY`M9R^dk^kB*7!ilKaeg zE9E4x2Q)^)sW@1DQ`!s^M9=H6964qas2SS6;RGDUJn170;prQ87;B{%(JM7xIaFfE zz}~yWee2yp8DG02zWyTjxRu}*tGIWVVdU)DT_2GRlDd2ZLb{g+bCOQ1_MF4EfvDm| z!JQ*h`d5G1Fw~K%-S7*$oasvp^FFrW44r7|#iqkHVLV>}AON{|DbA8w6l+~UxdLd$R@NyDx(5a*pay3C{lIYk^f)YJy7m>l z7%GDxu-;Ht&yejJIlua|f9Fum?9YGZl)th+oI2d(NB{UYNOF`q_>5nJeqCYH85%+! z>O`S#*w@ZKNWx{iMp1^pqtwcjm_pvSHA?5;k%!;YLVM zEtU$OgIHG8wN1{h3D-u@v2K!>PT{KvU2=#4pfYNb$F-7s&V-vgdXL;ROq7nXig zX^iZvN6hDe)^9PGRSf2lFVYN@P6haZQtS;Yw~4eeU5mVNa?M|Z z#KH-N^E8REj{i&6H%q^lfQLvh?PDc;azU2l77mFiV~(@5Eyf{&O86MP!q_i-|8)KB zWMI^JyAj6pOD_SSknPyrpl_uLFP-FGe3IpdS;Bn1=g@8(c$0n7s9V_EVmN->W1uam zv5MRC308ki2D`>OOL>fmaqySNba)dkoMrv{W1N9;0O2Na1YvX<(pb2ZNEPQb=$iI= zvkG8K=8l78II>}b)kBnt4o08Gkec&Lo&yka=!8E<6KD^|Lh9$=Y#-jSKO~kjFX+B> zw`c0>e-q~~x))XsFFbHvVvZ(N}aRKs9}zO7%4hSgm~B=fYP zch8y2=Gi8l*#wBCO#a%I8W9dg=?F;d&>n4#z*~y6-WL*gucQQ8`w4J3xvImE90H~R0XFz(m6dn zhWo>_cBZE({zTlk8L!(khF+mzXi(UDghl5Ec5N3LHRcsO?q4&7V{ACa&<5bOz3|c zT0nvUiW2}=a}$0iS4J5_#tXjE&h!TcvTB2v{K8Qa>GGOv3aP7fSWRaK_@DBKksl6aarvJ z58a}hn;L9-h+S{(6`{BkQ?6$ZR#A9tmT}mz*v@T1ULQz z?=^iOD3W5f8=5_yoE{D^q6ps#^U zWjcNZmGgvVlLfO+&};& z0#MS90{hX%#N_YD|wS7!qf!Yc-iX05nu=eEd`yRp&|3DRBml$$*koliP6NXc15Yhhc}G-dPR~l6Coc6 z59evZ%WyM}M;>IGwhU_^8O(h^kL*7#L(e$=F6q6H>aDRZ0A$c$)?OF|!};Z`!_+q) z9apjWi7DJ7IzC%zC(gq`c1J0lc{OGC!^V#!~H|L0>li`)u;%%Rjb# zpGyqRf)?*Nv>68;VbzrBR@gGp08;-bX` z007|$K*A@4Pn*QRkMJ?x=FMzlUwA{2hwbvZQfVG{;s>2#w}5J<`Rtz~UHH-~)sfiM zT#fGo?G+PoEq@nu6-Ny-^pjtZ+R#_U#qSP9oviBZy)HvjKDck(5z=Y%VNdx7=5B7nQi%Xn$QGRKshH-_Sn&-f2G?m)N(!W@|f-1AqVLX;{h2 z<5l~37P-%V-s=W+R1l3f62>_#2fY68dGE#?SPNiC$%y$HV-`#!7j7E^evoezS#OZk z!3Y<5mWNhXpnXjqOTo>a@8L-N#fj3M?3xDkzEbgOuzpr~$WUCs{bA6?gvrD1@5y9M z(3Fx~ySQ&IJpw+{v-~+r?`o=sY!>u20UjhcBnFmH9R`o7C{^8}tN|y*VBw&*_JR#v z=;Q340Qyv&lEw!!2tNO0Au}i>ao1mGtzd@m6Rg z4GFuxc+@>YJ21r>4o&)CQN*JtS2sU(L2cZ4ZqI|!__XYuA|2c_vm8EV>Bc3gjxCPv~B za>YLsTU~w1kv_LMh>I-4E5Ra6_u!N*D#_rs#`qVXYVU)Jf?=Qj&h13~cj-vxf$A%r z8a(aT;z5k}ODf}LtHhc-Ny1g+L9b+;{M{-3ZIgHDlONVK*EclK>;JVYfr<&a`BfPi z5ffz#RpTdhFiA;AzWRa)h-#cz1})uvIJ`Gdy_@U9lJ6rR6OOR080NHt(^t28f`3L2 z!BISl;iCdqlrhwHX61pO848q0EgPrg4h$Px4Y=iv#cXarnSET7Q=%sm^xe(-GE2sz z(s?Z8{uM;MwWjU+*X#PnEH*%m1icrajUH>NoNg*Zrhj19SVMMl3(>fXkhBJgxF~UQ ziS;Z9Vd09z8jH0}s0Q?V1{R2GlwxCPP5K;lX^@l#_SrTUjmEzh|2+7{f5_oELdtY-7|_o^X@!en%K1!LjuG^lbBf zEQ!W&h5}2buQXXu%alF?ao0idT|%6opLx$**QOb+V#LGSCA`|W->6_U!OYJ#xb|QC zf=qcipG<=)l+FC6nINsDIr$AYZ&H<0%ZYyn)=*GsjaJ)e?DP7w;CkG+ zC@}X|Cm3$~yP*G7in?e3pqBr#AF z-zO_gh^jEDq{>5ZQRqYa^Vh;0@8T5y#M|6NWbQ3WT9j^yz^L|fN6}c;`5fK);>Ys=K!2iB zUM(sv97LNwN>8G~zSTM&tyEEbdFK5&6(V0vs8~AEdDWMIgIxP{lryOlSD%+@R~^)~ z?Zfm&#le;H_uaXm7XqM`zlqnDq@_l$JD+oH%{6tF%3{d@SXbTfVlKip1L()fGKFHJ+%OlC~g z=WoC(B*=;w#K{<`dKH5`TR%!fJ1P)8f&&Bh%|M0G*y42}XC0gH2y*&QzgVsK7F&`w z8q{+PGF78ne)YXUV2$!>$RU)M3}UIW`~7PIO}^{%nyBfAaZ5KP5ZVSv+()@@P{m2M zDWd|jO?wqrwIEel0{>)o&qJJeZ!%7vsie;T<=4BNJ^)PL)E6kU!?i#GMV60^SKv{ zu~iC2V+@sS{$OrBFBxh7D}CoES@A~e>AiUwM(qHr3rhGJVFVfVdt|t+s^!OY!G;V( z%zH-;N}cT1Ql#DS2%U~ENK3f@KgTn(YpLFL_%q{|&` zFFJ=6u;`2)pD|;8-`r2k8s5G320&gKO6RFD&>Sbf=#k3E5w#=iz(h2_Fv)1tdclhR zD3?I2LvTK`ZP|a>{BtRxJ9Z4(r_cr7n9jxFU}dV;)1oz6Bq!wIxU_g-FEGi@Ncdpz zKV~%yW_1I7U%DX{rSMBQ!~3u$=~Sn?_ww=fz_FV^I4`Y>Is6T&w!qh(TU0guhb z_JJ4jMEsfGr>qzz2T_>2q;0NtapXNaBmgtUlgkK``gN>@K7p)n6v!ZMy{zftYA>C| zFZXDAL&5|5q9$`hSxI^x%HL$F-McfEhI0e@ZzGH?2d>g4M5g}RcQ~){gF&?wrNQV= zrD9NfxK!~$8Hj?t{ZKBBJ5z-{S1KU`nYj5;7{MolnlX!ETZIef-kyGL_zLlIbnZHMXC%*?ES#+{Rg(scJ8_N+~>LX zJf7D+FFVB25gz{?PTCD#6)ve{(#sUKmMZ@o#(O`y>UFcwYzs}F`tRgt6dtCsJ=ITS z4S%V@<{~?$O+8u z-XM8pQ=(%##9BKP{^Wr*XVM^dgogJ~jgl%WJ?AQxE?gM0&QrcnRbTf5d34eCHr~lz2~S@UPrr-{RrLdLn=Ji z?JGT@I7k))K~LI?gXZkHqNe_7Sb5uv?}yUep)|(f{r`L!>EyHjv|l}R;lN?vx}-!OIF>{L`aC%MAaK8&)fp?N~^logE{Hu#kVid zb|X9`uRhOLWmaEaI(Yn*HHQNSfUPl9Ta;lWFltuTQ;fH9>?=G5HGSV0ptU!*jLl_+ z4k8}(rfBnK@-jWxH7FG^FFM1|NZu;_HPXK=YTRBD*x9QYop$>23(25 z=gOQ12nR#O^&`m3ngrAfkdA)>9i8BgG|;5eIP47x2rP_Cw%;MbJoF~octpCsT%>R& zsNnaL)Rr1Qow{X-kqYmkD#~m0%B0kuKJTiGA-;5ueoD<&a zf@ukRT4;{7>euO3rB!6(?lrfN+D9XHG+TzWpI$RmzSn=A%>l1jU|cB zBBC6&J5?mKHO^Oc<68guyZc!Gj!&VZ>I4E_fm_5c+nC-8O#(7w>Y}cNTgynOapZXm5r#oAU-wxu}x$4jM8;ETm zAL1SI+K9w6&JN;vgdiTsdeVDh`I_`-)yW4$TkqqDR-ZD4$S4=PGq0r75EzPy$)cG;XEeSHZ?km7f%?uRRbI`8$7@jE(FuF7mlqR*bzr zXK;Z@TIBrIU-io|0`!jR)E!Wcfu-x~>_umVT#{T`F?wFaw18sgFHKz!2J6(-l zY#N4ZY?aKG>6(YhqYtJt zAzFDm1NbX8=>p>ZVuNlx<$M{t?<)(>Lt4nH=sqjQ$+8aI61?EhdPKOSpjKO!__B56 zjHDOLkzz7lT+fDN#UkaK%P4Y>^WU>i>RuNHr^H8tpkV-B5WH?&xERIwsmB$wMLKCwZbn^;ASQdmnTQw9~TEBA@gBip3M>a?@Ynq z+QwPAgo+@ShYp z5!oRF3h(g;6auVLTMO4F14$QIlChEodli4B-~A1nN!}i>$QBYwSs2wVH4=OwxxoE7 zF7zqAfDXF3bnj7ETgXD-g?uH>6=Fzs06SXXrhWy#)S$q?QTQk=T!xpEn$$2&7C|qL zPVU^}as&YNlxS1}$cU%ru>ORk9aePf@CCD^kztwIouLrINz;b|O}7x5xAkT!_ieKi z*+~&5t^y^Chp6wB6`c|WZtJ(v2K1)p^W&l2j@WZ>qDb*Q6qU5W`bQl~3aIBBWUy45 zS@3Y?zMX_(!`-qEyrOH?$0Q#r+&_}%&h1z?;Ut9nh3p0{5ZWf|k>(w0YZLdjUUQ-I z(-hO0Pvp5cQwU4r86C+Y_^D{9wXsO3ILOJ27fG(vmtA68eEEZVi*G@~}dCCqWfCVdJ0L8+Or<@E@G++3$yY&Zu%QAAh zg_G$Co?tc$;;{Yj?N8Jr-@Eyf003khg-Vump@osBJYd{@<5M*WMh04TR#AyTb&P>{ z#&2>fzMEC*G;uc1TYu$T@Aexh8I@TQRc4KMz{5NQj07OpE}D+(MxcEPb3*m@;OR zVWp(+iz)BeC$UykbZlBrUATFql(U$aqe;Ng?WEmgixB^(k9^6P;Erj2^1-eu(DsYh ze=dr_6o6s54+OA7v#)SkpvO0QZuWRHkBG7en7M9jf$?E4KwC;w`cnQT_N^uV7Ek+= znL{5wGRW^TBWn*)vAb(>n*ZLJRpZ9lR+H<;6;@xI#@Vqy*2lSPz4>ms6-}-Bdx$@dDfGiyIq#YaC;z1y#G2MLAL%Duh(jXrykmMCl!9kB z?Zu^R+ZAVeeKd1APxj}l@dFf206c~_!BR47S(oMUS7$~FzEh9Wa8@d3hNw`Fw|7Fo z6bL>w(rWIHy2B0rlFhe2*OxEa7X{lC^+WlPQm9q?-OIf<;&~r%2L~3WpkaRM=qQbl zo!j76rq-dprm}2AEh0fHC4laXQA0H@ey(CK(QKajMj88gvS5K0{l50z^0c$})!Tzf zr#dCNj}w3r3WM~Xsp!JVHKV7A`EnN&T8mMtLutupi+dY=8>(`5C`p)1LwfFzcE!-z z>nOn_`qguHwQyxh zu|!K!;>^(uk@yD?>&GW_suXqgm7FG^xY9K>r!>}C*_nCl!J)lyl(vL9vl&3mqwtd_ z_)pu_=(V(@qcX;nAxW;J8?OUqoh;@gpST#t1Rusk_qy2o5VzA61w6#BzwPSTx;t{d zbAH(!@6`7J?A{qB4xZ?iK5_dFX_S^6qaBO7IH_MSn>rs_sz#jN61RLDmKR@GHvVB? zCY}fvmAt{khs+ug;Yau@#T!Uqiu9Jy0@cmM#u0Z3!1i?4C3*XxYkx=yXJhzEs{ef5jGAqJL30iZ#V z0yR>m8Cou>!^R0hz(=E}Lc-*hQqmK=BMelh9+8r-EZ)%!?*6^Ku%cGp_SwDEIy2~5 z+dm9wwv&dz1g+pDd&DFOotN?GB-K!33SEw|8$s%&0fy4JuP-!|wS2l>^^HZHE|x!2 z42Z!1U84Q0r52bUknYkO^xe`4jE+o->lj@3h^E%uD?jFZYTB}YtGMMLy4p^D8EUux zj10G5j8kL+LfvrkTZtB2I?pI{`rhgCNhR;~yk9WH(Mf|OKy=yzVpUF3*ZXo`R7=}b z7}a>K!oalolCKcT^!yBD$_w^bEwzWzs~c|m_Id1g6)gSf^uL5~yNKMcNVopR00`p1 zk8PPb^|FVpSW}UNM8>Cz!|9!M8okSnbi=ci9`J%v10~<=IYwDSMTqq|4S;S!P*& zWx5?N9c_LmhLm2M0s7A8v48$)+uc**%j-9e$_#vAq0dpSCPFEf27ts#;tV4m6D@^% zU4{Ztzz>l4VjukVS6SS9QsO+f40UzW7$Wl(!$wG;V^V>Jdw-4Te^o}BNdbjS4nUaZ zKs>~#)N^R}S=4E$k9J~;*Begkvxt$=C{>+U()bMVfwn(SE=PP2l~d{-l@Pi)`rtXfeEJn$3K%mO?Ba&@ z7M)B%Mm|uuPkd4)g(G>WSf2x*>_o20{!>IW>6w=MLJrj6J+@Et93@g^D+@vC$O5&BayWm6%HC0FarF{VfMXI-dfN^YX(=UC3(P03QA(eWiGLo;1QLmVJ1Go+KiF}X zS|dIU7PhLrX=tYSJS^ehsnnguVgCCIoUv6*sVu|Nj;u*q1{+RN28Wge#?pcqZb6;q zmBshZ6ub7N*AmX{v1R%3=gWN*sLV@+MI;h+0Dui*b5;8h4lnE6_{$dGf4{w5REA*2 zOJLhL-ik{AWe;`z`U%bDuXZOmcllaP=dqLu;%)f^cq(4%CXTEn3&iZW@%p~wdFQ#? zEUkaO+Yw>f;IIisp@~oYm(KD(T?|3*{K3Q2fM_ceZDp+uavQtyQ(c0JIc5VRL9LgH zA3?%M#1br{(aTcw>UnwK9JY)OEq`vaAjEx@pPcmX>`|D}q$_ZAb7v=)tAuk^Rm)Y0 z7lub@2=<8dmkkJ z$kIWO+fP6Ye)tTzyWk+H!Bt2q>m;BxqJcl zAGsf7%(ebj>=u-vj91Tm#G;*=Az57kOJ?ykXTAYtDG11c1+kCRvJEpc;}Fb`PRGC% z`wy)Xr+<)qsg~1O3N-0q0&6^WB2A_)!%Jw|GkIW z|Cbp}Og0AHPdxojtiIHBfAM0|U1iA>>HRd0dL*11nBcul3X>5sQbz-aX`8`>Dt^5i z)RA4Z(0_I+Z>$GlQAS$sg;qa2y|7?<)7n$k-1X4$#K{1r;zfGmG@8hB&ja(EzkMQsZ&T~C`#gRHg{E4)zD4T&v~tHa4qnmrRlm>%~=|! z1iVJ4jq(-U?I*RD;XQRI;7wxLORWt?Ue#f+?Zvi~O0YC5g~k+mn}^azH)^I783p`+ z-4@M&-12gI(ScwG-q$FWE;&pvyCe6!Vr(-4RM=dm;9I#iLzGDmi6SW#2L_uvo>{N* zi-~`>Fc5&g#3oTUhV&^{%#l$0b z*G56EDQk%f$h}&Bch;<<=iw?^+15uyq6$C@!j#NoNaq{Ei@M(rHnCQIT{e_{*3h0i zuqOf!V#$Y!y1S(MapmxPewLu*{Bi#`h$BSQrS)@kN^;?WfTx#6+-bOJxP7KW#};T2 z#0Z_lU-KfjeOV$uy)-Quo@azt< zXoUlbJ!u*H0SU_u=>l!^o|V8=8y&!)pn?&(#`cgDX}`m5vS=^@x+#*a7Y2a2qCYd! z)ZnL+r(~aOxP7IIgIwk$c9u02EH{k*wlAQ`ly_ttn_j3-g@3&BF z-*b7Wznd*@Nt2VAG~32c5yb-foO6NDHxPJ1P}}%{_>>69xyU0i^M#+H2w1z$1rU2^TEI7G3 z9@Zf5E7$jHyDdHCTUHyGCOp9=s0>ba&ph<&1x`Ch~;mL6@d7=nB@Wp(Cz*3 zXe%4gX}dbx9f72O95at+W9d%HmiK#DX=-wR*Sm=)>KXU3%NuJ`~zSHz!au*AkFYc(~?P%t5QBa;p}i$HFlb; zalLP(aGe)aQmAImkMy87{$qW(id@m*M_1o8wqQ_(D^6`mTND~_)k0QP(LaqpeP&m8 zUp@URNtf1qBdl00WYY zS|$=~g(GFKW{!z_ptLeTGa%N(mH@S}CCB$fU4m=G-h7 zZ5T0!GAI?RvNDV4^TQ&^U=H_2S=Nfet_{2@*RT*FBxKr7PmSaY^<6D~JM-TS-r3Cy zSB;MlO)S2m^8K6KAJ}&rxk$Ot@R-nnh*ixbT!9+xAqUT0Lj2YZ_tvGy>mVIh*MAgt zP|tzXCc3~XYVP|X-jiZt3VN%2;|Re7HnZ8)A#MPQSs*U2h~y$n{5ANF-Cf%$3sPh5 zGeL^-wHAeK^8|Ral+W}#svSFeQ@P$Yl#Np0O}{!~5>%e}Ap0?p1#+w5lZt{H4SMR^ z5<*(lhnLB+7HENhWn!Z!viZUi`P|zbGrhiZ`cl}V5#ACF>=Z9TGvPHnJXN8S)_ZMi zc@?KR?=7*Rq*Tn97S!NVO=$Vs>?Z=pA8Kc8X9mPYJs$I?Sq>PnPuL75s$C}%FBR0Z z$OVUX0Eig(AX~e*w^K_E^D-tV2Pp?>q4$1N{+cMkfPF zV+JMz2Lq?^Dx(%SET#Y0{2Yg`6=)n!;ob0J*{#2aekqNGw!c`?5caV+4rOfQx;i zQsdcKxPqt%y&!v?Z|v5sV|O;*7TkbEAxTxT2so0G?o(vq^+vk=ce?t&ao^vxzEnKb zg^yIRyUU8Cj)!MBoIgDa>F_!x5_d`gatzq!JsRyGNy{TiA0)*ITt~{H%~_!-^o&)$ zG}y{;skEC2P(c&C=3z3kIplON>WbrfV#?s3$0y~Ax4;vx<>$A2G@{-s)slOuQpbMMCpXbNk5#336&%qiad6LvFfp+< zM>|x(Gu{>{|Iv`|bOKB8;2#~xB{33osH%2av4)dpQ9Og9lXy$DvR3_9QnJJh8PC$1 zlb`1cm&M3F8;GR??WiBns>3V-32azDIS=A@!a;$i}yi@r&4$f@V z4LNuvu4Vb&m5+#7{teMb58BF*0E2&%oNZT}IF&lDIBI*`)(%T%z}{(D`U$|q27zre zRUa}X_vFMl-6Pw^ihc`}-hSBZyDFSj=^td-9(I>Th31QZ60~2v?)No_;;U>$e>2AO zcV`{M?OjRN0ssKXBf*rE0G>8?qH((qjqlS&P5>31=m|QKG6k#>W~EFq=D-dixchh_ zlOZ9==ESa1r4WlUS#MVnR!91PUrc2`CS0?N%6~|atojxn@>+W<>iMu1n8EeW?H`ww z`n(DxjDzsC;s{(jdu8}5EGx5KsRDIqR;cUNA*Z9l_S6{d2GN(ppseZeWet46L3|bN zUL(e;O-WuDs_SA1ANler@N=vGi^27f?X%l#M>H4kX^ zrvG?ye-^Ex>9c{KS|6G76|#9t`ViYn6u-~=tCC$`24uv6ShUsh1C&8iaLFi8B*(~TR!+E=+FSvEOlhab3&9eB8^t)!z=u0O4K&|z@i*YFAtFxa`zIz*9% zIzb7J_j4URTGh9`S!j|FKB^K2v^>yU=vu*AfW#pwEWmT=5ufnzRqw)y0;X@vhMqMt zClTv>)JAJyzX$aHUfIRcuu>t@hz%iUmk7a)xp_4%{Lt^Lx$yg`DZuS7R>4U_Et5)g zM@9CRh{Ofrace{;0D#OoArich$QW3gJ`011%L|&xUKlMT%M=NCog@lG*qq;>W;EsS zc!ad?7hPn|y@T%lBi(5;I7Q}yQ)N}PDu3E2DqtpCeWf;}<@*2)`HXK5s$6N^VDk&< z+wH~wu2*WHr2rX;D4ezOCb{=!rJdUM(I>*JECg(~(V{V2VSrXz(rl}9Nw)x@?%Mw5 z-ppc~!Db?0;G400eV;2vFztutO1>A}hZ*@h0~N%EO4XYmYQ8#7nq5r#T-)71u<0=+i@3_=X4I;>2z06^K`;H{|x*mF$fzrn(zJ#;h>JHII^|^@Vx>U zm3z)uqyL^uz7$tweZ?a+;7_DyXX?6!Y^Jum9s`e-`HRBo#DQ782~v_movCG~(Lzgg zHY*t$!+o>GVK3Ic3xu|yO8eXzZLKWcP9-g@7g=unDazA%AheKnb#x@Fps=GJCd@46 zV(Y*I?G8sgdt}WWitGL|I9i1@w6jk}V~?;?AR33`?{(|r2#x|OMep=yuCHmoEEL5i z!?|WTOKPvztX16=z&v$YyK5jcp=^m0tf$*hW2iiwz~W=&oEb(;YNpkyR!CKwX@zQ!u+h8#Y*us!)w`^NyZ3~v#E zt78=&oiN_M`D}XCR{4rjLYABaV<;C=1UI7x218MLes^iF=?*)!IHQj#si*-~7I6y) zM*WwQ3DfBjY|QnBYa6Xz0sX71OvJMJEV;4J0I1x<`9be7<`qMGlGazc} z2OhxiD=t%{e={C>rUUb&X8zplxZD-f=Ohd@{G5^JM9LLFFw|)*`7= zYEyNo_|~*!_fV1_SM8D<9gR3P+68&odVEdAbpKh!JugoH21!K8zzHhSW2qa7ICgl3 zgswNG4htJz9NR|$uoAlh)c$43qyVa9G@k(U2SoloG;gjHYYJn2v^;Pvs*6&F4?oyp_joM2k??}4Ow-m&1Cl#*tj&~%0);C z8ZVeIETPKA$peg_22x&A)}ID@WMw{CCh+6;q72y5D1-*Tt}_I|m)RA3?D}+PrMq6< z1Qi7?ih{iie%1ZeJ`H>2?3R)3%Zt?tSAYcpVpj{bql<+Urmr>-jM3W%(4I7SUy6}V z|G39y_y{T0h>d42hM8tpv^Sekp^D7YMC#R%_{}Y>svc9?Fe^K_!O4iuFy4XM>ykq& z5#RzrE%|IgZ!@Tis~<#x3Yo>Ow@Dfr^>l4?l=<*^^|L4ZB0eSi&%4w)8HD!lmoK$l z#>vv7$<}3nTpJK6<&P&54BKEvhDx9eCDQAZXp%$e?xQfBg>f0#YSZ2paUkr0F38jO zOfr;YcR1q(oLiZ5-e0xyW!xR(xvCR!*1WMF)?~!FoKa zG2pG2`ziy7mb1dT7R%}*j>*hCH3gVz<*m9FGHVxjjO{%jGWezLbCd#g{L@Q%f=Z@V6OD$Kb!ja&L-mQ`#1d&jeAlOfi1t8A{56$ zm%r=z?>zRsz$J?jNLFEYF|oC^Q?pfN_bZ4(;gw#9RQzHg43(s@KcuOGfUt$#-}%h^ zb1P@uO2x&{w0%7{^-@>%3qy@y#oC|SC65vr-o99rk3z5CBQa|LMPG1e9Wb`!9mr`` zM&o#b`2*d=C}Xm9Vw) zspM}WeFYF?!@m+P$O)$;LrVmCcX#$ayjBeaFHdhbnHV~4n6{b<3z13=+`rEqI2HQCfz`c=O#X>_V3d zv{mT{2$R=zQvSXD`$7C?*TX01#%ZMnr;^HNW3??!A$> zqjBnUaU270AKb2)ag$(aG5(V7xAx9w?03_X#kx30fx9E%R~a>7j28&~0F-rk)i1{7 z?0GaHehy-k=wfM61-(~hwDt=lgy!oOKL`sf)gk&$e~HAMI&f(W8F>|+%5nHDn+d^B zrK>Rr<_Z;K6@11rzOEr&y+t7rqnlM%b0U_qv$ozGTSA4D$gxpJucQ1rDk1Be*pkw_`=jWaQ`(;d?u%$T;^&9gPwux z8z=}eZ0KFbyw@4Y6NAlI3Spbg>gRdm;<;T#CAOYj_AwTdBJ3MCSg5JLCE*mw=75b2 zDlJK)=4(S&cvWtpJ9 zHCba5e6b&6FJBzs;2~Sm^JVWjHS?OwFj}y)=s*uhT`Sf}mGM0nY2AV{F@utmfGE=RTihCL1WB|VRFlq6O^nhz zRW zC>wj_qO#kb?CdFVr|NXrfdVhS=Y#Vt87u-Dbvjj0w$ov@`OMR5y|JCux<<(`grMw z%`dKs3<7!dV`uIMKJ-ujs?xjw`)rs4gY5$1F9WWBx%gW)54t~+asYj-w&rsL9@4Be zf0p@lf?FsvKeP(!02X}VCmPgODG}TwL`jjLa=RKchE;yjtC~PqE}5tu$hWezqkeWc zf2)mydFtr4vynT&DkgXS59@X3O+(_pj>|KJT>xUvtA3H?+@Tf}fsP6k?GF69YvrNl zbl)q}sn(Zfh{SX296Z1;0;n;`;h}|b4xyDWpHRz{)KE=imF=yYpN!PY`bDznH}uv2}9vAF{dU{%AESDYcFRWHAFS256e_3uyE1#YeBlPhGz5BSF{FTnoq9( zcQ?6Ui^G7l5rURd6yhj>CGz4ZAL_A4vbAXTgycf$Nj_C1)gH)&^$SaZf(Hk!kV(!H zdqV>Kr*vvwdDk2dyI96Q!`pX568~x(t9vN^wU177DG4%o#WSqpxa*2JYn;1uF_O>o zO{E;ClGf8 zt<_xIwSo`jCQczA&~di5T8mTX7W%|ko-{hK=&LAyuLaMJ_8^g@yTs!|{^~*Cb>%%JeC3Aab;5%AI*c z9A^Z6o(@wh?Sy%yB7F%dkF5n}IraUZE+lD6BC~T{pJkDb8bbqF5;gCPN@t*z(5oH;Yge z5j2+AC(wHvBpRq$K3m5!6ov2I!ckW@y=`ul=PsX{gp&sg)f6ON`#w;8r6d8$4&2(J z9jeM6W%rn(QyiljOlK6s3H@v2+ zbm#T48;zRp-B0Hdl?u*eia0T``s+f1eYEATi!a97&-0I7-~pBjz*u^m68*)CF0U!k zGEs8bU5%P!aGZifJOd1OKN0yz7~EWJ`TO)cZ_?CjC+nv&vNI4Jfg6uRSlK1SR@52b zY}?wZS3Vm4lZd!{E*B4uL>c_JCaG|@#6>7jO#z-8%jvm8(p8pHJi1WQe6#7=7exbr zakqfNsj-s}@}Nm~41E18sFr}MH*&?8l=j%KQt|K$wp$jdDs3$mE0sD%;1TB3Y5;MImQk9jv*Ld76@9VNmkl`5aj`Xkpccv2YLn#z&(8f8n!1n2 zRh_KRIDp_W2`nExs*p;>rJX2!L;p^A2G*v`*VUM(?R$VT8x1cqZp}j1) zYL>YDCv=7i+*+hmyaS|?c39+$MrCfW|RhbM8Py- zqLXKb5LUJ6MzHt!`DMX?uTjSb`|yvxcWNlPc01l_(tX;y||mT#6^)`(`yaJ9Nx zg@@8Uh&8;R(6 zyL%hA^t+VnLF@hil97X87+i{$qRviNtVc_>8(h(;q1e`rP;touMlA_VPS|_z4e?G6R-wQ6QDvo7*70PE0Ct7vYoJ(^!8E(UHDwytphopgaWo{bq)!R z)Se&7D=K1`n|!8dpuVVE`dB6 z`CSdfEyLlo$X7RGejeQd$biEyR;5A(LOGIKm{Ia#T8Gd%a+S9jC)&?S4yw5p#UJjx zZfFj5$M zaTIXhL9<^3?`@}PhQmiWBHf~uCKCTTJ&wUJG6~+Xt6C_P=^Bdfpm6aIFR>LWX_8lkY8e)c?ESqriDQ45>QXDe_GSpZMKM=01s|xM%QMRhfM! zU#U^S{hsR$QFS0?=|L}2Kf>_td|MEn568t#stlT;)-zIIex z-4j`u&hkm-PsG-X7cs~_#pfYhXhZ=R2p`r-ipNaEkPtmwK_?e`L-f>6_gVPVUaHAo z8Ay-mr(x<(-S!Og5W#igF+WNMzk~7Bk)_C!7}%2z>$2O5;?G zx^I=I-uC;o)zJ1GCoFdPQ%ud6!XJ}T@t2;E%*!j*Z9rL`cOWMR>un!leh}y=M(O@c zSa-vzP%6oO^S>QQ^_L&pl3!s!iFjt$Ys{#cyj$k@`f%zkIyTG!p!yFz_trdxQQ;UbaA5A6*fZs5gNLQj=eRHpg83Ga=w+ z0l^?BRhmJ8#pma?a-x)X-@bgFy>P4}a{5wwfSo+L+fX>19G)qsOjwtdG3Y5pcfVqYSJj9+`Ev*f^?6SJC8mM-Q(mh1I<|g011&^yV@Bpje-xJeX>ZVFmfX< z_qx2BFy+1qNOx|%wOi$S8+-mQ@;ItN;xy5+ulCNYkSeVRL%6I6OZTAnMUp^1u_ zxR+1-RJ%kZ9>2&T-NB%tl^z}yX*Y$oQL;`m89b0NIBYx+Z8V4~83a5s4WDHEOZh?d z&2x#7JFnL3p#Ck-~Air#5E-xLGqy7K#vDOWerYh48juJ<0?C9PB#cAfq7Xz>1y zZ}(C5QFba`HM6LiK1b$0`*G4^PTD{sabb_{vx*Vu7i?P=g661OwwJ6COe}sXzii*L zED0ymp(Zs<7C=Dj#HW(oDy-!olA^4ZX08uBifD$5DYS;Hn(e&TwpMB=wcXi_Z@|%3 z+b+A`V@-VY9`w)C>5vjPCr2YM5c+|K+!G$S7~8f{v7qx4P@vd*EUIfiP-sfy?EkD_ z-L#EJ+!(bGyq*k<_DSm%upV_cakKCXdoL*9@)v# zcYO2_*Thlzh+@3d!@0kEc;JUl)U;7OWW$o>pJ~<7dR~A$=Mg*Bmi#3RoI0%JV}uG9 z&3~!%hxlfts?i3>Xd09uk*r;yXA#++6+H;JCnvt=0n$UE2J@H5KsqGNjHNj#PeD1%~!l|_s?(?UYan8_$$d>36oipGGO{jETd(AAO&WYv@e^gG2?r~3+KzM^x6$Jq2-SJ;Fbf;2sws5A zSA_JnTK*{5y~|LYdYAzMF>xE{%JG4CI7_PB$Y|p5E90xQq>&NWE2?qSlt6qx`tB7w zlMl5+ps`wM4vkwh0f3CV`GYWre81)a-B^uZIm5zQNAENSM+~1SHaxp=rKjivH(w*` zx1%;?J))m!%mqR_5fBw#wKM`7+7;@2=I1O%5%ZX5r`^3vBc<{vib~A7_U+l#c_?I`{gzoN&qQ)GsH1 zjFf{*7H3(^{bq}qjVNy?|Dn$(eN)7?XEmS5z6CeA^%xgjL-LB4W1js7&_v1dniUie z0kzz67xVcfOZVxj1p~Rc?RRN|UROAjLoWOkYwe;wV5F_sgdToD;rRSPOR(y?*qwWR z$9F(pF7xjHI*85|4sO{*PM(^Fq7^A$JctvYE?f)OjlQ#CK+8-Xu42wOdY<%Hk!iXy zNlY)7rR^Y7^;KzxYa^*oO5{b`j{~fB#&k+c+BgMw68QeXXQgyM6xt(_IFhKM<7!w+ zOibV{F7JMb&HYiV96b9={A;l*fX9&VxW9K$4HcZSUgx~W!jdk!W)tjZD4NC`h;;9_ zL+6<=r>Nu|-$}59=d{7wU_s~u_aEfLa=`DbecfE5ahyp`3j)v8)GoE}9P@|ttpjJw zFdeb!GRpxF1C)9jK@#ImLbmGXqCkl(zNWj@w_^9yp|U-p-Kl{{{5}1abR`G?8TCVL zq6u&mYaN*7pCI8bKPcZ1()m?ct@ueq4(LtbB##C&`TMvylHZn|Pa0n(hUwd89nu|2 zVPgaDH^n+j?f&F^r;xs_Am<}Hm>xI7y!U=g-0H9PRl|cPLye;s7&$7{ff4|b_uw_j zwGRCUrq{{uYmMX3%P|5LYK#J!(1@^%g294xPPOC|kGPs5E{IvSJKt<=RZ!~N*)B}l zbN_bP7Pp;DL~AB+Q*Efuy@TUJaqAkAwjyM8HGel2L3VO zGk-bF%!F)>&le}zf6$?s%CX(LO8pu0fvEo7xZkN^z`}Q+{JFMj9#P5T;9coumIHd= zC=K~+7H=oWCTSp_0_9#ByC-g>%O|xYv7OLN|FAE2sP?Mc^?zm0c$Kcf|^%AXsEKc^tL6^*M{Hn@h<>4_+Bf3p4XCCTJC>M z?ipm@bhCH6f&87NX*ay4A8*jf^`+@!Ke712M3rb{f`c}%ejVWu7mc?)CU^O5|o)22z|$ci2{Rdt>WIc zkQvrM4KYew8n2#nUsobc3Dro!9$m@7P~E|tvh4c5-#Qm5{k-~h0v=%T@P0S6#g)up zzuqEBh|-|VLL2f-NNHW)?nLTh;H$Ppxug9uCst9yCUN%ZCHj@yrP1xXhb|p6L?Ju2 zQ#9k|=9F^i1Zz@C4*P1CiZN&MU}+Sk)U3SXZ?PrfU&W(tx()-n);4_1cdci(#GI~(x%4gYQP1!_0(#91@%#_KMbA~>g#NTSzE^1b zQn{#F-udUnN^by9Bw4OVvcIb})HB_@tje zvE8}K8qeECK52$+y|rha9X-Dl6U>MKISY)rhCT99mshOoPzW<>Ag7IG*^Y;t$&XKg zf{-C_QHRV@oDUB(c8N#+{mw+<-#x*w;dGwH@G1lbV1I+j{}zWcOX=t?T9&i})T_%# z8JL0pk#rS)O}=0B-9|IIdvxPS0YOH0BU0k%?iQ8N-QAr6f(R0dbb~YqqO_C7?-!UOPU{DIQruJp5=CG^r$#+H{`h+X)Kw8OZo#8CV1JuBSO(PkD z5%yfmHmc-}^Q^Ppyd`D=w$K^c%XPdI6(D5I5? z1vOWq**P%P9)t3;Oa&)3@J4c z5<2#X%V+7fHOE$X|K7PED!uRP@vAgQ$1I5bl*o5N; z4XY1GUtP3g098^ zpSv-CZGuOV8o$UE)clKw^FO83&7rj;!~h@%WEwFKT<~DTh5J#Ct(Zrr_$9WZNi?is zLKIZQ;jk-G#zqrwmFkr@j?b6sL+J_l78({-eoPBj-t8z;y3x&lO{UvcV!qxr(R6B` z3yC7iUO=y9?h+_;6i((3VjI&!8L;aSKYp&v-*!2A-=WGOAKj1}t`=`cFaw*Z*XQzU`*g{TlU>Guy{Q+pCSp z_n*1vANo@8=AJ-#@4PDpqWKe8a)j3HJ1sIKxD!DHlOUNy&pNH>Ny$+cT#P(Lxve(s z_p&X6jg5JX9Y5dczE2q5b@mY>N+hrdkFgITsSnBOt0-mhb^EvU-QsJe=G~UPtKUDynMEz0Z$8-9dS|5G`{DaS`RD1!Ab9n0 z=O+p<&OkCA%`?jtI1~hLiKZZ&N+@l29GT^8HfPWI>I@`&^UU)tPevXEmPW@vW&Uce zWn@5!K5;DU)X_B`dp#)AYH)SZGU`nrNx-KBp+PqsTl{^fypxQ0@_<+MKl^2uWFHLR z4_xH-jaJ0)+Mq&*VhT-#uj$G)P!)=0i!gS12pLP$pw~%;`IJkOj@~>Rf)C;!lyiN$ z7jGDc;N^P=1Zm)yo2NeGt4|JLy6qdA>E1bODPpxtlh=iUlQV(FF9)JT&M~{2?JU zKdN__qA}!I{EF0KccY`D(UfwezTi8d{`|KlgE0bvgE%|8TziIwuJ9yU2m`Tbz3xDD zF3pNi4h@M{Ep`if>$-*jP!L@-q^52>JOS?$XJJc%g9bfu2rLOPm9I2NZmTAqCC_@K zud+n_a=*r<+ILiknONwQqmj{@#;H_s-Eq;5zmDMktZNN5J4{FD#cGu1^BODc4Eiq# z0s)Lu!syY;P)AY6x4%5mqkWh)JU~z@%Rp&8+@AJbUf^-cfv4onP-@h?vF*c}*FmVPptI9F%@p0Vzv3%^|8C`wwh}Z!lwLl0We8a!3nL<+b9 zl-X&V;ra9zj`PkxUL(oa7PLHtUs9EaS8 zey@*B+5|AcWezE0?vt^Q?;xH>>leFDciz{u)Oe-*pONujUBfj3fmGvnm3%8nt++P3d)#;)szo%~C}Knb!KgDLpnPZ1(@^_;lX)pG z=bq>8bM2Lq#-)(x0j~RxNJbnJzkjS>|0dX)8XflM@QVE^$43gg3kOxnfw`uLy$&Sj z9ZHe#*_R2~evfI7ikYmg6A0A|S#_D7ZT`F1?dV-4d9%W8L{mrCeDJp*zNuY2aEwsV z6KQYda_(JRkDFuW3%I2t%e{>@3=U5(@IjP2w9WvsY!B z-7pXSe-6QDxJH>`SODiJ=LJ7U5#qXXPF?p&h(1MKKscjm)f7=tdDnwAd2Fw}O_2%3 zfpY=?!2b~-i5HuKKfEtlHet)DPB(p~g`6cHjD`2`(`qH~)QYO_+ufCJE1z-JNp?VOIhBN;^`v%yCLUjRQ>2@#nq@+UhNazmCql!Z(fA|r&#!Nv#F?eY%kG* zs-K#&?bEzu;Fd;GXOVu=h%clZj72h#pcTc@U8DL|u`f<*CvZ^JiDDbIM;54^Uih_XPJ278PD1mvwL_G>AccSR6820v$9^$)NKCK!<5q`W((y((-#ga^YR=gj*wzjyIY z*b^CW*MpgiLeOE+cd78C8j&yI@K)7j?%qGzm8SpK z5KMt}l&O$m6K&r`ggtEwWpbT*;D_r{@Jq%pln{9Rx_STShDk|PvCkgZbte=jL(*4C zcyo!bYLU}?Wg^`Mtj5R)lg{CBk|TQSisc%MzqIB{B=5pavd1@Z@97${XK%IiN)8Q+ zdQnk1kiPKh^0Bk<=h?{>{@@!!{MiM!nvzT5^VQb-d}DD(^&SwsO*m!|YA*vC(L`nl zp9<&8)5Vif!*!=A$C(NLf?`G^*Bcg z^TmHw4wnf7#rrnoF;hZ?>( z9a}S0_LK(om~z9IF@*?R4RT<|g>W1s&mSSonjLN}f)FWv5`%oY;by8Jt#idEa#d2< z=^w<1?hI}RB#W3NnzaqTOVXYrf3e$h04{*wBX4rTkHj-J(At(hn@c>o6uCtlfDfhP zf6eV3v}3+nl6B8DTWs6EOY4B{UmzVU6WaC^?DR>Gc?t_#uiF8gYrBS3!kV0Y{NQ5J zx#)(^ez!vZ@d;};S~dFs6xahXJ9kzaS#3L)Z@Zx)o%k$h}J#FvmlW;GNSmp~G>N|h(1&X*DKjfT9CjxWNt;spJa9lO`M zE&R*knvDVI>;N^|d<+-!P)O&4ydRKvtP;`$n?smTMRsKc{8ZNcH{Vqj=YtFi%#u3Z z5m5W~4*08Z_)%uHe2uhw8d`$Jl!rTzZe={B7Fu1W;s~iI#v-&I9kIba+7rx#w1d?P z_$kW~XC!W{g<-x{rbu_1?ZxO{Ps%Xx|S33pXY^<6zk4n#voHB z59IRm$Tbl3rY%TJZ&!a^)u%h!;$eKnBc}p*Db)VqP`L!+HKrV2BcqHyp}S}> zc;N+Y+!$&Exkp-q=hlB_xk=O8Od!`TO`9n%zH@*@w{C1|F2!BQeu~LZg)TfMUe8BB zfsy0a{mwF-+P1_QBVmldwECS#Q?oco)p9cSzdAjI!mo2e=g9*+rDj#9!CwaNs=J33 z{CZZvf1zB@6u)clNN1B^4mN7^6cj#3kM)jGX9w4Eujs~~E>Y)xT+&#$DmVTmT21{HJWZd~W3UDlUYgK9W;V1eAKsnGV2r)Vl z;j!x!o|Il0Kb_av1K$4A!T=ClcV_~A1x*=vxDJaRf$AOe9Qq8vhlkw3u9%D(p7F4z zy6wHStLo&dtdztH_rRJLiQok&*3o>$9zLdK)!|h8=Hco1p<15hh7%_6B9YP?TjALtB0Q%BY#bFNvRzw5@q%39hFt%=N^p z%R=4@f?}~nU+9cJ+}wfxn#}GgdJ89(6G$x5V_;Es)m`3M%(VOz`{s|r6?hxn2MyF9 z+9v|0==7wV4_0f+7x9=xT+qIZ^*^Yzqz{(>NQ9{L_~M+?Oc@CLks zb5~GAE{I4?Z0&OD;c}BO8{FgNmxNg3&^}CLu83h(|7y`BkZsc3@~!rh;LwNPmc?|) z2zTL-{prL=Zuo@Lpf-*6l;mN@Vjf0$XxLcgKe^mj0;M{R@dY!A&BylRvR*uQi1WfZ zb$5sRb4a;Zrs+HR1;Hb51-uCWvOF+di4nHu<$vw*6m{~`;6zopq1 zmp71WJd$A@bI(!%1nc?aK3|$Ze5LRV*fFY3nv8kv98QoHf&L;hr9Ac?A5t#+$dnMK zz{eri^7ro-;Fjk07{;9>mGORp#VEW|V4Gk`AjObn-RCBR#-Z!Z1UWP240amy8I6hp z#uAzHIij($gvC!BhSCV^rP$VN6Bk=%&z*C3ysv2W9ju+cvu z=>0NjbMVrrXuQx8)cm7ztZaQGpRO7|W3N$lDtcdc6Y2P-UFmEjA-F!lZgl+Mzdio^ z@nIh~b8FZ0SvBYQd3&=_O)=O2voFoG60BtrYBtm6@}g1Z&EBX9omA@j;W7Cu{7&Uj zlN*-&U;fFCJzfFwxbg37V~7h2snmYe`(wM@5hfDCF}4sQy6H3&yyNF60=Iw6?CCt* z|CFq9W17BG0X$x~{qWBpu+qXLCuVvK%1SCUWHt)fHLg4j)J0BCh< z-)7Zjipi|iw0dx!jzi~m8oK|harkgdJTtg!x}AaFB(hhWI;Fuy-N3g!wIz1zgz}{Va30TmZus7kb9g^21{T=yq{W{C^9fx z4y9wXRX(^`fkvpllKPs~Od35zm+|eTK%wh*63z;hdhxqxJwq!?Q5&5IBJ!4!EtJ6| zUYAvPgBt!wu`FNRThog8R;BuI{4fV>PxfE}oC*qn7o1SBkHkcoyfiurHlnjCUn?#E zXD+mJS3eSkGBj`2PqpL|-?M2?w$7C?h(8ps!XXOzV-;$pNBp;W%$}7$UBD-4G^5Oi zpe#-$W3gekb%%8=Vhu-Uxbqf!!TvA*4+G8QGIH6G6f$uXAVE9UCO%C@d^;HK&rP@6 zcn9y4&xf9>0MNLe_SYkpt)@C~M;E>A>{yG!+IOy1 zidx2_0^}(Jzeqpaj?g&&?Y-%z%ZLANJy)zK`;AYGfKw`|XuHJfy{>8K`^9LpbttJV z^8WSrfqZAc7R=O7n#k)58(vW!wtG{Ix6YlAkJ&|;PJ z*n-F>ul4ZCp1E;c_IFZR5`lsWqfMQfcjt~>BrI*sRx~ttOHw_C17~4WDAI1(^+~2( zFHd6s2EJ21C?53vB4V_~1f7yg$uk=_>N*c*8FOXd!+uIxT459vxZ9Lm;*ZWhLwPG= zMDu6o>O?C_)C&t4HXT(o1-y7fh*uMNbz9uG!YS;g-{zfV z5b{6%c=P_BYLlHY2B+vtBD)#+_sb0CWm64m%R_;f1oD<+51@O-o z0C&LX*h4HP$9gb|Qha}b0}4N9rmt)w;jV8MT{J5+d-p+`)uZZVyBqlkL(bgAUtH6V zI)5UC72zs;(`eMS1jMe)aDv;&g5bYSN&u%Sy_K{_3AZbc zpJ#@ep;N>Iz2zQ`(MWG-k&F)q?CA*$aG_l+e)X5vJFHm?;KJl%X%rT>u1tC+oQg)KR53|@b$xX6p)rJoI%9<7v*j^>#;WC zhBh1v-`t|QPlPieg$mhQb~B1AEIXd+ewS-7r7~APJt-W>m53K$muoh*H7d0`ck$=D z-=M6Nzz|>@n0PJ0S0=-GGVOGB>f0SQ@~GIgt*9uTX)x?KZ$&2W19Yq zdDr7{?M)aM=6BZ=>NA2XgbFZnZ0PFOQKJiEb{4P!3c46mVbS5~6cSRZZ>ox7eI6bg zxtI_vyc$!3=aJ~9j+K#yMKd8I`WvR>oIG*xR5{8w7vxpmmy;N&OCDFxOJVFpYN{&Wa*8vFXFRoOKeD1gV2J~3M z(ox+>(^`HyojO-E;s&o7MIhu1KX%J8HzehwKX~l?yd-M&{pXx#D3F|*Hj~?J0jL>; z!{}jxKgE)#g2h}mVT?AL8iN(E*o5|kpf3AwuPpSxgW%2m^&MP)0Sqob7sf0?O&=#W zHSVoyZy=|zkcb>}=Q65HO`6B-I{}%lhlp-kbFTjIQ~qzSj%*ty7xD-FhhuD9wV?H zq_KKb^o)^O(ii%I%qmM*Y=(+pi7zo-nGj%;o#%&tL(DZNUq=@9Mt2#$`KK8I90oD} ze02K~yuG7(|KODh99o@$>u3;xkjU>Hq1WSkhU`Pvc`JJGhZAb6$g-0h{1UD3tMq=6 znk_euuw$kFc8{98hEvHV?nv=3A#}#nd^d+d;dJ=^C*|U)%zsTmAb7ZEGP0N-41qv% zEe0@!@ei`5TS%&^j8Q+y$8_2kgoWp|D+?gVvJC^d%qOW!%VM!fB1Un(+ozdA`ih^# z^I97zR|~ol>Hlu*hT!t>R*}4PYI$+zAC_rZ?`x(8U;rIg5~&uh($w_ZNzrNItP4dd zxVNUrjvA#2)C;LkK7A?lbS0^^#*RK^OVvoiwH%3je_OIU7OQ`wocsDoW>Ik@d8jU_ z!3#RoX-&?T^|HL~8vk8~2w*&Hqdi_s$j4D;H(p9|#C$>QO>cGjZ7Hkj-zIe`w6{`W z$&J5>FJH&;SmcolP64G-(wQPNHV)r)T+JigDtVsmjjT5J=!bBf%KjQ|%2D$b|Bi)C_cmE8#&0J5n9qqm zOCWoMJ|!O`{nP8>ZJbijt-1FnV{_ktLKwIL-o^oh8}7IpPKve%glA$X(aX1~$b^w7 z(iJs5#$0;(*$+)mI@v3PC;tG`GSr;u5kNw>E))les` z>yM94{T9=Dx_j#tSJrO?I^KEQ#uXXy-AuP8Ak5nITM{$WszLCN5TamtH;J5qh7k{i zJCa7%kx`?@FhU;AL#1D)fG6Q%aJYoG5>JV#g#S>AQmJ_Q(696jtLd~It;4=fq|!uR zb5-Ljd){AyEQ_O`#qX=R;0%lf%P7_&Y3i9y6xtc77HyB8V09{sxE57Wl_gN05uB52 zB&{cBetID^5=q}@prOHN12p%QIj8$1sU=fjB6h!JmrJ=Bh!3ls?QS)hRvU@+4w>2F zzBrTn9fE&5pSj?aPL-MSUy`1aGea02nl0+Yxj6#z5xTR~)IW5b5AXB=OhfSta#5+A01c?fok!`|u|T zuX}~W$q^^U;=!KJ+ms%Iqsw&^&XsUUdQ+yuCpHM04~M zF*($<^ySRXHrGm>yBKs`h%*^q3}Z+Z;8*tcIOF+`Y1l80Gi?^ZpJl4WfeXPinzx5) z?+bQWj5jiIExR4wc={}I>nbVCx!rl)Ki~bn>oEx4dXGB*ez z425@cA4!!#<*vpvPSiF%TXuJ!pJ$?Lp>c+}J$`^ar)!8+F-d+JeV&T>b5TwC($cpr zu|cPqm`c+pK1`>E64^f+VjY;5-rdK#z<+&rM^_LnRpBIfj7v5}L*JgTx4atJRxuY^ z53P-%%-2W9hEN$lnQ>cvq))TDOMmXJqrtpt?ox<1J2c^bMxMq(BHoFXpkqQcTjcQ!AGeiS^($JX5criJmFr?O-*(Lz$A0?nXVz$s(JtWZ^ar$ zv-=Vz60B=Tn{7d#hRNb*k94}DKJ}|v9fyJ7B?{%xJtm}^hM@nOpG8tYC)$7)gOdnJ zCs<`BH^l%guMtr;=i*XZ)jleER1rKD&w8$B8g)yUhD8qMhb_qMW@b9N?JOKFV^%wu zXXmq`odbKmzroIjFJ>efHV7JN_`c#a8;eeWW(p(z6fQXiqNr$_9RtJy=+{#wCvcPe zd_vEoXFl3!{(HK~v(nqoVxKb;0#Ohs0Uuoqk#vxUP9lmTkR?*)G@0^!E58Aamx#O3cJ01$)mF+?bZ8cp&dzsA^4dKWdG~uAW^P~6=(ZuP8V#o{%lNuq3%|4W>Ba*<$ z5yurM3%rnxNFWR}xeWa`Y(UR#`S!r_XwljDB`2XDzFV5ILWWpwKmD0A-{p(8ryf{@ z_5f%Vhk+X*lbm^Fm#&uvGqFJc$G5j0koT$EFVKjR=TnH-oj zwi{$p#%mJiBiVwqzPbfERzSqkgOJVri$YKy?tM&lW6 z!ce3!jWjAa=8#NGWcseTn3%azS$z0j0c~3IUK!^z^#wlVY-E5uU zn|Bz#5l*S2M1imHQv7OS&a;HSeU0cLgM@lM_>_|(N5|IkPc2tI4d3(kUs<(#MSeIp zd3Gec2f>>;p8#R}57lTWj6BI57)eM*B!x7?#wE8_(HlfmGfcZE_ak`xpE>0#Km}pJ zN=`TJcCP2Ka=8;9eqA~Dt!L?##Zy^lw_RLN0G3?EsVmWc2+K}Nj?$9=TGlY)G-UK= zGQadzm+WNVRknR!9pX_-Y|cqUyViD&(kh(9gr~LB5Rb316C(}ad;0W0r>CJ}CJUoS zrZ*ACp#;KwdU0P)uE3k#6x(ED*JT|fG`$5Uy&{1!S@lA!1vyFHhJq2ZWDv#bh=&_e z5#UIR_rD}?xyTmo_!gzuG*(~jOA#)8|FY%oY;lqFRA~BrkNZ10%etzJKa!%Xh+aQd zmg701ek4JSZPDOObyE49ud4_pP8XA1+7?pu<~6;UX4M>QT*tnQd9WUi`;d-&9OI=5 zOoTV0x-n-_=KVV`lQ%MYJWK*B0(jZxZs7+bMKA$`SV?&#n4zK3;Jz^85aG9rk z?~P-cJ#{H@8NK8p2?f$A;wgrCi+(4sjUYY6wqhnIbiK65CjP(dvIC4#E8FC;4wJOQ zkl6}0cd06j5&Puy6SAG;w|QqYbciU_3RWv&alieYR3CsL>QIBp=x$T{g#!4EF!R2- zDjrvImZxR_(UdrhskMBVf6eXXbImpB;yNToO>oWNL!9DQ-Cv@Ad)Mg9&XPCYR}J#U zvvV65JOJA%oPX#EI$yS(*fxS7_+?RB2jCT1j^uLWf9@q6+o&-)pcR9edsgz;WLpg| z#T>z&OD;9s(|A|6AUNXq7qG9dO!Kz3r;k@no*~cZH!za1*=6%np3z*Q(eNH;X}U}3 zlz|cs*)LJT!7@MVBbCxh9U9PYYMZajBee2lK4#DhQz4*rupKS!BH{8+`D~bBmTMnGXO)ZQ!0w&tGuBeFTlio>Jxx!geJ34 zV>3YCe0s5rftK~Aakrq3khHdUE1(c>Vl_5{#3uHl2$ie{N;#XaB-N5R8QX8wGfUUP zPP|{Ue<_roO(S{#OZUQl@q6159rv_s{%%Ha`R3R@*Z;d*B!pSLr+~p59eI7@#UlqN z@Z-d0XhxA$PQT6TJloY4eK-COdnGb$1OQ53wZ7O#RSc4reXt~=z5$n!SiDDQ188U@ zK`J+nJpAUOmqqRikNa$3D680y=kYS)laxXdVga-oaJzSZ*-S>&G z<6mp2>Q_><)cyoP5`*9LouAID>!$nkYt+bf2yg;IC)1L=+m8}2MI2t9OgjgD3NGW= z{*Zn{|3|UHT=aAMMr}L3BKu9js3Cd+%}U#a%W=KegPj_EHdhzO=fF0-zB@ zy&oA``JG1)hvSSSYAo{HrbKM(*3QupDua4l%lY36wF$#WcEdl+hUL|+Ozqtbn;bg- zo-UErQlCc8Qn8kHP>UGNK<&SNu3BS;W8W(>{1Q9{mzs}N8*qSUsI&?s3VP$`{`)i0 z<^;pC4i6y?QEaA?Do2bg94G7kCfAo3)>lVd!*UAB+AwVnJVsm+CujB%c;$+WO*hCU)_%E|E@ff@iE) z;?zIIA)q$Dh$;vKeYCBD@%ZqRO*ajs0syd|(y9hR!r&n1g6tEsCB~dvNsx2(#eZTa~A^D!#HV-d290$HHfCF zC_UZHGt`UbvsV1VF)cTtp+4kKN8IvYo38uxO&TptxRvTkWJhN}L~Pc7tK6pc2iOGN z_VjqW90JNB2&)~Zd2~Ml(}D!f8VjF**wYiNO5Z+%)E#!jbf zSQR?+i9C?J1KyXWl7y1}W4UJaA3~=XSR|WjTcP5n8|Uf(R;C~krdwO*D{4=1aCwz1 zlJetypw_A`q3ZGNd+>JX!J;ET0yMQ&d0BBQxn0v&)Hs->mqV9&`|O-&Zs^;pajp2L z--s_*pWb~)8}ZVIc2r8S@n87&e8I$_uA7R=tHh_*7(x}t0rodtS^3{d`IBt377?YO zItb3a=k)`~@p#fGa68}(%v(erD~1AAUm!#A87ery2l7j215r7B-1_ynxmVx1+Gshc zy!$B_$R9aJo(RK+igSsyQRDc^ql(*T;Nwkcx2dXeX(;`4>17NJ2;M9=nrr&-#hKk_ z>R5$!_r?;ThaicG06oKHc=|cH=I&Wp8Y4qfgF#!-RTsXS!Jgz3cN-k7&(=ZkiIW-J z7bwLr732vyDTR=fkEYw6d6Q9}`AJ+)c^b)lLq*Rz2;P2QttON%w1w&$xIzk;^G`lT z%Ei*>DaPSYqww_zIURV&W5`%6XpcK7p5&{)^gN+%CYsECy(()6co@aaCgH(FL~0^p zOx%5GjKfv}KQa{J)eD*{W4qiw5OK?d$k7^&kd-tA`gytjC=)R-Glo@M~XDdiHYcy z7xolD@P^IqK@vZTuh`u8BNM?bdG7A&gjJ?*@Js4o1sz3un(#VhS<7iuWY|{PxoX4~qU1ReZ_Qg^yXRqI-s0~CGf*`2&@j)NNwu-p-BSS*p zO!=6J4++6I{zqZ$fP^(q&lj)SU;!My6_0Z7>OJo3QVY43@P~3X$_PtFux7)V0=XDj z1rb5NE~wsmNnJ^Ys`A9x5_i7`Z*RM^0Sx`RidKGo+;T}1dt5c+l_;2&FJ0G%h!dSh zk|PqK)ds;ff$o|QIV2YzWUs^sE#E8FZ&nnJ!hHwXrI1Zbr;)=6z^QnN_#|&Z$kmq~ z#2&euLJP|^Q@Hmn&5e5oi_ifEM!_&r7|eup7##cRg{MX2?4wYdMmaM6$vc>0d{OgCnBd260s1ST0j9Q+32Mvx7`rYN( z(efh3)O12pRcNA7U?^!EQ?jc8h{^}mDn@+a^z%RZnJ9esAbR=#Z;B{=caY{P3w-0* zfm|@j;~}>;FcU0_3u3WQHG3wjUz77DQu)aPDIo!V0uN(RWqGdar>e~9ydfroP7?%s znIt%QkkGZ~7eBsE$~4Pe2cq5{_7hZ39Ug+<&!C{lT5H^qbx3o)0oCquqVBl#%!S@T zB`lqLfU&QtDauaw)}TlbqMaB@TyGkY{jZe|9~$>-*#9V%S?BY)fptJy9vCQ$X3X+B z)YXpIP)ckREQNOGViEcdz+73??u9TRT`R9g{9Htc&aiXHDcqB^E+9AW*ZtvFIUsoX zFH5?A-69&OOb(|hA!bv%i*jU7MxoDE@`�iSZj~m>CA?pj#|5FpF1oHQ;W_1Mcxm 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 z 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 Am;e9( literal 254192 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 z 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 milliseconds){ + if ((new Date().getTime() - start) > milliseconds) { break; } } @@ -503,7 +503,7 @@ if (errorConnectingToDomain) { updateOverlays(errorConnectingToDomain); // setting hover id to invisible - Overlays.editOverlay(loadingToTheSpotHoverID, {visible: false}); + Overlays.editOverlay(loadingToTheSpotHoverID, { visible: false }); endAudio(); currentDomain = "no domain"; timer = null; @@ -519,7 +519,7 @@ } else if ((physicsEnabled && (currentProgress >= (TOTAL_LOADING_PROGRESS - EPSILON)))) { updateOverlays((physicsEnabled || connectionToDomainFailed)); // setting hover id to invisible - Overlays.editOverlay(loadingToTheSpotHoverID, {visible: false}); + Overlays.editOverlay(loadingToTheSpotHoverID, { visible: false }); endAudio(); currentDomain = "no domain"; timer = null; @@ -527,23 +527,23 @@ } timer = Script.setTimeout(update, BASIC_TIMER_INTERVAL_MS); } - var whiteColor = {red: 255, green: 255, blue: 255}; - var greyColor = {red: 125, green: 125, blue: 125}; + var whiteColor = { red: 255, green: 255, blue: 255 }; + var greyColor = { red: 125, green: 125, blue: 125 }; Overlays.mouseReleaseOnOverlay.connect(clickedOnOverlay); Overlays.hoverEnterOverlay.connect(onEnterOverlay); Overlays.hoverLeaveOverlay.connect(onLeaveOverlay); location.hostChanged.connect(domainChanged); - location.lookupResultsFinished.connect(function() { - Script.setTimeout(function() { + location.lookupResultsFinished.connect(function () { + Script.setTimeout(function () { connectionToDomainFailed = !location.isConnected; }, 1200); }); Window.redirectErrorStateChanged.connect(toggleInterstitialPage); MyAvatar.sensorToWorldScaleChanged.connect(scaleInterstitialPage); - MyAvatar.sessionUUIDChanged.connect(function() { + MyAvatar.sessionUUIDChanged.connect(function () { var avatarSessionUUID = MyAvatar.sessionUUID; Overlays.editOverlay(loadingSphereID, { parentID: avatarSessionUUID }); }); @@ -553,7 +553,7 @@ tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton(BUTTON_PROPERTIES); - button.clicked.connect(function() { + button.clicked.connect(function () { toggle = !toggle; updateOverlays(toggle); }); From fcde53fc65091309d14932193d6423b3149dfac9 Mon Sep 17 00:00:00 2001 From: Flame Soulis Date: Fri, 19 Oct 2018 08:22:43 -0400 Subject: [PATCH 155/362] Include black-sphere.fbx and update linked url --- scripts/system/assets/models/black-sphere.fbx | Bin 0 -> 56208 bytes scripts/system/interstitialPage.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 scripts/system/assets/models/black-sphere.fbx diff --git a/scripts/system/assets/models/black-sphere.fbx b/scripts/system/assets/models/black-sphere.fbx new file mode 100644 index 0000000000000000000000000000000000000000..2e6dea233f762a19946e328df85727020e49d9ec GIT binary patch literal 56208 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 I4%tHgf0O8X)Bpeg literal 0 HcmV?d00001 diff --git a/scripts/system/interstitialPage.js b/scripts/system/interstitialPage.js index 6655e1ef9d..d54a8415a0 100644 --- a/scripts/system/interstitialPage.js +++ b/scripts/system/interstitialPage.js @@ -64,7 +64,7 @@ name: "Loading-Sphere", position: Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0.0, y: -1.0, z: 0.0 }), Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0.95, z: 0 })), orientation: Quat.multiply(Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), MyAvatar.orientation), - url: "http://hifi-content.s3.amazonaws.com/alexia/LoadingScreens/black-sphere.fbx", + url: Script.resolvePath("/~/system/assets/models/black-sphere.fbx"), dimensions: DEFAULT_DIMENSIONS, alpha: 1, visible: isVisible, From ba459eda9edc2f653acf95d84f68f57abcd3b6ad Mon Sep 17 00:00:00 2001 From: Flame Soulis Date: Fri, 19 Oct 2018 08:41:04 -0400 Subject: [PATCH 156/362] Removed duplicate backgroundAlpha --- scripts/system/interstitialPage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/system/interstitialPage.js b/scripts/system/interstitialPage.js index d54a8415a0..7ee4abb7d1 100644 --- a/scripts/system/interstitialPage.js +++ b/scripts/system/interstitialPage.js @@ -112,7 +112,6 @@ backgroundAlpha: 1, lineHeight: 0.13, visible: isVisible, - backgroundAlpha: 0, ignoreRayIntersection: true, drawInFront: true, grabbable: false, From 5db7c7821e81537935a88912858c4b62107d4444 Mon Sep 17 00:00:00 2001 From: Flame Soulis Date: Fri, 19 Oct 2018 08:51:13 -0400 Subject: [PATCH 157/362] Proper formatting (aka no spaces after anonymous funcs) --- scripts/system/interstitialPage.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/system/interstitialPage.js b/scripts/system/interstitialPage.js index 7ee4abb7d1..a5a952444d 100644 --- a/scripts/system/interstitialPage.js +++ b/scripts/system/interstitialPage.js @@ -12,7 +12,7 @@ /* global Script, Controller, Overlays, Quat, MyAvatar, Entities, print, Vec3, AddressManager, Render, Window, Toolbars, Camera, HMD, location, Account, Xform*/ -(function () { +(function() { Script.include("/~/system/libraries/Xform.js"); var DEBUG = false; var MIN_LOADING_PROGRESS = 3.6; @@ -310,7 +310,7 @@ var url = Account.metaverseServerURL + '/api/v1/places/' + domain; request({ uri: url - }, function (error, data) { + }, function(error, data) { if (data.status === "success") { var domainInfo = data.data; var domainDescriptionText = domainInfo.place.description; @@ -534,15 +534,15 @@ Overlays.hoverLeaveOverlay.connect(onLeaveOverlay); location.hostChanged.connect(domainChanged); - location.lookupResultsFinished.connect(function () { - Script.setTimeout(function () { + location.lookupResultsFinished.connect(function() { + Script.setTimeout(function() { connectionToDomainFailed = !location.isConnected; }, 1200); }); Window.redirectErrorStateChanged.connect(toggleInterstitialPage); MyAvatar.sensorToWorldScaleChanged.connect(scaleInterstitialPage); - MyAvatar.sessionUUIDChanged.connect(function () { + MyAvatar.sessionUUIDChanged.connect(function() { var avatarSessionUUID = MyAvatar.sessionUUID; Overlays.editOverlay(loadingSphereID, { parentID: avatarSessionUUID }); }); @@ -552,7 +552,7 @@ tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton(BUTTON_PROPERTIES); - button.clicked.connect(function () { + button.clicked.connect(function() { toggle = !toggle; updateOverlays(toggle); }); From 0837f790ebd748add984b1ac2cd21a34909fa512 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 19 Oct 2018 07:46:16 -0700 Subject: [PATCH 158/362] combo box changes --- .../resources/qml/hifi/avatarapp/Settings.qml | 23 +++++++++++++++++-- interface/src/avatar/MyAvatar.cpp | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index fd72d70106..64ae03237c 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -366,7 +366,7 @@ Rectangle { // sit stand combo box HifiControlsUit.ComboBox { id: boxy - //textRole: "text" + comboBox.textRole: "text" currentIndex: 2 model: ListModel { id: cbItems @@ -378,7 +378,26 @@ Rectangle { //displayText: "fred" //label: cbItems.get(currentIndex).text width: 200 - onCurrentIndexChanged: { + onCurrentIndexChanged: { + if (cbItems.get(currentIndex).text === "Force Sitting") { + sitRadiobutton.checked = true + lockSitStandStateCheckbox.checked = true + settings.lockStateEnabled = true + settings.sittingEnabled = true + } else if (cbItems.get(currentIndex).text === "Force Standing") { + sitRadiobutton.checked = false + lockSitStandStateCheckbox.checked = true + settings.lockStateEnabled = true + settings.sittingEnabled = false + } else if (cbItems.get(currentIndex).text === "auto") { + sitRadiobutton.checked = false + lockSitStandStateCheckbox.checked = false + settings.lockStateEnabled = false + settings.sittingEnabled = false + } else if (cbItems.get(currentIndex).text === "Disable Recentering") { + settings.lockStateEnabled = false + settings.sittingEnabled = false + } console.debug(cbItems.get(currentIndex).text + ", " + cbItems.get(currentIndex).color) console.debug("line 2") } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 2c9b83b636..95141d614f 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -533,6 +533,7 @@ void MyAvatar::update(float deltaTime) { float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); + qCDebug(interfaceapp) << " the sit state is " << _isInSittingState.get() << " the lock is " << _lockSitStandState.get(); // put the average hand azimuth into sensor space. // then mix it with head facing direction to determine rotation recenter From ef740140750e2ef1a143f86f2d2672e883ccfd91 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Fri, 19 Oct 2018 13:55:56 -0700 Subject: [PATCH 159/362] Small avatars now have a minimum jump height of 0.25 meters This should improve the quality of the jump animation and improve the mobility of small avatars. --- interface/src/avatar/MySkeletonModel.cpp | 2 +- libraries/animation/src/Rig.cpp | 16 ++++++++++++---- libraries/animation/src/Rig.h | 3 ++- libraries/physics/src/CharacterController.cpp | 8 +++++--- libraries/shared/src/AvatarConstants.h | 7 ++++--- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index c1a49d7a10..08a1e190f1 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -298,7 +298,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { auto velocity = myAvatar->getLocalVelocity() / myAvatar->getSensorToWorldScale(); auto position = myAvatar->getLocalPosition(); auto orientation = myAvatar->getLocalOrientation(); - _rig.computeMotionAnimationState(deltaTime, position, velocity, orientation, ccState); + _rig.computeMotionAnimationState(deltaTime, position, velocity, orientation, ccState, myAvatar->getSensorToWorldScale()); // evaluate AnimGraph animation and update jointStates. Model::updateRig(deltaTime, parentTransform); diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 341b554949..335cddf218 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -30,6 +30,7 @@ #include "AnimOverlay.h" #include "AnimSkeleton.h" #include "AnimUtil.h" +#include "AvatarConstants.h" #include "IKTarget.h" @@ -629,7 +630,8 @@ bool Rig::getRelativeDefaultJointTranslation(int index, glm::vec3& translationOu } } -void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState) { +void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, + const glm::quat& worldRotation, CharacterControllerState ccState, float sensorToWorldScale) { glm::vec3 forward = worldRotation * IDENTITY_FORWARD; glm::vec3 workingVelocity = worldVelocity; @@ -924,9 +926,15 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos } _animVars.set("isNotInAir", false); - // compute blend based on velocity - const float JUMP_SPEED = 3.5f; - float alpha = glm::clamp(-workingVelocity.y / JUMP_SPEED, -1.0f, 1.0f) + 1.0f; + // We want to preserve the apparent jump height in sensor space. + const float jumpHeight = std::max(sensorToWorldScale * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT); + + // convert jump height to a initial jump speed with the given gravity. + const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight); + + // compute inAirAlpha blend based on velocity + float alpha = glm::clamp((-workingVelocity.y * sensorToWorldScale) / jumpSpeed, -1.0f, 1.0f) + 1.0f; + _animVars.set("inAirAlpha", alpha); } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index ed0b70d4b6..5330a06a01 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -172,7 +172,8 @@ public: AnimPose getJointPose(int jointIndex) const; // Start or stop animations as needed. - void computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState); + void computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, + const glm::quat& worldRotation, CharacterControllerState ccState, float sensorToWorldScale); // Regardless of who started the animations or how many, update the joints. void updateAnimations(float deltaTime, const glm::mat4& rootTransform, const glm::mat4& rigToWorldTransform); diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 626184d1dc..8fd6d4eada 100755 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -766,14 +766,16 @@ void CharacterController::updateState() { SET_STATE(State::InAir, "takeoff done"); // compute jumpSpeed based on the scaled jump height for the default avatar in default gravity. - float jumpSpeed = sqrtf(2.0f * DEFAULT_AVATAR_GRAVITY * _scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT); + const float jumpHeight = std::max(_scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT); + const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight); velocity += jumpSpeed * _currentUp; _rigidBody->setLinearVelocity(velocity); } break; case State::InAir: { - const float JUMP_SPEED = _scaleFactor * DEFAULT_AVATAR_JUMP_SPEED; - if ((velocity.dot(_currentUp) <= (JUMP_SPEED / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) { + const float jumpHeight = std::max(_scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT); + const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight); + if ((velocity.dot(_currentUp) <= (jumpSpeed / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) { SET_STATE(State::Ground, "hit ground"); } else if (_flyingAllowed) { btVector3 desiredVelocity = _targetVelocity; diff --git a/libraries/shared/src/AvatarConstants.h b/libraries/shared/src/AvatarConstants.h index 6c38d08c96..87da47a27a 100644 --- a/libraries/shared/src/AvatarConstants.h +++ b/libraries/shared/src/AvatarConstants.h @@ -70,9 +70,10 @@ const float DEFAULT_AVATAR_MAX_WALKING_BACKWARD_SPEED = 2.2f; // meters / second const float DEFAULT_AVATAR_MAX_FLYING_SPEED = 30.0f; // meters / second const float DEFAULT_AVATAR_WALK_SPEED_THRESHOLD = 0.15f; -const float DEFAULT_AVATAR_GRAVITY = -5.0f; // meters / second^2 -const float DEFAULT_AVATAR_JUMP_SPEED = 3.5f; // meters / second -const float DEFAULT_AVATAR_JUMP_HEIGHT = (DEFAULT_AVATAR_JUMP_SPEED * DEFAULT_AVATAR_JUMP_SPEED) / (2.0f * DEFAULT_AVATAR_GRAVITY); // meters +const float DEFAULT_AVATAR_GRAVITY = -5.0f; // meters / second^2 (world) +const float DEFAULT_AVATAR_JUMP_SPEED = 3.5f; // meters / second (sensor) +const float DEFAULT_AVATAR_JUMP_HEIGHT = (DEFAULT_AVATAR_JUMP_SPEED * DEFAULT_AVATAR_JUMP_SPEED) / (2.0f * -DEFAULT_AVATAR_GRAVITY); // meters (sensor) +const float DEFAULT_AVATAR_MIN_JUMP_HEIGHT = 0.25f; // meters (world) // hack const float DEFAULT_AVATAR_FALL_HEIGHT = 20.0f; // meters const float DEFAULT_AVATAR_MIN_HOVER_HEIGHT = 2.5f; // meters From 31d099907aea02a6a11ca357b86ef80d3896413a Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 19 Oct 2018 14:00:52 -0700 Subject: [PATCH 160/362] Fix warnings --- libraries/animation/src/Rig.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index df7e322da7..35f076c78d 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1138,7 +1138,7 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons } } if (_sendNetworkNode) { - for (int i = 0; i < _networkPoseSet._relativePoses.size(); i++) { + for (auto i = 0; i < _networkPoseSet._relativePoses.size(); i++) { _networkPoseSet._relativePoses[i].blend(_internalPoseSet._relativePoses[i], (alpha > 1.0f ? 1.0f : alpha)); } } From e59f1516efc6d2f9ea5ab8ba7cb2848696101485 Mon Sep 17 00:00:00 2001 From: David Back Date: Fri, 19 Oct 2018 16:01:53 -0700 Subject: [PATCH 161/362] CR changes --- scripts/system/html/css/edit-style.css | 14 +- scripts/system/html/js/entityProperties.js | 190 ++++++++++++++------- 2 files changed, 133 insertions(+), 71 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 8205fcd340..c4ab00e689 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -614,8 +614,8 @@ hr { margin-top: 0; } -.checkbox-sub-props { - margin-top: 18px; +.checkbox-sub-props { + margin-top: 18px; } .property .number { @@ -969,12 +969,12 @@ div.refresh input[type="button"] { margin-top: 6px; } -fieldset .checkbox-sub-props { - margin-top: 0; -} +fieldset .checkbox-sub-props { + margin-top: 0; +} -fieldset .checkbox-sub-props .property:first-child { - margin-top: 0; +fieldset .checkbox-sub-props .property:first-child { + margin-top: 0; } .column { diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 1a52cf939d..d3e31e4b9b 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1,6 +1,7 @@ // entityProperties.js // // Created by Ryan Huffman on 13 Nov 2014 +// Modified by David Back on 19 Oct 2018 // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -26,9 +27,7 @@ const ICON_FOR_TYPE = { Material: "" }; -const PI = 3.14159265358979; -const DEGREES_TO_RADIANS = PI / 180.0; -const RADIANS_TO_DEGREES = 180.0 / PI; +const DEGREES_TO_RADIANS = Math.PI / 180.0; const NO_SELECTION = "No selection"; @@ -564,7 +563,7 @@ const GROUPS = [ type: "vec2", vec2Type: "xy", min: 0, - min: 1, + max: 1, step: 0.1, decimals: 4, subLabels: [ "x", "y" ], @@ -883,7 +882,7 @@ const GROUPS = [ propertyID: "azimuthFinish", }, { - label: "Verical Angle Start", + label: "Vertical Angle Start", type: "slider", min: 0, max: 180, @@ -893,7 +892,7 @@ const GROUPS = [ propertyID: "polarStart", }, { - label: "Verical Angle Finish", + label: "Vertical Angle Finish", type: "slider", min: 0, max: 180, @@ -1233,12 +1232,53 @@ const GROUPS_PER_TYPE = { const EDITOR_TIMEOUT_DURATION = 1500; const DEBOUNCE_TIMEOUT = 125; + +const COLOR_MIN = 0; +const COLOR_MAX = 255; +const COLOR_STEP = 1; + const KEY_P = 80; // Key code for letter p used for Parenting hotkey. const MATERIAL_PREFIX_STRING = "mat::"; const PENDING_SCRIPT_STATUS = "[ Fetching status ]"; +const PROPERTY_NAME_DIVISION = { + GROUP: 0, + PROPERTY: 1, + SUBPROPERTY: 2, +}; + +const VECTOR_ELEMENTS = { + X_INPUT: 0, + Y_INPUT: 1, + Z_INPUT: 2, +}; + +const COLOR_ELEMENTS = { + COLOR_PICKER: 0, + RED_INPUT: 1, + GREEN_INPUT: 2, + BLUE_INPUT: 3, +}; + +const SLIDER_ELEMENTS = { + SLIDER: 0, + NUMBER_INPUT: 1, +}; + +const ICON_ELEMENTS = { + ICON: 0, + LABEL: 1, +}; + +const TEXTURE_ELEMENTS = { + IMAGE: 0, + TEXT_INPUT: 1, +}; + +const JSON_EDITOR_ROW_DIV_INDEX = 2; + var elGroups = {}; var properties = {}; var colorPickers = {}; @@ -1420,26 +1460,26 @@ function showGroupsForType(type) { } } -function getPropertyValue(propertyName) { +function getPropertyValue(originalPropertyName) { // if this is a compound property name (i.e. animation.running) // then split it by . up to 3 times to find property value let propertyValue; - let splitPropertyName = propertyName.split('.'); + let splitPropertyName = originalPropertyName.split('.'); if (splitPropertyName.length > 1) { - let propertyGroupName = splitPropertyName[0]; - let subPropertyName = splitPropertyName[1]; + let propertyGroupName = splitPropertyName[PROPERTY_NAME_DIVISION.GROUP]; + let propertyName = splitPropertyName[PROPERTY_NAME_DIVISION.PROPERTY]; let groupProperties = selectedEntityProperties[propertyGroupName]; - if (groupProperties === undefined || groupProperties[subPropertyName] === undefined) { + if (groupProperties === undefined || groupProperties[propertyName] === undefined) { return undefined; } - if (splitPropertyName.length === 3) { - let subSubPropertyName = splitPropertyName[2]; - propertyValue = groupProperties[subPropertyName][subSubPropertyName]; + if (splitPropertyName.length === PROPERTY_NAME_DIVISION.SUBPROPERTY + 1) { + let subPropertyName = splitPropertyName[PROPERTY_NAME_DIVISION.SUBPROPERTY]; + propertyValue = groupProperties[propertyName][subPropertyName]; } else { - propertyValue = groupProperties[subPropertyName]; + propertyValue = groupProperties[propertyName]; } } else { - propertyValue = selectedEntityProperties[propertyName]; + propertyValue = selectedEntityProperties[originalPropertyName]; } return propertyValue; } @@ -1449,23 +1489,23 @@ function getPropertyValue(propertyName) { * PROPERTY UPDATE FUNCTIONS */ -function updateProperty(propertyName, propertyValue) { +function updateProperty(originalPropertyName, propertyValue) { let propertyUpdate = {}; // if this is a compound property name (i.e. animation.running) then split it by . up to 3 times - let splitPropertyName = propertyName.split('.'); + let splitPropertyName = originalPropertyName.split('.'); if (splitPropertyName.length > 1) { - let propertyGroupName = splitPropertyName[0]; - let subPropertyName = splitPropertyName[1]; + let propertyGroupName = splitPropertyName[PROPERTY_NAME_DIVISION.GROUP]; + let propertyName = splitPropertyName[PROPERTY_NAME_DIVISION.PROPERTY]; propertyUpdate[propertyGroupName] = {}; - if (splitPropertyName.length === 3) { - let subSubPropertyName = splitPropertyName[2]; - propertyUpdate[propertyGroupName][subPropertyName] = {}; - propertyUpdate[propertyGroupName][subPropertyName][subSubPropertyName] = propertyValue; + if (splitPropertyName.length === PROPERTY_NAME_DIVISION.SUBPROPERTY + 1) { + let subPropertyName = splitPropertyName[PROPERTY_NAME_DIVISION.SUBPROPERTY]; + propertyUpdate[propertyGroupName][propertyName] = {}; + propertyUpdate[propertyGroupName][propertyName][subPropertyName] = propertyValue; } else { - propertyUpdate[propertyGroupName][subPropertyName] = propertyValue; + propertyUpdate[propertyGroupName][propertyName] = propertyValue; } } else { - propertyUpdate[propertyName] = propertyValue; + propertyUpdate[originalPropertyName] = propertyValue; } updateProperties(propertyUpdate); } @@ -1719,7 +1759,10 @@ function createSliderProperty(property, elProperty, elLabel) { elDiv.appendChild(elInput); elProperty.appendChild(elDiv); - return [ elSlider, elInput ]; + let elResult = []; + elResult[SLIDER_ELEMENTS.SLIDER] = elSlider; + elResult[SLIDER_ELEMENTS.NUMBER_INPUT] = elInput; + return elResult; } function createVec3Property(property, elProperty, elLabel) { @@ -1737,11 +1780,11 @@ function createVec3Property(property, elProperty, elLabel) { elProperty.appendChild(elLabel); elProperty.appendChild(elTuple); - let elInputX = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[0], + let elInputX = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[VECTOR_ELEMENTS.X_INPUT], propertyData.min, propertyData.max, propertyData.step); - let elInputY = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[1], + let elInputY = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Y_INPUT], propertyData.min, propertyData.max, propertyData.step); - let elInputZ = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[2], + let elInputZ = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Z_INPUT], propertyData.min, propertyData.max, propertyData.step); let inputChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elInputX, elInputY, @@ -1750,7 +1793,11 @@ function createVec3Property(property, elProperty, elLabel) { elInputY.addEventListener('change', inputChangeFunction); elInputZ.addEventListener('change', inputChangeFunction); - return [ elInputX, elInputY, elInputZ ]; + let elResult = []; + elResult[VECTOR_ELEMENTS.X_INPUT] = elInputX; + elResult[VECTOR_ELEMENTS.Y_INPUT] = elInputY; + elResult[VECTOR_ELEMENTS.Z_INPUT] = elInputZ; + return elResult; } function createVec2Property(property, elProperty, elLabel) { @@ -1768,9 +1815,9 @@ function createVec2Property(property, elProperty, elLabel) { elProperty.appendChild(elLabel); elProperty.appendChild(elTuple); - let elInputX = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[0], + let elInputX = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[VECTOR_ELEMENTS.X_INPUT], propertyData.min, propertyData.max, propertyData.step); - let elInputY = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[1], + let elInputY = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Y_INPUT], propertyData.min, propertyData.max, propertyData.step); let inputChangeFunction = createEmitVec2PropertyUpdateFunction(propertyName, elInputX, @@ -1778,7 +1825,10 @@ function createVec2Property(property, elProperty, elLabel) { elInputX.addEventListener('change', inputChangeFunction); elInputY.addEventListener('change', inputChangeFunction); - return [elInputX, elInputY]; + let elResult = []; + elResult[VECTOR_ELEMENTS.X_INPUT] = elInputX; + elResult[VECTOR_ELEMENTS.Y_INPUT] = elInputY; + return elResult; } function createColorProperty(property, elProperty, elLabel) { @@ -1798,9 +1848,9 @@ function createColorProperty(property, elProperty, elLabel) { elProperty.appendChild(elLabel); elProperty.appendChild(elTuple); - let elInputR = createTupleNumberInput(elTuple, elementID, "red", 0, 255, 1); - let elInputG = createTupleNumberInput(elTuple, elementID, "green", 0, 255, 1); - let elInputB = createTupleNumberInput(elTuple, elementID, "blue", 0, 255, 1); + let elInputR = createTupleNumberInput(elTuple, elementID, "red", COLOR_MIN, COLOR_MAX, COLOR_STEP); + let elInputG = createTupleNumberInput(elTuple, elementID, "green", COLOR_MIN, COLOR_MAX, COLOR_STEP); + let elInputB = createTupleNumberInput(elTuple, elementID, "blue", COLOR_MIN, COLOR_MAX, COLOR_STEP); let inputChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elInputR, elInputG, elInputB); elInputR.addEventListener('change', inputChangeFunction); @@ -1832,7 +1882,12 @@ function createColorProperty(property, elProperty, elLabel) { } }); - return [elColorPicker, elInputR, elInputG, elInputB]; + let elResult = []; + elResult[COLOR_ELEMENTS.COLOR_PICKER] = elColorPicker; + elResult[COLOR_ELEMENTS.RED_INPUT] = elInputR; + elResult[COLOR_ELEMENTS.GREEN_INPUT] = elInputG; + elResult[COLOR_ELEMENTS.BLUE_INPUT] = elInputB; + return elResult; } function createDropdownProperty(property, propertyID, elProperty, elLabel) { @@ -1902,7 +1957,10 @@ function createIconProperty(property, elProperty, elLabel) { elProperty.appendChild(elSpan); elProperty.appendChild(elLabel); - return [ elSpan, elLabel ]; + let elResult = []; + elResult[ICON_ELEMENTS.ICON] = elSpan; + elResult[ICON_ELEMENTS.LABEL] = elLabel; + return elResult; } function createTextureProperty(property, elProperty, elLabel) { @@ -1952,8 +2010,11 @@ function createTextureProperty(property, elProperty, elLabel) { elProperty.appendChild(elLabel); elProperty.appendChild(elDiv); elProperty.appendChild(elInput); - - return [ elImage, elInput ]; + + let elResult = []; + elResult[TEXTURE_ELEMENTS.IMAGE] = elImage; + elResult[TEXTURE_ELEMENTS.TEXT_INPUT] = elInput; + return elResult; } function createButtonsProperty(property, elProperty, elLabel) { @@ -2468,7 +2529,7 @@ function saveJSONMaterialData(noUpdate) { function bindAllNonJSONEditorElements() { var inputs = $('input'); var i; - for (i = 0; i < inputs.length; i++) { + for (i = 0; i < inputs.length; ++i) { var input = inputs[i]; var field = $(input); // TODO FIXME: (JSHint) Functions declared within loops referencing @@ -2497,7 +2558,7 @@ function bindAllNonJSONEditorElements() { function setDropdownText(dropdown) { let lis = dropdown.parentNode.getElementsByTagName("li"); let text = ""; - for (let i = 0; i < lis.length; i++) { + for (let i = 0; i < lis.length; ++i) { if (String(lis[i].getAttribute("value")) === String(dropdown.value)) { text = lis[i].textContent; } @@ -2584,6 +2645,7 @@ function loaded() { let propertyID = propertyData.propertyID; let propertyName = propertyData.propertyName !== undefined ? propertyData.propertyName : propertyID; let propertyElementID = "property-" + propertyID; + propertyElementID = propertyElementID.replace('.', '-'); let elProperty; if (propertyType === "sub-header") { @@ -2644,29 +2706,29 @@ function loaded() { } case 'slider': { let elSlider = createSliderProperty(property, elProperty, elLabel); - properties[propertyID].elSlider = elSlider[0]; - properties[propertyID].elInput = elSlider[1]; + properties[propertyID].elSlider = elSlider[SLIDER_ELEMENTS.SLIDER]; + properties[propertyID].elInput = elSlider[SLIDER_ELEMENTS.NUMBER_INPUT]; break; } case 'vec3': { let elVec3 = createVec3Property(property, elProperty, elLabel); - properties[propertyID].elInputX = elVec3[0]; - properties[propertyID].elInputY = elVec3[1]; - properties[propertyID].elInputZ = elVec3[2]; + properties[propertyID].elInputX = elVec3[VECTOR_ELEMENTS.X_INPUT]; + properties[propertyID].elInputY = elVec3[VECTOR_ELEMENTS.Y_INPUT]; + properties[propertyID].elInputZ = elVec3[VECTOR_ELEMENTS.Z_INPUT]; break; } case 'vec2': { let elVec2 = createVec2Property(property, elProperty, elLabel); - properties[propertyID].elInputX = elVec2[0]; - properties[propertyID].elInputY = elVec2[1]; + properties[propertyID].elInputX = elVec2[VECTOR_ELEMENTS.X_INPUT]; + properties[propertyID].elInputY = elVec2[VECTOR_ELEMENTS.Y_INPUT]; break; } case 'color': { let elColor = createColorProperty(property, elProperty, elLabel); - properties[propertyID].elColorPicker = elColor[0]; - properties[propertyID].elInputR = elColor[1]; - properties[propertyID].elInputG = elColor[2]; - properties[propertyID].elInputB = elColor[3]; + properties[propertyID].elColorPicker = elColor[COLOR_ELEMENTS.COLOR_PICKER]; + properties[propertyID].elInputR = elColor[COLOR_ELEMENTS.RED_INPUT]; + properties[propertyID].elInputG = elColor[COLOR_ELEMENTS.GREEN_INPUT]; + properties[propertyID].elInputB = elColor[COLOR_ELEMENTS.BLUE_INPUT]; break; } case 'dropdown': { @@ -2679,14 +2741,14 @@ function loaded() { } case 'icon': { let elIcon = createIconProperty(property, elProperty, elLabel); - properties[propertyID].elSpan = elIcon[0]; - properties[propertyID].elLabel = elIcon[1]; + properties[propertyID].elSpan = elIcon[ICON_ELEMENTS.ICON]; + properties[propertyID].elLabel = elIcon[ICON_ELEMENTS.LABEL]; break; } case 'texture': { let elTexture = createTextureProperty(property, elProperty, elLabel); - properties[propertyID].elImage = elTexture[0]; - properties[propertyID].elInput = elTexture[1]; + properties[propertyID].elImage = elTexture[TEXTURE_ELEMENTS.IMAGE]; + properties[propertyID].elInput = elTexture[TEXTURE_ELEMENTS.TEXT_INPUT]; break; } case 'buttons': { @@ -2785,7 +2847,7 @@ function loaded() { let types = {}; let numTypes = 0; - for (let i = 0; i < selections.length; i++) { + for (let i = 0; i < selections.length; ++i) { ids.push(selections[i].id); let currentSelectedType = selections[i].properties.type; if (types[currentSelectedType] === undefined) { @@ -2810,7 +2872,7 @@ function loaded() { disableProperties(); } else { - selectedEntityProperties = data.selections[0].properties; + selectedEntityProperties = data.selections[0].properties; if (lastEntityID !== '"' + selectedEntityProperties.id + '"' && lastEntityID !== null) { if (editor !== null) { @@ -3061,7 +3123,7 @@ function loaded() { let elUserDataSaved = document.createElement('span'); elUserDataSaved.setAttribute("id", userDataElementID + "-saved"); elUserDataSaved.innerText = "Saved!"; - elDiv.childNodes[2].appendChild(elUserDataSaved); + elDiv.childNodes[JSON_EDITOR_ROW_DIV_INDEX].appendChild(elUserDataSaved); elDiv.insertBefore(elStaticUserData, elUserData); elDiv.insertBefore(elUserDataEditor, elUserData); @@ -3077,7 +3139,7 @@ function loaded() { let elMaterialDataSaved = document.createElement('span'); elMaterialDataSaved.setAttribute("id", materialDataElementID + "-saved"); elMaterialDataSaved.innerText = "Saved!"; - elDiv.childNodes[2].appendChild(elMaterialDataSaved); + elDiv.childNodes[JSON_EDITOR_ROW_DIV_INDEX].appendChild(elMaterialDataSaved); elDiv.insertBefore(elStaticMaterialData, elMaterialData); elDiv.insertBefore(elMaterialDataEditor, elMaterialData); @@ -3156,7 +3218,7 @@ function loaded() { for (let optionIndex = 0; optionIndex < options.length; ++optionIndex) { if (options[optionIndex].getAttribute("selected") === "selected") { selectedOption = optionIndex; - // TODO: Shouldn't there be a break here? + break; } } let div = elDropdown.parentNode; @@ -3225,7 +3287,7 @@ function loaded() { // For input and textarea elements, select all of the text on focus let els = document.querySelectorAll("input, textarea"); - for (let i = 0; i < els.length; i++) { + for (let i = 0; i < els.length; ++i) { els[i].onfocus = function (e) { e.target.select(); }; From a43985ef64088a88b02ab2bf11a1788fe611a6cd Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 19 Oct 2018 16:03:53 -0700 Subject: [PATCH 162/362] combo box for recenter model is added --- interface/resources/qml/hifi/AvatarApp.qml | 3 +- .../resources/qml/hifi/avatarapp/Settings.qml | 85 +------------------ interface/src/avatar/MyAvatar.cpp | 35 +++++++- interface/src/avatar/MyAvatar.h | 24 +++++- scripts/system/avatarapp.js | 32 ++----- 5 files changed, 71 insertions(+), 108 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index bf647b65bb..2d714c0d33 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -252,8 +252,7 @@ Rectangle { var avatarSettings = { dominantHand : settings.dominantHandIsLeft ? 'left' : 'right', collisionsEnabled : settings.avatarCollisionsOn, - sittingEnabled : settings.avatarSittingOn, - lockStateEnabled : settings.avatarLockSitStandStateOn, + recenterModel : settings.avatarRecenterModelOn, animGraphOverrideUrl : settings.avatarAnimationOverrideJSON, collisionSoundUrl : settings.avatarCollisionSoundUrl }; diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index 64ae03237c..cd71442bca 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -20,8 +20,7 @@ Rectangle { property real scaleValue: scaleSlider.value / 10 property alias dominantHandIsLeft: leftHandRadioButton.checked property alias avatarCollisionsOn: collisionsEnabledRadiobutton.checked - property alias avatarSittingOn: sitRadiobutton.checked - property alias avatarLockSitStandStateOn: lockSitStandStateCheckbox.checked + property alias avatarRecenterModelOn: boxy.currentIndex property alias avatarAnimationOverrideJSON: avatarAnimationUrlInputText.text property alias avatarAnimationJSON: avatarAnimationUrlInputText.placeholderText property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text @@ -47,21 +46,11 @@ Rectangle { collisionsDisabledRadioButton.checked = true; } - if (settings.sittingEnabled) { - sitRadiobutton.checked = true; - } else { - standRadioButton.checked = true; - } - - if (settings.lockStateEnabled) { - lockSitStandStateCheckbox.checked = true; - } else { - lockSitStandStateCheckbox.checked = false; - } - avatarAnimationJSON = settings.animGraphUrl; avatarAnimationOverrideJSON = settings.animGraphOverrideUrl; avatarCollisionSoundUrl = settings.collisionSoundUrl; + print("values " + avatarRecenterModelOn + " " + settings.recenterModel); + avatarRecenterModelOn = settings.recenterModel; visible = true; } @@ -305,7 +294,7 @@ Rectangle { } // TextStyle9 - + RalewaySemiBold { size: 17; Layout.row: 2 @@ -318,51 +307,6 @@ Rectangle { id: sitStand } - HifiControlsUit.RadioButton { - id: sitRadiobutton - - Layout.row: 2 - Layout.column: 1 - Layout.leftMargin: -40 - - ButtonGroup.group: sitStand - checked: true - - colorScheme: hifi.colorSchemes.light - fontSize: 17 - letterSpacing: 1.4 - text: "Sit" - boxSize: 20 - } - - HifiControlsUit.RadioButton { - id: standRadioButton - - Layout.row: 2 - Layout.column: 2 - Layout.rightMargin: 20 - - ButtonGroup.group: sitStand - - colorScheme: hifi.colorSchemes.light - fontSize: 17 - letterSpacing: 1.4 - text: "Stand" - boxSize: 20 - } - - // "Lock State" Checkbox - - HifiControlsUit.CheckBox { - id: lockSitStandStateCheckbox - visible: activeTab == "nearbyTab" - anchors.right: reloadNearbyContainer.left - anchors.rightMargin: 20 - checked: settings.lockStateEnabled - text: "lock" - boxSize: 24 - } - // sit stand combo box HifiControlsUit.ComboBox { id: boxy @@ -375,29 +319,8 @@ Rectangle { ListElement { text: "Auto Mode"; color: "Brown" } ListElement { text: "Disable Recentering"; color: "Red" } } - //displayText: "fred" - //label: cbItems.get(currentIndex).text width: 200 onCurrentIndexChanged: { - if (cbItems.get(currentIndex).text === "Force Sitting") { - sitRadiobutton.checked = true - lockSitStandStateCheckbox.checked = true - settings.lockStateEnabled = true - settings.sittingEnabled = true - } else if (cbItems.get(currentIndex).text === "Force Standing") { - sitRadiobutton.checked = false - lockSitStandStateCheckbox.checked = true - settings.lockStateEnabled = true - settings.sittingEnabled = false - } else if (cbItems.get(currentIndex).text === "auto") { - sitRadiobutton.checked = false - lockSitStandStateCheckbox.checked = false - settings.lockStateEnabled = false - settings.sittingEnabled = false - } else if (cbItems.get(currentIndex).text === "Disable Recentering") { - settings.lockStateEnabled = false - settings.sittingEnabled = false - } console.debug(cbItems.get(currentIndex).text + ", " + cbItems.get(currentIndex).color) console.debug("line 2") } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 95141d614f..14829a7b2e 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -533,7 +533,7 @@ void MyAvatar::update(float deltaTime) { float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); - qCDebug(interfaceapp) << " the sit state is " << _isInSittingState.get() << " the lock is " << _lockSitStandState.get(); + //qCDebug(interfaceapp) << " the sit state is " << _isInSittingState.get() << " the lock is " << _lockSitStandState.get(); // put the average hand azimuth into sensor space. // then mix it with head facing direction to determine rotation recenter @@ -3867,6 +3867,10 @@ bool MyAvatar::getIsInSittingState() const { return _isInSittingState.get(); } +MyAvatar::SitStandModelType MyAvatar::getRecenterModel() const { + return _recenterModel.get(); +} + bool MyAvatar::getIsSitStandStateLocked() const { return _lockSitStandState.get(); } @@ -3903,9 +3907,38 @@ void MyAvatar::setIsInSittingState(bool isSitting) { setCenterOfGravityModelEnabled(true); } setSitStandStateChange(true); + emit sittingEnabledChanged(isSitting); } +void MyAvatar::setRecenterModel(MyAvatar::SitStandModelType modelName) { + + _recenterModel.set(modelName); + //int temp = 0; + qCDebug(interfaceapp) << "recenter property changed " << modelName; + switch (modelName) { + case SitStandModelType::ForceSit: + setIsInSittingState(true); + setIsSitStandStateLocked(true); + break; + case SitStandModelType::ForceStand: + setIsInSittingState(false); + setIsSitStandStateLocked(true); + break; + case SitStandModelType::Auto: + setIsInSittingState(false); + setIsSitStandStateLocked(false); + break; + case SitStandModelType::DisableHMDLean: + setHMDLeanRecenterEnabled(false); + setIsInSittingState(false); + setIsSitStandStateLocked(false); + break; + } + qCDebug(interfaceapp) << "recenter property changed " << modelName << " sit " << _isInSittingState.get() << " lock " << _lockSitStandState.get(); + emit recenterModelChanged((int)modelName); +} + void MyAvatar::setIsSitStandStateLocked(bool isLocked) { _lockSitStandState.set(isLocked); _sitStandStateTimer = 0.0f; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index be8b5fa1b2..c59bdcd66d 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -143,6 +143,7 @@ class MyAvatar : public Avatar { * @property {number} walkBackwardSpeed * @property {number} sprintSpeed * @property {number} isInSittingState + * @property {number} recenterModel * * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the * registration point of the 3D model. @@ -244,6 +245,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(float walkBackwardSpeed READ getWalkBackwardSpeed WRITE setWalkBackwardSpeed); Q_PROPERTY(float sprintSpeed READ getSprintSpeed WRITE setSprintSpeed); Q_PROPERTY(bool isInSittingState READ getIsInSittingState WRITE setIsInSittingState); + Q_PROPERTY(MyAvatar::SitStandModelType recenterModel READ getRecenterModel WRITE setRecenterModel); Q_PROPERTY(bool isSitStandStateLocked READ getIsSitStandStateLocked WRITE setIsSitStandStateLocked); const QString DOMINANT_LEFT_HAND = "left"; @@ -265,6 +267,15 @@ public: }; Q_ENUM(DriveKeys) + enum SitStandModelType { + ForceSit = 0, + ForceStand, + Auto, + DisableHMDLean, + NumSitStandTypes + }; + Q_ENUM(SitStandModelType) + explicit MyAvatar(QThread* thread); virtual ~MyAvatar(); @@ -1106,6 +1117,8 @@ public: bool getIsInWalkingState() const; void setIsInSittingState(bool isSitting); bool getIsInSittingState() const; + void setRecenterModel(MyAvatar::SitStandModelType modelName); + MyAvatar::SitStandModelType getRecenterModel() const; void setIsSitStandStateLocked(bool isLocked); bool getIsSitStandStateLocked() const; void setWalkSpeed(float value); @@ -1530,6 +1543,14 @@ signals: */ void sittingEnabledChanged(bool enabled); + /**jsdoc + * Triggered when the recenter model is changed + * @function MyAvatar.recenterModelChanged + * @param {int} modeltype + * @ + */ + void recenterModelChanged(int modelName); + /**jsdoc * Triggered when the sit state is enabled or disabled * @function MyAvatar.sitStandStateLockEnabledChanged @@ -1829,7 +1850,7 @@ private: void updateChildCauterization(SpatiallyNestablePointer object, bool cauterize); - const float DEFAULT_FLOOR_HEIGHT = 0.0f; + // height of user in sensor space, when standing erect. ThreadSafeValueCache _userHeight { DEFAULT_AVATAR_HEIGHT }; @@ -1844,6 +1865,7 @@ private: float _walkSpeedScalar { AVATAR_WALK_SPEED_SCALAR }; bool _isInWalkingState { false }; ThreadSafeValueCache _isInSittingState { false }; + ThreadSafeValueCache _recenterModel { MyAvatar::SitStandModelType::Auto }; float _sitStandStateTimer { 0.0f }; float _squatTimer { 0.0f }; float _tippingPoint { _userHeight.get() }; diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 4a25ab9551..d64454c534 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -64,8 +64,7 @@ function getMyAvatarSettings() { return { dominantHand: MyAvatar.getDominantHand(), collisionsEnabled : MyAvatar.getCollisionsEnabled(), - sittingEnabled: MyAvatar.isInSittingState, - lockStateEnabled: MyAvatar.isSitStandStateLocked, + recenterModel: MyAvatar.recenterModel, collisionSoundUrl : MyAvatar.collisionSoundURL, animGraphUrl: MyAvatar.getAnimGraphUrl(), animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(), @@ -138,19 +137,11 @@ function onCollisionsEnabledChanged(enabled) { } } -function onSittingEnabledChanged(isSitting) { - if (currentAvatarSettings.sittingEnabled !== isSitting) { - currentAvatarSettings.sittingEnabled = isSitting; - print("emit sitting changed"); - sendToQml({ 'method': 'settingChanged', 'name': 'sittingEnabled', 'value': isSitting }) - } -} - -function onSitStandStateLockedEnabledChanged(isLocked) { - if (currentAvatarSettings.lockStateEnabled !== isLocked) { - currentAvatarSettings.lockStateEnabled = isLocked; - print("emit lock sit stand state changed"); - sendToQml({ 'method': 'settingChanged', 'name': 'lockStateEnabled', 'value': isLocked }) +function onRecenterModelChanged(modelName) { + if (currentAvatarSettings.recenterModel !== modelName) { + currentAvatarSettings.recenterModel = modelName; + print("emit recenter model changed"); + sendToQml({ 'method': 'settingChanged', 'name': 'recenterModel', 'value': modelName }) } } @@ -329,14 +320,11 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'saveSettings': MyAvatar.setAvatarScale(message.avatarScale); currentAvatar.avatarScale = message.avatarScale; - MyAvatar.setDominantHand(message.settings.dominantHand); MyAvatar.setCollisionsEnabled(message.settings.collisionsEnabled); - MyAvatar.isInSittingState = message.settings.sittingEnabled; - MyAvatar.isSitStandStateLocked = message.settings.lockStateEnabled; + MyAvatar.recenterModel = message.settings.recenterModel; MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl); - settings = getMyAvatarSettings(); break; default: @@ -527,8 +515,7 @@ function off() { MyAvatar.skeletonModelURLChanged.disconnect(onSkeletonModelURLChanged); MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); MyAvatar.collisionsEnabledChanged.disconnect(onCollisionsEnabledChanged); - MyAvatar.sittingEnabledChanged.disconnect(onSittingEnabledChanged); - MyAvatar.sitStandStateLockEnabledChanged.disconnect(onSitStandStateLockedEnabledChanged); + MyAvatar.recenterModelChanged.disconnect(onRecenterModelChanged); MyAvatar.newCollisionSoundURL.disconnect(onNewCollisionSoundUrl); MyAvatar.animGraphUrlChanged.disconnect(onAnimGraphUrlChanged); MyAvatar.targetScaleChanged.disconnect(onTargetScaleChanged); @@ -543,8 +530,7 @@ function on() { MyAvatar.skeletonModelURLChanged.connect(onSkeletonModelURLChanged); MyAvatar.dominantHandChanged.connect(onDominantHandChanged); MyAvatar.collisionsEnabledChanged.connect(onCollisionsEnabledChanged); - MyAvatar.sittingEnabledChanged.connect(onSittingEnabledChanged); - MyAvatar.sitStandStateLockEnabledChanged.connect(onSitStandStateLockedEnabledChanged); + MyAvatar.recenterModelChanged.connect(onRecenterModelChanged); MyAvatar.newCollisionSoundURL.connect(onNewCollisionSoundUrl); MyAvatar.animGraphUrlChanged.connect(onAnimGraphUrlChanged); MyAvatar.targetScaleChanged.connect(onTargetScaleChanged); From 070a517423af7e6304f3ddc50564b0e65dd01bd0 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 19 Oct 2018 18:07:03 -0700 Subject: [PATCH 163/362] changed the recenterModel variables to userRecenterModel --- interface/resources/qml/hifi/AvatarApp.qml | 2 +- .../resources/qml/hifi/avatarapp/Settings.qml | 17 ++- interface/src/avatar/MyAvatar.cpp | 108 +++++++++--------- interface/src/avatar/MyAvatar.h | 20 ++-- scripts/system/avatarapp.js | 18 +-- 5 files changed, 80 insertions(+), 85 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 2d714c0d33..549db20ab1 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -252,7 +252,7 @@ Rectangle { var avatarSettings = { dominantHand : settings.dominantHandIsLeft ? 'left' : 'right', collisionsEnabled : settings.avatarCollisionsOn, - recenterModel : settings.avatarRecenterModelOn, + userRecenterModel : settings.avatarRecenterModelOn, animGraphOverrideUrl : settings.avatarAnimationOverrideJSON, collisionSoundUrl : settings.avatarCollisionSoundUrl }; diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index cd71442bca..eb994b86de 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -20,7 +20,7 @@ Rectangle { property real scaleValue: scaleSlider.value / 10 property alias dominantHandIsLeft: leftHandRadioButton.checked property alias avatarCollisionsOn: collisionsEnabledRadiobutton.checked - property alias avatarRecenterModelOn: boxy.currentIndex + property alias avatarRecenterModelOn: userModelComboBox.currentIndex property alias avatarAnimationOverrideJSON: avatarAnimationUrlInputText.text property alias avatarAnimationJSON: avatarAnimationUrlInputText.placeholderText property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text @@ -49,8 +49,7 @@ Rectangle { avatarAnimationJSON = settings.animGraphUrl; avatarAnimationOverrideJSON = settings.animGraphOverrideUrl; avatarCollisionSoundUrl = settings.collisionSoundUrl; - print("values " + avatarRecenterModelOn + " " + settings.recenterModel); - avatarRecenterModelOn = settings.recenterModel; + avatarRecenterModelOn = settings.userRecenterModel; visible = true; } @@ -294,22 +293,21 @@ Rectangle { } // TextStyle9 - + RalewaySemiBold { size: 17; Layout.row: 2 Layout.column: 0 - text: "Sitting State" + text: "User Model:" } - ButtonGroup { - id: sitStand - } // sit stand combo box HifiControlsUit.ComboBox { - id: boxy + Layout.row: 2 + Layout.column: 1 + id: userModelComboBox comboBox.textRole: "text" currentIndex: 2 model: ListModel { @@ -325,7 +323,6 @@ Rectangle { console.debug("line 2") } } - } ColumnLayout { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 14829a7b2e..0b58c2c158 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -470,57 +470,58 @@ void MyAvatar::updateSitStandState(float newHeightReading, float dt) { const float SITTING_TIMEOUT = 4.0f; // 4 seconds const float STANDING_TIMEOUT = 0.3333f; // 1/3 second const float SITTING_UPPER_BOUND = 1.52f; - - if (!getIsSitStandStateLocked() && !getIsAway() && qApp->isHMDMode()) { - if (getIsInSittingState()) { - if (newHeightReading > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { - // if we recenter upwards then no longer in sitting state - _sitStandStateTimer += dt; - if (_sitStandStateTimer > STANDING_TIMEOUT) { - _averageUserHeightSensorSpace = newHeightReading; - _tippingPoint = newHeightReading; - setIsInSittingState(false); - } - } else if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) { - // if we are mis labelled as sitting but we are standing in the real world this will - // make sure that a real sit is still recognized so we won't be stuck in sitting unable to change state - _sitStandStateTimer += dt; - if (_sitStandStateTimer > SITTING_TIMEOUT) { - _averageUserHeightSensorSpace = newHeightReading; - _tippingPoint = newHeightReading; - // here we stay in sit state but reset the average height - setIsInSittingState(true); + if (!getIsSitStandStateLocked()) { + if (!getIsAway() && qApp->isHMDMode()) { + if (getIsInSittingState()) { + if (newHeightReading > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { + // if we recenter upwards then no longer in sitting state + _sitStandStateTimer += dt; + if (_sitStandStateTimer > STANDING_TIMEOUT) { + _averageUserHeightSensorSpace = newHeightReading; + _tippingPoint = newHeightReading; + setIsInSittingState(false); + } + } else if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) { + // if we are mis labelled as sitting but we are standing in the real world this will + // make sure that a real sit is still recognized so we won't be stuck in sitting unable to change state + _sitStandStateTimer += dt; + if (_sitStandStateTimer > SITTING_TIMEOUT) { + _averageUserHeightSensorSpace = newHeightReading; + _tippingPoint = newHeightReading; + // here we stay in sit state but reset the average height + setIsInSittingState(true); + } + } else { + // sanity check if average height greater than 5ft they are not sitting(or get off your dangerous barstool please) + if (_averageUserHeightSensorSpace > SITTING_UPPER_BOUND) { + setIsInSittingState(false); + } else { + // tipping point is average height when sitting. + _tippingPoint = _averageUserHeightSensorSpace; + _sitStandStateTimer = 0.0f; + } } } else { - // sanity check if average height greater than 5ft they are not sitting(or get off your dangerous barstool please) - if (_averageUserHeightSensorSpace > SITTING_UPPER_BOUND) { - setIsInSittingState(false); + // in the standing state + if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) { + _sitStandStateTimer += dt; + if (_sitStandStateTimer > SITTING_TIMEOUT) { + _averageUserHeightSensorSpace = newHeightReading; + _tippingPoint = newHeightReading; + setIsInSittingState(true); + } } else { - // tipping point is average height when sitting. - _tippingPoint = _averageUserHeightSensorSpace; + // use the mode height for the tipping point when we are standing. + _tippingPoint = getCurrentStandingHeight(); _sitStandStateTimer = 0.0f; } } } else { - // in the standing state - if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) { - _sitStandStateTimer += dt; - if (_sitStandStateTimer > SITTING_TIMEOUT) { - _averageUserHeightSensorSpace = newHeightReading; - _tippingPoint = newHeightReading; - setIsInSittingState(true); - } - } else { - // use the mode height for the tipping point when we are standing. - _tippingPoint = getCurrentStandingHeight(); - _sitStandStateTimer = 0.0f; - } + //if you are away then reset the average and set state to standing. + _averageUserHeightSensorSpace = _userHeight.get(); + _tippingPoint = _userHeight.get(); + setIsInSittingState(false); } - } else { - // if you are away then reset the average and set state to standing. - _averageUserHeightSensorSpace = _userHeight.get(); - _tippingPoint = _userHeight.get(); - setIsInSittingState(false); } } @@ -533,7 +534,7 @@ void MyAvatar::update(float deltaTime) { float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); - //qCDebug(interfaceapp) << " the sit state is " << _isInSittingState.get() << " the lock is " << _lockSitStandState.get(); + qCDebug(interfaceapp) << " the sit state is " << _isInSittingState.get() << " the lock is " << _lockSitStandState.get(); // put the average hand azimuth into sensor space. // then mix it with head facing direction to determine rotation recenter @@ -3867,8 +3868,8 @@ bool MyAvatar::getIsInSittingState() const { return _isInSittingState.get(); } -MyAvatar::SitStandModelType MyAvatar::getRecenterModel() const { - return _recenterModel.get(); +MyAvatar::SitStandModelType MyAvatar::getUserRecenterModel() const { + return _userRecenterModel.get(); } bool MyAvatar::getIsSitStandStateLocked() const { @@ -3907,25 +3908,25 @@ void MyAvatar::setIsInSittingState(bool isSitting) { setCenterOfGravityModelEnabled(true); } setSitStandStateChange(true); - - emit sittingEnabledChanged(isSitting); } -void MyAvatar::setRecenterModel(MyAvatar::SitStandModelType modelName) { +void MyAvatar::setUserRecenterModel(MyAvatar::SitStandModelType modelName) { + + _userRecenterModel.set(modelName); - _recenterModel.set(modelName); - //int temp = 0; - qCDebug(interfaceapp) << "recenter property changed " << modelName; switch (modelName) { case SitStandModelType::ForceSit: + setHMDLeanRecenterEnabled(true); setIsInSittingState(true); setIsSitStandStateLocked(true); break; case SitStandModelType::ForceStand: + setHMDLeanRecenterEnabled(true); setIsInSittingState(false); setIsSitStandStateLocked(true); break; case SitStandModelType::Auto: + setHMDLeanRecenterEnabled(true); setIsInSittingState(false); setIsSitStandStateLocked(false); break; @@ -3936,7 +3937,7 @@ void MyAvatar::setRecenterModel(MyAvatar::SitStandModelType modelName) { break; } qCDebug(interfaceapp) << "recenter property changed " << modelName << " sit " << _isInSittingState.get() << " lock " << _lockSitStandState.get(); - emit recenterModelChanged((int)modelName); + emit userRecenterModelChanged((int)modelName); } void MyAvatar::setIsSitStandStateLocked(bool isLocked) { @@ -3949,7 +3950,6 @@ void MyAvatar::setIsSitStandStateLocked(bool isLocked) { // always start the auto transition mode in standing state. setIsInSittingState(false); } - emit sitStandStateLockEnabledChanged(isLocked); } void MyAvatar::setWalkSpeed(float value) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index c59bdcd66d..691ab80530 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -143,7 +143,7 @@ class MyAvatar : public Avatar { * @property {number} walkBackwardSpeed * @property {number} sprintSpeed * @property {number} isInSittingState - * @property {number} recenterModel + * @property {number} userRecenterModel * * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the * registration point of the 3D model. @@ -245,7 +245,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(float walkBackwardSpeed READ getWalkBackwardSpeed WRITE setWalkBackwardSpeed); Q_PROPERTY(float sprintSpeed READ getSprintSpeed WRITE setSprintSpeed); Q_PROPERTY(bool isInSittingState READ getIsInSittingState WRITE setIsInSittingState); - Q_PROPERTY(MyAvatar::SitStandModelType recenterModel READ getRecenterModel WRITE setRecenterModel); + Q_PROPERTY(MyAvatar::SitStandModelType userRecenterModel READ getUserRecenterModel WRITE setUserRecenterModel); Q_PROPERTY(bool isSitStandStateLocked READ getIsSitStandStateLocked WRITE setIsSitStandStateLocked); const QString DOMINANT_LEFT_HAND = "left"; @@ -1117,8 +1117,8 @@ public: bool getIsInWalkingState() const; void setIsInSittingState(bool isSitting); bool getIsInSittingState() const; - void setRecenterModel(MyAvatar::SitStandModelType modelName); - MyAvatar::SitStandModelType getRecenterModel() const; + void setUserRecenterModel(MyAvatar::SitStandModelType modelName); + MyAvatar::SitStandModelType getUserRecenterModel() const; void setIsSitStandStateLocked(bool isLocked); bool getIsSitStandStateLocked() const; void setWalkSpeed(float value); @@ -1545,11 +1545,11 @@ signals: /**jsdoc * Triggered when the recenter model is changed - * @function MyAvatar.recenterModelChanged - * @param {int} modeltype - * @ + * @function MyAvatar.userRecenterModelChanged + * @param {int} userRecenteringModeltype + * @returns {Signal} */ - void recenterModelChanged(int modelName); + void userRecenterModelChanged(int modelName); /**jsdoc * Triggered when the sit state is enabled or disabled @@ -1850,8 +1850,6 @@ private: void updateChildCauterization(SpatiallyNestablePointer object, bool cauterize); - - // height of user in sensor space, when standing erect. ThreadSafeValueCache _userHeight { DEFAULT_AVATAR_HEIGHT }; float _averageUserHeightSensorSpace { _userHeight.get() }; @@ -1865,7 +1863,7 @@ private: float _walkSpeedScalar { AVATAR_WALK_SPEED_SCALAR }; bool _isInWalkingState { false }; ThreadSafeValueCache _isInSittingState { false }; - ThreadSafeValueCache _recenterModel { MyAvatar::SitStandModelType::Auto }; + ThreadSafeValueCache _userRecenterModel { MyAvatar::SitStandModelType::Auto }; float _sitStandStateTimer { 0.0f }; float _squatTimer { 0.0f }; float _tippingPoint { _userHeight.get() }; diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index d64454c534..b30f87c944 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -64,7 +64,7 @@ function getMyAvatarSettings() { return { dominantHand: MyAvatar.getDominantHand(), collisionsEnabled : MyAvatar.getCollisionsEnabled(), - recenterModel: MyAvatar.recenterModel, + userRecenterModel: MyAvatar.userRecenterModel, collisionSoundUrl : MyAvatar.collisionSoundURL, animGraphUrl: MyAvatar.getAnimGraphUrl(), animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(), @@ -137,11 +137,11 @@ function onCollisionsEnabledChanged(enabled) { } } -function onRecenterModelChanged(modelName) { - if (currentAvatarSettings.recenterModel !== modelName) { - currentAvatarSettings.recenterModel = modelName; - print("emit recenter model changed"); - sendToQml({ 'method': 'settingChanged', 'name': 'recenterModel', 'value': modelName }) +function onUserRecenterModelChanged(modelName) { + if (currentAvatarSettings.userRecenterModel !== modelName) { + currentAvatarSettings.userRecenterModel = modelName; + print("emit user recenter model changed"); + sendToQml({ 'method': 'settingChanged', 'name': 'userRecenterModel', 'value': modelName }) } } @@ -322,7 +322,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See currentAvatar.avatarScale = message.avatarScale; MyAvatar.setDominantHand(message.settings.dominantHand); MyAvatar.setCollisionsEnabled(message.settings.collisionsEnabled); - MyAvatar.recenterModel = message.settings.recenterModel; + MyAvatar.userRecenterModel = message.settings.userRecenterModel; MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl); settings = getMyAvatarSettings(); @@ -515,7 +515,7 @@ function off() { MyAvatar.skeletonModelURLChanged.disconnect(onSkeletonModelURLChanged); MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); MyAvatar.collisionsEnabledChanged.disconnect(onCollisionsEnabledChanged); - MyAvatar.recenterModelChanged.disconnect(onRecenterModelChanged); + MyAvatar.userRecenterModelChanged.disconnect(onUserRecenterModelChanged); MyAvatar.newCollisionSoundURL.disconnect(onNewCollisionSoundUrl); MyAvatar.animGraphUrlChanged.disconnect(onAnimGraphUrlChanged); MyAvatar.targetScaleChanged.disconnect(onTargetScaleChanged); @@ -530,7 +530,7 @@ function on() { MyAvatar.skeletonModelURLChanged.connect(onSkeletonModelURLChanged); MyAvatar.dominantHandChanged.connect(onDominantHandChanged); MyAvatar.collisionsEnabledChanged.connect(onCollisionsEnabledChanged); - MyAvatar.recenterModelChanged.connect(onRecenterModelChanged); + MyAvatar.userRecenterModelChanged.connect(onUserRecenterModelChanged); MyAvatar.newCollisionSoundURL.connect(onNewCollisionSoundUrl); MyAvatar.animGraphUrlChanged.connect(onAnimGraphUrlChanged); MyAvatar.targetScaleChanged.connect(onTargetScaleChanged); From 55daeb11cdcdb007ec7cfcabbc8eac18d46a4270 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 19 Oct 2018 18:43:02 -0700 Subject: [PATCH 164/362] moved the radio buttons on the avatar settings to even them out better. will be corrected I'm sure --- interface/resources/qml/hifi/avatarapp/Settings.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index eb994b86de..dbf38eb5fc 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -32,7 +32,7 @@ Rectangle { scaleSlider.notify = false; scaleSlider.value = Math.round(avatarScale * 10); - scaleSlider.notify = true;; + scaleSlider.notify = true; if (settings.dominantHand === 'left') { leftHandRadioButton.checked = true; @@ -191,7 +191,7 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right - rows: 2 + rows: 3 rowSpacing: 25 columns: 3 @@ -214,7 +214,7 @@ Rectangle { Layout.row: 0 Layout.column: 1 - Layout.leftMargin: -40 + Layout.leftMargin: 20 ButtonGroup.group: leftRight checked: true @@ -231,7 +231,7 @@ Rectangle { Layout.row: 0 Layout.column: 2 - Layout.rightMargin: 20 + Layout.rightMargin: -20 ButtonGroup.group: leftRight @@ -260,7 +260,7 @@ Rectangle { Layout.row: 1 Layout.column: 1 - Layout.leftMargin: -40 + Layout.leftMargin: 20 ButtonGroup.group: onOff colorScheme: hifi.colorSchemes.light @@ -281,7 +281,7 @@ Rectangle { Layout.row: 1 Layout.column: 2 - Layout.rightMargin: 20 + Layout.rightMargin: -20 ButtonGroup.group: onOff colorScheme: hifi.colorSchemes.light From a427ddb23543dac8e9cf1a111b24c9c6267ac797 Mon Sep 17 00:00:00 2001 From: amantley Date: Sat, 20 Oct 2018 09:16:05 -0700 Subject: [PATCH 165/362] removed extraneous print statements --- interface/resources/qml/hifi/avatarapp/Settings.qml | 4 ---- interface/src/avatar/MyAvatar.cpp | 2 -- 2 files changed, 6 deletions(-) diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index dbf38eb5fc..b7083bda13 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -318,10 +318,6 @@ Rectangle { ListElement { text: "Disable Recentering"; color: "Red" } } width: 200 - onCurrentIndexChanged: { - console.debug(cbItems.get(currentIndex).text + ", " + cbItems.get(currentIndex).color) - console.debug("line 2") - } } } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 0b58c2c158..c8d33a770e 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -534,7 +534,6 @@ void MyAvatar::update(float deltaTime) { float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); - qCDebug(interfaceapp) << " the sit state is " << _isInSittingState.get() << " the lock is " << _lockSitStandState.get(); // put the average hand azimuth into sensor space. // then mix it with head facing direction to determine rotation recenter @@ -3936,7 +3935,6 @@ void MyAvatar::setUserRecenterModel(MyAvatar::SitStandModelType modelName) { setIsSitStandStateLocked(false); break; } - qCDebug(interfaceapp) << "recenter property changed " << modelName << " sit " << _isInSittingState.get() << " lock " << _lockSitStandState.get(); emit userRecenterModelChanged((int)modelName); } From 5efb889de53ea3fd5d4375a05db885ce4bf02787 Mon Sep 17 00:00:00 2001 From: amantley Date: Sat, 20 Oct 2018 13:42:17 -0700 Subject: [PATCH 166/362] removed unused variables --- interface/src/avatar/MyAvatar.cpp | 44 ++++++++++++++----------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c8d33a770e..94a6c6f514 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3606,12 +3606,9 @@ glm::vec3 MyAvatar::computeCounterBalance() { glm::vec3 counterBalancedCg = (1.0f / DEFAULT_AVATAR_HIPS_MASS) * counterBalancedForHead; // find the height of the hips - const float UPPER_LEG_FRACTION = 0.3333f; glm::vec3 xzDiff((cgHeadMass.position.x - counterBalancedCg.x), 0.0f, (cgHeadMass.position.z - counterBalancedCg.z)); float headMinusHipXz = glm::length(xzDiff); float headHipDefault = glm::length(tposeHead - tposeHips); - float hipFootDefault = tposeHips.y - tposeRightFoot.y; - float sitSquatThreshold = tposeHips.y - (UPPER_LEG_FRACTION * hipFootDefault); float hipHeight = 0.0f; if (headHipDefault > headMinusHipXz) { hipHeight = sqrtf((headHipDefault * headHipDefault) - (headMinusHipXz * headMinusHipXz)); @@ -3914,26 +3911,26 @@ void MyAvatar::setUserRecenterModel(MyAvatar::SitStandModelType modelName) { _userRecenterModel.set(modelName); switch (modelName) { - case SitStandModelType::ForceSit: - setHMDLeanRecenterEnabled(true); - setIsInSittingState(true); - setIsSitStandStateLocked(true); - break; - case SitStandModelType::ForceStand: - setHMDLeanRecenterEnabled(true); - setIsInSittingState(false); - setIsSitStandStateLocked(true); - break; - case SitStandModelType::Auto: - setHMDLeanRecenterEnabled(true); - setIsInSittingState(false); - setIsSitStandStateLocked(false); - break; - case SitStandModelType::DisableHMDLean: - setHMDLeanRecenterEnabled(false); - setIsInSittingState(false); - setIsSitStandStateLocked(false); - break; + case MyAvatar::SitStandModelType::ForceSit: + setHMDLeanRecenterEnabled(true); + setIsInSittingState(true); + setIsSitStandStateLocked(true); + break; + case MyAvatar::SitStandModelType::ForceStand: + setHMDLeanRecenterEnabled(true); + setIsInSittingState(false); + setIsSitStandStateLocked(true); + break; + case MyAvatar::SitStandModelType::Auto: + setHMDLeanRecenterEnabled(true); + setIsInSittingState(false); + setIsSitStandStateLocked(false); + break; + case MyAvatar::SitStandModelType::DisableHMDLean: + setHMDLeanRecenterEnabled(false); + setIsInSittingState(false); + setIsSitStandStateLocked(false); + break; } emit userRecenterModelChanged((int)modelName); } @@ -4190,7 +4187,6 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, co const float CYLINDER_TOP = 0.1f; const float CYLINDER_BOTTOM = -1.5f; const float SITTING_BOTTOM = -0.02f; - const int SQUATTY_COUNT_THRESHOLD = 1800; glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); bool returnValue = false; From f8d67d124fc8b80623f4557eeffeb944314f1de6 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Sat, 20 Oct 2018 15:22:25 -0700 Subject: [PATCH 167/362] Hold flickering fixed and refactor --- interface/src/avatar/AvatarManager.cpp | 14 +++--- interface/src/avatar/MyAvatar.cpp | 1 - libraries/animation/src/Rig.cpp | 2 +- .../src/avatars-renderer/Avatar.cpp | 45 +++++-------------- .../src/avatars-renderer/Avatar.h | 10 ++--- libraries/avatars/src/AvatarData.cpp | 29 +++++------- libraries/avatars/src/AvatarData.h | 15 +++++-- 7 files changed, 46 insertions(+), 70 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index dd56c03d26..bd3fba9a69 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -79,14 +79,12 @@ AvatarManager::AvatarManager(QObject* parent) : } }); - const float AVATAR_TRANSIT_TRIGGER_DISTANCE = 1.0f; - const int AVATAR_TRANSIT_FRAME_COUNT = 11; // Based on testing - const float AVATAR_TRANSIT_FRAMES_PER_METER = 0.5f; // Based on testing - _transitConfig._totalFrames = AVATAR_TRANSIT_FRAME_COUNT; - _transitConfig._triggerDistance = AVATAR_TRANSIT_TRIGGER_DISTANCE; + _transitConfig._minTriggerDistance = AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE; + _transitConfig._maxTriggerDistance = AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE; _transitConfig._framesPerMeter = AVATAR_TRANSIT_FRAMES_PER_METER; - _transitConfig._isDistanceBased = true; + _transitConfig._isDistanceBased = AVATAR_TRANSIT_DISTANCE_BASED; + _transitConfig._abortDistance = AVATAR_TRANSIT_ABORT_DISTANCE; } AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { @@ -169,7 +167,7 @@ void AvatarManager::updateMyAvatar(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "AvatarManager::updateMyAvatar()"); - AvatarTransit::Status status = _myAvatar->updateTransit(deltaTime, _myAvatar->getNextPosition(), _transitConfig); + AvatarTransit::Status status = _myAvatar->updateTransit(deltaTime, _myAvatar->getNextPosition(), _myAvatar->getSensorToWorldScale(), _transitConfig); handleTransitAnimations(status); _myAvatar->update(deltaTime); @@ -300,7 +298,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { if (inView && avatar->hasNewJointData()) { numAvatarsUpdated++; } - auto transitStatus = avatar->_transit.update(deltaTime, avatar->_lastPosition, _transitConfig); + auto transitStatus = avatar->_transit.update(deltaTime, avatar->_serverPosition, _transitConfig); if (avatar->getIsNewAvatar() && (transitStatus == AvatarTransit::Status::START_TRANSIT || transitStatus == AvatarTransit::Status::ABORT_TRANSIT)) { avatar->_transit.reset(); avatar->setIsNewAvatar(false); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b347963cf1..ca283616a5 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -929,7 +929,6 @@ void MyAvatar::updateSensorToWorldMatrix() { updateJointFromController(controller::Action::RIGHT_HAND, _controllerRightHandMatrixCache); if (hasSensorToWorldScaleChanged) { - setTransitScale(sensorToWorldScale); emit sensorToWorldScaleChanged(sensorToWorldScale); } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 35f076c78d..31c90a3070 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1138,7 +1138,7 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons } } if (_sendNetworkNode) { - for (auto i = 0; i < _networkPoseSet._relativePoses.size(); i++) { + for (size_t i = 0; i < _networkPoseSet._relativePoses.size(); i++) { _networkPoseSet._relativePoses[i].blend(_internalPoseSet._relativePoses[i], (alpha > 1.0f ? 1.0f : alpha)); } } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 705ebc0b13..c140f1bede 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -115,10 +115,8 @@ void Avatar::setShowNamesAboveHeads(bool show) { AvatarTransit::Status AvatarTransit::update(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config) { float oneFrameDistance = _isActive ? glm::length(avatarPosition - _endPosition) : glm::length(avatarPosition - _lastPosition); - const float TRANSIT_TRIGGER_MAX_DISTANCE = 30.0f; - float scaledMaxTransitDistance = TRANSIT_TRIGGER_MAX_DISTANCE * _scale; - if (oneFrameDistance > config._triggerDistance) { - if (oneFrameDistance < scaledMaxTransitDistance) { + if (oneFrameDistance > (config._minTriggerDistance * _scale)) { + if (oneFrameDistance < (config._maxTriggerDistance * _scale)) { start(deltaTime, _lastPosition, avatarPosition, config); } else { _lastPosition = avatarPosition; @@ -128,9 +126,7 @@ AvatarTransit::Status AvatarTransit::update(float deltaTime, const glm::vec3& av _lastPosition = avatarPosition; _status = updatePosition(deltaTime); - const float SETTLE_ABORT_DISTANCE = 0.1f; - float scaledSettleAbortDistance = SETTLE_ABORT_DISTANCE * _scale; - if (_isActive && oneFrameDistance > scaledSettleAbortDistance && _status == Status::POST_TRANSIT) { + if (_isActive && oneFrameDistance > (config._abortDistance * _scale) && _status == Status::POST_TRANSIT) { reset(); _status = Status::ENDED; } @@ -150,14 +146,10 @@ void AvatarTransit::start(float deltaTime, const glm::vec3& startPosition, const _totalDistance = glm::length(_transitLine); _easeType = config._easeType; - const float REFERENCE_FRAMES_PER_SECOND = 30.0f; - const float PRE_TRANSIT_FRAME_COUNT = 10.0f; - const float POST_TRANSIT_FRAME_COUNT = 27.0f; - - _preTransitTime = PRE_TRANSIT_FRAME_COUNT / REFERENCE_FRAMES_PER_SECOND; - _postTransitTime = POST_TRANSIT_FRAME_COUNT / REFERENCE_FRAMES_PER_SECOND; + _preTransitTime = AVATAR_PRE_TRANSIT_FRAME_COUNT / AVATAR_TRANSIT_FRAMES_PER_SECOND; + _postTransitTime = AVATAR_POST_TRANSIT_FRAME_COUNT / AVATAR_TRANSIT_FRAMES_PER_SECOND; int transitFrames = (!config._isDistanceBased) ? config._totalFrames : config._framesPerMeter * _totalDistance; - _transitTime = (float)transitFrames / REFERENCE_FRAMES_PER_SECOND; + _transitTime = (float)transitFrames / AVATAR_TRANSIT_FRAMES_PER_SECOND; _totalTime = _transitTime + _preTransitTime + _postTransitTime; _currentTime = _isActive ? _preTransitTime : 0.0f; _isActive = true; @@ -554,13 +546,10 @@ void Avatar::relayJointDataToChildren() { void Avatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "simulate"); - - if (_transit.isActive()) { - _globalPosition = _transit.getCurrentPosition(); - _globalPositionChanged = usecTimestampNow(); - if (!hasParent()) { - setLocalPosition(_transit.getCurrentPosition()); - } + + _globalPosition = _transit.isActive() ? _transit.getCurrentPosition() : _serverPosition; + if (!hasParent()) { + setLocalPosition(_globalPosition); } _simulationRate.increment(); @@ -1994,22 +1983,12 @@ float Avatar::getUnscaledEyeHeightFromSkeleton() const { } } -AvatarTransit::Status Avatar::updateTransit(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config) { +AvatarTransit::Status Avatar::updateTransit(float deltaTime, const glm::vec3& avatarPosition, float avatarScale, const AvatarTransit::TransitConfig& config) { std::lock_guard lock(_transitLock); + _transit.setScale(avatarScale); return _transit.update(deltaTime, avatarPosition, config); } -void Avatar::setTransitScale(float scale) { - std::lock_guard lock(_transitLock); - _transit.setScale(scale); -} - -void Avatar::overrideNextPacketPositionData(const glm::vec3& position) { - std::lock_guard lock(_transitLock); - _overrideGlobalPosition = true; - _globalPositionOverride = position; -} - void Avatar::addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) { std::lock_guard lock(_materialsLock); _materials[parentMaterialName].push(material); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 2995c0f7b4..3df923b048 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -76,7 +76,9 @@ public: int _totalFrames { 0 }; float _framesPerMeter { 0.0f }; bool _isDistanceBased { false }; - float _triggerDistance { 0 }; + float _minTriggerDistance { 0.0f }; + float _maxTriggerDistance { 0.0f }; + float _abortDistance{ 0.0f }; EaseType _easeType { EaseType::EASE_OUT }; }; @@ -434,11 +436,7 @@ public: virtual scriptable::ScriptableModelBase getScriptableModel() override; std::shared_ptr getTransit() { return std::make_shared(_transit); }; - - AvatarTransit::Status updateTransit(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config); - void setTransitScale(float scale); - - void overrideNextPacketPositionData(const glm::vec3& position); + AvatarTransit::Status updateTransit(float deltaTime, const glm::vec3& avatarPosition, float avatarScale, const AvatarTransit::TransitConfig& config); signals: void targetScaleChanged(float targetScale); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index a22cc4a1d3..4ff3c0192d 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -371,12 +371,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent if (hasAvatarGlobalPosition) { auto startSection = destinationBuffer; - if (_overrideGlobalPosition) { - AVATAR_MEMCPY(_globalPositionOverride); - } else { - AVATAR_MEMCPY(_globalPosition); - } - + AVATAR_MEMCPY(_globalPosition); int numBytes = destinationBuffer - startSection; @@ -894,20 +889,22 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { offset = glm::vec3(row * SPACE_BETWEEN_AVATARS, 0.0f, col * SPACE_BETWEEN_AVATARS); } - auto newValue = glm::vec3(data->globalPosition[0], data->globalPosition[1], data->globalPosition[2]) + offset; - if (_globalPosition != newValue) { - _lastPosition = _globalPosition = newValue; + _serverPosition = glm::vec3(data->globalPosition[0], data->globalPosition[1], data->globalPosition[2]) + offset; + auto oneStepDistance = glm::length(_globalPosition - _serverPosition); + if (oneStepDistance <= AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE || oneStepDistance >= AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE) { + _globalPosition = _serverPosition; + // if we don't have a parent, make sure to also set our local position + if (!hasParent()) { + setLocalPosition(_serverPosition); + } + } + if (_globalPosition != _serverPosition) { _globalPositionChanged = now; } sourceBuffer += sizeof(AvatarDataPacket::AvatarGlobalPosition); int numBytesRead = sourceBuffer - startSection; _globalPositionRate.increment(numBytesRead); _globalPositionUpdateRate.increment(); - - // if we don't have a parent, make sure to also set our local position - if (!hasParent()) { - setLocalPosition(newValue); - } } if (hasAvatarBoundingBox) { @@ -2110,10 +2107,6 @@ void AvatarData::sendAvatarDataPacket(bool sendAll) { } } - if (_overrideGlobalPosition) { - _overrideGlobalPosition = false; - } - doneEncoding(cullSmallData); static AvatarDataSequenceNumber sequenceNumber = 0; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index dcdaa70ad7..fcbe05ca52 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -327,6 +327,17 @@ const float AVATAR_DISTANCE_LEVEL_5 = 200.0f; // meters // This is the start location in the Sandbox (xyz: 6270, 211, 6000). const glm::vec3 START_LOCATION(6270, 211, 6000); +// Avatar Transit Constants +const float AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE = 1.0f; +const float AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE = 30.0f; +const int AVATAR_TRANSIT_FRAME_COUNT = 11; +const float AVATAR_TRANSIT_FRAMES_PER_METER = 0.5f; +const float AVATAR_TRANSIT_ABORT_DISTANCE = 0.1f; +const bool AVATAR_TRANSIT_DISTANCE_BASED = true; +const float AVATAR_TRANSIT_FRAMES_PER_SECOND = 30.0f; +const float AVATAR_PRE_TRANSIT_FRAME_COUNT = 10.0f; +const float AVATAR_POST_TRANSIT_FRAME_COUNT = 27.0f; + enum KeyState { NO_KEY_DOWN = 0, INSERT_KEY_DOWN, @@ -1378,9 +1389,7 @@ protected: // where Entities are located. This is currently only used by the mixer to decide how often to send // updates about one avatar to another. glm::vec3 _globalPosition { 0, 0, 0 }; - glm::vec3 _lastPosition { 0, 0, 0 }; - glm::vec3 _globalPositionOverride { 0, 0, 0 }; - bool _overrideGlobalPosition { false }; + glm::vec3 _serverPosition { 0, 0, 0 }; quint64 _globalPositionChanged { 0 }; quint64 _avatarBoundingBoxChanged { 0 }; From eb3db9a5715ad53c0344bee64647e67aa0de8eac Mon Sep 17 00:00:00 2001 From: amantley Date: Sat, 20 Oct 2018 16:13:41 -0700 Subject: [PATCH 168/362] added default to case statement --- interface/src/avatar/MyAvatar.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b2146fb14d..dd40a748af 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3970,6 +3970,8 @@ void MyAvatar::setUserRecenterModel(MyAvatar::SitStandModelType modelName) { setIsInSittingState(false); setIsSitStandStateLocked(false); break; + default: + break; } emit userRecenterModelChanged((int)modelName); } From 092f88e5586bf79b0ef8afb511dd0d6793c87ea7 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 22 Oct 2018 10:55:50 -0700 Subject: [PATCH 169/362] Change parameter name from `--no-login` to `--no-login-suggestion` --- interface/src/Application.cpp | 2 +- tools/auto-tester/src/TestRunner.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ba51ff9cec..f07983fddf 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2331,7 +2331,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo AndroidHelper::instance().notifyLoadComplete(); #else // Do not show login dialog if requested not to on the command line - const QString HIFI_NO_LOGIN_COMMAND_LINE_KEY = "--no-login"; + const QString HIFI_NO_LOGIN_COMMAND_LINE_KEY = "--no-login-suggestion"; int index = arguments().indexOf(HIFI_NO_LOGIN_COMMAND_LINE_KEY); if (index == -1) { // request not found diff --git a/tools/auto-tester/src/TestRunner.cpp b/tools/auto-tester/src/TestRunner.cpp index 674cf6f8e8..01ec04f254 100644 --- a/tools/auto-tester/src/TestRunner.cpp +++ b/tools/auto-tester/src/TestRunner.cpp @@ -340,7 +340,7 @@ void TestRunner::runInterfaceWithTestScript() { QString testScript = QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js"; - QString commandLine = exeFile + " --url " + url + " --no-updater --no-login" + " --testScript " + testScript + + QString commandLine = exeFile + " --url " + url + " --no-updater --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished --testResultsLocation " + snapshotFolder; interfaceWorker->setCommandLine(commandLine); From d008bfa523e6f61879290b09fd1d7a8cc61b8037 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 22 Oct 2018 11:29:36 -0700 Subject: [PATCH 170/362] partial requested code changes --- .../controllers/controllerModules/webSurfaceLaserInput.js | 2 -- scripts/system/interstitialPage.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index 9f4133674a..c2949bebe4 100644 --- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js +++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -90,7 +90,6 @@ Script.include("/~/system/libraries/controllers.js"); action: 'add', id: objectID }; - print("ignoreing entity " + entityIndex); Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)); this.ignoredObjects.push(objectID); } @@ -99,7 +98,6 @@ Script.include("/~/system/libraries/controllers.js"); this.restoreIgnoredObjects = function() { for (var index = 0; index < this.ignoredObjects.length; index++) { - print("removing"); var data = { action: 'remove', id: this.ignoredObjects[index] diff --git a/scripts/system/interstitialPage.js b/scripts/system/interstitialPage.js index a611f386ff..b29347872a 100644 --- a/scripts/system/interstitialPage.js +++ b/scripts/system/interstitialPage.js @@ -241,7 +241,7 @@ } function lerp(a, b, t) { - return (((1 - t) * a) + (t * b)); + return ((1 - t) * a + t * b); } function resetValues() { From ddbe02dd4e9b5993027a86ab72e9a405d80c5056 Mon Sep 17 00:00:00 2001 From: David Back Date: Mon, 22 Oct 2018 11:56:47 -0700 Subject: [PATCH 171/362] CR changes and fix extra info toggle --- scripts/system/html/css/edit-style.css | 15 +++++--- scripts/system/html/entityList.html | 6 ++-- scripts/system/html/js/entityList.js | 48 +++++++++++++------------- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 1b0094cfb7..13952c5e38 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1063,10 +1063,10 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { .multiselect { position: relative; } -.selectBox { +.select-box { position: absolute; } -.selectBox select { +.select-box select { font-family: FiraSans-SemiBold; font-size: 15px; color: #afafaf; @@ -1076,7 +1076,7 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { width: 107px; text-align-last: center; } -.overSelect { +.over-select { position: absolute; left: 0; right: 0; @@ -1084,7 +1084,7 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { bottom: 0; } -#filter-type-selectBox select { +#filter-type-select-box select { border-radius: 14.5px; } #filter-type-checkboxes { @@ -1158,6 +1158,13 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { #filter-radius-and-unit label { margin-left: 2px; } +#filter-radius-and-unit span { + position: relative; + top: 25px; + right: 9px; + z-index: 2; + font-style: italic; +} #filter-radius-and-unit input { width: 120px; border-radius: 14.5px; diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 434f8a5f87..187aa70347 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -31,11 +31,11 @@

-
+
-
+
@@ -47,7 +47,7 @@
- +
diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 66ad08e27c..2d248e48e6 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -131,7 +131,7 @@ function loaded() { elToggleLocked = document.getElementById("locked"); elToggleVisible = document.getElementById("visible"); elDelete = document.getElementById("delete"); - elFilterTypeSelectBox = document.getElementById("filter-type-selectBox"); + elFilterTypeSelectBox = document.getElementById("filter-type-select-box"); elFilterTypeText = document.getElementById("filter-type-text"); elFilterTypeCheckboxes = document.getElementById("filter-type-checkboxes"); elFilterSearch = document.getElementById("filter-search"); @@ -155,31 +155,31 @@ function loaded() { document.getElementById("entity-url").onclick = function() { setSortColumn('url'); }; - document.getElementById("entity-locked").onclick = function () { + document.getElementById("entity-locked").onclick = function() { setSortColumn('locked'); }; - document.getElementById("entity-visible").onclick = function () { + document.getElementById("entity-visible").onclick = function() { setSortColumn('visible'); }; - document.getElementById("entity-verticesCount").onclick = function () { + document.getElementById("entity-verticesCount").onclick = function() { setSortColumn('verticesCount'); }; - document.getElementById("entity-texturesCount").onclick = function () { + document.getElementById("entity-texturesCount").onclick = function() { setSortColumn('texturesCount'); }; - document.getElementById("entity-texturesSize").onclick = function () { + document.getElementById("entity-texturesSize").onclick = function() { setSortColumn('texturesSize'); }; - document.getElementById("entity-hasTransparent").onclick = function () { + document.getElementById("entity-hasTransparent").onclick = function() { setSortColumn('hasTransparent'); }; - document.getElementById("entity-isBaked").onclick = function () { + document.getElementById("entity-isBaked").onclick = function() { setSortColumn('isBaked'); }; - document.getElementById("entity-drawCalls").onclick = function () { + document.getElementById("entity-drawCalls").onclick = function() { setSortColumn('drawCalls'); }; - document.getElementById("entity-hasScript").onclick = function () { + document.getElementById("entity-hasScript").onclick = function() { setSortColumn('hasScript'); }; elRefresh.onclick = function() { @@ -203,12 +203,9 @@ function loaded() { elFilterSearch.onkeyup = refreshEntityList; elFilterSearch.onsearch = refreshEntityList; elFilterInView.onclick = toggleFilterInView; + elFilterRadius.onkeyup = onRadiusChange; elFilterRadius.onchange = onRadiusChange; - elFilterRadius.oninput = function(event) { - if (event.target.value.length > MAX_LENGTH_RADIUS) { - event.target.value = event.target.value.slice(0, MAX_LENGTH_RADIUS); - } - } + elInfoToggle.onclick = toggleInfo; // create filter type dropdown checkboxes with label and icon for each type elFilterTypeSelectBox.onclick = toggleTypeDropdown; @@ -225,9 +222,10 @@ function loaded() { let elInput = document.createElement('input'); elInput.setAttribute("type", "checkbox"); elInput.setAttribute("id", typeFilterID); + elInput.setAttribute("filterType", type); elInput.checked = true; // all types are checked initially - toggleTypeFilter(type, false); // add all types to the initial types filter - elInput.onclick = onToggleTypeFilter(type); + toggleTypeFilter(elInput, false); // add all types to the initial types filter + elInput.onclick = onToggleTypeFilter; elDiv.appendChild(elInput); elLabel.insertBefore(elSpan, elLabel.childNodes[0]); elDiv.appendChild(elLabel); @@ -642,6 +640,7 @@ function loaded() { } function onRadiusChange() { + elFilterRadius.value = elFilterRadius.value.replace(/[^0-9]/g, ''); elFilterRadius.value = Math.max(elFilterRadius.value, 0); EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elFilterRadius.value })); refreshEntities(); @@ -655,11 +654,14 @@ function loaded() { elFilterTypeCheckboxes.style.display = isTypeDropdownVisible() ? "none" : "block"; } - function toggleTypeFilter(type, refresh) { + function toggleTypeFilter(elInput, refresh) { + let type = elInput.getAttribute("filterType"); + let typeChecked = elInput.checked; + let typeFilterIndex = typeFilters.indexOf(type); - if (typeFilterIndex > -1) { + if (!typeChecked && typeFilterIndex > -1) { typeFilters.splice(typeFilterIndex, 1); - } else { + } else if (typeChecked && typeFilterIndex === -1) { typeFilters.push(type); } @@ -676,10 +678,8 @@ function loaded() { } } - function onToggleTypeFilter(type) { - return function() { - toggleTypeFilter(type, true); - }; + function onToggleTypeFilter(event) { + toggleTypeFilter(this, true); } function onBodyClick(event) { From 1ce1902fa8f9d2dd0571d651e0094abe425a5445 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Mon, 22 Oct 2018 12:49:24 -0700 Subject: [PATCH 172/362] remove magic numbers --- scripts/system/interstitialPage.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/scripts/system/interstitialPage.js b/scripts/system/interstitialPage.js index b29347872a..670d21c7a7 100644 --- a/scripts/system/interstitialPage.js +++ b/scripts/system/interstitialPage.js @@ -459,13 +459,14 @@ } } + var MAX_TEXTURE_STABILITY_COUNT = 30; + var INTERVAL_PROGRESS = 0.04; function update() { var renderStats = Render.getConfig("Stats"); var physicsEnabled = Window.isPhysicsEnabled(); var thisInterval = Date.now(); var deltaTime = (thisInterval - lastInterval); lastInterval = thisInterval; - var deltaTimeMS = deltaTime / 1000; var domainLoadingProgressPercentage = Window.domainLoadingProgress(); var progress = ((TOTAL_LOADING_PROGRESS * 0.4) * domainLoadingProgressPercentage); @@ -485,7 +486,7 @@ textureMemSizeAtLastCheck = textureResourceGPUMemSize; - if (textureMemSizeStabilityCount >= 30) { + if (textureMemSizeStabilityCount >= MAX_TEXTURE_STABILITY_COUNT) { if (textureResourceGPUMemSize > 0) { var gpuPercantage = (TOTAL_LOADING_PROGRESS * 0.6) * (texturePopulatedGPUMemSize / textureResourceGPUMemSize); @@ -501,11 +502,7 @@ target = TOTAL_LOADING_PROGRESS; } - if (deltaTime > 1.0) { - deltaTimeMS = 0.02; - } - - currentProgress = lerp(currentProgress, target, (deltaTimeMS * 2.0)); + currentProgress = lerp(currentProgress, target, INTERVAL_PROGRESS); var properties = { localPosition: { x: (1.85 - (currentProgress / 2) - (-0.029 * (currentProgress / TOTAL_LOADING_PROGRESS))), y: -0.935, z: 0.0 }, dimensions: { From 3aac294a92d9b3331fe2765032266f2844902e07 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 20 Sep 2018 16:57:22 -0700 Subject: [PATCH 173/362] optimize sockAddrBelongsToNode and reduce connection filter usage --- libraries/networking/src/LimitedNodeList.cpp | 12 +++++++++++- libraries/networking/src/LimitedNodeList.h | 2 +- libraries/networking/src/udt/Socket.cpp | 10 +++++----- libraries/networking/src/udt/Socket.h | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index db6ed15792..7cae5d8a71 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -1187,12 +1187,22 @@ void LimitedNodeList::sendPeerQueryToIceServer(const HifiSockAddr& iceServerSock SharedNodePointer LimitedNodeList::findNodeWithAddr(const HifiSockAddr& addr) { QReadLocker locker(&_nodeMutex); - auto it = std::find_if(std::begin(_nodeHash), std::end(_nodeHash), [&](const UUIDNodePair& pair) { + auto it = std::find_if(std::begin(_nodeHash), std::end(_nodeHash), [&addr](const UUIDNodePair& pair) { return pair.second->getActiveSocket() ? (*pair.second->getActiveSocket() == addr) : false; }); return (it != std::end(_nodeHash)) ? it->second : SharedNodePointer(); } +bool LimitedNodeList::sockAddrBelongsToNode(const HifiSockAddr& sockAddr) { + QReadLocker locker(&_nodeMutex); + auto it = std::find_if(std::begin(_nodeHash), std::end(_nodeHash), [&sockAddr](const UUIDNodePair& pair) { + return pair.second->getPublicSocket() == sockAddr + || pair.second->getLocalSocket() == sockAddr + || pair.second->getSymmetricSocket() == sockAddr; + }); + return it != std::end(_nodeHash); +} + void LimitedNodeList::sendPacketToIceServer(PacketType packetType, const HifiSockAddr& iceServerSockAddr, const QUuid& clientID, const QUuid& peerID) { auto icePacket = NLPacket::create(packetType); diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index cffc49521a..dacefa8e40 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -386,7 +386,7 @@ protected: void sendPacketToIceServer(PacketType packetType, const HifiSockAddr& iceServerSockAddr, const QUuid& clientID, const QUuid& peerRequestID = QUuid()); - bool sockAddrBelongsToNode(const HifiSockAddr& sockAddr) { return findNodeWithAddr(sockAddr) != SharedNodePointer(); } + bool sockAddrBelongsToNode(const HifiSockAddr& sockAddr); NodeHash _nodeHash; mutable QReadWriteLock _nodeMutex { QReadWriteLock::Recursive }; diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 6de43219e5..25e6fae023 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -228,13 +228,13 @@ qint64 Socket::writeDatagram(const QByteArray& datagram, const HifiSockAddr& soc return bytesWritten; } -Connection* Socket::findOrCreateConnection(const HifiSockAddr& sockAddr) { +Connection* Socket::findOrCreateConnection(const HifiSockAddr& sockAddr, bool filterCreate) { auto it = _connectionsHash.find(sockAddr); if (it == _connectionsHash.end()) { // we did not have a matching connection, time to see if we should make one - if (_connectionCreationFilterOperator && !_connectionCreationFilterOperator(sockAddr)) { + if (filterCreate && _connectionCreationFilterOperator && !_connectionCreationFilterOperator(sockAddr)) { // the connection creation filter did not allow us to create a new connection #ifdef UDT_CONNECTION_DEBUG qCDebug(networking) << "Socket::findOrCreateConnection refusing to create connection for" << sockAddr @@ -376,7 +376,7 @@ void Socket::readPendingDatagrams() { controlPacket->setReceiveTime(receiveTime); // move this control packet to the matching connection, if there is one - auto connection = findOrCreateConnection(senderSockAddr); + auto connection = findOrCreateConnection(senderSockAddr, true); if (connection) { connection->processControl(move(controlPacket)); @@ -394,7 +394,7 @@ void Socket::readPendingDatagrams() { if (!_packetFilterOperator || _packetFilterOperator(*packet)) { if (packet->isReliable()) { // if this was a reliable packet then signal the matching connection with the sequence number - auto connection = findOrCreateConnection(senderSockAddr); + auto connection = findOrCreateConnection(senderSockAddr, true); if (!connection || !connection->processReceivedSequenceNumber(packet->getSequenceNumber(), packet->getDataSize(), @@ -409,7 +409,7 @@ void Socket::readPendingDatagrams() { } if (packet->isPartOfMessage()) { - auto connection = findOrCreateConnection(senderSockAddr); + auto connection = findOrCreateConnection(senderSockAddr, true); if (connection) { connection->queueReceivedMessagePacket(std::move(packet)); } diff --git a/libraries/networking/src/udt/Socket.h b/libraries/networking/src/udt/Socket.h index 30058e1d23..99266e105e 100644 --- a/libraries/networking/src/udt/Socket.h +++ b/libraries/networking/src/udt/Socket.h @@ -109,7 +109,7 @@ private slots: private: void setSystemBufferSizes(); - Connection* findOrCreateConnection(const HifiSockAddr& sockAddr); + Connection* findOrCreateConnection(const HifiSockAddr& sockAddr, bool filterCreation = false); bool socketMatchesNodeOrDomain(const HifiSockAddr& sockAddr); // privatized methods used by UDTTest - they are private since they must be called on the Socket thread From 493262052cde39fd38090960d9332d05f6602504 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 19 Oct 2018 11:32:31 -0700 Subject: [PATCH 174/362] Remove extraneous code changes; remove QUrlAncestry code; remove extra logging --- assignment-client/src/Agent.cpp | 2 +- assignment-client/src/AssignmentClient.cpp | 11 +- interface/src/Application.cpp | 7 +- interface/src/assets/ATPAssetMigrator.cpp | 14 +- .../scripting/ClipboardScriptingInterface.h | 2 +- interface/src/ui/overlays/Web3DOverlay.cpp | 5 +- libraries/avatars/src/AvatarData.cpp | 18 +- libraries/entities/src/EntityEditFilters.cpp | 34 ++-- libraries/entities/src/EntityTree.cpp | 16 +- libraries/entities/src/EntityTree.h | 10 +- libraries/fbx/src/GLTFReader.cpp | 190 +++++++++--------- libraries/fbx/src/OBJReader.cpp | 26 +-- .../networking/src/AssetResourceRequest.cpp | 4 +- .../networking/src/FileResourceRequest.h | 8 +- .../networking/src/HTTPResourceRequest.h | 7 +- .../networking/src/NetworkAccessManager.cpp | 2 +- libraries/networking/src/ResourceCache.cpp | 19 +- libraries/networking/src/ResourceManager.cpp | 3 - libraries/networking/src/ResourceRequest.cpp | 3 +- libraries/networking/src/ResourceRequest.h | 11 +- libraries/octree/src/Octree.cpp | 21 +- libraries/octree/src/Octree.h | 11 +- libraries/qml/src/qml/OffscreenSurface.cpp | 5 - .../src/FileScriptingInterface.cpp | 4 +- .../script-engine/src/XMLHttpRequestClass.cpp | 4 +- .../EntityItemWeakPointerWithUrlAncestry.h | 31 --- libraries/shared/src/QUrlAncestry.cpp | 35 ---- libraries/shared/src/QUrlAncestry.h | 32 --- .../shared/src/ResourceRequestObserver.cpp | 16 +- .../shared/src/ResourceRequestObserver.h | 4 +- 30 files changed, 210 insertions(+), 345 deletions(-) delete mode 100644 libraries/shared/src/EntityItemWeakPointerWithUrlAncestry.h delete mode 100644 libraries/shared/src/QUrlAncestry.cpp delete mode 100644 libraries/shared/src/QUrlAncestry.h diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 1561af4d25..4490474599 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -895,7 +895,7 @@ void Agent::aboutToFinish() { { DependencyManager::get()->shutdownScripting(); } - + DependencyManager::destroy(); DependencyManager::destroy(); diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index 06b3f4da86..76ff5ab2ed 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -37,7 +37,6 @@ #include "AssignmentFactory.h" #include "ResourceRequestObserver.h" - const QString ASSIGNMENT_CLIENT_TARGET_NAME = "assignment-client"; const long long ASSIGNMENT_REQUEST_INTERVAL_MSECS = 1 * 1000; @@ -162,7 +161,7 @@ void AssignmentClient::setUpStatusToMonitor() { void AssignmentClient::sendStatusPacketToACM() { // tell the assignment client monitor what this assignment client is doing (if anything) auto nodeList = DependencyManager::get(); - + quint8 assignmentType = Assignment::Type::AllTypes; if (_currentAssignment) { @@ -173,7 +172,7 @@ void AssignmentClient::sendStatusPacketToACM() { statusPacket->write(_childAssignmentUUID.toRfc4122()); statusPacket->writePrimitive(assignmentType); - + nodeList->sendPacket(std::move(statusPacket), _assignmentClientMonitorSocket); } @@ -259,10 +258,10 @@ void AssignmentClient::handleCreateAssignmentPacket(QSharedPointer message) { const HifiSockAddr& senderSockAddr = message->getSenderSockAddr(); - + if (senderSockAddr.getAddress() == QHostAddress::LocalHost || senderSockAddr.getAddress() == QHostAddress::LocalHostIPv6) { - + qCDebug(assignment_client) << "AssignmentClientMonitor at" << senderSockAddr << "requested stop via PacketType::StopNode."; QCoreApplication::quit(); } else { @@ -315,6 +314,6 @@ void AssignmentClient::assignmentCompleted() { nodeList->setOwnerType(NodeType::Unassigned); nodeList->reset(); nodeList->resetNodeInterestSet(); - + _isAssigned = false; } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 242445b0fe..e515a22403 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -170,7 +170,6 @@ #include "ModelPackager.h" #include "scripting/Audio.h" #include "networking/CloseEventSender.h" -#include "QUrlAncestry.h" #include "scripting/TestScriptingInterface.h" #include "scripting/AssetMappingsScriptingInterface.h" #include "scripting/ClipboardScriptingInterface.h" @@ -947,6 +946,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); @@ -1783,7 +1783,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo updateHeartbeat(); QTimer* settingsTimer = new QTimer(); moveToNewNamedThread(settingsTimer, "Settings Thread", [this, settingsTimer]{ - // This needs to run on the settings thread, so we need to pass the `settingsTimer` as the + // This needs to run on the settings thread, so we need to pass the `settingsTimer` as the // receiver object, otherwise it will run on the application thread and trigger a warning // about trying to kill the timer on the main thread. connect(qApp, &Application::beforeAboutToQuit, settingsTimer, [this, settingsTimer]{ @@ -5029,7 +5029,8 @@ bool Application::importEntities(const QString& urlOrFilename, const bool isObse bool success = false; _entityClipboard->withWriteLock([&] { _entityClipboard->eraseAllOctreeElements(); - success = _entityClipboard->readFromURL(urlOrFilename, isObservable, callerId, QUrlAncestry()); + + success = _entityClipboard->readFromURL(urlOrFilename, isObservable, callerId); if (success) { _entityClipboard->reaverageOctreeElements(); } diff --git a/interface/src/assets/ATPAssetMigrator.cpp b/interface/src/assets/ATPAssetMigrator.cpp index 6912c69db8..be7f2014cc 100644 --- a/interface/src/assets/ATPAssetMigrator.cpp +++ b/interface/src/assets/ATPAssetMigrator.cpp @@ -203,7 +203,7 @@ void ATPAssetMigrator::loadEntityServerFile() { void ATPAssetMigrator::migrateResource(ResourceRequest* request) { // use an asset client to upload the asset auto assetClient = DependencyManager::get(); - + auto upload = assetClient->createUpload(request->getData()); // add this URL to our hash of AssetUpload to original URL @@ -243,7 +243,7 @@ void ATPAssetMigrator::assetUploadFinished(AssetUpload *upload, const QString& h } checkIfFinished(); - + upload->deleteLater(); } @@ -299,24 +299,24 @@ void ATPAssetMigrator::checkIfFinished() { bool ATPAssetMigrator::wantsToMigrateResource(const QUrl& url) { static bool hasAskedForCompleteMigration { false }; static bool wantsCompleteMigration { false }; - + if (!hasAskedForCompleteMigration) { // this is the first resource migration - ask the user if they just want to migrate everything static const QString COMPLETE_MIGRATION_TEXT { "Do you want to migrate all assets found in this entity-server file?\n"\ "Select \"Yes\" to upload all discovered assets to the current asset-server immediately.\n"\ "Select \"No\" to be prompted for each discovered asset." }; - + auto button = OffscreenUi::question(_dialogParent, MESSAGE_BOX_TITLE, COMPLETE_MIGRATION_TEXT, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - + if (button == QMessageBox::Yes) { wantsCompleteMigration = true; } - + hasAskedForCompleteMigration = true; } - + if (wantsCompleteMigration) { return true; } else { diff --git a/interface/src/scripting/ClipboardScriptingInterface.h b/interface/src/scripting/ClipboardScriptingInterface.h index 535ccfd5ab..60b6ca2e03 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.h +++ b/interface/src/scripting/ClipboardScriptingInterface.h @@ -64,7 +64,7 @@ public: * @returns {boolean} true if the export was successful, otherwise false. */ Q_INVOKABLE bool exportEntities(const QString& filename, const QVector& entityIDs); - + /**jsdoc * Export the entities with centers within a cube to a JSON file. * @function Clipboard.exportEntities diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 53505c2013..084615cae2 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -61,7 +61,6 @@ #include "AboutUtil.h" #include "ResourceRequestObserver.h" - static int MAX_WINDOW_SIZE = 4096; static const float METERS_TO_INCHES = 39.3701f; static const float OPAQUE_ALPHA_THRESHOLD = 0.99f; @@ -539,7 +538,7 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) { * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. * * @property {string} name="" - A friendly name for the overlay. - * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and * start. * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a * parentID set, otherwise the same value as position. @@ -564,7 +563,7 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) { * @property {string} url - The URL of the Web page to display. * @property {string} scriptURL="" - The URL of a JavaScript file to inject into the Web page. * @property {number} dpi=30 - The dots per inch to display the Web page at, on the overlay. - * @property {Vec2} dimensions=1,1 - The size of the overlay to display the Web page on, in meters. Synonyms: + * @property {Vec2} dimensions=1,1 - The size of the overlay to display the Web page on, in meters. Synonyms: * scale, size. * @property {number} maxFPS=10 - The maximum update rate for the Web overlay content, in frames/second. * @property {boolean} showKeyboardFocusHighlight=true - If true, the Web overlay is highlighted when it has diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 753707f0fe..032ffe25f7 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -380,7 +380,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } else { AVATAR_MEMCPY(_globalPosition); } - + int numBytes = destinationBuffer - startSection; @@ -648,7 +648,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent if (!data.translationIsDefaultPose) { if (sendAll || last.translationIsDefaultPose || (!cullSmallChanges && last.translation != data.translation) || (cullSmallChanges && glm::distance(data.translation, lastSentJointData[i].translation) > minTranslation)) { - + validity |= (1 << validityBit); #ifdef WANT_DEBUG translationSentCount++; @@ -1055,7 +1055,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { auto newHasProceduralEyeFaceMovement = oneAtBit16(bitItems, PROCEDURAL_EYE_FACE_MOVEMENT); auto newHasProceduralBlinkFaceMovement = oneAtBit16(bitItems, PROCEDURAL_BLINK_FACE_MOVEMENT); - + bool keyStateChanged = (_keyState != newKeyState); bool handStateChanged = (_handState != newHandState); bool faceStateChanged = (_headData->_isFaceTrackerConnected != newFaceTrackerConnected); @@ -1527,7 +1527,7 @@ glm::vec3 AvatarData::getJointTranslation(int index) const { } glm::vec3 AvatarData::getJointTranslation(const QString& name) const { - // Can't do this, because the lock needs to cover the entire set of logic. In theory, the joints could change + // Can't do this, because the lock needs to cover the entire set of logic. In theory, the joints could change // on another thread in between the call to getJointIndex and getJointTranslation // return getJointTranslation(getJointIndex(name)); return readLockWithNamedJointIndex(name, [this](int index) { @@ -1608,7 +1608,7 @@ bool AvatarData::isJointDataValid(const QString& name) const { // return isJointDataValid(getJointIndex(name)); return readLockWithNamedJointIndex(name, false, [&](int index) { - // This is technically superfluous.... the lambda is only called if index is a valid + // This is technically superfluous.... the lambda is only called if index is a valid // offset for _jointData. Nevertheless, it would be confusing to leave the lamdba as // `return true` return index < _jointData.size(); @@ -1827,7 +1827,7 @@ qint64 AvatarData::packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { bytesWritten += destination.writePrimitive(traitVersion); } - + AvatarTraits::TraitWireSize encodedURLSize = encodedSkeletonURL.size(); bytesWritten += destination.writePrimitive(encodedURLSize); @@ -1936,7 +1936,7 @@ void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { if (expanded == _skeletonModelURL) { return; } - + _skeletonModelURL = expanded; qCDebug(avatars) << "Changing skeleton model for avatar" << getSessionUUID() << "to" << _skeletonModelURL.toString(); @@ -2163,7 +2163,7 @@ void AvatarData::updateJointMappings() { if (_skeletonModelURL.fileName().toLower().endsWith(".fst")) { //// - // TODO: Should we rely upon HTTPResourceRequest instead? + // TODO: Should we rely upon HTTPResourceRequest for ResourceRequestObserver instead? // HTTPResourceRequest::doSend() covers all of the following and // then some. It doesn't cover the connect() call, so we may // want to add a HTTPResourceRequest::doSend() method that does @@ -2402,7 +2402,7 @@ QJsonObject AvatarData::toJson() const { for (auto entityID : _avatarEntityData.keys()) { QVariantMap entityData; QUuid newId = _avatarEntityForRecording.size() == _avatarEntityData.size() ? _avatarEntityForRecording.values()[entityCount++] : entityID; - entityData.insert("id", newId); + entityData.insert("id", newId); entityData.insert("properties", _avatarEntityData.value(entityID).toBase64()); avatarEntityJson.push_back(QVariant(entityData).toJsonObject()); } diff --git a/libraries/entities/src/EntityEditFilters.cpp b/libraries/entities/src/EntityEditFilters.cpp index c88ae138bf..4865c0ba1e 100644 --- a/libraries/entities/src/EntityEditFilters.cpp +++ b/libraries/entities/src/EntityEditFilters.cpp @@ -24,7 +24,7 @@ QList EntityEditFilters::getZonesByPosition(glm::vec3& position) { for (auto id : zoneIDs) { if (!id.isInvalidID()) { // for now, look it up in the tree (soon we need to cache or similar?) - EntityItemPointer itemPtr = _tree->findEntityByEntityItemID(id); + EntityItemPointer itemPtr = _tree->findEntityByEntityItemID(id); auto zone = std::dynamic_pointer_cast(itemPtr); if (!zone) { // TODO: maybe remove later? @@ -33,7 +33,7 @@ QList EntityEditFilters::getZonesByPosition(glm::vec3& position) { zones.append(id); } } else { - // the null id is the global filter we put in the domain server's + // the null id is the global filter we put in the domain server's // advanced entity server settings zones.append(id); } @@ -43,7 +43,7 @@ QList EntityEditFilters::getZonesByPosition(glm::vec3& position) { bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, EntityTree::FilterType filterType, EntityItemID& itemID, EntityItemPointer& existingEntity) { - + // get the ids of all the zones (plus the global entity edit filter) that the position // lies within auto zoneIDs = getZonesByPosition(position); @@ -51,12 +51,12 @@ bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& proper if (!itemID.isInvalidID() && id == itemID) { continue; } - - // get the filter pair, etc... + + // get the filter pair, etc... _lock.lockForRead(); FilterData filterData = _filterDataMap.value(id); _lock.unlock(); - + if (filterData.valid()) { if (filterData.rejectAll) { return false; @@ -153,13 +153,13 @@ bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& proper // otherwise, assume it wants to pass all properties propertiesOut = propertiesIn; wasChanged = false; - + } else { return false; } } } - // if we made it here, + // if we made it here, return true; } @@ -175,23 +175,23 @@ void EntityEditFilters::removeFilter(EntityItemID entityID) { void EntityEditFilters::addFilter(EntityItemID entityID, QString filterURL) { QUrl scriptURL(filterURL); - - // setting it to an empty string is same as removing + + // setting it to an empty string is same as removing if (filterURL.size() == 0) { removeFilter(entityID); return; } - + // The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp) if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == URL_SCHEME_FILE)) { qWarning() << "Cannot load script from local filesystem, because assignment may be on a different computer."; scriptRequestFinished(entityID); return; } - + // first remove any existing info for this entity removeFilter(entityID); - + // reject all edits until we load the script FilterData filterData; filterData.rejectAll = true; @@ -199,7 +199,7 @@ void EntityEditFilters::addFilter(EntityItemID entityID, QString filterURL) { _lock.lockForWrite(); _filterDataMap.insert(entityID, filterData); _lock.unlock(); - + auto scriptRequest = DependencyManager::get()->createResourceRequest( this, scriptURL, true, -1, "EntityEditFilters::addFilter"); if (!scriptRequest) { @@ -265,7 +265,7 @@ void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) { FilterData filterData; filterData.engine = engine; filterData.rejectAll = false; - + // define the uncaughtException function QScriptEngine& engineRef = *engine; filterData.uncaughtExceptions = [&engineRef, urlString]() { return hadUncaughtExceptions(engineRef, urlString); }; @@ -369,11 +369,11 @@ void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) { _lock.unlock(); qDebug() << "script request filter processed for entity id " << entityID; - + emit filterAdded(entityID, true); return; } - } + } } else if (scriptRequest) { const QString urlString = scriptRequest->getUrl().toString(); qCritical() << "Failed to download script at" << urlString; diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 8992157681..0b3b8abba2 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -38,8 +38,6 @@ #include "LogHandler.h" #include "EntityEditFilters.h" #include "EntityDynamicFactoryInterface.h" -#include "QUrlAncestry.h" - static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50; const float EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME = 60 * 60; // 1 hour @@ -100,7 +98,7 @@ EntityTree::~EntityTree() { eraseAllOctreeElements(false); } -void EntityTree::setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist) { +void EntityTree::setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist) { _entityScriptSourceWhitelist = entityScriptSourceWhitelist.split(',', QString::SkipEmptyParts); } @@ -862,7 +860,7 @@ float findRayIntersectionSortingOp(const OctreeElementPointer& element, void* ex EntityItemID EntityTree::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, QVector entityIdsToInclude, QVector entityIdsToDiscard, - bool visibleOnly, bool collidableOnly, bool precisionPicking, + bool visibleOnly, bool collidableOnly, bool precisionPicking, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo, Octree::lockType lockType, bool* accurateResult) { @@ -1353,7 +1351,7 @@ bool EntityTree::verifyNonce(const QString& certID, const QString& nonce, Entity key = sent.second; } - QString annotatedKey = "-----BEGIN PUBLIC KEY-----\n" + key.insert(64, "\n") + "\n-----END PUBLIC KEY-----\n"; + QString annotatedKey = "-----BEGIN PUBLIC KEY-----\n" + key.insert(64, "\n") + "\n-----END PUBLIC KEY-----\n"; QByteArray hashedActualNonce = QCryptographicHash::hash(QByteArray(actualNonce.toUtf8()), QCryptographicHash::Sha256); bool verificationSuccess = EntityItemProperties::verifySignature(annotatedKey.toUtf8(), hashedActualNonce, QByteArray::fromBase64(nonce.toUtf8())); @@ -1797,7 +1795,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c if (newEntity) { newEntity->markAsChangedOnServer(); notifyNewlyCreatedEntity(*newEntity, senderNode); - + startLogging = usecTimestampNow(); if (wantEditLogging()) { qCDebug(entities) << "User [" << senderNode->getUUID() << "] added entity. ID:" @@ -1822,7 +1820,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } } else { HIFI_FCDEBUG(entities(), "Edit failed. [" << message.getType() <<"] " << - "entity id:" << entityItemID << + "entity id:" << entityItemID << "existingEntity pointer:" << existingEntity.get()); } } @@ -2043,7 +2041,7 @@ bool EntityTree::hasEntitiesDeletedSince(quint64 sinceTime) { if (hasSomethingNewer) { int elapsed = usecTimestampNow() - considerEntitiesSince; int difference = considerEntitiesSince - sinceTime; - qCDebug(entities) << "EntityTree::hasEntitiesDeletedSince() sinceTime:" << sinceTime + qCDebug(entities) << "EntityTree::hasEntitiesDeletedSince() sinceTime:" << sinceTime << "considerEntitiesSince:" << considerEntitiesSince << "elapsed:" << elapsed << "difference:" << difference; } #endif @@ -2495,7 +2493,7 @@ bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer return true; } -bool EntityTree::readFromMap(QVariantMap& map, const QUrlAncestry& ancestry) { +bool EntityTree::readFromMap(QVariantMap& map) { // These are needed to deal with older content (before adding inheritance modes) int contentVersion = map["Version"].toInt(); bool needsConversion = (contentVersion < (int)EntityVersion::ZoneLightInheritModes); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 8c787f8eb8..2f971b8566 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -22,8 +22,6 @@ #include "EntityTreeElement.h" #include "DeleteEntityOperator.h" #include "MovingEntitiesOperator.h" -#include "QUrlAncestry.h" - class EntityTree; using EntityTreePointer = std::shared_ptr; @@ -96,7 +94,7 @@ public: virtual EntityItemID findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, QVector entityIdsToInclude, QVector entityIdsToDiscard, - bool visibleOnly, bool collidableOnly, bool precisionPicking, + bool visibleOnly, bool collidableOnly, bool precisionPicking, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo, Octree::lockType lockType = Octree::TryLock, bool* accurateResult = NULL); @@ -172,7 +170,7 @@ public: void addNewlyCreatedHook(NewlyCreatedEntityHook* hook); void removeNewlyCreatedHook(NewlyCreatedEntityHook* hook); - bool hasAnyDeletedEntities() const { + bool hasAnyDeletedEntities() const { QReadLocker locker(&_recentlyDeletedEntitiesLock); return _recentlyDeletedEntityItemIDs.size() > 0; } @@ -180,7 +178,7 @@ public: bool hasEntitiesDeletedSince(quint64 sinceTime); static quint64 getAdjustedConsiderSince(quint64 sinceTime); - QMultiMap getRecentlyDeletedEntityIDs() const { + QMultiMap getRecentlyDeletedEntityIDs() const { QReadLocker locker(&_recentlyDeletedEntitiesLock); return _recentlyDeletedEntityItemIDs; } @@ -225,7 +223,7 @@ public: virtual bool writeToMap(QVariantMap& entityDescription, OctreeElementPointer element, bool skipDefaultValues, bool skipThoseWithBadParents) override; - virtual bool readFromMap(QVariantMap& entityDescription, const QUrlAncestry& ancestry = {}) override; + virtual bool readFromMap(QVariantMap& entityDescription) override; glm::vec3 getContentsDimensions(); float getContentsLargestDimension(); diff --git a/libraries/fbx/src/GLTFReader.cpp b/libraries/fbx/src/GLTFReader.cpp index 89592c399c..b93dc3541b 100644 --- a/libraries/fbx/src/GLTFReader.cpp +++ b/libraries/fbx/src/GLTFReader.cpp @@ -40,7 +40,7 @@ GLTFReader::GLTFReader() { } -bool GLTFReader::getStringVal(const QJsonObject& object, const QString& fieldname, +bool GLTFReader::getStringVal(const QJsonObject& object, const QString& fieldname, QString& value, QMap& defined) { bool _defined = (object.contains(fieldname) && object[fieldname].isString()); if (_defined) { @@ -60,7 +60,7 @@ bool GLTFReader::getBoolVal(const QJsonObject& object, const QString& fieldname, return _defined; } -bool GLTFReader::getIntVal(const QJsonObject& object, const QString& fieldname, +bool GLTFReader::getIntVal(const QJsonObject& object, const QString& fieldname, int& value, QMap& defined) { bool _defined = (object.contains(fieldname) && !object[fieldname].isNull()); if (_defined) { @@ -70,7 +70,7 @@ bool GLTFReader::getIntVal(const QJsonObject& object, const QString& fieldname, return _defined; } -bool GLTFReader::getDoubleVal(const QJsonObject& object, const QString& fieldname, +bool GLTFReader::getDoubleVal(const QJsonObject& object, const QString& fieldname, double& value, QMap& defined) { bool _defined = (object.contains(fieldname) && object[fieldname].isDouble()); if (_defined) { @@ -79,7 +79,7 @@ bool GLTFReader::getDoubleVal(const QJsonObject& object, const QString& fieldnam defined.insert(fieldname, _defined); return _defined; } -bool GLTFReader::getObjectVal(const QJsonObject& object, const QString& fieldname, +bool GLTFReader::getObjectVal(const QJsonObject& object, const QString& fieldname, QJsonObject& value, QMap& defined) { bool _defined = (object.contains(fieldname) && object[fieldname].isObject()); if (_defined) { @@ -89,7 +89,7 @@ bool GLTFReader::getObjectVal(const QJsonObject& object, const QString& fieldnam return _defined; } -bool GLTFReader::getIntArrayVal(const QJsonObject& object, const QString& fieldname, +bool GLTFReader::getIntArrayVal(const QJsonObject& object, const QString& fieldname, QVector& values, QMap& defined) { bool _defined = (object.contains(fieldname) && object[fieldname].isArray()); if (_defined) { @@ -104,7 +104,7 @@ bool GLTFReader::getIntArrayVal(const QJsonObject& object, const QString& fieldn return _defined; } -bool GLTFReader::getDoubleArrayVal(const QJsonObject& object, const QString& fieldname, +bool GLTFReader::getDoubleArrayVal(const QJsonObject& object, const QString& fieldname, QVector& values, QMap& defined) { bool _defined = (object.contains(fieldname) && object[fieldname].isArray()); if (_defined) { @@ -119,7 +119,7 @@ bool GLTFReader::getDoubleArrayVal(const QJsonObject& object, const QString& fie return _defined; } -bool GLTFReader::getObjectArrayVal(const QJsonObject& object, const QString& fieldname, +bool GLTFReader::getObjectArrayVal(const QJsonObject& object, const QString& fieldname, QJsonArray& objects, QMap& defined) { bool _defined = (object.contains(fieldname) && object[fieldname].isArray()); if (_defined) { @@ -229,7 +229,7 @@ bool GLTFReader::setAsset(const QJsonObject& object) { QJsonObject jsAsset; bool isAssetDefined = getObjectVal(object, "asset", jsAsset, _file.defined); if (isAssetDefined) { - if (!getStringVal(jsAsset, "version", _file.asset.version, + if (!getStringVal(jsAsset, "version", _file.asset.version, _file.asset.defined) || _file.asset.version != "2.0") { return false; } @@ -241,7 +241,7 @@ bool GLTFReader::setAsset(const QJsonObject& object) { bool GLTFReader::addAccessor(const QJsonObject& object) { GLTFAccessor accessor; - + getIntVal(object, "bufferView", accessor.bufferView, accessor.defined); getIntVal(object, "byteOffset", accessor.byteOffset, accessor.defined); getIntVal(object, "componentType", accessor.componentType, accessor.defined); @@ -261,7 +261,7 @@ bool GLTFReader::addAccessor(const QJsonObject& object) { bool GLTFReader::addAnimation(const QJsonObject& object) { GLTFAnimation animation; - + QJsonArray channels; if (getObjectArrayVal(object, "channels", channels, animation.defined)) { foreach(const QJsonValue & v, channels) { @@ -272,7 +272,7 @@ bool GLTFReader::addAnimation(const QJsonObject& object) { if (getObjectVal(v.toObject(), "target", jsChannel, channel.defined)) { getIntVal(jsChannel, "node", channel.target.node, channel.target.defined); getIntVal(jsChannel, "path", channel.target.path, channel.target.defined); - } + } } } } @@ -291,7 +291,7 @@ bool GLTFReader::addAnimation(const QJsonObject& object) { } } } - + _file.animations.push_back(animation); return true; @@ -299,20 +299,20 @@ bool GLTFReader::addAnimation(const QJsonObject& object) { bool GLTFReader::addBufferView(const QJsonObject& object) { GLTFBufferView bufferview; - + getIntVal(object, "buffer", bufferview.buffer, bufferview.defined); getIntVal(object, "byteLength", bufferview.byteLength, bufferview.defined); getIntVal(object, "byteOffset", bufferview.byteOffset, bufferview.defined); getIntVal(object, "target", bufferview.target, bufferview.defined); - + _file.bufferviews.push_back(bufferview); - + return true; } bool GLTFReader::addBuffer(const QJsonObject& object) { GLTFBuffer buffer; - + getIntVal(object, "byteLength", buffer.byteLength, buffer.defined); if (getStringVal(object, "uri", buffer.uri, buffer.defined)) { if (!readBinary(buffer.uri, buffer.blob)) { @@ -320,13 +320,13 @@ bool GLTFReader::addBuffer(const QJsonObject& object) { } } _file.buffers.push_back(buffer); - + return true; } bool GLTFReader::addCamera(const QJsonObject& object) { GLTFCamera camera; - + QJsonObject jsPerspective; QJsonObject jsOrthographic; QString type; @@ -346,28 +346,28 @@ bool GLTFReader::addCamera(const QJsonObject& object) { } else if (getStringVal(object, "type", type, camera.defined)) { camera.type = getCameraType(type); } - + _file.cameras.push_back(camera); - + return true; } bool GLTFReader::addImage(const QJsonObject& object) { GLTFImage image; - + QString mime; getStringVal(object, "uri", image.uri, image.defined); if (getStringVal(object, "mimeType", mime, image.defined)) { image.mimeType = getImageMimeType(mime); } getIntVal(object, "bufferView", image.bufferView, image.defined); - + _file.images.push_back(image); return true; } -bool GLTFReader::getIndexFromObject(const QJsonObject& object, const QString& field, +bool GLTFReader::getIndexFromObject(const QJsonObject& object, const QString& field, int& outidx, QMap& defined) { QJsonObject subobject; if (getObjectVal(object, field, subobject, defined)) { @@ -393,20 +393,20 @@ bool GLTFReader::addMaterial(const QJsonObject& object) { getDoubleVal(object, "alphaCutoff", material.alphaCutoff, material.defined); QJsonObject jsMetallicRoughness; if (getObjectVal(object, "pbrMetallicRoughness", jsMetallicRoughness, material.defined)) { - getDoubleArrayVal(jsMetallicRoughness, "baseColorFactor", - material.pbrMetallicRoughness.baseColorFactor, + getDoubleArrayVal(jsMetallicRoughness, "baseColorFactor", + material.pbrMetallicRoughness.baseColorFactor, material.pbrMetallicRoughness.defined); - getIndexFromObject(jsMetallicRoughness, "baseColorTexture", - material.pbrMetallicRoughness.baseColorTexture, + getIndexFromObject(jsMetallicRoughness, "baseColorTexture", + material.pbrMetallicRoughness.baseColorTexture, material.pbrMetallicRoughness.defined); - getDoubleVal(jsMetallicRoughness, "metallicFactor", - material.pbrMetallicRoughness.metallicFactor, + getDoubleVal(jsMetallicRoughness, "metallicFactor", + material.pbrMetallicRoughness.metallicFactor, material.pbrMetallicRoughness.defined); - getDoubleVal(jsMetallicRoughness, "roughnessFactor", - material.pbrMetallicRoughness.roughnessFactor, + getDoubleVal(jsMetallicRoughness, "roughnessFactor", + material.pbrMetallicRoughness.roughnessFactor, material.pbrMetallicRoughness.defined); - getIndexFromObject(jsMetallicRoughness, "metallicRoughnessTexture", - material.pbrMetallicRoughness.metallicRoughnessTexture, + getIndexFromObject(jsMetallicRoughness, "metallicRoughnessTexture", + material.pbrMetallicRoughness.metallicRoughnessTexture, material.pbrMetallicRoughness.defined); } _file.materials.push_back(material); @@ -428,7 +428,7 @@ bool GLTFReader::addMesh(const QJsonObject& object) { getIntVal(jsPrimitive, "mode", primitive.mode, primitive.defined); getIntVal(jsPrimitive, "indices", primitive.indices, primitive.defined); getIntVal(jsPrimitive, "material", primitive.material, primitive.defined); - + QJsonObject jsAttributes; if (getObjectVal(jsPrimitive, "attributes", jsAttributes, primitive.defined)) { QStringList attrKeys = jsAttributes.keys(); @@ -455,7 +455,7 @@ bool GLTFReader::addMesh(const QJsonObject& object) { primitive.targets.push_back(target); } } - } + } mesh.primitives.push_back(primitive); } } @@ -469,7 +469,7 @@ bool GLTFReader::addMesh(const QJsonObject& object) { bool GLTFReader::addNode(const QJsonObject& object) { GLTFNode node; - + getStringVal(object, "name", node.name, node.defined); getIntVal(object, "camera", node.camera, node.defined); getIntVal(object, "mesh", node.mesh, node.defined); @@ -524,10 +524,10 @@ bool GLTFReader::addSkin(const QJsonObject& object) { } bool GLTFReader::addTexture(const QJsonObject& object) { - GLTFTexture texture; + GLTFTexture texture; getIntVal(object, "sampler", texture.sampler, texture.defined); getIntVal(object, "source", texture.source, texture.defined); - + _file.textures.push_back(texture); return true; @@ -535,7 +535,7 @@ bool GLTFReader::addTexture(const QJsonObject& object) { bool GLTFReader::parseGLTF(const QByteArray& model) { PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr); - + QJsonDocument d = QJsonDocument::fromJson(model); QJsonObject jsFile = d.object(); @@ -707,25 +707,25 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { foreach(int child, node.children) nodeDependencies[child].push_back(nodecount); nodecount++; } - + nodecount = 0; foreach(auto &node, _file.nodes) { // collect node transform - _file.nodes[nodecount].transforms.push_back(getModelTransform(node)); + _file.nodes[nodecount].transforms.push_back(getModelTransform(node)); if (nodeDependencies[nodecount].size() == 1) { int parentidx = nodeDependencies[nodecount][0]; while (true) { // iterate parents // collect parents transforms - _file.nodes[nodecount].transforms.push_back(getModelTransform(_file.nodes[parentidx])); + _file.nodes[nodecount].transforms.push_back(getModelTransform(_file.nodes[parentidx])); if (nodeDependencies[parentidx].size() == 1) { parentidx = nodeDependencies[parentidx][0]; } else break; } } - + nodecount++; } - + //Build default joints geometry.joints.resize(1); geometry.joints[0].isFree = false; @@ -756,7 +756,7 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { setFBXMaterial(fbxMaterial, _file.materials[i]); } - + nodecount = 0; // Build meshes @@ -789,11 +789,11 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { QVector raw_vertices; QVector raw_normals; - bool success = addArrayOfType(indicesBuffer.blob, - indicesBufferview.byteOffset + indicesAccBoffset, - indicesAccessor.count, - part.triangleIndices, - indicesAccessor.type, + bool success = addArrayOfType(indicesBuffer.blob, + indicesBufferview.byteOffset + indicesAccBoffset, + indicesAccessor.count, + part.triangleIndices, + indicesAccessor.type, indicesAccessor.componentType); if (!success) { @@ -813,10 +813,10 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { int accBoffset = accessor.defined["byteOffset"] ? accessor.byteOffset : 0; if (key == "POSITION") { QVector vertices; - success = addArrayOfType(buffer.blob, - bufferview.byteOffset + accBoffset, - accessor.count, vertices, - accessor.type, + success = addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, vertices, + accessor.type, accessor.componentType); if (!success) { qWarning(modelformat) << "There was a problem reading glTF POSITION data for model " << _url; @@ -827,11 +827,11 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { } } else if (key == "NORMAL") { QVector normals; - success = addArrayOfType(buffer.blob, - bufferview.byteOffset + accBoffset, - accessor.count, - normals, - accessor.type, + success = addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, + normals, + accessor.type, accessor.componentType); if (!success) { qWarning(modelformat) << "There was a problem reading glTF NORMAL data for model " << _url; @@ -842,11 +842,11 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { } } else if (key == "TEXCOORD_0") { QVector texcoords; - success = addArrayOfType(buffer.blob, - bufferview.byteOffset + accBoffset, - accessor.count, - texcoords, - accessor.type, + success = addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, + texcoords, + accessor.type, accessor.componentType); if (!success) { qWarning(modelformat) << "There was a problem reading glTF TEXCOORD_0 data for model " << _url; @@ -857,11 +857,11 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { } } else if (key == "TEXCOORD_1") { QVector texcoords; - success = addArrayOfType(buffer.blob, - bufferview.byteOffset + accBoffset, - accessor.count, - texcoords, - accessor.type, + success = addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, + texcoords, + accessor.type, accessor.componentType); if (!success) { qWarning(modelformat) << "There was a problem reading glTF TEXCOORD_1 data for model " << _url; @@ -888,8 +888,8 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { mesh.meshExtents.addPoint(vertex); geometry.meshExtents.addPoint(vertex); } - - // since mesh.modelTransform seems to not have any effect I apply the transformation the model + + // since mesh.modelTransform seems to not have any effect I apply the transformation the model for (int h = 0; h < mesh.vertices.size(); h++) { glm::vec4 ver = glm::vec4(mesh.vertices[h], 1); if (node.transforms.size() > 0) { @@ -901,18 +901,18 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { mesh.meshIndex = geometry.meshes.size(); FBXReader::buildModelMesh(mesh, url.toString()); } - + } nodecount++; } - + return true; } -FBXGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping, +FBXGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping, const QUrl& url, bool loadLightmaps, float lightmapLevel) { - + _url = url; // Normalize url for local files @@ -928,10 +928,10 @@ FBXGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping FBXGeometry& geometry = *geometryPtr; buildGeometry(geometry, url); - + //fbxDebugDump(geometry); return geometryPtr; - + } bool GLTFReader::readBinary(const QString& url, QByteArray& outdata) { @@ -940,7 +940,7 @@ bool GLTFReader::readBinary(const QString& url, QByteArray& outdata) { qCDebug(modelformat) << "binaryUrl: " << binaryUrl << " OriginalUrl: " << _url; bool success; std::tie(success, outdata) = requestData(binaryUrl); - + return success; } @@ -1000,7 +1000,7 @@ QNetworkReply* GLTFReader::request(QUrl& url, bool isTest) { FBXTexture GLTFReader::getFBXTexture(const GLTFTexture& texture) { FBXTexture fbxtex = FBXTexture(); fbxtex.texcoordSet = 0; - + if (texture.defined["source"]) { QString url = _file.images[texture.source].uri; QString fname = QUrl(url).fileName(); @@ -1020,10 +1020,10 @@ void GLTFReader::setFBXMaterial(FBXMaterial& fbxmat, const GLTFMaterial& materia if (material.defined["name"]) { fbxmat.name = fbxmat.materialID = material.name; } - + if (material.defined["emissiveFactor"] && material.emissiveFactor.size() == 3) { - glm::vec3 emissive = glm::vec3(material.emissiveFactor[0], - material.emissiveFactor[1], + glm::vec3 emissive = glm::vec3(material.emissiveFactor[0], + material.emissiveFactor[1], material.emissiveFactor[2]); fbxmat._material->setEmissive(emissive); } @@ -1032,12 +1032,12 @@ void GLTFReader::setFBXMaterial(FBXMaterial& fbxmat, const GLTFMaterial& materia fbxmat.emissiveTexture = getFBXTexture(_file.textures[material.emissiveTexture]); fbxmat.useEmissiveMap = true; } - + if (material.defined["normalTexture"]) { fbxmat.normalTexture = getFBXTexture(_file.textures[material.normalTexture]); fbxmat.useNormalMap = true; } - + if (material.defined["occlusionTexture"]) { fbxmat.occlusionTexture = getFBXTexture(_file.textures[material.occlusionTexture]); fbxmat.useOcclusionMap = true; @@ -1045,7 +1045,7 @@ void GLTFReader::setFBXMaterial(FBXMaterial& fbxmat, const GLTFMaterial& materia if (material.defined["pbrMetallicRoughness"]) { fbxmat.isPBSMaterial = true; - + if (material.pbrMetallicRoughness.defined["metallicFactor"]) { fbxmat.metallic = material.pbrMetallicRoughness.metallicFactor; } @@ -1063,23 +1063,23 @@ void GLTFReader::setFBXMaterial(FBXMaterial& fbxmat, const GLTFMaterial& materia if (material.pbrMetallicRoughness.defined["roughnessFactor"]) { fbxmat._material->setRoughness(material.pbrMetallicRoughness.roughnessFactor); } - if (material.pbrMetallicRoughness.defined["baseColorFactor"] && + if (material.pbrMetallicRoughness.defined["baseColorFactor"] && material.pbrMetallicRoughness.baseColorFactor.size() == 4) { - glm::vec3 dcolor = glm::vec3(material.pbrMetallicRoughness.baseColorFactor[0], - material.pbrMetallicRoughness.baseColorFactor[1], + glm::vec3 dcolor = glm::vec3(material.pbrMetallicRoughness.baseColorFactor[0], + material.pbrMetallicRoughness.baseColorFactor[1], material.pbrMetallicRoughness.baseColorFactor[2]); fbxmat.diffuseColor = dcolor; fbxmat._material->setAlbedo(dcolor); fbxmat._material->setOpacity(material.pbrMetallicRoughness.baseColorFactor[3]); - } + } } } template -bool GLTFReader::readArray(const QByteArray& bin, int byteOffset, int count, +bool GLTFReader::readArray(const QByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType) { - + QDataStream blobstream(bin); blobstream.setByteOrder(QDataStream::LittleEndian); blobstream.setVersion(QDataStream::Qt_5_9); @@ -1134,9 +1134,9 @@ bool GLTFReader::readArray(const QByteArray& bin, int byteOffset, int count, return true; } template -bool GLTFReader::addArrayOfType(const QByteArray& bin, int byteOffset, int count, +bool GLTFReader::addArrayOfType(const QByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType, int componentType) { - + switch (componentType) { case GLTFAccessorComponentType::BYTE: {} case GLTFAccessorComponentType::UNSIGNED_BYTE: { @@ -1158,8 +1158,8 @@ bool GLTFReader::addArrayOfType(const QByteArray& bin, int byteOffset, int count return false; } -void GLTFReader::retriangulate(const QVector& inIndices, const QVector& in_vertices, - const QVector& in_normals, QVector& outIndices, +void GLTFReader::retriangulate(const QVector& inIndices, const QVector& in_vertices, + const QVector& in_normals, QVector& outIndices, QVector& out_vertices, QVector& out_normals) { for (int i = 0; i < inIndices.size(); i = i + 3) { diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 00109e9030..7a2cbff497 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -15,7 +15,7 @@ #include "OBJReader.h" #include // .obj files are not locale-specific. The C/ASCII charset applies. -#include +#include #include #include @@ -263,16 +263,16 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { default: materials[matName] = currentMaterial; #ifdef WANT_DEBUG - qCDebug(modelformat) << + qCDebug(modelformat) << "OBJ Reader Last material illumination model:" << currentMaterial.illuminationModel << - " shininess:" << currentMaterial.shininess << + " shininess:" << currentMaterial.shininess << " opacity:" << currentMaterial.opacity << - " diffuse color:" << currentMaterial.diffuseColor << - " specular color:" << currentMaterial.specularColor << - " emissive color:" << currentMaterial.emissiveColor << - " diffuse texture:" << currentMaterial.diffuseTextureFilename << - " specular texture:" << currentMaterial.specularTextureFilename << - " emissive texture:" << currentMaterial.emissiveTextureFilename << + " diffuse color:" << currentMaterial.diffuseColor << + " specular color:" << currentMaterial.specularColor << + " emissive color:" << currentMaterial.emissiveColor << + " diffuse texture:" << currentMaterial.diffuseTextureFilename << + " specular texture:" << currentMaterial.specularTextureFilename << + " emissive texture:" << currentMaterial.emissiveTextureFilename << " bump texture:" << currentMaterial.bumpTextureFilename << " opacity texture:" << currentMaterial.opacityTextureFilename; #endif @@ -352,7 +352,7 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { } } } -} +} void OBJReader::parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions) { // Texture options reference http://paulbourke.net/dataformats/mtl/ @@ -794,7 +794,7 @@ FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m n0 = checked_at(normals, face.normalIndices[0]); n1 = checked_at(normals, face.normalIndices[1]); n2 = checked_at(normals, face.normalIndices[2]); - } else { + } else { // generate normals from triangle plane if not provided n0 = n1 = n2 = glm::cross(v1 - v0, v2 - v0); } @@ -924,7 +924,7 @@ FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m bool applyNonMetallic = false; bool fresnelOn = false; - // Illumination model reference http://paulbourke.net/dataformats/mtl/ + // Illumination model reference http://paulbourke.net/dataformats/mtl/ switch (objMaterial.illuminationModel) { case 0: // Color on and Ambient off // We don't support ambient = do nothing? @@ -968,7 +968,7 @@ FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m case 10: // Casts shadows onto invisible surfaces // Do nothing? break; - } + } if (applyTransparency) { fbxMaterial.opacity = std::max(fbxMaterial.opacity, ILLUMINATION_MODEL_MIN_OPACITY); diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index 23ab1548a0..6f5b13f98d 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -39,7 +39,7 @@ AssetResourceRequest::~AssetResourceRequest() { if (_assetMappingRequest) { _assetMappingRequest->deleteLater(); } - + if (_assetRequest) { _assetRequest->deleteLater(); } @@ -82,7 +82,7 @@ void AssetResourceRequest::requestMappingForPath(const AssetUtils::AssetPath& pa // make sure we'll hear about the result of the get mapping request connect(_assetMappingRequest, &GetMappingRequest::finished, this, [this, path](GetMappingRequest* request){ auto statTracker = DependencyManager::get(); - + Q_ASSERT(_state == InProgress); Q_ASSERT(request == _assetMappingRequest); diff --git a/libraries/networking/src/FileResourceRequest.h b/libraries/networking/src/FileResourceRequest.h index 12b5b7d72e..fa9a1617a9 100644 --- a/libraries/networking/src/FileResourceRequest.h +++ b/libraries/networking/src/FileResourceRequest.h @@ -12,19 +12,19 @@ #ifndef hifi_FileResourceRequest_h #define hifi_FileResourceRequest_h -#include "ResourceRequest.h" -#include "QUrlAncestry.h" +#include +#include "ResourceRequest.h" class FileResourceRequest : public ResourceRequest { Q_OBJECT public: FileResourceRequest( - const QUrlAncestry& urlAncestry, + const QUrl& url, const bool isObservable = true, const qint64 callerId = -1, const QString& extra = "" - ) : ResourceRequest(urlAncestry, isObservable, callerId, extra) { } + ) : ResourceRequest(url, isObservable, callerId, extra) { } protected: virtual void doSend() override; diff --git a/libraries/networking/src/HTTPResourceRequest.h b/libraries/networking/src/HTTPResourceRequest.h index 41f605e856..c725934f2f 100644 --- a/libraries/networking/src/HTTPResourceRequest.h +++ b/libraries/networking/src/HTTPResourceRequest.h @@ -13,21 +13,20 @@ #define hifi_HTTPResourceRequest_h #include +#include #include #include "ResourceRequest.h" -#include "QUrlAncestry.h" - class HTTPResourceRequest : public ResourceRequest { Q_OBJECT public: HTTPResourceRequest( - const QUrlAncestry& urlAncestry, + const QUrl& url, const bool isObservable = true, const qint64 callerId = -1, const QString& = "" - ) : ResourceRequest(urlAncestry, isObservable, callerId) { } + ) : ResourceRequest(url, isObservable, callerId) { } ~HTTPResourceRequest(); protected: diff --git a/libraries/networking/src/NetworkAccessManager.cpp b/libraries/networking/src/NetworkAccessManager.cpp index c5229d65ac..f73243e675 100644 --- a/libraries/networking/src/NetworkAccessManager.cpp +++ b/libraries/networking/src/NetworkAccessManager.cpp @@ -22,7 +22,7 @@ QNetworkAccessManager& NetworkAccessManager::getInstance() { if (!networkAccessManagers.hasLocalData()) { networkAccessManagers.setLocalData(new QNetworkAccessManager()); } - + return *networkAccessManagers.localData(); } diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 186addbd86..f2fbe41804 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -320,7 +320,7 @@ QVariantList ResourceCache::getResourceList() { return list; } - + void ResourceCache::setRequestLimit(uint32_t limit) { auto sharedItems = DependencyManager::get(); sharedItems->setRequestLimit(limit); @@ -337,7 +337,6 @@ QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl& QReadLocker locker(&_resourcesLock); resource = _resources.value(url).lock(); } - if (resource) { removeUnusedResource(resource); } @@ -382,7 +381,7 @@ void ResourceCache::addUnusedResource(const QSharedPointer& resource) return; } reserveUnusedResource(resource->getBytes()); - + resource->setLRUKey(++_lastLRUKey); { @@ -411,7 +410,7 @@ void ResourceCache::reserveUnusedResource(qint64 resourceSize) { _unusedResourcesSize + resourceSize > _unusedResourcesMaxSize) { // unload the oldest resource QMap >::iterator it = _unusedResources.begin(); - + it.value()->setCache(nullptr); auto size = it.value()->getBytes(); @@ -477,7 +476,7 @@ void ResourceCache::updateTotalSize(const qint64& deltaSize) { emit dirty(); } - + QList> ResourceCache::getLoadingRequests() { return DependencyManager::get()->getLoadingRequests(); } @@ -592,7 +591,7 @@ void Resource::refresh() { _request = nullptr; ResourceCache::requestCompleted(_self); } - + _activeUrl = _url; init(); ensureLoading(); @@ -606,7 +605,7 @@ void Resource::allReferencesCleared() { } if (_cache && isCacheable()) { - // create and reinsert new shared pointer + // create and reinsert new shared pointer QSharedPointer self(this, &Resource::deleter); setSelf(self); reinsert(); @@ -631,10 +630,10 @@ void Resource::init(bool resetLoaded) { _loaded = false; } _attempts = 0; - + if (_url.isEmpty()) { _startedLoading = _loaded = true; - + } else if (!(_url.isValid())) { _startedLoading = _failedToLoad = true; } @@ -756,7 +755,7 @@ void Resource::handleReplyFinished() { } else { handleFailedRequest(result); } - + _request->disconnect(this); _request->deleteLater(); _request = nullptr; diff --git a/libraries/networking/src/ResourceManager.cpp b/libraries/networking/src/ResourceManager.cpp index d9774e3437..9539a10c2d 100644 --- a/libraries/networking/src/ResourceManager.cpp +++ b/libraries/networking/src/ResourceManager.cpp @@ -26,7 +26,6 @@ #include "NetworkAccessManager.h" #include "NetworkLogging.h" - ResourceManager::ResourceManager(bool atpSupportEnabled) : _atpSupportEnabled(atpSupportEnabled) { _thread.setObjectName("Resource Manager Thread"); @@ -125,7 +124,6 @@ ResourceRequest* ResourceManager::createResourceRequest( ResourceRequest* request = nullptr; - qDebug() << "!!!! in createResourceRequest " << callerId; if (scheme == URL_SCHEME_FILE || scheme == URL_SCHEME_QRC) { request = new FileResourceRequest(normalizedURL, isObservable, callerId, extra); } else if (scheme == URL_SCHEME_HTTP || scheme == URL_SCHEME_HTTPS || scheme == URL_SCHEME_FTP) { @@ -146,7 +144,6 @@ ResourceRequest* ResourceManager::createResourceRequest( QObject::connect(parent, &QObject::destroyed, request, &QObject::deleteLater); } request->moveToThread(&_thread); - return request; } diff --git a/libraries/networking/src/ResourceRequest.cpp b/libraries/networking/src/ResourceRequest.cpp index a651e9a2b6..acaf657dfe 100644 --- a/libraries/networking/src/ResourceRequest.cpp +++ b/libraries/networking/src/ResourceRequest.cpp @@ -21,9 +21,8 @@ void ResourceRequest::send() { if (_isObservable) { DependencyManager::get()->update( - _urlAncestry, _callerId, _extra + " => ResourceRequest::send" ); + _url, _callerId, _extra + " => ResourceRequest::send" ); } - if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "send", Qt::QueuedConnection); return; diff --git a/libraries/networking/src/ResourceRequest.h b/libraries/networking/src/ResourceRequest.h index 3ce1a9da2b..eb306ca5be 100644 --- a/libraries/networking/src/ResourceRequest.h +++ b/libraries/networking/src/ResourceRequest.h @@ -18,8 +18,6 @@ #include #include "ByteRange.h" -#include "QUrlAncestry.h" - const QString STAT_ATP_REQUEST_STARTED = "StartedATPRequest"; const QString STAT_HTTP_REQUEST_STARTED = "StartedHTTPRequest"; @@ -46,15 +44,14 @@ public: static const bool IS_NOT_OBSERVABLE = false; ResourceRequest( - const QUrlAncestry& urlAncestry, + const QUrl& url, const bool isObservable = IS_OBSERVABLE, const qint64 callerId = -1, const QString& extra = "" - ) : _urlAncestry(urlAncestry), + ) : _url(url), _isObservable(isObservable), _callerId(callerId), - _extra(extra), - _url(urlAncestry.last()) + _extra(extra) { } virtual ~ResourceRequest() = default; @@ -103,7 +100,6 @@ protected: virtual void doSend() = 0; void recordBytesDownloadedInStats(const QString& statName, int64_t bytesReceived); - QUrl _url; QUrl _relativePathURL; State _state { NotStarted }; @@ -119,7 +115,6 @@ protected: bool _isObservable; qint64 _callerId; QString _extra; - QUrlAncestry _urlAncestry; }; #endif diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index a50438dd54..efddf3c14f 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -669,7 +669,7 @@ OctreeElementPointer Octree::getElementEnclosingPoint(const glm::vec3& point, Oc return args.element; } -bool Octree::readFromFile(const char* fileName, const QUrlAncestry& urlAncestry) { +bool Octree::readFromFile(const char* fileName) { QString qFileName = findMostRecentFileExtension(fileName, PERSIST_EXTENSIONS); if (qFileName.endsWith(".json.gz")) { @@ -689,7 +689,7 @@ bool Octree::readFromFile(const char* fileName, const QUrlAncestry& urlAncestry) qCDebug(octree) << "Loading file" << qFileName << "..."; - bool success = readFromStream(fileLength, fileInputStream, "", urlAncestry); + bool success = readFromStream(fileLength, fileInputStream, ""); file.close(); @@ -737,8 +737,7 @@ QString getMarketplaceID(const QString& urlString) { bool Octree::readFromURL( const QString& urlString, const bool isObservable, - const qint64 callerId, - const QUrlAncestry& urlAncestry + const qint64 callerId ) { QString trimmedUrl = urlString.trimmed(); QString marketplaceID = getMarketplaceID(trimmedUrl); @@ -767,19 +766,18 @@ bool Octree::readFromURL( if (wasCompressed) { QDataStream inputStream(uncompressedJsonData); - return readFromStream(uncompressedJsonData.size(), inputStream, marketplaceID, urlAncestry); + return readFromStream(uncompressedJsonData.size(), inputStream, marketplaceID); } QDataStream inputStream(data); - return readFromStream(data.size(), inputStream, marketplaceID, urlAncestry); + return readFromStream(data.size(), inputStream, marketplaceID); } bool Octree::readFromStream( uint64_t streamLength, QDataStream& inputStream, - const QString& marketplaceID, - const QUrlAncestry& urlAncestry + const QString& marketplaceID ) { // decide if this is binary SVO or JSON-formatted SVO QIODevice *device = inputStream.device(); @@ -792,7 +790,7 @@ bool Octree::readFromStream( return false; } else { qCDebug(octree) << "Reading from JSON SVO Stream length:" << streamLength; - return readJSONFromStream(streamLength, inputStream, marketplaceID, urlAncestry); + return readJSONFromStream(streamLength, inputStream, marketplaceID); } } @@ -824,8 +822,7 @@ const int READ_JSON_BUFFER_SIZE = 2048; bool Octree::readJSONFromStream( uint64_t streamLength, QDataStream& inputStream, - const QString& marketplaceID, /*=""*/ - const QUrlAncestry& urlAncestry + const QString& marketplaceID /*=""*/ ) { // if the data is gzipped we may not have a useful bytesAvailable() result, so just keep reading until // we get an eof. Leave streamLength parameter for consistency. @@ -851,7 +848,7 @@ bool Octree::readJSONFromStream( } QVariant asVariant = asDocument.toVariant(); QVariantMap asMap = asVariant.toMap(); - bool success = readFromMap(asMap, urlAncestry); + bool success = readFromMap(asMap); delete[] rawData; return success; } diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 53acbc5a60..44b429582a 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -29,7 +29,6 @@ #include "OctreePacketData.h" #include "OctreeSceneStats.h" #include "OctreeUtils.h" -#include "QUrlAncestry.h" class ReadBitstreamToTreeParams; class Octree; @@ -210,13 +209,13 @@ public: bool skipThoseWithBadParents) = 0; // Octree importers - bool readFromFile(const char* filename, const QUrlAncestry& urlAncestry = {}); - bool readFromURL(const QString& url, const bool isObservable = true, const qint64 callerId = -1, const QUrlAncestry& urlAncestry = {}); // will support file urls as well... - bool readFromStream(uint64_t streamLength, QDataStream& inputStream, const QString& marketplaceID="", const QUrlAncestry& urlAncestry = {}); + bool readFromFile(const char* filename); + bool readFromURL(const QString& url, const bool isObservable = true, const qint64 callerId = -1); // will support file urls as well... + bool readFromStream(uint64_t streamLength, QDataStream& inputStream, const QString& marketplaceID=""); bool readSVOFromStream(uint64_t streamLength, QDataStream& inputStream); - bool readJSONFromStream(uint64_t streamLength, QDataStream& inputStream, const QString& marketplaceID="", const QUrlAncestry& urlAncestry = {}); + bool readJSONFromStream(uint64_t streamLength, QDataStream& inputStream, const QString& marketplaceID=""); bool readJSONFromGzippedFile(QString qFileName); - virtual bool readFromMap(QVariantMap& entityDescription, const QUrlAncestry& urlAncestry = {}) = 0; + virtual bool readFromMap(QVariantMap& entityDescription) = 0; uint64_t getOctreeElementsCount(); diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp index eccb812f09..cbcafe9c7d 100644 --- a/libraries/qml/src/qml/OffscreenSurface.cpp +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -258,29 +258,24 @@ void OffscreenSurface::setMaxFps(uint8_t maxFps) { } void OffscreenSurface::load(const QUrl& qmlSource, QQuickItem* parent, const QJSValue& callback) { - qDebug() << "Here 1"; loadInternal(qmlSource, false, parent, [callback](QQmlContext* context, QQuickItem* newItem) { QJSValue(callback).call(QJSValueList() << context->engine()->newQObject(newItem)); }); } void OffscreenSurface::load(const QUrl& qmlSource, bool createNewContext, const QmlContextObjectCallback& callback) { - qDebug() << "Here 2"; loadInternal(qmlSource, createNewContext, nullptr, callback); } void OffscreenSurface::loadInNewContext(const QUrl& qmlSource, const QmlContextObjectCallback& callback, const QmlContextCallback& contextCallback) { - qDebug() << "Here 3"; loadInternal(qmlSource, true, nullptr, callback, contextCallback); } void OffscreenSurface::load(const QUrl& qmlSource, const QmlContextObjectCallback& callback) { - qDebug() << "Here 4"; load(qmlSource, false, callback); } void OffscreenSurface::load(const QString& qmlSourceFile, const QmlContextObjectCallback& callback) { - qDebug() << "Here 5"; return load(QUrl(qmlSourceFile), callback); } diff --git a/libraries/script-engine/src/FileScriptingInterface.cpp b/libraries/script-engine/src/FileScriptingInterface.cpp index 103ed6d232..4e07877d57 100644 --- a/libraries/script-engine/src/FileScriptingInterface.cpp +++ b/libraries/script-engine/src/FileScriptingInterface.cpp @@ -50,7 +50,7 @@ void FileScriptingInterface::runUnzip(QString path, QUrl url, bool autoAdd, bool tempDir = zipTemp.path(); path.remove("file:///"); } - + qCDebug(scriptengine) << "Temporary directory at: " + tempDir; if (!isTempDir(tempDir)) { qCDebug(scriptengine) << "Temporary directory mismatch; risk of losing files"; @@ -58,7 +58,7 @@ void FileScriptingInterface::runUnzip(QString path, QUrl url, bool autoAdd, bool } QStringList fileList = unzipFile(path, tempDir); - + if (!fileList.isEmpty()) { qCDebug(scriptengine) << "First file to upload: " + fileList.first(); } else { diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 297d3bb924..a74d185c6a 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -63,7 +63,7 @@ QScriptValue XMLHttpRequestClass::constructor(QScriptContext* context, QScriptEn QScriptValue XMLHttpRequestClass::getStatus() const { if (_reply) { return QScriptValue(_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } + } return QScriptValue(0); } @@ -144,7 +144,7 @@ void XMLHttpRequestClass::open(const QString& method, const QString& url, bool a if (url.toLower().left(METAVERSE_API_URL.length()) == METAVERSE_API_URL) { auto accountManager = DependencyManager::get(); - + if (accountManager->hasValidAccessToken()) { static const QString HTTP_AUTHORIZATION_HEADER = "Authorization"; QString bearerString = "Bearer " + accountManager->getAccountInfo().getAccessToken().token; diff --git a/libraries/shared/src/EntityItemWeakPointerWithUrlAncestry.h b/libraries/shared/src/EntityItemWeakPointerWithUrlAncestry.h deleted file mode 100644 index fd3647b19e..0000000000 --- a/libraries/shared/src/EntityItemWeakPointerWithUrlAncestry.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// EntityItemWeakPointerWithUrlAncestry.h -// libraries/shared/src/ -// -// Created by Kerry Ivan Kurian 10/15/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 -// - -#ifndef hifi_EntityItemWeakPointerWithUrlAncestry_h -#define hifi_EntityItemWeakPointerWithUrlAncestry_h - -#include "EntityTypes.h" -#include "QUrlAncestry.h" - - -struct EntityItemWeakPointerWithUrlAncestry { - EntityItemWeakPointerWithUrlAncestry( - const EntityItemWeakPointer& a, - const QUrlAncestry& b - ) : entityItemWeakPointer(a), urlAncestry(b) {} - - EntityItemWeakPointer entityItemWeakPointer; - QUrlAncestry urlAncestry; -}; - - -#endif // hifi_EntityItemWeakPointerWithUrlAncestry_h - diff --git a/libraries/shared/src/QUrlAncestry.cpp b/libraries/shared/src/QUrlAncestry.cpp deleted file mode 100644 index f38c663803..0000000000 --- a/libraries/shared/src/QUrlAncestry.cpp +++ /dev/null @@ -1,35 +0,0 @@ -// -// QUrlAncestry.cpp -// libraries/shared/src/ -// -// Created by Kerry Ivan Kurian on 10/12/18. -// Copyright (c) 2018 High Fidelity, Inc. All rights reserved. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "QUrlAncestry.h" - - -QUrlAncestry::QUrlAncestry(const QUrl& resource, const QUrl& referrer) { - this->append(referrer); - this->append(resource); -} - -QUrlAncestry::QUrlAncestry( - const QUrl& resource, - const QUrlAncestry& ancestors) : QVector(ancestors) -{ - this->append(resource); -} - -void QUrlAncestry::toJson(QJsonArray& array) const { - for (auto const& qurl : *this) { - array.append(qurl.toDisplayString()); - } -} - -const QUrl QUrlAncestry::url() const { - return this->last(); -} diff --git a/libraries/shared/src/QUrlAncestry.h b/libraries/shared/src/QUrlAncestry.h deleted file mode 100644 index 84c32ff7c1..0000000000 --- a/libraries/shared/src/QUrlAncestry.h +++ /dev/null @@ -1,32 +0,0 @@ -// -// QUrlAncestry.h -// libraries/shared/src/ -// -// Created by Kerry Ivan Kurian on 10/12/18. -// Copyright (c) 2018 High Fidelity, Inc. All rights reserved. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#ifndef hifi_QUrlAncestry_H -#define hifi_QUrlAncestry_H - -#include -#include -#include - - -class QUrlAncestry : public QVector { -public: - QUrlAncestry() {} - QUrlAncestry(const QUrl& resource, const QUrl& referrer = QUrl("__NONE__")); - QUrlAncestry(const QUrl& resource, const QUrlAncestry& ancestors); - - void toJson(QJsonArray& array) const; - const QUrl url() const; -}; - - -#endif // hifi_QUrlVector_H diff --git a/libraries/shared/src/ResourceRequestObserver.cpp b/libraries/shared/src/ResourceRequestObserver.cpp index 6c52fcdc79..5e0925520a 100644 --- a/libraries/shared/src/ResourceRequestObserver.cpp +++ b/libraries/shared/src/ResourceRequestObserver.cpp @@ -15,22 +15,12 @@ #include #include #include "ResourceRequestObserver.h" -#include "QUrlAncestry.h" - -// void ResourceRequestObserver::update(const QNetworkRequest& request, const qint64 callerId, const QString& extra) { -// update(QUrlAncestry(request.url()), callerId, extra); -// } - -void ResourceRequestObserver::update( - const QUrlAncestry& urlAncestry, +void ResourceRequestObserver::update(const QUrl& requestUrl, const qint64 callerId, - const QString& extra -) { + const QString& extra) { QJsonArray array; - urlAncestry.toJson(array); - QJsonObject data { - { "url", array }, + QJsonObject data { { "url", requestUrl.toString() }, { "callerId", callerId }, { "extra", extra } }; diff --git a/libraries/shared/src/ResourceRequestObserver.h b/libraries/shared/src/ResourceRequestObserver.h index edccdb5e48..1b1bc322e5 100644 --- a/libraries/shared/src/ResourceRequestObserver.h +++ b/libraries/shared/src/ResourceRequestObserver.h @@ -15,7 +15,6 @@ #include #include "DependencyManager.h" -#include "QUrlAncestry.h" class ResourceRequestObserver : public QObject, public Dependency { @@ -23,8 +22,7 @@ class ResourceRequestObserver : public QObject, public Dependency { SINGLETON_DEPENDENCY public: - // void update(const QNetworkRequest& request, const qint64 callerId = -1, const QString& extra = ""); - void update(const QUrlAncestry& urlAncestry, const qint64 callerId = -1, const QString& extra = ""); + void update(const QUrl& requestUrl, const qint64 callerId = -1, const QString& extra = ""); signals: void resourceRequestEvent(QVariantMap result); From bccca94111f175e9d9e5daded7e3e219ee9a9f6e Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 19 Oct 2018 13:21:52 -0700 Subject: [PATCH 175/362] Prevent duplicate resources in logs. Thanks to Roxanne! --- libraries/networking/src/ResourceRequest.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/networking/src/ResourceRequest.cpp b/libraries/networking/src/ResourceRequest.cpp index acaf657dfe..c63bd4c563 100644 --- a/libraries/networking/src/ResourceRequest.cpp +++ b/libraries/networking/src/ResourceRequest.cpp @@ -19,15 +19,15 @@ void ResourceRequest::send() { - if (_isObservable) { - DependencyManager::get()->update( - _url, _callerId, _extra + " => ResourceRequest::send" ); - } if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "send", Qt::QueuedConnection); return; } + if (_isObservable) { + DependencyManager::get()->update(_url, _callerId, _extra + " => ResourceRequest::send"); + } + Q_ASSERT(_state == NotStarted); _state = InProgress; From d44f9e2ccc9b51a4a93a990997f61cc86c4b6be7 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 19 Oct 2018 15:21:37 -0700 Subject: [PATCH 176/362] Tons of improvements --- .../marketplaceItemTester/ItemUnderTest.qml | 25 ++++++--- .../MarketplaceItemTester.qml | 38 +++----------- interface/src/commerce/QmlCommerce.cpp | 25 ++++++--- interface/src/commerce/QmlCommerce.h | 2 +- .../scripting/ClipboardScriptingInterface.cpp | 10 +--- scripts/system/marketplaces/marketplaces.js | 52 ++++++++++++------- 6 files changed, 79 insertions(+), 73 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml index dcb67f3f12..c5aaf20a8f 100644 --- a/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml @@ -29,7 +29,7 @@ Rectangle { "forward": function(resource, assetType, resourceObjectId){ switch(assetType) { case "application": - Commerce.openApp(resource); + Commerce.installApp(resource, true); break; case "avatar": MyAvatar.useFullAvatarURL(resource); @@ -137,7 +137,7 @@ Rectangle { "entity": hifi.glyphs.wand, "trash": hifi.glyphs.trash, "unknown": hifi.glyphs.circleSlash, - "wearable": hifi.glyphs.hat, + "wearable": hifi.glyphs.hat } property int color: hifi.buttons.blue; property int colorScheme: hifi.colorSchemes.dark; @@ -149,7 +149,18 @@ Rectangle { enabled: comboBox.model[comboBox.currentIndex] !== "unknown" onClicked: { - root.actions["forward"](resource, comboBox.currentText, resourceObjectId); + if (model.currentlyRecordingResources) { + model.currentlyRecordingResources = false; + } else { + model.resourceAccessEventText = ""; + model.currentlyRecordingResources = true; + root.actions["forward"](resource, comboBox.currentText, resourceObjectId); + } + sendToScript({ + method: "tester_updateResourceRecordingStatus", + objectId: resourceListModel.get(index).resourceObjectId, + status: model.currentlyRecordingResources + }); } background: Rectangle { @@ -189,7 +200,7 @@ Rectangle { contentItem: Item { HifiStylesUit.HiFiGlyphs { id: rezIcon; - text: actionButton.glyphs[comboBox.model[comboBox.currentIndex]]; + text: model.currentlyRecordingResources ? hifi.glyphs.scriptStop : actionButton.glyphs[comboBox.model[comboBox.currentIndex]]; anchors.fill: parent size: 30; horizontalAlignment: Text.AlignHCenter; @@ -304,9 +315,9 @@ Rectangle { color: hifi.colors.white text: { if (root.detailsExpanded) { - return resourceAccessEventText + return model.resourceAccessEventText } else { - return (resourceAccessEventText.split("\n").length - 1).toString() + " resources loaded..." + return (model.resourceAccessEventText.split("\n").length - 1).toString() + " resources loaded..." } } font: Qt.font({ family: "Courier", pointSize: 8, weight: Font.Normal }) @@ -340,7 +351,7 @@ Rectangle { anchors.top: detailsTextContainer.bottom anchors.topMargin: 8 anchors.right: parent.right - width: 150 + width: 160 height: 30 text: "Copy to Clipboard" diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml index 5f2268132c..2a4f2d0e22 100644 --- a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml @@ -24,17 +24,13 @@ Rectangle { id: root property string installedApps - property string resourceAccessEventText property var nextResourceObjectId: 0 - property var startDate HifiStylesUit.HifiConstants { id: hifi } ListModel { id: resourceListModel } color: hifi.colors.darkGray - Component.onCompleted: startDate = new Date() - // // TITLE BAR START // @@ -147,8 +143,7 @@ Rectangle { model: resourceListModel spacing: 8 - delegate: ItemUnderTest { - } + delegate: ItemUnderTest { } } Item { @@ -194,8 +189,6 @@ Rectangle { if (resource) { print("!!!! building resource object"); var resourceObj = buildResourceObj(resource); - print("!!!! installing resource object"); - installResourceObj(resourceObj); print("!!!! notifying script of resource object"); sendToScript({ method: 'tester_newResourceObject', @@ -235,6 +228,7 @@ Rectangle { switch (message.method) { case "newResourceObjectInTest": var resourceObject = message.resourceObject; + resourceListModel.clear(); // REMOVE THIS once we support specific referrers resourceListModel.append(resourceObject); spinner.visible = false; break; @@ -244,21 +238,9 @@ Rectangle { spinner.visible = false; break; case "resourceRequestEvent": - try { - var date = new Date(JSON.parse(message.data.date)); - } catch(err) { - print("!!!!! Date conversion failed: " + JSON.stringify(message.data)); - } - // XXX Eventually this date check goes away b/c we will - // be able to match up resouce access events to resource - // object ids, ignoring those that have -1 resource - // object ids. - if (date >= startDate) { - resourceAccessEventText += ( - "[" + date.toISOString() + "] " + - message.data.url.toString().replace("__NONE__,", "") + "\n" - ); - } + // When we support multiple items under test simultaneously, + // we'll have to replace "0" with the correct index. + resourceListModel.setProperty(0, "resourceAccessEventText", message.resourceAccessEventText); break; } } @@ -270,17 +252,13 @@ Rectangle { resource.match(/\.json\.gz$/) ? "content set" : resource.match(/\.json$/) ? "entity or wearable" : "unknown"); - return { "resourceObjectId": nextResourceObjectId++, + // Uncomment this once we support more than one item in test at the same time + //nextResourceObjectId++; + return { "resourceObjectId": nextResourceObjectId, "resource": resource, "assetType": assetType }; } - function installResourceObj(resourceObj) { - if ("application" === resourceObj.assetType) { - Commerce.installApp(resourceObj.resource); - } - } - function toUrl(resource) { var httpPattern = /^http/i; return httpPattern.test(resource) ? resource : "file:///" + resource; diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 83907df103..ffe89ffc5b 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -315,7 +315,7 @@ QString QmlCommerce::getInstalledApps(const QString& justInstalledAppID) { return installedAppsFromMarketplace; } -bool QmlCommerce::installApp(const QString& itemHref) { +bool QmlCommerce::installApp(const QString& itemHref, const bool& alsoOpenImmediately) { if (!QDir(_appsPath).exists()) { if (!QDir().mkdir(_appsPath)) { qCDebug(commerce) << "Couldn't make _appsPath directory."; @@ -325,8 +325,8 @@ bool QmlCommerce::installApp(const QString& itemHref) { QUrl appHref(itemHref); - auto request = DependencyManager::get()->createResourceRequest( - this, appHref, true, -1, "QmlCommerce::installApp"); + auto request = + DependencyManager::get()->createResourceRequest(this, appHref, true, -1, "QmlCommerce::installApp"); if (!request) { qCDebug(commerce) << "Couldn't create resource request for app."; @@ -358,13 +358,22 @@ bool QmlCommerce::installApp(const QString& itemHref) { QJsonObject appFileJsonObject = appFileJsonDocument.object(); QString scriptUrl = appFileJsonObject["scriptURL"].toString(); - if ((DependencyManager::get()->loadScript(scriptUrl.trimmed())).isNull()) { - qCDebug(commerce) << "Couldn't load script."; - return false; + // Don't try to re-load (install) a script if it's already running + QStringList runningScripts = DependencyManager::get()->getRunningScripts(); + if (!runningScripts.contains(scriptUrl)) { + if ((DependencyManager::get()->loadScript(scriptUrl.trimmed())).isNull()) { + qCDebug(commerce) << "Couldn't load script."; + return false; + } + + QFileInfo appFileInfo(appFile); + emit appInstalled(appFileInfo.baseName()); + } + + if (alsoOpenImmediately) { + QmlCommerce::openApp(itemHref); } - QFileInfo appFileInfo(appFile); - emit appInstalled(appFileInfo.baseName()); return true; }); request->send(); diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index bee30e1b62..2e3c0ec24d 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -88,7 +88,7 @@ protected: Q_INVOKABLE void replaceContentSet(const QString& itemHref, const QString& certificateID); Q_INVOKABLE QString getInstalledApps(const QString& justInstalledAppID = ""); - Q_INVOKABLE bool installApp(const QString& appHref); + Q_INVOKABLE bool installApp(const QString& appHref, const bool& alsoOpenImmediately = false); Q_INVOKABLE bool uninstallApp(const QString& appHref); Q_INVOKABLE bool openApp(const QString& appHref); diff --git a/interface/src/scripting/ClipboardScriptingInterface.cpp b/interface/src/scripting/ClipboardScriptingInterface.cpp index c14f4ea895..c2d2b69883 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.cpp +++ b/interface/src/scripting/ClipboardScriptingInterface.cpp @@ -46,17 +46,11 @@ bool ClipboardScriptingInterface::exportEntities(const QString& filename, float return retVal; } -bool ClipboardScriptingInterface::importEntities( - const QString& filename, - const bool isObservable, - const qint64 callerId -) { +bool ClipboardScriptingInterface::importEntities(const QString& filename) { bool retVal; BLOCKING_INVOKE_METHOD(qApp, "importEntities", Q_RETURN_ARG(bool, retVal), - Q_ARG(const QString&, filename), - Q_ARG(const bool, isObservable), - Q_ARG(const qint64, callerId)); + Q_ARG(const QString&, filename)); return retVal; } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index d59a6b89d5..d6056f83a6 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -52,27 +52,30 @@ var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't writ var resourceRequestEvents = []; function signalResourceRequestEvent(data) { + // Once we can tie resource request events to specific resources, + // we will have to update the "0" in here. + resourceObjectsInTest[0].resourceAccessEventText += "[" + data.date.toISOString() + "] " + + data.url.toString().replace("__NONE__,", "") + "\n"; + ui.tablet.sendToQml({ method: "resourceRequestEvent", - data: data }); + data: data, + resourceAccessEventText: resourceObjectsInTest[0].resourceAccessEventText + }); } function onResourceRequestEvent(data) { - var resourceRequestEvent = { - "date": JSON.stringify(new Date()), - "url": data.url, - "callerId": data.callerId, - "extra": data.extra }; - resourceRequestEvents.push(resourceRequestEvent); - signalResourceRequestEvent(resourceRequestEvent); -} - -function pushResourceRequestEvents() { - var length = resourceRequestEvents.length - for (var i = 0; i < length; i++) { - if (i in resourceRequestEvents) { - signalResourceRequestEvent(resourceRequestEvents[i]); - } + // Once we can tie resource request events to specific resources, + // we will have to update the "0" in here. + if (resourceObjectsInTest[0] && resourceObjectsInTest[0].currentlyRecordingResources) { + var resourceRequestEvent = { + "date": new Date(), + "url": data.url, + "callerId": data.callerId, + "extra": data.extra + }; + resourceRequestEvents.push(resourceRequestEvent); + signalResourceRequestEvent(resourceRequestEvent); } } @@ -849,7 +852,8 @@ var resourceObjectsInTest = []; function signalNewResourceObjectInTest(resourceObject) { ui.tablet.sendToQml({ method: "newResourceObjectInTest", - resourceObject: resourceObject }); + resourceObject: resourceObject + }); } var onQmlMessageReceived = function onQmlMessageReceived(message) { @@ -915,6 +919,9 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { break; case 'tester_newResourceObject': var resourceObject = message.resourceObject; + resourceObjectsInTest = []; // REMOVE THIS once we support specific referrers + resourceObject.currentlyRecordingResources = false; + resourceObject.resourceAccessEventText = ""; resourceObjectsInTest[resourceObject.resourceObjectId] = resourceObject; signalNewResourceObjectInTest(resourceObject); break; @@ -924,6 +931,12 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'tester_deleteResourceObject': delete resourceObjectsInTest[message.objectId]; break; + case 'tester_updateResourceRecordingStatus': + resourceObjectsInTest[message.objectId].currentlyRecordingResources = message.status; + if (message.status) { + resourceObjectsInTest[message.objectId].resourceAccessEventText = ""; + } + break; case 'header_marketplaceImageClicked': case 'purchases_backClicked': openMarketplace(message.referrerURL); @@ -1076,7 +1089,9 @@ function pushResourceObjectsInTest() { // that the marketplace item tester QML has heard from us, at least // so that it can indicate to the user that all of the resoruce // objects in test have been transmitted to it. - ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: maxResourceObjectId + 1 }); + //ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: maxResourceObjectId + 1 }); + // Since, for now, we only support 1 object in test, always send id: 0 + ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: 0 }); } // Function Name: onTabletScreenChanged() @@ -1165,7 +1180,6 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { // variable amount of time to come up, in practice less than // 750ms. Script.setTimeout(pushResourceObjectsInTest, 750); - Script.setTimeout(pushResourceRequestEvents, 750); } console.debug(ui.buttonName + " app reports: Tablet screen changed.\nNew screen type: " + type + From de93bbb08bcc0f61722cdb10d216fd38eb98ce01 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 19 Oct 2018 15:43:28 -0700 Subject: [PATCH 177/362] Only show unique resources --- .../marketplaceItemTester/ItemUnderTest.qml | 5 ++-- scripts/system/marketplaces/marketplaces.js | 24 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml index c5aaf20a8f..fcb4eaae43 100644 --- a/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml @@ -314,10 +314,11 @@ Rectangle { readOnly: true color: hifi.colors.white text: { - if (root.detailsExpanded) { + var numUniqueResources = (model.resourceAccessEventText.split("\n").length - 1); + if (root.detailsExpanded && numUniqueResources > 0) { return model.resourceAccessEventText } else { - return (model.resourceAccessEventText.split("\n").length - 1).toString() + " resources loaded..." + return numUniqueResources.toString() + " unique resource" + (numUniqueResources === 1 ? "" : "s") + " loaded - expand for details" } } font: Qt.font({ family: "Courier", pointSize: 8, weight: Font.Normal }) diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index d6056f83a6..487920b764 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -54,14 +54,24 @@ var resourceRequestEvents = []; function signalResourceRequestEvent(data) { // Once we can tie resource request events to specific resources, // we will have to update the "0" in here. - resourceObjectsInTest[0].resourceAccessEventText += "[" + data.date.toISOString() + "] " + - data.url.toString().replace("__NONE__,", "") + "\n"; + if (!resourceObjectsInTest[0].resourceUrls) { + resourceObjectsInTest[0].resourceUrls = []; + } - ui.tablet.sendToQml({ - method: "resourceRequestEvent", - data: data, - resourceAccessEventText: resourceObjectsInTest[0].resourceAccessEventText - }); + var resourceUrl = data.url.toString().replace("__NONE__,", ""); + + if (resourceObjectsInTest[0].resourceUrls.indexOf(resourceUrl) === -1) { + resourceObjectsInTest[0].resourceUrls.push(resourceUrl); + + resourceObjectsInTest[0].resourceAccessEventText += "[" + data.date.toISOString() + "] " + + resourceUrl + "\n"; + + ui.tablet.sendToQml({ + method: "resourceRequestEvent", + data: data, + resourceAccessEventText: resourceObjectsInTest[0].resourceAccessEventText + }); + } } function onResourceRequestEvent(data) { From f9cc4f5a696c15790ce4c04f4ab89b65cd738fe4 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 19 Oct 2018 16:01:49 -0700 Subject: [PATCH 178/362] Count unique source and url pairs --- .../commerce/marketplaceItemTester/ItemUnderTest.qml | 2 +- scripts/system/marketplaces/marketplaces.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml index fcb4eaae43..27277a28f4 100644 --- a/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml @@ -318,7 +318,7 @@ Rectangle { if (root.detailsExpanded && numUniqueResources > 0) { return model.resourceAccessEventText } else { - return numUniqueResources.toString() + " unique resource" + (numUniqueResources === 1 ? "" : "s") + " loaded - expand for details" + return numUniqueResources.toString() + " unique source/resource url pair" + (numUniqueResources === 1 ? "" : "s") + " recorded" } } font: Qt.font({ family: "Courier", pointSize: 8, weight: Font.Normal }) diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 487920b764..59279bada7 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -54,17 +54,17 @@ var resourceRequestEvents = []; function signalResourceRequestEvent(data) { // Once we can tie resource request events to specific resources, // we will have to update the "0" in here. - if (!resourceObjectsInTest[0].resourceUrls) { - resourceObjectsInTest[0].resourceUrls = []; + if (!resourceObjectsInTest[0].resourceDataArray) { + resourceObjectsInTest[0].resourceDataArray = []; } - var resourceUrl = data.url.toString().replace("__NONE__,", ""); + var resourceData = "from: " + data.extra + ": " + data.url.toString().replace("__NONE__,", ""); - if (resourceObjectsInTest[0].resourceUrls.indexOf(resourceUrl) === -1) { - resourceObjectsInTest[0].resourceUrls.push(resourceUrl); + if (resourceObjectsInTest[0].resourceDataArray.indexOf(resourceData) === -1) { + resourceObjectsInTest[0].resourceDataArray.push(resourceData); resourceObjectsInTest[0].resourceAccessEventText += "[" + data.date.toISOString() + "] " + - resourceUrl + "\n"; + resourceData + "\n"; ui.tablet.sendToQml({ method: "resourceRequestEvent", From 1023e6d2b9ceec7052b1b39e29b3047d7cbe4210 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 19 Oct 2018 16:03:05 -0700 Subject: [PATCH 179/362] Fix ClipboardScriptingInterface: --- .../src/scripting/ClipboardScriptingInterface.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/interface/src/scripting/ClipboardScriptingInterface.cpp b/interface/src/scripting/ClipboardScriptingInterface.cpp index c2d2b69883..c14f4ea895 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.cpp +++ b/interface/src/scripting/ClipboardScriptingInterface.cpp @@ -46,11 +46,17 @@ bool ClipboardScriptingInterface::exportEntities(const QString& filename, float return retVal; } -bool ClipboardScriptingInterface::importEntities(const QString& filename) { +bool ClipboardScriptingInterface::importEntities( + const QString& filename, + const bool isObservable, + const qint64 callerId +) { bool retVal; BLOCKING_INVOKE_METHOD(qApp, "importEntities", Q_RETURN_ARG(bool, retVal), - Q_ARG(const QString&, filename)); + Q_ARG(const QString&, filename), + Q_ARG(const bool, isObservable), + Q_ARG(const qint64, callerId)); return retVal; } From f5a5c0dad3511fe73782dfd92ea7951fe83797ab Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 19 Oct 2018 16:48:23 -0700 Subject: [PATCH 180/362] Attempt to fix wearable bug? --- scripts/system/marketplaces/marketplaces.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 59279bada7..004f3fcbb8 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -54,10 +54,6 @@ var resourceRequestEvents = []; function signalResourceRequestEvent(data) { // Once we can tie resource request events to specific resources, // we will have to update the "0" in here. - if (!resourceObjectsInTest[0].resourceDataArray) { - resourceObjectsInTest[0].resourceDataArray = []; - } - var resourceData = "from: " + data.extra + ": " + data.url.toString().replace("__NONE__,", ""); if (resourceObjectsInTest[0].resourceDataArray.indexOf(resourceData) === -1) { @@ -933,6 +929,7 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { resourceObject.currentlyRecordingResources = false; resourceObject.resourceAccessEventText = ""; resourceObjectsInTest[resourceObject.resourceObjectId] = resourceObject; + resourceObjectsInTest[resourceObject.resourceObjectId].resourceDataArray = []; signalNewResourceObjectInTest(resourceObject); break; case 'tester_updateResourceObjectAssetType': @@ -944,6 +941,7 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'tester_updateResourceRecordingStatus': resourceObjectsInTest[message.objectId].currentlyRecordingResources = message.status; if (message.status) { + resourceObjectsInTest[message.objectId].resourceDataArray = []; resourceObjectsInTest[message.objectId].resourceAccessEventText = ""; } break; From a1bb68517626d4a31823a9d2782b0ed77d890672 Mon Sep 17 00:00:00 2001 From: Kerry Ivan Kurian Date: Mon, 22 Oct 2018 11:04:31 -0700 Subject: [PATCH 181/362] Remove cruft --- libraries/octree/src/Octree.cpp | 2 +- scripts/system/marketplaces/marketplaces.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index efddf3c14f..06db92bcf7 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -689,7 +689,7 @@ bool Octree::readFromFile(const char* fileName) { qCDebug(octree) << "Loading file" << qFileName << "..."; - bool success = readFromStream(fileLength, fileInputStream, ""); + bool success = readFromStream(fileLength, fileInputStream); file.close(); diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 004f3fcbb8..3085145176 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -564,7 +564,6 @@ function defaultFor(arg, val) { function rezEntity(itemHref, itemType, marketplaceItemTesterId) { var isWearable = itemType === "wearable"; - print("!!!!! Clipboard.importEntities " + marketplaceItemTesterId); var success = Clipboard.importEntities(itemHref, true, marketplaceItemTesterId); var wearableLocalPosition = null; var wearableLocalRotation = null; @@ -920,7 +919,6 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'checkout_rezClicked': case 'purchases_rezClicked': case 'tester_rezClicked': - print("!!!!! marketplaces tester_rezClicked"); rezEntity(message.itemHref, message.itemType, message.itemId); break; case 'tester_newResourceObject': From a2a6acd45c2d7b5d7a082e84c567ba0ada31802d Mon Sep 17 00:00:00 2001 From: Kerry Ivan Kurian Date: Mon, 22 Oct 2018 14:04:59 -0700 Subject: [PATCH 182/362] Improve code formatting --- libraries/networking/src/ResourceCache.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index f2fbe41804..6f7cad8a04 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -349,7 +349,8 @@ QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl& resource = createResource( url, fallback.isValid() ? getResource(fallback, QUrl()) : QSharedPointer(), - extra); resource->setSelf(resource); + extra); + resource->setSelf(resource); resource->setCache(this); resource->moveToThread(qApp->thread()); connect(resource.data(), &Resource::updateSize, this, &ResourceCache::updateTotalSize); From e5caf016818854b7b68be61ab2fde98837145d52 Mon Sep 17 00:00:00 2001 From: David Back Date: Mon, 22 Oct 2018 14:51:54 -0700 Subject: [PATCH 183/362] CR change for body click event --- scripts/system/html/js/entityList.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 2d248e48e6..a4e4c0dcd1 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -200,15 +200,16 @@ function loaded() { elDelete.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); } + elFilterTypeSelectBox.onclick = onToggleTypeDropdown; elFilterSearch.onkeyup = refreshEntityList; elFilterSearch.onsearch = refreshEntityList; elFilterInView.onclick = toggleFilterInView; elFilterRadius.onkeyup = onRadiusChange; elFilterRadius.onchange = onRadiusChange; + elFilterRadius.onclick = onRadiusChange; elInfoToggle.onclick = toggleInfo; // create filter type dropdown checkboxes with label and icon for each type - elFilterTypeSelectBox.onclick = toggleTypeDropdown; for (let i = 0; i < FILTER_TYPES.length; ++i) { let type = FILTER_TYPES[i]; let typeFilterID = "filter-type-" + type; @@ -225,11 +226,11 @@ function loaded() { elInput.setAttribute("filterType", type); elInput.checked = true; // all types are checked initially toggleTypeFilter(elInput, false); // add all types to the initial types filter - elInput.onclick = onToggleTypeFilter; elDiv.appendChild(elInput); elLabel.insertBefore(elSpan, elLabel.childNodes[0]); elDiv.appendChild(elLabel); elFilterTypeCheckboxes.appendChild(elDiv); + elDiv.onclick = onToggleTypeFilter; } entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow, @@ -654,6 +655,11 @@ function loaded() { elFilterTypeCheckboxes.style.display = isTypeDropdownVisible() ? "none" : "block"; } + function onToggleTypeDropdown(event) { + toggleTypeDropdown(); + event.stopPropagation(); + } + function toggleTypeFilter(elInput, refresh) { let type = elInput.getAttribute("filterType"); let typeChecked = elInput.checked; @@ -679,14 +685,17 @@ function loaded() { } function onToggleTypeFilter(event) { - toggleTypeFilter(this, true); + let elTarget = event.target; + if (elTarget instanceof HTMLInputElement) { + toggleTypeFilter(elTarget, true); + } + event.stopPropagation(); } function onBodyClick(event) { - // if clicking anywhere outside of the type filter dropdown and it's open then close it - let elTarget = event.target; - if (isTypeDropdownVisible() && !elFilterTypeSelectBox.contains(elTarget) && - !elFilterTypeCheckboxes.contains(elTarget)) { + // if clicking anywhere outside of the type filter dropdown (since click event bubbled up to onBodyClick and + // propagation wasn't stopped by onToggleTypeFilter or onToggleTypeDropdown) and the dropdown is open then close it + if (isTypeDropdownVisible()) { toggleTypeDropdown(); } } From 3cc78896ac67357638e9f0e670a9adff76bf10f0 Mon Sep 17 00:00:00 2001 From: Alexia Mandeville Date: Mon, 22 Oct 2018 15:39:23 -0700 Subject: [PATCH 184/362] Removed duplicate speech control menu item --- interface/src/Menu.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 1464ec6826..91019975cd 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -788,18 +788,6 @@ Menu::Menu() { // Developer > Show Animation Statistics addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::AnimStats); - // Developer > Scripting > Enable Speech Control API -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) - auto speechRecognizer = DependencyManager::get(); - QAction* speechRecognizerAction = addCheckableActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::ControlWithSpeech, - Qt::CTRL | Qt::SHIFT | Qt::Key_C, - speechRecognizer->getEnabled(), - speechRecognizer.data(), - SLOT(setEnabled(bool)), - UNSPECIFIED_POSITION); - connect(speechRecognizer.data(), SIGNAL(enabledUpdated(bool)), speechRecognizerAction, SLOT(setChecked(bool))); -#endif - // Developer > Scripting > Console... addActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::Console, Qt::CTRL | Qt::ALT | Qt::Key_J, DependencyManager::get().data(), From 91f92276864161e9ea93f73014b371b663a1d0b6 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 22 Oct 2018 15:44:16 -0700 Subject: [PATCH 185/362] Case 19446 - Update via Sandbox Popup -- opens Old Interface --- server-console/src/main.js | 4 ---- server-console/src/modules/hf-notifications.js | 12 ++++++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/server-console/src/main.js b/server-console/src/main.js index 5c7913d775..0263f8fd65 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -877,10 +877,6 @@ function onContentLoaded() { hasShownUpdateNotification = true; } }); - notifier.on('click', function(notifierObject, options) { - log.debug("Got click", options.url); - shell.openExternal(options.url); - }); } deleteOldFiles(logPath, DELETE_LOG_FILES_OLDER_THAN_X_SECONDS, LOG_FILE_REGEX); diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js index b1f337bbc3..e1ea508e47 100644 --- a/server-console/src/modules/hf-notifications.js +++ b/server-console/src/modules/hf-notifications.js @@ -5,6 +5,8 @@ const process = require('process'); const hfApp = require('./hf-app'); const path = require('path'); const AccountInfo = require('./hf-acctinfo').AccountInfo; +const url = require('url'); +const shell = require('electron').shell; const GetBuildInfo = hfApp.getBuildInfo; const buildInfo = GetBuildInfo(); const osType = os.type(); @@ -154,8 +156,14 @@ function HifiNotifications(config, menuNotificationCallback) { var _menuNotificationCallback = menuNotificationCallback; notifier.on('click', function (notifierObject, options) { - StartInterface(options.url); - _menuNotificationCallback(options.notificationType, false); + const optUrl = url.parse(options.url); + if ((optUrl.protocol === "hifi") || (optUrl.protocol === "hifiapp")) { + StartInterface(options.url); + _menuNotificationCallback(options.notificationType, false); + } + else { + shell.openExternal(options.url); + } }); } From 1d8994993c0e640ecaeadd8557fe09ea9a6f5fe2 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 22 Oct 2018 17:05:31 -0700 Subject: [PATCH 186/362] Whitelist TTS scripting interface to TTS app, which is now a default script --- interface/resources/qml/hifi/tts/TTS.qml | 304 ++++++++++++++++++ interface/src/Application.cpp | 12 +- .../src/scripting/TTSScriptingInterface.cpp | 4 +- scripts/defaultScripts.js | 3 +- scripts/system/tts/TTS.js | 28 ++ scripts/system/tts/tts-a.svg | 9 + scripts/system/tts/tts-i.svg | 9 + 7 files changed, 363 insertions(+), 6 deletions(-) create mode 100644 interface/resources/qml/hifi/tts/TTS.qml create mode 100644 scripts/system/tts/TTS.js create mode 100644 scripts/system/tts/tts-a.svg create mode 100644 scripts/system/tts/tts-i.svg diff --git a/interface/resources/qml/hifi/tts/TTS.qml b/interface/resources/qml/hifi/tts/TTS.qml new file mode 100644 index 0000000000..114efd0cca --- /dev/null +++ b/interface/resources/qml/hifi/tts/TTS.qml @@ -0,0 +1,304 @@ +// +// TTS.qml +// +// TTS App +// +// Created by Zach Fox on 2018-10-10 +// 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 +// + +import Hifi 1.0 as Hifi +import QtQuick 2.10 +import QtQuick.Controls 2.3 +import "qrc:////qml//styles-uit" as HifiStylesUit +import "qrc:////qml//controls-uit" as HifiControlsUit +import "qrc:////qml//controls" as HifiControls + +Rectangle { + HifiStylesUit.HifiConstants { id: hifi; } + + id: root; + // Style + color: hifi.colors.darkGray; + property bool keyboardRaised: false; + + // + // TITLE BAR START + // + Item { + id: titleBarContainer; + // Size + width: root.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // Title bar text + HifiStylesUit.RalewaySemiBold { + id: titleBarText; + text: "Text-to-Speech"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + width: paintedWidth; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } + } + // + // TITLE BAR END + // + + + Item { + id: tagButtonContainer; + anchors.top: titleBarContainer.bottom; + anchors.topMargin: 2; + anchors.left: parent.left; + anchors.right: parent.right; + height: 70; + + HifiStylesUit.RalewaySemiBold { + id: tagButtonTitle; + text: "Insert Tag:"; + // Text size + size: 18; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 35; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + HifiControlsUit.Button { + id: pitch10Button; + focusPolicy: Qt.NoFocus; + color: hifi.buttons.none; + colorScheme: hifi.colorSchemes.dark; + anchors.top: tagButtonTitle.bottom; + anchors.left: parent.left; + anchors.leftMargin: 3; + width: parent.width/6 - 6; + height: 30; + text: "Pitch 10"; + onClicked: { + messageToSpeak.insert(messageToSpeak.cursorPosition, ""); + } + } + + HifiControlsUit.Button { + id: pitch0Button; + focusPolicy: Qt.NoFocus; + color: hifi.buttons.none; + colorScheme: hifi.colorSchemes.dark; + anchors.top: tagButtonTitle.bottom; + anchors.left: pitch10Button.right; + anchors.leftMargin: 6; + width: parent.width/6 - anchors.leftMargin; + height: 30; + text: "Pitch 0"; + onClicked: { + messageToSpeak.insert(messageToSpeak.cursorPosition, ""); + } + } + + HifiControlsUit.Button { + id: pitchNeg10Button; + focusPolicy: Qt.NoFocus; + color: hifi.buttons.none; + colorScheme: hifi.colorSchemes.dark; + anchors.top: tagButtonTitle.bottom; + anchors.left: pitch0Button.right; + anchors.leftMargin: 6; + width: parent.width/6 - anchors.leftMargin; + height: 30; + text: "Pitch -10"; + onClicked: { + messageToSpeak.insert(messageToSpeak.cursorPosition, ""); + } + } + + HifiControlsUit.Button { + id: speed5Button; + focusPolicy: Qt.NoFocus; + color: hifi.buttons.none; + colorScheme: hifi.colorSchemes.dark; + anchors.top: tagButtonTitle.bottom; + anchors.left: pitchNeg10Button.right; + anchors.leftMargin: 6; + width: parent.width/6 - anchors.leftMargin; + height: 30; + text: "Speed 5"; + onClicked: { + messageToSpeak.insert(messageToSpeak.cursorPosition, ""); + } + } + + HifiControlsUit.Button { + id: speed0Button; + focusPolicy: Qt.NoFocus; + color: hifi.buttons.none; + colorScheme: hifi.colorSchemes.dark; + anchors.top: tagButtonTitle.bottom; + anchors.left: speed5Button.right; + anchors.leftMargin: 6; + width: parent.width/6 - anchors.leftMargin; + height: 30; + text: "Speed 0"; + onClicked: { + messageToSpeak.insert(messageToSpeak.cursorPosition, ""); + } + } + + HifiControlsUit.Button { + id: speedNeg10Button; + focusPolicy: Qt.NoFocus; + color: hifi.buttons.none; + colorScheme: hifi.colorSchemes.dark; + anchors.top: tagButtonTitle.bottom; + anchors.left: speed0Button.right; + anchors.leftMargin: 6; + width: parent.width/6 - anchors.leftMargin; + height: 30; + text: "Speed -10"; + onClicked: { + messageToSpeak.insert(messageToSpeak.cursorPosition, ""); + } + } + } + + Item { + anchors.top: tagButtonContainer.bottom; + anchors.topMargin: 8; + anchors.bottom: keyboardContainer.top; + anchors.bottomMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + + TextArea { + id: messageToSpeak; + placeholderText: "Message to Speak"; + font.family: "Fira Sans SemiBold"; + font.pixelSize: 20; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: speakButton.top; + anchors.bottomMargin: 8; + // Style + background: Rectangle { + anchors.fill: parent; + color: parent.activeFocus ? hifi.colors.black : hifi.colors.baseGrayShadow; + border.width: parent.activeFocus ? 1 : 0; + border.color: parent.activeFocus ? hifi.colors.primaryHighlight : hifi.colors.textFieldLightBackground; + } + color: hifi.colors.white; + textFormat: TextEdit.PlainText; + wrapMode: TextEdit.Wrap; + activeFocusOnPress: true; + activeFocusOnTab: true; + Keys.onPressed: { + if (event.key == Qt.Key_Return || event.key == Qt.Key_Enter) { + TextToSpeech.speakText(messageToSpeak.text, 480, 10, 24000, 16, true); + event.accepted = true; + } + } + } + + HifiControlsUit.Button { + id: speakButton; + focusPolicy: Qt.NoFocus; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + width: 215; + height: 40; + text: "Speak"; + onClicked: { + TextToSpeech.speakText(messageToSpeak.text, 480, 10, 24000, 16, true); + } + } + + HifiControlsUit.Button { + id: clearButton; + focusPolicy: Qt.NoFocus; + color: hifi.buttons.white; + colorScheme: hifi.colorSchemes.dark; + anchors.right: speakButton.left; + anchors.rightMargin: 16; + anchors.bottom: parent.bottom; + width: 100; + height: 40; + text: "Clear"; + onClicked: { + messageToSpeak.text = ""; + } + } + + HifiControlsUit.Button { + id: stopButton; + focusPolicy: Qt.NoFocus; + color: hifi.buttons.red; + colorScheme: hifi.colorSchemes.dark; + anchors.right: clearButton.left; + anchors.rightMargin: 16; + anchors.bottom: parent.bottom; + width: 100; + height: 40; + text: "Stop Last"; + onClicked: { + TextToSpeech.stopLastSpeech(); + } + } + } + + Item { + id: keyboardContainer; + z: 998; + visible: keyboard.raised; + property bool punctuationMode: false; + anchors { + bottom: parent.bottom; + left: parent.left; + right: parent.right; + } + + HifiControlsUit.Keyboard { + id: keyboard; + raised: HMD.mounted && root.keyboardRaised; + numeric: parent.punctuationMode; + anchors { + bottom: parent.bottom; + left: parent.left; + right: parent.right; + } + } + } +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 836e12e60a..9cfdc8a9bb 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2906,7 +2906,7 @@ void Application::initializeUi() { LoginDialog::registerType(); Tooltip::registerType(); UpdateDialog::registerType(); - QmlContextCallback callback = [](QQmlContext* context) { + QmlContextCallback commerceCallback = [](QQmlContext* context) { context->setContextProperty("Commerce", new QmlCommerce()); }; OffscreenQmlSurface::addWhitelistContextHandler({ @@ -2932,7 +2932,13 @@ void Application::initializeUi() { QUrl{ "hifi/commerce/wallet/Wallet.qml" }, QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, QUrl{ "hifi/commerce/wallet/WalletSetup.qml" }, - }, callback); + }, commerceCallback); + QmlContextCallback ttsCallback = [](QQmlContext* context) { + context->setContextProperty("TextToSpeech", DependencyManager::get().data()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/tts/TTS.qml" } + }, ttsCallback); qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); qmlRegisterType("Hifi", 1, 0, "Preference"); qmlRegisterType("HifiWeb", 1, 0, "WebBrowserSuggestionsEngine"); @@ -3135,7 +3141,6 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); surfaceContext->setContextProperty("Wallet", DependencyManager::get().data()); surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); - surfaceContext->setContextProperty("TextToSpeech", DependencyManager::get().data()); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); @@ -6818,7 +6823,6 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("Wallet", DependencyManager::get().data()); scriptEngine->registerGlobalObject("AddressManager", DependencyManager::get().data()); scriptEngine->registerGlobalObject("HifiAbout", AboutUtil::getInstance()); - scriptEngine->registerGlobalObject("TextToSpeech", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine.data(), OverlayIDtoScriptValue, OverlayIDfromScriptValue); diff --git a/interface/src/scripting/TTSScriptingInterface.cpp b/interface/src/scripting/TTSScriptingInterface.cpp index 5fb47a73c3..0cdb24e15d 100644 --- a/interface/src/scripting/TTSScriptingInterface.cpp +++ b/interface/src/scripting/TTSScriptingInterface.cpp @@ -151,7 +151,9 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak, _lastSoundByteArray.resize(0); _lastSoundByteArray.append(buf1, dwSize); - emit ttsSampleCreated(_lastSoundByteArray, newChunkSize, timerInterval); + // Commented out because this doesn't work completely :) + // Obviously, commenting this out isn't fit for production, but it's fine for a test PR + //emit ttsSampleCreated(_lastSoundByteArray, newChunkSize, timerInterval); if (alsoInject) { AudioInjectorOptions options; diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 9efb040624..2398973dfd 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,7 +32,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/emote.js", - "system/miniTablet.js" + "system/miniTablet.js", + "system/tts/TTS.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/system/tts/TTS.js b/scripts/system/tts/TTS.js new file mode 100644 index 0000000000..36259cfda0 --- /dev/null +++ b/scripts/system/tts/TTS.js @@ -0,0 +1,28 @@ +"use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Script, */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// TTS.js +// +// Created by Zach Fox on 2018-10-10 +// 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 +var AppUi = Script.require('appUi'); + +var ui; +function startup() { + ui = new AppUi({ + buttonName: "TTS", + //home: Script.resolvePath("TTS.qml") + home: "hifi/tts/TTS.qml", + graphicsDirectory: Script.resolvePath("./") // speech by Danil Polshin from the Noun Project + }); +} +startup(); +}()); // END LOCAL_SCOPE diff --git a/scripts/system/tts/tts-a.svg b/scripts/system/tts/tts-a.svg new file mode 100644 index 0000000000..9dac3a2d53 --- /dev/null +++ b/scripts/system/tts/tts-a.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/scripts/system/tts/tts-i.svg b/scripts/system/tts/tts-i.svg new file mode 100644 index 0000000000..1c52ec3193 --- /dev/null +++ b/scripts/system/tts/tts-i.svg @@ -0,0 +1,9 @@ + + + + + + + + + From d7dce456b269d9bc83c570292d1fb7f35445ff27 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 22 Oct 2018 18:29:27 -0700 Subject: [PATCH 187/362] Fix incorrect default entity properties in Create --- scripts/system/edit.js | 110 +++++++++-------------------------------- 1 file changed, 24 insertions(+), 86 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 71b4b2c54d..a1e8c52d1f 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -299,7 +299,6 @@ function checkEditPermissionsAndUpdate() { const DEFAULT_ENTITY_PROPERTIES = { All: { - collisionless: true, description: "", rotation: { x: 0, y: 0, z: 0, w: 1 }, collidesWith: "static,dynamic,kinematic,otherAvatar", @@ -344,12 +343,22 @@ const DEFAULT_ENTITY_PROPERTIES = { }, Text: { text: "Text", + dimensions: { + x: 0.65, + y: 0.3, + z: 0.01 + }, textColor: { red: 255, green: 255, blue: 255 }, backgroundColor: { red: 0, green: 0, blue: 0 }, lineHeight: 0.06, faceCamera: false, }, Zone: { + dimensions: { + x: 10, + y: 10, + z: 10 + }, flyingAllowed: true, ghostingAllowed: true, filter: "", @@ -359,7 +368,8 @@ const DEFAULT_ENTITY_PROPERTIES = { intensity: 1.0, direction: { x: 0.0, - y: Math.PI / 4, + y: -0.707106769084930, // 45 degrees + z: 0.7071067690849304 }, castShadows: true }, @@ -376,7 +386,7 @@ const DEFAULT_ENTITY_PROPERTIES = { hazeColor: { red: 128, green: 154, - blue: 129 + blue: 179 }, hazeBackgroundBlend: 0, hazeEnableGlare: false, @@ -389,7 +399,6 @@ const DEFAULT_ENTITY_PROPERTIES = { bloomMode: "inherit" }, Model: { - modelURL: "", collisionShape: "none", compoundShapeURL: "", animation: { @@ -413,9 +422,14 @@ const DEFAULT_ENTITY_PROPERTIES = { shapeType: "box", collisionless: true, modelURL: IMAGE_MODEL, - textures: JSON.stringify({ "tex.picture": DEFAULT_IMAGE }) + textures: JSON.stringify({ "tex.picture": "" }) }, Web: { + dimensions: { + x: 1.6, + y: 0.9, + z: 0.01 + }, sourceUrl: "https://highfidelity.com/", dpi: 30, }, @@ -462,14 +476,15 @@ const DEFAULT_ENTITY_PROPERTIES = { spinFinish: 0, spinSpread: 0, rotateWithEntity: false, - azimuthStart: 0, - azimuthFinish: 0, - polarStart: -Math.PI, - polarFinish: Math.PI + polarStart: 0, + polarFinish: 0, + azimuthStart: -Math.PI, + azimuthFinish: Math.PI }, Light: { color: { red: 255, green: 255, blue: 255 }, intensity: 5.0, + dimensions: DEFAULT_LIGHT_DIMENSIONS, falloffRadius: 1.0, isSpotlight: false, exponent: 1.0, @@ -874,14 +889,12 @@ var toolBar = (function () { addButton("newLightButton", function () { createNewEntity({ type: "Light", - dimensions: DEFAULT_LIGHT_DIMENSIONS, isSpotlight: false, color: { red: 150, green: 150, blue: 150 }, - constantAttenuation: 1, linearAttenuation: 0, quadraticAttenuation: 0, @@ -893,23 +906,6 @@ var toolBar = (function () { addButton("newTextButton", function () { createNewEntity({ type: "Text", - dimensions: { - x: 0.65, - y: 0.3, - z: 0.01 - }, - backgroundColor: { - red: 64, - green: 64, - blue: 64 - }, - textColor: { - red: 255, - green: 255, - blue: 255 - }, - text: "some text", - lineHeight: 0.06 }); }); @@ -922,76 +918,18 @@ var toolBar = (function () { addButton("newWebButton", function () { createNewEntity({ type: "Web", - dimensions: { - x: 1.6, - y: 0.9, - z: 0.01 - }, - sourceUrl: "https://highfidelity.com/" }); }); addButton("newZoneButton", function () { createNewEntity({ type: "Zone", - dimensions: { - x: 10, - y: 10, - z: 10 - } }); }); addButton("newParticleButton", function () { createNewEntity({ type: "ParticleEffect", - isEmitting: true, - emitterShouldTrail: true, - color: { - red: 200, - green: 200, - blue: 200 - }, - colorSpread: { - red: 0, - green: 0, - blue: 0 - }, - colorStart: { - red: 200, - green: 200, - blue: 200 - }, - colorFinish: { - red: 0, - green: 0, - blue: 0 - }, - emitAcceleration: { - x: -0.5, - y: 2.5, - z: -0.5 - }, - accelerationSpread: { - x: 0.5, - y: 1, - z: 0.5 - }, - emitRate: 5.5, - emitSpeed: 0, - speedSpread: 0, - lifespan: 1.5, - maxParticles: 10, - particleRadius: 0.25, - radiusStart: 0, - radiusFinish: 0.1, - radiusSpread: 0, - alpha: 0, - alphaStart: 1, - alphaFinish: 0, - polarStart: 0, - polarFinish: 0, - textures: "https://content.highfidelity.com/DomainContent/production/Particles/wispy-smoke.png" }); }); From d5dc93e8002d0349e6248576e841bf5f7dcd3f0f Mon Sep 17 00:00:00 2001 From: David Back Date: Mon, 22 Oct 2018 18:31:04 -0700 Subject: [PATCH 188/362] QA fixes and reset server script status --- scripts/system/html/js/entityProperties.js | 95 ++++++++++++---------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index d3e31e4b9b..d4202bd6bc 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -52,6 +52,11 @@ const GROUPS = [ propertyID: "id", readOnly: true, }, + { + label: "Description", + type: "string", + propertyID: "description", + }, { label: "Parent", type: "string", @@ -174,21 +179,21 @@ const GROUPS = [ showPropertyRule: { "keyLightMode": "enabled" }, }, { - label: "Light Altitude", - type: "number", - decimals: 2, - unit: "deg", - propertyID: "keyLight.direction.y", - showPropertyRule: { "keyLightMode": "enabled" }, - }, - { - label: "Light Azimuth", + label: "Light Horizontal Angle", type: "number", decimals: 2, unit: "deg", propertyID: "keyLight.direction.x", showPropertyRule: { "keyLightMode": "enabled" }, }, + { + label: "Light Vertical Angle", + type: "number", + decimals: 2, + unit: "deg", + propertyID: "keyLight.direction.y", + showPropertyRule: { "keyLightMode": "enabled" }, + }, { label: "Cast Shadows", type: "bool", @@ -208,18 +213,11 @@ const GROUPS = [ showPropertyRule: { "skyboxMode": "enabled" }, }, { - label: "Skybox URL", + label: "Skybox Source", type: "string", propertyID: "skybox.url", showPropertyRule: { "skyboxMode": "enabled" }, }, - { - type: "buttons", - buttons: [ { id: "copy", label: "Copy URL To Ambient", - className: "black", onClick: copySkyboxURLToAmbientURL } ], - propertyID: "copyURLToAmbient", - showPropertyRule: { "skyboxMode": "enabled" }, - }, { label: "Ambient Light", type: "dropdown", @@ -237,11 +235,18 @@ const GROUPS = [ showPropertyRule: { "ambientLightMode": "enabled" }, }, { - label: "Ambient URL", + label: "Ambient Source", type: "string", propertyID: "ambientLight.ambientURL", showPropertyRule: { "ambientLightMode": "enabled" }, }, + { + type: "buttons", + buttons: [ { id: "copy", label: "Copy from Skybox", + className: "black", onClick: copySkyboxURLToAmbientURL } ], + propertyID: "copyURLToAmbient", + showPropertyRule: { "skyboxMode": "enabled" }, + }, { label: "Haze", type: "dropdown", @@ -395,16 +400,16 @@ const GROUPS = [ type: "bool", propertyID: "animation.running", }, - { - label: "Allow Transition", - type: "bool", - propertyID: "animation.allowTranslation", - }, { label: "Loop", type: "bool", propertyID: "animation.loop", }, + { + label: "Allow Transition", + type: "bool", + propertyID: "animation.allowTranslation", + }, { label: "Hold", type: "bool", @@ -1109,7 +1114,7 @@ const GROUPS = [ column: 1, }, { // below properties having no column number means place them after two columns div - label: "Can cast shadow", + label: "Cast shadows", type: "bool", propertyID: "canCastShadow", }, @@ -1216,18 +1221,18 @@ const GROUPS = [ ]; const GROUPS_PER_TYPE = { - None: [ 'base', 'spatial', 'collision', 'behavior', 'physics' ], - Shape: [ 'base', 'shape', 'spatial', 'collision', 'behavior', 'physics' ], - Text: [ 'base', 'text', 'spatial', 'collision', 'behavior', 'physics' ], - Zone: [ 'base', 'zone', 'spatial', 'collision', 'behavior', 'physics' ], - Model: [ 'base', 'model', 'spatial', 'collision', 'behavior', 'physics' ], - Image: [ 'base', 'image', 'spatial', 'collision', 'behavior', 'physics' ], - Web: [ 'base', 'web', 'spatial', 'collision', 'behavior', 'physics' ], - Light: [ 'base', 'light', 'spatial', 'collision', 'behavior', 'physics' ], + None: [ 'base', 'spatial', 'behavior', 'collision', 'physics' ], + Shape: [ 'base', 'shape', 'spatial', 'behavior', 'collision', 'physics' ], + Text: [ 'base', 'text', 'spatial', 'behavior', 'collision', 'physics' ], + Zone: [ 'base', 'zone', 'spatial', 'behavior', 'collision', 'physics' ], + Model: [ 'base', 'model', 'spatial', 'behavior', 'collision', 'physics' ], + Image: [ 'base', 'image', 'spatial', 'behavior', 'collision', 'physics' ], + Web: [ 'base', 'web', 'spatial', 'behavior', 'collision', 'physics' ], + Light: [ 'base', 'light', 'spatial', 'behavior', 'collision', 'physics' ], Material: [ 'base', 'material', 'spatial', 'behavior' ], ParticleEffect: [ 'base', 'particles', 'particles_emit', 'particles_size', 'particles_color', 'particles_alpha', 'particles_acceleration', 'particles_spin', 'particles_constraints', 'spatial', 'behavior', 'physics' ], - Multiple: [ 'base', 'spatial', 'collision', 'behavior', 'physics' ], + Multiple: [ 'base', 'spatial', 'behavior', 'collision', 'physics' ], }; const EDITOR_TIMEOUT_DURATION = 1500; @@ -1242,6 +1247,15 @@ const KEY_P = 80; // Key code for letter p used for Parenting hotkey. const MATERIAL_PREFIX_STRING = "mat::"; const PENDING_SCRIPT_STATUS = "[ Fetching status ]"; +const NOT_RUNNING_SCRIPT_STATUS = "Not running"; +const ENTITY_SCRIPT_STATUS = { + pending: "Pending", + loading: "Loading", + error_loading_script: "Error loading script", // eslint-disable-line camelcase + error_running_script: "Error running script", // eslint-disable-line camelcase + running: "Running", + unloaded: "Unloaded" +}; const PROPERTY_NAME_DIVISION = { GROUP: 0, @@ -1441,6 +1455,11 @@ function resetProperties() { } } } + + let elServerScriptError = document.getElementById("property-serverScripts-error"); + let elServerScriptStatus = document.getElementById("property-serverScripts-status"); + elServerScriptError.parentElement.style.display = "none"; + elServerScriptStatus.innerText = NOT_RUNNING_SCRIPT_STATUS; } function showGroupsForType(type) { @@ -2796,17 +2815,9 @@ function loaded() { if (data.statusRetrieved === false) { elServerScriptStatus.innerText = "Failed to retrieve status"; } else if (data.isRunning) { - var ENTITY_SCRIPT_STATUS = { - pending: "Pending", - loading: "Loading", - error_loading_script: "Error loading script", // eslint-disable-line camelcase - error_running_script: "Error running script", // eslint-disable-line camelcase - running: "Running", - unloaded: "Unloaded" - }; elServerScriptStatus.innerText = ENTITY_SCRIPT_STATUS[data.status] || data.status; } else { - elServerScriptStatus.innerText = "Not running"; + elServerScriptStatus.innerText = NOT_RUNNING_SCRIPT_STATUS; } } else if (data.type === "update" && data.selections) { if (data.selections.length === 0) { From 9afb111fbeb5232c56dcc438ea25e7779ea8f71f Mon Sep 17 00:00:00 2001 From: David Back Date: Mon, 22 Oct 2018 18:42:06 -0700 Subject: [PATCH 189/362] add link, fix group order --- scripts/system/html/js/entityProperties.js | 187 +++++++++++---------- 1 file changed, 96 insertions(+), 91 deletions(-) diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index d4202bd6bc..e5bb80ef2c 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -969,6 +969,102 @@ const GROUPS = [ }, ] }, + { + id: "behavior", + label: "BEHAVIOR", + twoColumn: true, + properties: [ + { + label: "Grabbable", + type: "bool", + propertyID: "grab.grabbable", + column: 1, + }, + { + label: "Triggerable", + type: "bool", + propertyID: "grab.triggerable", + column: 2, + }, + { + label: "Cloneable", + type: "bool", + propertyID: "cloneable", + column: 1, + }, + { + label: "Follow Controller", + type: "bool", + propertyID: "grab.grabFollowsController", + column: 2, + }, + { + label: "Clone Lifetime", + type: "number", + unit: "s", + propertyID: "cloneLifetime", + showPropertyRule: { "cloneable": "true" }, + column: 1, + }, + { + label: "Clone Limit", + type: "number", + propertyID: "cloneLimit", + showPropertyRule: { "cloneable": "true" }, + column: 1, + }, + { + label: "Clone Dynamic", + type: "bool", + propertyID: "cloneDynamic", + showPropertyRule: { "cloneable": "true" }, + column: 1, + }, + { + label: "Clone Avatar Entity", + type: "bool", + propertyID: "cloneAvatarEntity", + showPropertyRule: { "cloneable": "true" }, + column: 1, + }, + { // below properties having no column number means place them after two columns div + label: "Cast shadows", + type: "bool", + propertyID: "canCastShadow", + }, + { + label: "Link", + type: "string", + propertyID: "href", + }, + { + label: "Script", + type: "string", + buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadScripts } ], + propertyID: "script", + }, + { + label: "Server Script", + type: "string", + buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadServerScripts } ], + propertyID: "serverScripts", + }, + { + label: "Lifetime", + type: "number", + unit: "s", + propertyID: "lifetime", + }, + { + label: "User Data", + type: "textarea", + buttons: [ { id: "clear", label: "Clear User Data", className: "red", onClick: clearUserData }, + { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONEditor }, + { id: "save", label: "Save User Data", className: "black", onClick: saveUserData } ], + propertyID: "userData", + }, + ] + }, { id: "collision", label: "COLLISION", @@ -1055,97 +1151,6 @@ const GROUPS = [ }, ] }, - { - id: "behavior", - label: "BEHAVIOR", - twoColumn: true, - properties: [ - { - label: "Grabbable", - type: "bool", - propertyID: "grab.grabbable", - column: 1, - }, - { - label: "Triggerable", - type: "bool", - propertyID: "grab.triggerable", - column: 2, - }, - { - label: "Cloneable", - type: "bool", - propertyID: "cloneable", - column: 1, - }, - { - label: "Follow Controller", - type: "bool", - propertyID: "grab.grabFollowsController", - column: 2, - }, - { - label: "Clone Lifetime", - type: "number", - unit: "s", - propertyID: "cloneLifetime", - showPropertyRule: { "cloneable": "true" }, - column: 1, - }, - { - label: "Clone Limit", - type: "number", - propertyID: "cloneLimit", - showPropertyRule: { "cloneable": "true" }, - column: 1, - }, - { - label: "Clone Dynamic", - type: "bool", - propertyID: "cloneDynamic", - showPropertyRule: { "cloneable": "true" }, - column: 1, - }, - { - label: "Clone Avatar Entity", - type: "bool", - propertyID: "cloneAvatarEntity", - showPropertyRule: { "cloneable": "true" }, - column: 1, - }, - { // below properties having no column number means place them after two columns div - label: "Cast shadows", - type: "bool", - propertyID: "canCastShadow", - }, - { - label: "Script", - type: "string", - buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadScripts } ], - propertyID: "script", - }, - { - label: "Server Script", - type: "string", - buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadServerScripts } ], - propertyID: "serverScripts", - }, - { - label: "Lifetime", - type: "number", - unit: "s", - propertyID: "lifetime", - }, - { - label: "User Data", - type: "textarea", - buttons: [ { id: "clear", label: "Clear User Data", className: "red", onClick: clearUserData }, - { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONEditor }, - { id: "save", label: "Save User Data", className: "black", onClick: saveUserData } ], - propertyID: "userData", - }, - ] - }, { id: "physics", label: "PHYSICS", From 794767144fe0a8837c361502d25b12cee5f53a3d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 23 Oct 2018 15:30:33 +1300 Subject: [PATCH 190/362] Fix Interface crash importing JSON with duplicate entity IDs in file --- libraries/entities/src/EntityTree.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 0b3b8abba2..1c201e7b46 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2752,9 +2752,11 @@ bool EntityTree::readFromMap(QVariantMap& map) { success = false; } - const QUuid& cloneOriginID = entity->getCloneOriginID(); - if (!cloneOriginID.isNull()) { - cloneIDs[cloneOriginID].push_back(entity->getEntityItemID()); + if (entity) { + const QUuid& cloneOriginID = entity->getCloneOriginID(); + if (!cloneOriginID.isNull()) { + cloneIDs[cloneOriginID].push_back(entity->getEntityItemID()); + } } } From 8cb09c37eed722dd701afa6b4868581381d03762 Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 22 Oct 2018 21:26:30 -0700 Subject: [PATCH 191/362] changed the ui to the settings/controls --- interface/src/avatar/MyAvatar.cpp | 2 +- interface/src/ui/PreferencesDialog.cpp | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index dd40a748af..c55a257d6a 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -533,7 +533,7 @@ void MyAvatar::update(float deltaTime) { float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); - + qCDebug(interfaceapp) << "lock " << _lockSitStandState.get() << " sit " << _isInSittingState.get() << " hmd lean "<< _hmdLeanRecenterEnabled; // put the average hand azimuth into sensor space. // then mix it with head facing direction to determine rotation recenter if (getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid()) { diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 5eccef5e9d..eeca96222b 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -243,6 +243,29 @@ void setupPreferences() { preference->setIndented(true); preferences->addPreference(preference); } + { + auto getter = [myAvatar]()->int { if (myAvatar->getUserRecenterModel() == MyAvatar::SitStandModelType::Auto) { + return 0; + } else if (myAvatar->getUserRecenterModel() == MyAvatar::SitStandModelType::ForceSit) { + return 1; + } else { + return 2; + }}; + auto setter = [myAvatar](int value) { if (value == 0) { + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::Auto); + } else if (value == 1) { + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::ForceSit); + } else { + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::DisableHMDLean); + }}; + auto preference = new RadioButtonsPreference(VR_MOVEMENT, "Auto / Force Sit / Disable Recenter", getter, setter); + QStringList items; + items << "Auto" << "Force Sitting" << "Disable Recenter"; + preference->setHeading("User Activity mode"); + preference->setItems(items); + preferences->addPreference(preference); + } + { auto getter = [myAvatar]()->int { return myAvatar->getSnapTurn() ? 0 : 1; }; auto setter = [myAvatar](int value) { myAvatar->setSnapTurn(value == 0); }; From 60447bc52176993e408dc189755632e19dc4c21a Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 22 Oct 2018 22:27:16 -0700 Subject: [PATCH 192/362] Adding the missing MetaType on the overlay... --- interface/src/ui/overlays/ModelOverlay.cpp | 1 + interface/src/ui/overlays/Overlay.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 5040842b3b..80fbe0fb95 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -803,5 +803,6 @@ render::ItemKey ModelOverlay::getKey() { if (!_isLODEnabled) { builder.withLODDisabled(); } + return builder.build(); } \ No newline at end of file diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index 22cf924727..1bf94adfa0 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -247,7 +247,7 @@ void Overlay::removeMaterial(graphics::MaterialPointer material, const std::stri } render::ItemKey Overlay::getKey() { - auto builder = render::ItemKey::Builder().withTypeShape(); + auto builder = render::ItemKey::Builder().withTypeShape().withTypeMeta(); builder.withViewSpace(); builder.withLayer(render::hifi::LAYER_2D); From 3e8663f8e9dbcf53fe86a8a4baf33681b0c8c4d1 Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 22 Oct 2018 22:31:26 -0700 Subject: [PATCH 193/362] removed the dropdown from the avatar app, because it is in settings controls now --- interface/resources/qml/hifi/AvatarApp.qml | 1 - .../resources/qml/hifi/avatarapp/Settings.qml | 40 +++---------------- scripts/system/avatarapp.js | 14 +------ 3 files changed, 7 insertions(+), 48 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index d68e5bfcd7..39590748cf 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -252,7 +252,6 @@ Rectangle { var avatarSettings = { dominantHand : settings.dominantHandIsLeft ? 'left' : 'right', collisionsEnabled : settings.avatarCollisionsOn, - userRecenterModel : settings.avatarRecenterModelOn, animGraphOverrideUrl : settings.avatarAnimationOverrideJSON, collisionSoundUrl : settings.avatarCollisionSoundUrl }; diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index ec2b176f54..bad1394133 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -20,7 +20,6 @@ Rectangle { property real scaleValue: scaleSlider.value / 10 property alias dominantHandIsLeft: leftHandRadioButton.checked property alias avatarCollisionsOn: collisionsEnabledRadiobutton.checked - property alias avatarRecenterModelOn: userModelComboBox.currentIndex property alias avatarAnimationOverrideJSON: avatarAnimationUrlInputText.text property alias avatarAnimationJSON: avatarAnimationUrlInputText.placeholderText property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text @@ -49,7 +48,6 @@ Rectangle { avatarAnimationJSON = settings.animGraphUrl; avatarAnimationOverrideJSON = settings.animGraphOverrideUrl; avatarCollisionSoundUrl = settings.collisionSoundUrl; - avatarRecenterModelOn = settings.userRecenterModel; visible = true; } @@ -191,7 +189,7 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right - rows: 3 + rows: 2 rowSpacing: 25 columns: 3 @@ -214,7 +212,7 @@ Rectangle { Layout.row: 0 Layout.column: 1 - Layout.leftMargin: 20 + Layout.leftMargin: -40 ButtonGroup.group: leftRight checked: true @@ -231,7 +229,7 @@ Rectangle { Layout.row: 0 Layout.column: 2 - Layout.rightMargin: -20 + Layout.rightMargin: 20 ButtonGroup.group: leftRight @@ -260,7 +258,7 @@ Rectangle { Layout.row: 1 Layout.column: 1 - Layout.leftMargin: 20 + Layout.leftMargin: -40 ButtonGroup.group: onOff colorScheme: hifi.colorSchemes.light @@ -281,7 +279,7 @@ Rectangle { Layout.row: 1 Layout.column: 2 - Layout.rightMargin: -20 + Layout.rightMargin: 20 ButtonGroup.group: onOff colorScheme: hifi.colorSchemes.light @@ -291,34 +289,6 @@ Rectangle { text: "OFF" boxSize: 20 } - - // TextStyle9 - - RalewaySemiBold { - size: 17; - Layout.row: 2 - Layout.column: 0 - - text: "User Model:" - } - - - // sit stand combo box - HifiControlsUit.ComboBox { - Layout.row: 2 - Layout.column: 1 - id: userModelComboBox - comboBox.textRole: "text" - currentIndex: 2 - model: ListModel { - id: cbItems - ListElement { text: "Force Sitting"; color: "Yellow" } - ListElement { text: "Force Standing"; color: "Green" } - ListElement { text: "Auto Mode"; color: "Brown" } - ListElement { text: "Disable Recentering"; color: "Red" } - } - width: 200 - } } ColumnLayout { diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 0c99460929..ece35acce7 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -64,7 +64,6 @@ function getMyAvatarSettings() { return { dominantHand: MyAvatar.getDominantHand(), collisionsEnabled : MyAvatar.getCollisionsEnabled(), - userRecenterModel: MyAvatar.userRecenterModel, collisionSoundUrl : MyAvatar.collisionSoundURL, animGraphUrl: MyAvatar.getAnimGraphUrl(), animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(), @@ -137,14 +136,6 @@ function onCollisionsEnabledChanged(enabled) { } } -function onUserRecenterModelChanged(modelName) { - if (currentAvatarSettings.userRecenterModel !== modelName) { - currentAvatarSettings.userRecenterModel = modelName; - print("emit user recenter model changed"); - sendToQml({ 'method': 'settingChanged', 'name': 'userRecenterModel', 'value': modelName }) - } -} - function onNewCollisionSoundUrl(url) { if(currentAvatarSettings.collisionSoundUrl !== url) { currentAvatarSettings.collisionSoundUrl = url; @@ -320,11 +311,12 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'saveSettings': MyAvatar.setAvatarScale(message.avatarScale); currentAvatar.avatarScale = message.avatarScale; + MyAvatar.setDominantHand(message.settings.dominantHand); MyAvatar.setCollisionsEnabled(message.settings.collisionsEnabled); - MyAvatar.userRecenterModel = message.settings.userRecenterModel; MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl); + settings = getMyAvatarSettings(); break; default: @@ -497,7 +489,6 @@ function off() { MyAvatar.skeletonModelURLChanged.disconnect(onSkeletonModelURLChanged); MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); MyAvatar.collisionsEnabledChanged.disconnect(onCollisionsEnabledChanged); - MyAvatar.userRecenterModelChanged.disconnect(onUserRecenterModelChanged); MyAvatar.newCollisionSoundURL.disconnect(onNewCollisionSoundUrl); MyAvatar.animGraphUrlChanged.disconnect(onAnimGraphUrlChanged); MyAvatar.targetScaleChanged.disconnect(onTargetScaleChanged); @@ -512,7 +503,6 @@ function on() { MyAvatar.skeletonModelURLChanged.connect(onSkeletonModelURLChanged); MyAvatar.dominantHandChanged.connect(onDominantHandChanged); MyAvatar.collisionsEnabledChanged.connect(onCollisionsEnabledChanged); - MyAvatar.userRecenterModelChanged.connect(onUserRecenterModelChanged); MyAvatar.newCollisionSoundURL.connect(onNewCollisionSoundUrl); MyAvatar.animGraphUrlChanged.connect(onAnimGraphUrlChanged); MyAvatar.targetScaleChanged.connect(onTargetScaleChanged); From 702f6229b87869b118ed2ff8308abebfbc1420a0 Mon Sep 17 00:00:00 2001 From: sam gateau Date: Tue, 23 Oct 2018 08:50:10 -0700 Subject: [PATCH 194/362] Removing the pseudo fix for lod culling that is not ready in this pr --- interface/src/ui/overlays/ModelOverlay.cpp | 43 +--------------------- interface/src/ui/overlays/ModelOverlay.h | 3 -- libraries/render-utils/src/Model.cpp | 31 ---------------- libraries/render-utils/src/Model.h | 3 -- libraries/render/src/render/CullTask.cpp | 4 +- libraries/render/src/render/Item.h | 9 ----- 6 files changed, 4 insertions(+), 89 deletions(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 80fbe0fb95..eee8222051 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -127,11 +127,6 @@ void ModelOverlay::update(float deltatime) { _model->setGroupCulled(_isGroupCulled, scene); metaDirty = true; } - if (_lodEnabledDirty) { - _lodEnabledDirty = false; - _model->setLODEnabled(_isLODEnabled, scene); - metaDirty = true; - } if (metaDirty) { transaction.updateItem(getRenderItemID(), [](Overlay& data) {}); } @@ -196,14 +191,6 @@ void ModelOverlay::setGroupCulled(bool groupCulled) { } } -void ModelOverlay::setLODEnabled(bool lodEnabled) { - if (lodEnabled != _isLODEnabled) { - _isLODEnabled = lodEnabled; - _lodEnabledDirty = true; - } -} - - void ModelOverlay::setProperties(const QVariantMap& properties) { auto origPosition = getWorldPosition(); auto origRotation = getWorldOrientation(); @@ -261,12 +248,6 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { setGroupCulled(groupCulledValue.toBool()); } - auto lodEnabledValue = properties["isLODEnabled"]; - if (lodEnabledValue.isValid() && lodEnabledValue.canConvert(QVariant::Bool)) { - setLODEnabled(lodEnabledValue.toBool()); - } - - // jointNames is read-only. // jointPositions is read-only. // jointOrientations is read-only. @@ -571,24 +552,8 @@ void ModelOverlay::locationChanged(bool tellPhysics) { // FIXME Start using the _renderTransform instead of calling for Transform and Dimensions from here, do the custom things needed in evalRenderTransform() if (_model && _model->isActive()) { - if (!_isLODEnabled) { - auto rot = _model->getRotation(); - auto tra = _model->getTranslation(); - - auto nrot = getWorldOrientation(); - auto ntra = getWorldPosition(); - if (glm::any(glm::notEqual(rot, nrot))) { - rot = nrot; - _model->setRotation(rot); - } - if (glm::any(glm::notEqual(tra, ntra))) { - tra = ntra; - _model->setTranslation(tra); - } - } else { - _model->setRotation(getWorldOrientation()); - _model->setTranslation(getWorldPosition()); - } + _model->setRotation(getWorldOrientation()); + _model->setTranslation(getWorldPosition()); } } @@ -800,9 +765,5 @@ render::ItemKey ModelOverlay::getKey() { if (_isGroupCulled) { builder.withMetaCullGroup(); } - if (!_isLODEnabled) { - builder.withLODDisabled(); - } - return builder.build(); } \ No newline at end of file diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index dc127afe48..bd922e258a 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -69,7 +69,6 @@ public: void setDrawInFront(bool drawInFront) override; void setDrawHUDLayer(bool drawHUDLayer) override; void setGroupCulled(bool groupCulled); - void setLODEnabled(bool lodEnabled); void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) override; void removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName) override; @@ -131,8 +130,6 @@ private: bool _drawInHUDDirty { false }; bool _isGroupCulled { false }; bool _groupCulledDirty { false }; - bool _isLODEnabled{ true }; - bool _lodEnabledDirty{ true }; void processMaterials(); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index da50fd6ff0..53009e8bfa 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -226,10 +226,6 @@ void Model::updateRenderItems() { bool isWireframe = self->isWireframe(); auto renderItemKeyGlobalFlags = self->getRenderItemKeyGlobalFlags(); - if (renderItemKeyGlobalFlags.isLODDisabled()) { - modelTransform.setScale(glm::vec3(1.0f)); - } - render::Transaction transaction; for (int i = 0; i < (int) self->_modelMeshRenderItemIDs.size(); i++) { @@ -249,8 +245,6 @@ void Model::updateRenderItems() { data.updateClusterBuffer(meshState.clusterMatrices); } - auto bound = data.getBound(); - Transform renderTransform = modelTransform; if (useDualQuaternionSkinning) { @@ -268,15 +262,6 @@ void Model::updateRenderItems() { } data.updateTransformForSkinnedMesh(renderTransform, modelTransform); - if (renderItemKeyGlobalFlags.isLODDisabled()) { - auto newBound = data.getBound(); - if (bound != newBound) { - data.updateTransformForSkinnedMesh(renderTransform, modelTransform); - } else { - data.updateTransformForSkinnedMesh(renderTransform, modelTransform); - } - } - data.updateKey(renderItemKeyGlobalFlags); data.setShapeKey(invalidatePayloadShapeKey, isWireframe, useDualQuaternionSkinning); }); @@ -906,12 +891,7 @@ void Model::updateRenderItemsKey(const render::ScenePointer& scene) { _needsFixupInScene = true; return; } - auto prevVal = _needsFixupInScene; auto renderItemsKey = _renderItemKeyGlobalFlags; - if (renderItemsKey.isLODDisabled()) { - _needsFixupInScene = true; - _needsFixupInScene = prevVal; - } render::Transaction transaction; foreach(auto item, _modelMeshRenderItemsMap.keys()) { transaction.updateItem(item, [renderItemsKey](ModelMeshPartPayload& data) { @@ -991,17 +971,6 @@ bool Model::isGroupCulled() const { return _renderItemKeyGlobalFlags.isSubMetaCulled(); } -void Model::setLODEnabled(bool isLODEnabled, const render::ScenePointer& scene) { - if (Model::isLODEnabled() != isLODEnabled) { - auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags); - _renderItemKeyGlobalFlags = (isLODEnabled ? keyBuilder.withLODEnabled() : keyBuilder.withLODDisabled()); - updateRenderItemsKey(scene); - } -} -bool Model::isLODEnabled() const { - return _renderItemKeyGlobalFlags.isLODEnabled(); -} - const render::ItemKey Model::getRenderItemKeyGlobalFlags() const { return _renderItemKeyGlobalFlags; } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 6988e870a5..71809821eb 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -120,9 +120,6 @@ public: bool isGroupCulled() const; void setGroupCulled(bool isGroupCulled, const render::ScenePointer& scene = nullptr); - bool isLODEnabled() const; - void setLODEnabled(bool isLODEnabled, const render::ScenePointer& scene = nullptr); - bool canCastShadow() const; void setCanCastShadow(bool canCastShadow, const render::ScenePointer& scene = nullptr); diff --git a/libraries/render/src/render/CullTask.cpp b/libraries/render/src/render/CullTask.cpp index ad9f6b7076..5e5c6b4c6e 100644 --- a/libraries/render/src/render/CullTask.cpp +++ b/libraries/render/src/render/CullTask.cpp @@ -294,7 +294,7 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext, auto& item = scene->getItem(id); if (filter.test(item.getKey())) { ItemBound itemBound(id, item.getBound()); - if (item.getKey().isLODDisabled() || test.solidAngleTest(itemBound.bound)) { + if (test.solidAngleTest(itemBound.bound)) { outItems.emplace_back(itemBound); if (item.getKey().isMetaCullGroup()) { item.fetchMetaSubItemBounds(outItems, (*scene)); @@ -329,7 +329,7 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext, if (filter.test(item.getKey())) { ItemBound itemBound(id, item.getBound()); if (test.frustumTest(itemBound.bound)) { - if (item.getKey().isLODDisabled() || test.solidAngleTest(itemBound.bound)) { + if (test.solidAngleTest(itemBound.bound)) { outItems.emplace_back(itemBound); if (item.getKey().isMetaCullGroup()) { item.fetchMetaSubItemBounds(outItems, (*scene)); diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h index 9d2c0928e0..a87e2031f9 100644 --- a/libraries/render/src/render/Item.h +++ b/libraries/render/src/render/Item.h @@ -101,7 +101,6 @@ public: SHADOW_CASTER, // Item cast shadows META_CULL_GROUP, // As a meta item, the culling of my sub items is based solely on my bounding box and my visibility in the view SUB_META_CULLED, // As a sub item of a meta render item set as cull group, need to be set to my culling to the meta render it - LOD_DISABLED, // Item LOD behavior is disabled, it won't be LODed because of lod behavior in the view FIRST_TAG_BIT, // 8 Tags available to organize the items and filter them against LAST_TAG_BIT = FIRST_TAG_BIT + NUM_TAGS, @@ -161,8 +160,6 @@ public: Builder& withoutMetaCullGroup() { _flags.reset(META_CULL_GROUP); return (*this); } Builder& withSubMetaCulled() { _flags.set(SUB_META_CULLED); return (*this); } Builder& withoutSubMetaCulled() { _flags.reset(SUB_META_CULLED); return (*this); } - Builder& withLODDisabled() { _flags.set(LOD_DISABLED); return (*this); } - Builder& withLODEnabled() { _flags.reset(LOD_DISABLED); return (*this); } Builder& withTag(Tag tag) { _flags.set(FIRST_TAG_BIT + tag); return (*this); } // Set ALL the tags in one call using the Tag bits @@ -206,9 +203,6 @@ public: bool isSubMetaCulled() const { return _flags[SUB_META_CULLED]; } void setSubMetaCulled(bool metaCulled) { (metaCulled ? _flags.set(SUB_META_CULLED) : _flags.reset(SUB_META_CULLED)); } - bool isLODEnabled() const { return !_flags[LOD_DISABLED]; } - bool isLODDisabled() const { return _flags[LOD_DISABLED]; } - bool isTag(Tag tag) const { return _flags[FIRST_TAG_BIT + tag]; } uint8_t getTagBits() const { return ((_flags.to_ulong() & KEY_TAG_BITS_MASK) >> FIRST_TAG_BIT); } @@ -280,9 +274,6 @@ public: Builder& withoutSubMetaCulled() { _value.reset(ItemKey::SUB_META_CULLED); _mask.set(ItemKey::SUB_META_CULLED); return (*this); } Builder& withSubMetaCulled() { _value.set(ItemKey::SUB_META_CULLED); _mask.set(ItemKey::SUB_META_CULLED); return (*this); } - Builder& withLODEnabled() { _value.reset(ItemKey::LOD_DISABLED); _mask.set(ItemKey::LOD_DISABLED); return (*this); } - Builder& withLODDisabled() { _value.set(ItemKey::LOD_DISABLED); _mask.set(ItemKey::LOD_DISABLED); return (*this); } - Builder& withoutTag(ItemKey::Tag tagIndex) { _value.reset(ItemKey::FIRST_TAG_BIT + tagIndex); _mask.set(ItemKey::FIRST_TAG_BIT + tagIndex); return (*this); } Builder& withTag(ItemKey::Tag tagIndex) { _value.set(ItemKey::FIRST_TAG_BIT + tagIndex); _mask.set(ItemKey::FIRST_TAG_BIT + tagIndex); return (*this); } // Set ALL the tags in one call using the Tag bits and the Tag bits touched From 25213febfde8c915ca44198eb33294e283b9a6ef Mon Sep 17 00:00:00 2001 From: sam gateau Date: Tue, 23 Oct 2018 10:05:10 -0700 Subject: [PATCH 195/362] REmoving the uneeded extra property --- scripts/system/libraries/WebTablet.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index 2ed54d12c8..8e5b41deda 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -133,8 +133,7 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) { dimensions: { x: tabletWidth, y: tabletHeight, z: tabletDepth }, parentID: MyAvatar.SELF_ID, visible: visible, - isGroupCulled: true, - isLODEnabled: false + isGroupCulled: true }; // compute position, rotation & parentJointIndex of the tablet From 39a16703e4c6efafd1e8f2beb4ec0b88964fdbf9 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 23 Oct 2018 10:52:14 -0700 Subject: [PATCH 196/362] updated wording for the radio buttons for avatar leaning model --- interface/src/avatar/MyAvatar.cpp | 2 +- interface/src/ui/PreferencesDialog.cpp | 45 +++++++++++++------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c55a257d6a..dd40a748af 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -533,7 +533,7 @@ void MyAvatar::update(float deltaTime) { float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); - qCDebug(interfaceapp) << "lock " << _lockSitStandState.get() << " sit " << _isInSittingState.get() << " hmd lean "<< _hmdLeanRecenterEnabled; + // put the average hand azimuth into sensor space. // then mix it with head facing direction to determine rotation recenter if (getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid()) { diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index eeca96222b..92628c23b7 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -243,29 +243,6 @@ void setupPreferences() { preference->setIndented(true); preferences->addPreference(preference); } - { - auto getter = [myAvatar]()->int { if (myAvatar->getUserRecenterModel() == MyAvatar::SitStandModelType::Auto) { - return 0; - } else if (myAvatar->getUserRecenterModel() == MyAvatar::SitStandModelType::ForceSit) { - return 1; - } else { - return 2; - }}; - auto setter = [myAvatar](int value) { if (value == 0) { - myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::Auto); - } else if (value == 1) { - myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::ForceSit); - } else { - myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::DisableHMDLean); - }}; - auto preference = new RadioButtonsPreference(VR_MOVEMENT, "Auto / Force Sit / Disable Recenter", getter, setter); - QStringList items; - items << "Auto" << "Force Sitting" << "Disable Recenter"; - preference->setHeading("User Activity mode"); - preference->setItems(items); - preferences->addPreference(preference); - } - { auto getter = [myAvatar]()->int { return myAvatar->getSnapTurn() ? 0 : 1; }; auto setter = [myAvatar](int value) { myAvatar->setSnapTurn(value == 0); }; @@ -282,6 +259,28 @@ void setupPreferences() { auto preference = new CheckPreference(VR_MOVEMENT, "Show room boundaries while teleporting", getter, setter); preferences->addPreference(preference); } + { + auto getter = [myAvatar]()->int { if (myAvatar->getUserRecenterModel() == MyAvatar::SitStandModelType::Auto) { + return 0; + } else if (myAvatar->getUserRecenterModel() == MyAvatar::SitStandModelType::ForceSit) { + return 1; + } else { + return 2; + }}; + auto setter = [myAvatar](int value) { if (value == 0) { + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::Auto); + } else if (value == 1) { + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::ForceSit); + } else { + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::DisableHMDLean); + }}; + auto preference = new RadioButtonsPreference(VR_MOVEMENT, "Auto / Force Sit / Disable Recenter", getter, setter); + QStringList items; + items << "Auto - turns on avatar leaning when standing in real world" << "Seated - disables all avatar leaning while sitting in real world" << "Disabled - allows avatar sitting on the floor [Experimental]"; + preference->setHeading("Avatar leaning behavior"); + preference->setItems(items); + preferences->addPreference(preference); + } { auto getter = [=]()->float { return myAvatar->getUserHeight(); }; auto setter = [=](float value) { myAvatar->setUserHeight(value); }; From 0d39343be4fee9954ef870c091ea6b6270d0191e Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 23 Oct 2018 11:19:47 -0700 Subject: [PATCH 197/362] changed if statement to switch in preferencesDialog.cpp, also removed print statement --- interface/src/ui/PreferencesDialog.cpp | 35 +++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 92628c23b7..2b8f991174 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -260,20 +260,27 @@ void setupPreferences() { preferences->addPreference(preference); } { - auto getter = [myAvatar]()->int { if (myAvatar->getUserRecenterModel() == MyAvatar::SitStandModelType::Auto) { - return 0; - } else if (myAvatar->getUserRecenterModel() == MyAvatar::SitStandModelType::ForceSit) { - return 1; - } else { - return 2; - }}; - auto setter = [myAvatar](int value) { if (value == 0) { - myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::Auto); - } else if (value == 1) { - myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::ForceSit); - } else { - myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::DisableHMDLean); - }}; + auto getter = [myAvatar]()->int { switch (myAvatar->getUserRecenterModel()) { + case MyAvatar::SitStandModelType::Auto: + default: + return 0; + case MyAvatar::SitStandModelType::ForceSit: + return 1; + case MyAvatar::SitStandModelType::DisableHMDLean: + return 2; + }}; + auto setter = [myAvatar](int value) { switch (value) { + case 0: + default: + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::Auto); + break; + case 1: + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::ForceSit); + break; + case 2: + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::DisableHMDLean); + break; + }}; auto preference = new RadioButtonsPreference(VR_MOVEMENT, "Auto / Force Sit / Disable Recenter", getter, setter); QStringList items; items << "Auto - turns on avatar leaning when standing in real world" << "Seated - disables all avatar leaning while sitting in real world" << "Disabled - allows avatar sitting on the floor [Experimental]"; From fa67e1b269fa2c569e478fc68b42b0c8f7377dc6 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Tue, 23 Oct 2018 11:47:50 -0700 Subject: [PATCH 198/362] Remove logs and magic numbers --- interface/src/avatar/AvatarManager.cpp | 4 ---- libraries/animation/src/Rig.cpp | 5 +++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index e12ea67230..dad4c44f4b 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -127,19 +127,15 @@ void AvatarManager::setSpace(workload::SpacePointer& space ) { void AvatarManager::handleTransitAnimations(AvatarTransit::Status status) { switch (status) { case AvatarTransit::Status::STARTED: - qDebug() << "START_FRAME"; _myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("preTransitAnim"); break; case AvatarTransit::Status::START_TRANSIT: - qDebug() << "START_TRANSIT"; _myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("transitAnim"); break; case AvatarTransit::Status::END_TRANSIT: - qDebug() << "END_TRANSIT"; _myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("postTransitAnim"); break; case AvatarTransit::Status::ENDED: - qDebug() << "END_FRAME"; _myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("idleAnim"); break; case AvatarTransit::Status::PRE_TRANSIT: diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index d198550d9a..83d375b55a 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1124,17 +1124,18 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons _internalPoseSet._relativePoses = _animNode->evaluate(_animVars, context, deltaTime, triggersOut); if (_networkNode) { _networkPoseSet._relativePoses = _networkNode->evaluate(_networkVars, context, deltaTime, networkTriggersOut); + const float NETWORK_ANIMATION_BLEND_FRAMES = 6.0f; float alpha = 1.0f; std::shared_ptr clip; if (_networkAnimState.clipNodeEnum == NetworkAnimState::PreTransit) { clip = std::dynamic_pointer_cast(_networkNode->findByName("preTransitAnim")); if (clip) { - alpha = (clip->getFrame() - clip->getStartFrame()) / 6.0f; + alpha = (clip->getFrame() - clip->getStartFrame()) / NETWORK_ANIMATION_BLEND_FRAMES; } } else if (_networkAnimState.clipNodeEnum == NetworkAnimState::PostTransit) { clip = std::dynamic_pointer_cast(_networkNode->findByName("postTransitAnim")); if (clip) { - alpha = (clip->getEndFrame() - clip->getFrame()) / 6.0f; + alpha = (clip->getEndFrame() - clip->getFrame()) / NETWORK_ANIMATION_BLEND_FRAMES; } } if (_sendNetworkNode) { From 2755cbcf4fb792e3816f402e45a5871cb29f3ed8 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 23 Oct 2018 12:00:50 -0700 Subject: [PATCH 199/362] enable interaction with locked web entity --- .../src/RenderableWebEntityItem.cpp | 12 +++--------- .../entities-renderer/src/RenderableWebEntityItem.h | 1 - 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index bc9ac84c91..d7fb20009b 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -108,10 +108,6 @@ bool WebEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointe return true; } - if (_lastLocked != entity->getLocked()) { - return true; - } - return false; } @@ -203,7 +199,6 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene } _lastDPI = entity->getDPI(); - _lastLocked = entity->getLocked(); glm::vec2 windowSize = getWindowSize(entity); _webSurface->resize(QSize(windowSize.x, windowSize.y)); @@ -361,7 +356,7 @@ glm::vec2 WebEntityRenderer::getWindowSize(const TypedEntityPointer& entity) con } void WebEntityRenderer::hoverEnterEntity(const PointerEvent& event) { - if (!_lastLocked && _webSurface) { + if (_webSurface) { PointerEvent webEvent = event; webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _lastDPI)); _webSurface->hoverBeginEvent(webEvent, _touchDevice); @@ -369,7 +364,7 @@ void WebEntityRenderer::hoverEnterEntity(const PointerEvent& event) { } void WebEntityRenderer::hoverLeaveEntity(const PointerEvent& event) { - if (!_lastLocked && _webSurface) { + if (_webSurface) { PointerEvent webEvent = event; webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _lastDPI)); _webSurface->hoverEndEvent(webEvent, _touchDevice); @@ -377,8 +372,7 @@ void WebEntityRenderer::hoverLeaveEntity(const PointerEvent& event) { } void WebEntityRenderer::handlePointerEvent(const PointerEvent& event) { - // Ignore mouse interaction if we're locked - if (!_lastLocked && _webSurface) { + if (_webSurface) { PointerEvent webEvent = event; webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _lastDPI)); _webSurface->handlePointerEvent(webEvent, _touchDevice); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 1ba8ed0ec7..12640f697d 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -65,7 +65,6 @@ private: gpu::TexturePointer _texture; QString _lastSourceUrl; uint16_t _lastDPI; - bool _lastLocked; QTimer _timer; uint64_t _lastRenderTime { 0 }; }; From 78f0afd39ff85a1467c460b4d5d0d9c78bf1dfe3 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 23 Oct 2018 12:13:41 -0700 Subject: [PATCH 200/362] fix crash on shutdown --- libraries/networking/src/ResourceCache.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 076db44ea6..220e487d3a 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -252,7 +252,9 @@ ResourceCache::ResourceCache(QObject* parent) : QObject(parent) { } } -ResourceCache::~ResourceCache() {} +ResourceCache::~ResourceCache() { + clearUnusedResources(); +} void ResourceCache::clearATPAssets() { { From 9bc92cb2a33cf982ff894057a0a65454d6cb7840 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 23 Oct 2018 12:29:04 -0700 Subject: [PATCH 201/362] removed leftover triggers --- interface/src/avatar/MyAvatar.cpp | 4 +--- interface/src/avatar/MyAvatar.h | 23 ----------------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index dd40a748af..78cfed9b30 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3961,6 +3961,7 @@ void MyAvatar::setUserRecenterModel(MyAvatar::SitStandModelType modelName) { setIsSitStandStateLocked(true); break; case MyAvatar::SitStandModelType::Auto: + default: setHMDLeanRecenterEnabled(true); setIsInSittingState(false); setIsSitStandStateLocked(false); @@ -3970,10 +3971,7 @@ void MyAvatar::setUserRecenterModel(MyAvatar::SitStandModelType modelName) { setIsInSittingState(false); setIsSitStandStateLocked(false); break; - default: - break; } - emit userRecenterModelChanged((int)modelName); } void MyAvatar::setIsSitStandStateLocked(bool isLocked) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 0f6f9d9411..8bbf163bd8 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1555,29 +1555,6 @@ signals: */ void disableHandTouchForIDChanged(const QUuid& entityID, bool disable); - /**jsdoc - * Triggered when the sit state is enabled or disabled - * @function MyAvatar.sittingEnabledChanged - * @param {boolean} enabled - * @returns {Signal} - */ - void sittingEnabledChanged(bool enabled); - - /**jsdoc - * Triggered when the recenter model is changed - * @function MyAvatar.userRecenterModelChanged - * @param {int} userRecenteringModeltype - * @returns {Signal} - */ - void userRecenterModelChanged(int modelName); - - /**jsdoc - * Triggered when the sit state is enabled or disabled - * @function MyAvatar.sitStandStateLockEnabledChanged - * @param {boolean} enabled - * @returns {Signal} - */ - void sitStandStateLockEnabledChanged(bool enabled); private slots: void leaveDomain(); From 947391e49eb28140d58047d3d2194ac3a6701348 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 23 Oct 2018 11:44:16 -0700 Subject: [PATCH 202/362] Fix build errors --- interface/src/scripting/TTSScriptingInterface.cpp | 8 ++++++++ interface/src/scripting/TTSScriptingInterface.h | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/interface/src/scripting/TTSScriptingInterface.cpp b/interface/src/scripting/TTSScriptingInterface.cpp index 0cdb24e15d..f51a638471 100644 --- a/interface/src/scripting/TTSScriptingInterface.cpp +++ b/interface/src/scripting/TTSScriptingInterface.cpp @@ -13,6 +13,7 @@ #include "avatar/AvatarManager.h" TTSScriptingInterface::TTSScriptingInterface() { +#ifdef WIN32 // // Create text to speech engine // @@ -36,11 +37,13 @@ TTSScriptingInterface::TTSScriptingInterface() { if (FAILED(hr)) { qDebug() << "Can't set default voice."; } +#endif } TTSScriptingInterface::~TTSScriptingInterface() { } +#ifdef WIN32 class ReleaseOnExit { public: ReleaseOnExit(IUnknown* p) : m_p(p) {} @@ -53,6 +56,7 @@ public: private: IUnknown* m_p; }; +#endif void TTSScriptingInterface::testTone(const bool& alsoInject) { QByteArray byteArray(480000, 0); @@ -81,6 +85,7 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak, const int& sampleRate, const int& bitsPerSample, const bool& alsoInject) { +#ifdef WIN32 WAVEFORMATEX fmt; fmt.wFormatTag = WAVE_FORMAT_PCM; fmt.nSamplesPerSec = sampleRate; @@ -161,6 +166,9 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak, _lastSoundAudioInjector = AudioInjector::playSound(_lastSoundByteArray, options); } +#else + qDebug() << "Text-to-Speech isn't currently supported on non-Windows platforms."; +#endif } void TTSScriptingInterface::stopLastSpeech() { diff --git a/interface/src/scripting/TTSScriptingInterface.h b/interface/src/scripting/TTSScriptingInterface.h index f6eca081ab..7e4f3afa9d 100644 --- a/interface/src/scripting/TTSScriptingInterface.h +++ b/interface/src/scripting/TTSScriptingInterface.h @@ -13,11 +13,14 @@ #include #include +#ifdef WIN32 +#pragma warning(disable : 4996) #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include // SAPI #include // SAPI Helper +#endif #include #include @@ -42,6 +45,7 @@ signals: void clearTTSBuffer(); private: +#ifdef WIN32 class CComAutoInit { public: // Initializes COM using CoInitialize. @@ -82,6 +86,7 @@ private: // Default voice token CComPtr m_voiceToken; +#endif QByteArray _lastSoundByteArray; AudioInjectorPointer _lastSoundAudioInjector; From 178b8ef37b9dfbccecbb56913378128508277d21 Mon Sep 17 00:00:00 2001 From: Alexia Mandeville Date: Tue, 23 Oct 2018 12:55:02 -0700 Subject: [PATCH 203/362] Removed duplicate menu items Removed duplicates: Console... API Debugger --- interface/src/Menu.cpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 91019975cd..5e1c9412e8 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -788,21 +788,6 @@ Menu::Menu() { // Developer > Show Animation Statistics addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::AnimStats); - // Developer > Scripting > Console... - addActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::Console, Qt::CTRL | Qt::ALT | Qt::Key_J, - DependencyManager::get().data(), - SLOT(toggleConsole()), - QAction::NoRole, - UNSPECIFIED_POSITION); - - // Developer > Scripting > API Debugger - action = addActionToQMenuAndActionHash(scriptingOptionsMenu, "API Debugger"); - connect(action, &QAction::triggered, [] { - QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); - defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/utilities/tools/currentAPI.js"); - DependencyManager::get()->loadScript(defaultScriptsLoc.toString()); - }); - // Developer > Log addActionToQMenuAndActionHash(developerMenu, MenuOption::Log, Qt::CTRL | Qt::SHIFT | Qt::Key_L, qApp, SLOT(toggleLogDialog())); From 95b86bf5b7592a174c3ec629fd0c4a26b78dc218 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 23 Oct 2018 13:08:39 -0700 Subject: [PATCH 204/362] initial limited commerce for oculus store prototype --- .../qml/hifi/commerce/purchases/Purchases.qml | 5 +- .../qml/hifi/commerce/wallet/Wallet.qml | 68 +++++++++++++------ scripts/system/commerce/wallet.js | 3 +- scripts/system/html/js/marketplacesInject.js | 1 + 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 015ec3a172..24b365ffc4 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -180,7 +180,8 @@ Rectangle { HifiCommerceCommon.EmulatedMarketplaceHeader { id: titleBarContainer; z: 997; - visible: !needsLogIn.visible; + visible: false; //HRS FIXME !needsLogIn.visible; + height: 100; // HRS FIXME // Size width: parent.width; // Anchors @@ -475,7 +476,7 @@ Rectangle { anchors.left: parent.left; anchors.leftMargin: 16; width: paintedWidth; - text: isShowingMyItems ? "My Items" : "My Purchases"; + text: "Inventory"; // HRS FIXME isShowingMyItems ? "My Items" : "My Purchases"; color: hifi.colors.black; size: 22; } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index cbb77883df..958906a839 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -20,6 +20,7 @@ import "../../../controls" as HifiControls import "../common" as HifiCommerceCommon import "../common/sendAsset" import "../.." as HifiCommon +import "../purchases" as HifiPurchases Rectangle { HifiConstants { id: hifi; } @@ -64,7 +65,7 @@ Rectangle { } } else if (walletStatus === 5) { if (root.activeView !== "walletSetup") { - root.activeView = "walletHome"; + root.activeView = "walletInventory"; // HRS FIXME "walletHome"; Commerce.getSecurityImage(); } } else { @@ -124,7 +125,7 @@ Rectangle { // Title Bar text RalewaySemiBold { id: titleBarText; - text: "WALLET"; + text: "ASSETS"; //"WALLET"; // Text size size: hifi.fontSizes.overlayTitle; // Anchors @@ -342,6 +343,17 @@ Rectangle { } } } + } + + HifiPurchases.Purchases { + id: walletInventory; + visible: root.activeView === "walletInventory"; + anchors.top: titleBarContainer.bottom; + anchors.bottom: tabButtonsContainer.top; + anchors.left: parent.left; + anchors.right: parent.right; + + } HifiCommon.RootHttpRequest { @@ -354,7 +366,6 @@ Rectangle { listModelName: "Send Money Connections"; z: 997; visible: root.activeView === "sendMoney"; - keyboardContainer: root; anchors.fill: parent; parentAppTitleBarHeight: titleBarContainer.height; parentAppNavBarHeight: tabButtonsContainer.height; @@ -449,13 +460,13 @@ Rectangle { visible: !walletSetup.visible; color: root.activeView === "walletHome" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; - anchors.left: parent.left; + anchors.left: exchangeMoneyButtonContainer.right; // HRS FIXME parent.left; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; HiFiGlyphs { id: homeTabIcon; - text: hifi.glyphs.home2; + text: hifi.glyphs.leftRightArrows; // HRS FIXME hifi.glyphs.home2; // Size size: 50; // Anchors @@ -463,11 +474,11 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: -2; // Style - color: root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + color: hifi.colors.lightGray50; // HRS FIXME root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; } RalewaySemiBold { - text: "WALLET HOME"; + text: "RECENT ACTIVITY"; //"WALLET HOME"; // Text size size: 16; // Anchors @@ -478,13 +489,13 @@ Rectangle { anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + color: hifi.colors.lightGray50; // HRS FIXME root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignTop; } - MouseArea { + /* HRS FIXME MouseArea { id: walletHomeTabMouseArea; anchors.fill: parent; hoverEnabled: enabled; @@ -494,22 +505,22 @@ Rectangle { } onEntered: parent.color = hifi.colors.blueHighlight; onExited: parent.color = root.activeView === "walletHome" ? hifi.colors.blueAccent : hifi.colors.black; - } + }*/ } // "EXCHANGE MONEY" tab button Rectangle { id: exchangeMoneyButtonContainer; visible: !walletSetup.visible; - color: hifi.colors.black; + color: root.activeView === "walletInventory" ? hifi.colors.blueAccent : hifi.colors.black; // HRS FIXME hifi.colors.black; anchors.top: parent.top; - anchors.left: walletHomeButtonContainer.right; + anchors.left: parent.left; // FIXME walletHomeButtonContainer.right; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; HiFiGlyphs { id: exchangeMoneyTabIcon; - text: hifi.glyphs.leftRightArrows; + text: hifi.glyphs.home2; // HRS FIXME hifi.glyphs.leftRightArrows; // Size size: 50; // Anchors @@ -517,11 +528,11 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: -2; // Style - color: hifi.colors.lightGray50; + color: root.activeView === "walletInventory" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; // HRS FIXMEhifi.colors.lightGray50; } RalewaySemiBold { - text: "EXCHANGE MONEY"; + text: "INVENTORY"; // HRS FIXME "EXCHANGE MONEY"; // Text size size: 16; // Anchors @@ -532,12 +543,24 @@ Rectangle { anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: hifi.colors.lightGray50; + color: root.activeView === "walletInventory" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; // HRS FIXME hifi.colors.lightGray50; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignTop; } + + MouseArea { + id: inventoryTabMouseArea; + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + root.activeView = "walletInventory"; + tabButtonsContainer.resetTabButtonColors(); + } + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = root.activeView === "walletInventory" ? hifi.colors.blueAccent : hifi.colors.black; + } } @@ -547,7 +570,7 @@ Rectangle { visible: !walletSetup.visible; color: root.activeView === "sendMoney" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; - anchors.left: exchangeMoneyButtonContainer.right; + anchors.left: walletHomeButtonContainer.right; // HRS FIXME exchangeMoneyButtonContainer.right; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; @@ -561,7 +584,7 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: -2; // Style - color: root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + color: hifi.colors.lightGray50; // HRS FIXME root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; } RalewaySemiBold { @@ -576,14 +599,14 @@ Rectangle { anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + color: hifi.colors.lightGray50; // HRS FIXME root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignTop; } - MouseArea { + /* HRS FIXME MouseArea { id: sendMoneyTabMouseArea; anchors.fill: parent; hoverEnabled: enabled; @@ -593,7 +616,7 @@ Rectangle { } onEntered: parent.color = hifi.colors.blueHighlight; onExited: parent.color = root.activeView === "sendMoney" ? hifi.colors.blueAccent : hifi.colors.black; - } + } */ } // "SECURITY" tab button @@ -710,6 +733,7 @@ Rectangle { sendMoneyButtonContainer.color = hifi.colors.black; securityButtonContainer.color = hifi.colors.black; helpButtonContainer.color = hifi.colors.black; + exchangeMoneyButtonContainer.color = hifi.colors.black; // HRS FIXME if (root.activeView === "walletHome") { walletHomeButtonContainer.color = hifi.colors.blueAccent; } else if (root.activeView === "sendMoney") { @@ -718,6 +742,8 @@ Rectangle { securityButtonContainer.color = hifi.colors.blueAccent; } else if (root.activeView === "help") { helpButtonContainer.color = hifi.colors.blueAccent; + } else if (root.activeView == "walletInventory") { // HRS FIXME + exchangeMoneyButtonContainer.color = hifi.colors.blueAccent; } } } diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 5b91afea33..afdd49c0d1 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -559,12 +559,13 @@ function uninstallMarketplaceItemTester() { } } -var BUTTON_NAME = "WALLET"; +var BUTTON_NAME = "ASSETS"; //HRS FIXME "WALLET"; var WALLET_QML_SOURCE = "hifi/commerce/wallet/Wallet.qml"; var ui; function startup() { ui = new AppUi({ buttonName: BUTTON_NAME, + buttonPrefix: "wallet-", sortOrder: 10, home: WALLET_QML_SOURCE, onOpened: walletOpened, diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 24a96023da..0bc0fac13d 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -219,6 +219,7 @@ purchasesElement.style = "height:100%;margin-top:18px;font-weight:bold;float:right;margin-right:" + (dropDownElement.offsetWidth + 30) + "px;position:relative;z-index:999;"; navbarBrandElement.parentNode.insertAdjacentElement('beforeend', purchasesElement); + $('#purchasesButton').css('display', 'none'); // HRS FIXME $('#purchasesButton').on('click', function () { EventBridge.emitWebEvent(JSON.stringify({ type: "PURCHASES", From be7a947ddd85689e67374d6301076e4a2c814044 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 23 Oct 2018 13:25:59 -0700 Subject: [PATCH 205/362] Fix coding-standards issue --- server-console/src/modules/hf-notifications.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js index e1ea508e47..3ee2bd13a4 100644 --- a/server-console/src/modules/hf-notifications.js +++ b/server-console/src/modules/hf-notifications.js @@ -160,8 +160,7 @@ function HifiNotifications(config, menuNotificationCallback) { if ((optUrl.protocol === "hifi") || (optUrl.protocol === "hifiapp")) { StartInterface(options.url); _menuNotificationCallback(options.notificationType, false); - } - else { + } else { shell.openExternal(options.url); } }); From 8807b90474cbb79122de3a4a1d3e720306cd4f9e Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 23 Oct 2018 13:33:14 -0700 Subject: [PATCH 206/362] fix emitRadiusStart --- .../entities/src/ParticleEffectEntityItem.cpp | 27 ++++++++++--------- .../entities/src/ParticleEffectEntityItem.h | 3 ++- .../particle_explorer/particleExplorer.js | 7 +++++ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index 46dcd1b006..acdeac0e93 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -90,10 +90,10 @@ bool operator==(const Properties& a, const Properties& b) { return (a.color == b.color) && (a.alpha == b.alpha) && + (a.radiusStart == b.radiusStart) && (a.radius == b.radius) && (a.spin == b.spin) && (a.rotateWithEntity == b.rotateWithEntity) && - (a.radiusStart == b.radiusStart) && (a.lifespan == b.lifespan) && (a.maxParticles == b.maxParticles) && (a.emission == b.emission) && @@ -117,18 +117,7 @@ bool Properties::valid() const { (alpha.range.start == glm::clamp(alpha.range.start, MINIMUM_ALPHA, MAXIMUM_ALPHA)) && (alpha.range.finish == glm::clamp(alpha.range.finish, MINIMUM_ALPHA, MAXIMUM_ALPHA)) && (alpha.gradient.spread == glm::clamp(alpha.gradient.spread, MINIMUM_ALPHA, MAXIMUM_ALPHA)) && - (lifespan == glm::clamp(lifespan, MINIMUM_LIFESPAN, MAXIMUM_LIFESPAN)) && - (emission.rate == glm::clamp(emission.rate, MINIMUM_EMIT_RATE, MAXIMUM_EMIT_RATE)) && - (emission.speed.target == glm::clamp(emission.speed.target, MINIMUM_EMIT_SPEED, MAXIMUM_EMIT_SPEED)) && - (emission.speed.spread == glm::clamp(emission.speed.spread, MINIMUM_EMIT_SPEED, MAXIMUM_EMIT_SPEED)) && - (emission.dimensions == glm::clamp(emission.dimensions, vec3(MINIMUM_EMIT_DIMENSION), vec3(MAXIMUM_EMIT_DIMENSION))) && (radiusStart == glm::clamp(radiusStart, MINIMUM_EMIT_RADIUS_START, MAXIMUM_EMIT_RADIUS_START)) && - (polar.start == glm::clamp(polar.start, MINIMUM_POLAR, MAXIMUM_POLAR)) && - (polar.finish == glm::clamp(polar.finish, MINIMUM_POLAR, MAXIMUM_POLAR)) && - (azimuth.start == glm::clamp(azimuth.start, MINIMUM_AZIMUTH, MAXIMUM_AZIMUTH)) && - (azimuth.finish == glm::clamp(azimuth.finish, MINIMUM_AZIMUTH, MAXIMUM_AZIMUTH)) && - (emission.acceleration.target == glm::clamp(emission.acceleration.target, vec3(MINIMUM_EMIT_ACCELERATION), vec3(MAXIMUM_EMIT_ACCELERATION))) && - (emission.acceleration.spread == glm::clamp(emission.acceleration.spread, vec3(MINIMUM_ACCELERATION_SPREAD), vec3(MAXIMUM_ACCELERATION_SPREAD))) && (radius.gradient.target == glm::clamp(radius.gradient.target, MINIMUM_PARTICLE_RADIUS, MAXIMUM_PARTICLE_RADIUS)) && (radius.range.start == glm::clamp(radius.range.start, MINIMUM_PARTICLE_RADIUS, MAXIMUM_PARTICLE_RADIUS)) && (radius.range.finish == glm::clamp(radius.range.finish, MINIMUM_PARTICLE_RADIUS, MAXIMUM_PARTICLE_RADIUS)) && @@ -136,7 +125,19 @@ bool Properties::valid() const { (spin.gradient.target == glm::clamp(spin.gradient.target, MINIMUM_PARTICLE_SPIN, MAXIMUM_PARTICLE_SPIN)) && (spin.range.start == glm::clamp(spin.range.start, MINIMUM_PARTICLE_SPIN, MAXIMUM_PARTICLE_SPIN)) && (spin.range.finish == glm::clamp(spin.range.finish, MINIMUM_PARTICLE_SPIN, MAXIMUM_PARTICLE_SPIN)) && - (spin.gradient.spread == glm::clamp(spin.gradient.spread, MINIMUM_PARTICLE_SPIN, MAXIMUM_PARTICLE_SPIN)); + (spin.gradient.spread == glm::clamp(spin.gradient.spread, MINIMUM_PARTICLE_SPIN, MAXIMUM_PARTICLE_SPIN)) && + (lifespan == glm::clamp(lifespan, MINIMUM_LIFESPAN, MAXIMUM_LIFESPAN)) && + (maxParticles == glm::clamp(maxParticles, MINIMUM_MAX_PARTICLES, MAXIMUM_MAX_PARTICLES)) && + (emission.rate == glm::clamp(emission.rate, MINIMUM_EMIT_RATE, MAXIMUM_EMIT_RATE)) && + (emission.speed.target == glm::clamp(emission.speed.target, MINIMUM_EMIT_SPEED, MAXIMUM_EMIT_SPEED)) && + (emission.speed.spread == glm::clamp(emission.speed.spread, MINIMUM_EMIT_SPEED, MAXIMUM_EMIT_SPEED)) && + (emission.acceleration.target == glm::clamp(emission.acceleration.target, vec3(MINIMUM_EMIT_ACCELERATION), vec3(MAXIMUM_EMIT_ACCELERATION))) && + (emission.acceleration.spread == glm::clamp(emission.acceleration.spread, vec3(MINIMUM_ACCELERATION_SPREAD), vec3(MAXIMUM_ACCELERATION_SPREAD)) && + (emission.dimensions == glm::clamp(emission.dimensions, vec3(MINIMUM_EMIT_DIMENSION), vec3(MAXIMUM_EMIT_DIMENSION))) && + (polar.start == glm::clamp(polar.start, MINIMUM_POLAR, MAXIMUM_POLAR)) && + (polar.finish == glm::clamp(polar.finish, MINIMUM_POLAR, MAXIMUM_POLAR)) && + (azimuth.start == glm::clamp(azimuth.start, MINIMUM_AZIMUTH, MAXIMUM_AZIMUTH)) && + (azimuth.finish == glm::clamp(azimuth.finish, MINIMUM_AZIMUTH, MAXIMUM_AZIMUTH))); } bool Properties::emitting() const { diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index a89d7afc06..89f1e834ea 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -177,9 +177,10 @@ namespace particle { Properties& operator =(const Properties& other) { color = other.color; alpha = other.alpha; + radiusStart = other.radiusStart; + radius = other.radius; spin = other.spin; rotateWithEntity = other.rotateWithEntity; - radius = other.radius; lifespan = other.lifespan; maxParticles = other.maxParticles; emission = other.emission; diff --git a/scripts/system/particle_explorer/particleExplorer.js b/scripts/system/particle_explorer/particleExplorer.js index f1b7c8600f..b4b888122c 100644 --- a/scripts/system/particle_explorer/particleExplorer.js +++ b/scripts/system/particle_explorer/particleExplorer.js @@ -170,6 +170,13 @@ step: 0.01 } }, + { + id: "emitRadiusStart", + name: "Emit Radius Start", + type: "SliderFloat", + max: 1, + min: 0 + }, { type: "Row" }, From 5cbddfe0c47c9c96d1c739726c72ab9b0427c7ae Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 22 Oct 2018 14:08:22 -0700 Subject: [PATCH 207/362] consider all sockets in findNodeWithAddr --- libraries/networking/src/LimitedNodeList.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 7cae5d8a71..37b914143e 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -1188,7 +1188,9 @@ void LimitedNodeList::sendPeerQueryToIceServer(const HifiSockAddr& iceServerSock SharedNodePointer LimitedNodeList::findNodeWithAddr(const HifiSockAddr& addr) { QReadLocker locker(&_nodeMutex); auto it = std::find_if(std::begin(_nodeHash), std::end(_nodeHash), [&addr](const UUIDNodePair& pair) { - return pair.second->getActiveSocket() ? (*pair.second->getActiveSocket() == addr) : false; + return pair.second->getPublicSocket() == addr + || pair.second->getLocalSocket() == addr + || pair.second->getSymmetricSocket() == addr; }); return (it != std::end(_nodeHash)) ? it->second : SharedNodePointer(); } @@ -1197,8 +1199,8 @@ bool LimitedNodeList::sockAddrBelongsToNode(const HifiSockAddr& sockAddr) { QReadLocker locker(&_nodeMutex); auto it = std::find_if(std::begin(_nodeHash), std::end(_nodeHash), [&sockAddr](const UUIDNodePair& pair) { return pair.second->getPublicSocket() == sockAddr - || pair.second->getLocalSocket() == sockAddr - || pair.second->getSymmetricSocket() == sockAddr; + || pair.second->getLocalSocket() == sockAddr + || pair.second->getSymmetricSocket() == sockAddr; }); return it != std::end(_nodeHash); } From 2df9d6c5e9065fbe9b83eb975dccec98a6273f2c Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 23 Oct 2018 13:37:56 -0700 Subject: [PATCH 208/362] addressed missing changes for luis --- interface/src/avatar/MyAvatar.cpp | 32 +++++++++--------- interface/src/ui/PreferencesDialog.cpp | 46 ++++++++++++++------------ 2 files changed, 42 insertions(+), 36 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 78cfed9b30..71c3ba4fc6 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -530,6 +530,7 @@ void MyAvatar::update(float deltaTime) { const float PERCENTAGE_WEIGHT_HEAD_VS_SHOULDERS_AZIMUTH = 0.0f; // 100 percent shoulders const float COSINE_THIRTY_DEGREES = 0.866f; const float SQUATTY_TIMEOUT = 30.0f; // 30 seconds + const float HEIGHT_FILTER_COEFFICIENT = 0.01f; float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); @@ -559,7 +560,7 @@ void MyAvatar::update(float deltaTime) { controller::Pose newHeightReading = getControllerPoseInSensorFrame(controller::Action::HEAD); if (newHeightReading.isValid()) { int newHeightReadingInCentimeters = glm::floor(newHeightReading.getTranslation().y * CENTIMETERS_PER_METER); - _averageUserHeightSensorSpace = lerp(_averageUserHeightSensorSpace, newHeightReading.getTranslation().y, 0.01f); + _averageUserHeightSensorSpace = lerp(_averageUserHeightSensorSpace, newHeightReading.getTranslation().y, HEIGHT_FILTER_COEFFICIENT); _recentModeReadings.insert(newHeightReadingInCentimeters); setCurrentStandingHeight(computeStandingHeightMode(newHeightReading)); setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); @@ -4232,21 +4233,22 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, co if (myAvatar.getSitStandStateChange()) { returnValue = true; - } - if (myAvatar.getIsInSittingState()) { - if (myAvatar.getIsSitStandStateLocked()) { - returnValue = (offset.y > CYLINDER_TOP); - } - if (offset.y < SITTING_BOTTOM) { - // we recenter more easily when in sitting state. - returnValue = true; - } } else { - // in the standing state - returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); - // finally check for squats in standing - if (_squatDetected) { - returnValue = true; + if (myAvatar.getIsInSittingState()) { + if (myAvatar.getIsSitStandStateLocked()) { + returnValue = (offset.y > CYLINDER_TOP); + } + if (offset.y < SITTING_BOTTOM) { + // we recenter more easily when in sitting state. + returnValue = true; + } + } else { + // in the standing state + returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + // finally check for squats in standing + if (_squatDetected) { + returnValue = true; + } } } return returnValue; diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 2b8f991174..34d80f50cf 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -260,27 +260,31 @@ void setupPreferences() { preferences->addPreference(preference); } { - auto getter = [myAvatar]()->int { switch (myAvatar->getUserRecenterModel()) { - case MyAvatar::SitStandModelType::Auto: - default: - return 0; - case MyAvatar::SitStandModelType::ForceSit: - return 1; - case MyAvatar::SitStandModelType::DisableHMDLean: - return 2; - }}; - auto setter = [myAvatar](int value) { switch (value) { - case 0: - default: - myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::Auto); - break; - case 1: - myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::ForceSit); - break; - case 2: - myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::DisableHMDLean); - break; - }}; + auto getter = [myAvatar]()->int { + switch (myAvatar->getUserRecenterModel()) { + case MyAvatar::SitStandModelType::Auto: + default: + return 0; + case MyAvatar::SitStandModelType::ForceSit: + return 1; + case MyAvatar::SitStandModelType::DisableHMDLean: + return 2; + } + }; + auto setter = [myAvatar](int value) { + switch (value) { + case 0: + default: + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::Auto); + break; + case 1: + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::ForceSit); + break; + case 2: + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::DisableHMDLean); + break; + } + }; auto preference = new RadioButtonsPreference(VR_MOVEMENT, "Auto / Force Sit / Disable Recenter", getter, setter); QStringList items; items << "Auto - turns on avatar leaning when standing in real world" << "Seated - disables all avatar leaning while sitting in real world" << "Disabled - allows avatar sitting on the floor [Experimental]"; From 4c3f5b3bbcd0326a68cdff02b07593fa9242aa88 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 11 Oct 2018 18:43:56 +0200 Subject: [PATCH 209/362] Entity List Context Menu --- scripts/system/html/css/edit-style.css | 39 +++++ scripts/system/html/entityList.html | 1 + scripts/system/html/js/entityList.js | 94 +++++++++++- .../system/html/js/entityListContextMenu.js | 143 ++++++++++++++++++ scripts/system/libraries/entityList.js | 14 +- 5 files changed, 285 insertions(+), 6 deletions(-) create mode 100644 scripts/system/html/js/entityListContextMenu.js diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 8e7b3f1ad5..e7e577a13b 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1801,3 +1801,42 @@ input[type=button]#export { body#entity-list-body { padding-bottom: 0; } + +.context-menu { + display: none; + position: fixed; + color: #000000; + background-color: #afafaf; + padding: 5px 0 5px 0; + cursor: default; +} +.context-menu li { + list-style-type: none; + padding: 0 5px 0 5px; + margin: 0; + white-space: nowrap; +} +.context-menu li:hover { + background-color: #e3e3e3; +} +.context-menu li.separator { + border-top: 1px solid #000000; + margin: 5px 5px; +} +.context-menu li.disabled { + color: #333333; +} +.context-menu li.separator:hover, .context-menu li.disabled:hover { + background-color: #afafaf; +} + +input.rename-entity { + height: 100%; + width: 100%; + border: none; + font-family: FiraSans-SemiBold; + font-size: 15px; + /* need this to show the text cursor when the input field is empty */ + padding-left: 2px; +} + diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index c62c785c99..ae994b56a4 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -16,6 +16,7 @@ + diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 0ced016d26..67187e6a3e 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -70,6 +70,8 @@ var selectedEntities = []; var entityList = null; // The ListView +var entityListContextMenu = new EntityListContextMenu(); + var currentSortColumn = 'type'; var currentSortOrder = ASCENDING_SORT; var isFilterInView = false; @@ -184,7 +186,83 @@ function loaded() { entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow, createRow, updateRow, clearRow, WINDOW_NONVARIABLE_HEIGHT); - + + entityListContextMenu.initialize(); + + + function startRenamingEntity(entityID) { + if (!entitiesByID[entityID] || !entitiesByID[entityID].elRow) { + return; + } + + let elCell = entitiesByID[entityID].elRow.childNodes[COLUMN_INDEX.NAME]; + let elRenameInput = document.createElement("input"); + elRenameInput.setAttribute('class', 'rename-entity'); + elRenameInput.value = entitiesByID[entityID].name; + elRenameInput.onclick = function(event) { + event.stopPropagation(); + }; + elRenameInput.onkeyup = function(keyEvent) { + if (keyEvent.key === "Enter") { + elRenameInput.blur(); + } + }; + + elRenameInput.onblur = function(event) { + let value = elRenameInput.value; + EventBridge.emitWebEvent(JSON.stringify({ + type: 'rename', + entityID: entityID, + name: value + })); + entitiesByID[entityID].name = value; + elCell.innerText = value; + }; + + elCell.innerHTML = ""; + elCell.appendChild(elRenameInput); + + elRenameInput.select(); + } + + entityListContextMenu.setCallback(function(optionName, selectedEntityID) { + switch (optionName) { + case "Copy": + EventBridge.emitWebEvent(JSON.stringify({ type: 'copy' })); + break; + case "Paste": + EventBridge.emitWebEvent(JSON.stringify({ type: 'paste' })); + break; + case "Rename": + startRenamingEntity(selectedEntityID); + break; + case "Duplicate": + EventBridge.emitWebEvent(JSON.stringify({ type: 'duplicate' })); + break; + case "Delete": + EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); + break; + } + }); + + function onRowContextMenu(clickEvent) { + let entityID = this.dataset.entityID; + + if (!selectedEntities.includes(entityID)) { + let selection = [entityID]; + updateSelectedEntities(selection); + + EventBridge.emitWebEvent(JSON.stringify({ + type: "selectionUpdate", + focus: false, + entityIds: selection, + })); + + refreshFooter(); + } + entityListContextMenu.open(clickEvent, entityID); + } + function onRowClicked(clickEvent) { let entityID = this.dataset.entityID; let selection = [entityID]; @@ -221,9 +299,9 @@ function loaded() { } } } else if (!clickEvent.ctrlKey && !clickEvent.shiftKey && selectedEntities.length === 1) { - // if reselecting the same entity then deselect it + // if reselecting the same entity then start renaming it if (selectedEntities[0] === entityID) { - selection = []; + startRenamingEntity(entityID); } } @@ -502,6 +580,7 @@ function loaded() { } row.appendChild(column); } + row.oncontextmenu = onRowContextMenu; row.onclick = onRowClicked; row.ondblclick = onRowDoubleClicked; return row; @@ -672,8 +751,15 @@ function loaded() { augmentSpinButtons(); - // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked document.addEventListener("contextmenu", function (event) { + entityListContextMenu.close.call(entityListContextMenu); + + // Disable default right-click context menu which is not visible in the HMD and makes it seem like the app has locked event.preventDefault(); }, false); + + // close context menu when switching focus to another window + $(window).blur(function(){ + entityListContextMenu.close.call(entityListContextMenu); + }); } diff --git a/scripts/system/html/js/entityListContextMenu.js b/scripts/system/html/js/entityListContextMenu.js new file mode 100644 index 0000000000..59ae2f1f73 --- /dev/null +++ b/scripts/system/html/js/entityListContextMenu.js @@ -0,0 +1,143 @@ +// +// entityListContextMenu.js +// +// exampleContextMenus.js was originally created by David Rowe on 22 Aug 2018. +// Modified to entityListContextMenu.js by Thijs Wenker on 10 Oct 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 +// + +/* eslint-env browser */ +const CONTEXT_MENU_CLASS = "context-menu"; + +/** + * ContextMenu class for EntityList + * @constructor + */ +function EntityListContextMenu() { + this._elContextMenu = null; + this._callback = null; +} + +EntityListContextMenu.prototype = { + + /** + * @private + */ + _elContextMenu: null, + + /** + * @private + */ + _callback: null, + + /** + * @private + */ + _selectedEntityID: null, + + /** + * Close the context menu + */ + close: function() { + if (this.isContextMenuOpen()) { + this._elContextMenu.style.display = "none"; + } + }, + + isContextMenuOpen: function() { + return this._elContextMenu.style.display === "block"; + }, + + /** + * Open the context menu + * @param clickEvent + * @param selectedEntityID + */ + open: function(clickEvent, selectedEntityID) { + this._selectedEntityID = selectedEntityID; + // If the right-clicked item has a context menu open it. + this._elContextMenu.style.display = "block"; + this._elContextMenu.style.left + = Math.min(clickEvent.pageX, document.body.offsetWidth - this._elContextMenu.offsetWidth).toString() + "px"; + this._elContextMenu.style.top + = Math.min(clickEvent.pageY, document.body.clientHeight - this._elContextMenu.offsetHeight).toString() + "px"; + clickEvent.stopPropagation(); + }, + + /** + * Set the event callback + * @param callback + */ + setCallback: function(callback) { + this._callback = callback; + }, + + /** + * Add a labeled item to the context menu + * @param itemLabel + * @param isEnabled + * @private + */ + _addListItem: function(itemLabel, isEnabled) { + let elListItem = document.createElement("li"); + elListItem.innerText = itemLabel; + + if (isEnabled === undefined || isEnabled) { + elListItem.addEventListener("click", function () { + if (this._callback) { + this._callback.call(this, itemLabel, this._selectedEntityID); + } + }.bind(this), false); + } else { + elListItem.setAttribute('class', 'disabled'); + } + this._elContextMenu.appendChild(elListItem); + }, + + /** + * Add a separator item to the context menu + * @private + */ + _addListSeparator: function() { + let elListItem = document.createElement("li"); + elListItem.setAttribute('class', 'separator'); + this._elContextMenu.appendChild(elListItem); + }, + + /** + * Initialize the context menu. + */ + initialize: function() { + this._elContextMenu = document.createElement("ul"); + this._elContextMenu.setAttribute("class", CONTEXT_MENU_CLASS); + document.body.appendChild(this._elContextMenu); + + // TODO: enable Copy, Paste and Duplicate items once implemented + this._addListItem("Copy", false); + this._addListItem("Paste", false); + this._addListSeparator(); + this._addListItem("Rename"); + this._addListItem("Duplicate", false); + this._addListItem("Delete"); + + // Ignore clicks on context menu background or separator. + this._elContextMenu.addEventListener("click", function(event) { + // Sink clicks on context menu background or separator but let context menu item clicks through. + if (event.target.classList.contains(CONTEXT_MENU_CLASS)) { + event.stopPropagation(); + } + }); + + // Provide means to close context menu without clicking menu item. + document.body.addEventListener("click", this.close.bind(this)); + document.body.addEventListener("keydown", function(event) { + // Close context menu with Esc key. + if (this.isContextMenuOpen() && event.key === "Escape") { + this.close(); + } + }.bind(this)); + } +}; diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 30e952723f..d3d1d76725 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -15,7 +15,7 @@ var PROFILING_ENABLED = false; var profileIndent = ''; const PROFILE_NOOP = function(_name, fn, args) { fn.apply(this, args); -} ; +}; PROFILE = !PROFILING_ENABLED ? PROFILE_NOOP : function(name, fn, args) { console.log("PROFILE-Script " + profileIndent + "(" + name + ") Begin"); var previousIndent = profileIndent; @@ -245,7 +245,7 @@ EntityListTool = function(shouldUseEditTabletApp) { Window.saveAsync("Select Where to Save", "", "*.json"); } } else if (data.type === "pal") { - var sessionIds = {}; // Collect the sessionsIds of all selected entitities, w/o duplicates. + var sessionIds = {}; // Collect the sessionsIds of all selected entities, w/o duplicates. selectionManager.selections.forEach(function (id) { var lastEditedBy = Entities.getEntityProperties(id, 'lastEditedBy').lastEditedBy; if (lastEditedBy) { @@ -271,6 +271,16 @@ EntityListTool = function(shouldUseEditTabletApp) { filterInView = data.filterInView === true; } else if (data.type === "radius") { searchRadius = data.radius; + } else if (data.type === "copy") { + Window.alert("Copy is not yet implemented."); + } else if (data.type === "paste") { + Window.alert("Paste is not yet implemented."); + } else if (data.type === "duplicate") { + Window.alert("Duplicate is not yet implemented."); + } else if (data.type === "rename") { + Entities.editEntity(data.entityID, {name: data.name}); + // make sure that the name also gets updated in the properties window + SelectionManager._update(); } }; From e5ef589a3cc24d0f18633ce7302ea7127209acf3 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 12 Oct 2018 21:15:25 +0200 Subject: [PATCH 210/362] style --- scripts/system/html/css/edit-style.css | 5 ++- scripts/system/html/js/entityList.js | 33 +++++++++----- .../system/html/js/entityListContextMenu.js | 45 ++++++++++++++----- 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index e7e577a13b..956ae529ab 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1812,7 +1812,7 @@ body#entity-list-body { } .context-menu li { list-style-type: none; - padding: 0 5px 0 5px; + padding: 4px 18px 4px 18px; margin: 0; white-space: nowrap; } @@ -1820,8 +1820,9 @@ body#entity-list-body { background-color: #e3e3e3; } .context-menu li.separator { - border-top: 1px solid #000000; + border-top: 1px solid #333333; margin: 5px 5px; + padding: 0 0; } .context-menu li.disabled { color: #333333; diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 67187e6a3e..749f750ecb 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -70,7 +70,10 @@ var selectedEntities = []; var entityList = null; // The ListView -var entityListContextMenu = new EntityListContextMenu(); +/** + * @type EntityListContextMenu + */ +var entityListContextMenu = null; var currentSortColumn = 'type'; var currentSortOrder = ASCENDING_SORT; @@ -187,21 +190,24 @@ function loaded() { entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow, createRow, updateRow, clearRow, WINDOW_NONVARIABLE_HEIGHT); - entityListContextMenu.initialize(); + entityListContextMenu = new EntityListContextMenu(); function startRenamingEntity(entityID) { - if (!entitiesByID[entityID] || !entitiesByID[entityID].elRow) { + let entity = entitiesByID[entityID]; + if (!entity || entity.locked || !entity.elRow) { return; } - let elCell = entitiesByID[entityID].elRow.childNodes[COLUMN_INDEX.NAME]; + let elCell = entity.elRow.childNodes[COLUMN_INDEX.NAME]; let elRenameInput = document.createElement("input"); elRenameInput.setAttribute('class', 'rename-entity'); - elRenameInput.value = entitiesByID[entityID].name; - elRenameInput.onclick = function(event) { + elRenameInput.value = entity.name; + let ignoreClicks = function(event) { event.stopPropagation(); }; + elRenameInput.onclick = ignoreClicks; + elRenameInput.ondblclick = ignoreClicks; elRenameInput.onkeyup = function(keyEvent) { if (keyEvent.key === "Enter") { elRenameInput.blur(); @@ -215,7 +221,7 @@ function loaded() { entityID: entityID, name: value })); - entitiesByID[entityID].name = value; + entity.name = value; elCell.innerText = value; }; @@ -260,13 +266,20 @@ function loaded() { refreshFooter(); } - entityListContextMenu.open(clickEvent, entityID); + + let enabledContextMenuItems = []; + if (entitiesByID[entityID] && !entitiesByID[entityID].locked) { + enabledContextMenuItems.push('Rename'); + enabledContextMenuItems.push('Delete'); + } + + entityListContextMenu.open(clickEvent, entityID, enabledContextMenuItems); } function onRowClicked(clickEvent) { let entityID = this.dataset.entityID; let selection = [entityID]; - + if (clickEvent.ctrlKey) { let selectedIndex = selectedEntities.indexOf(entityID); if (selectedIndex >= 0) { @@ -759,7 +772,7 @@ function loaded() { }, false); // close context menu when switching focus to another window - $(window).blur(function(){ + $(window).blur(function() { entityListContextMenu.close.call(entityListContextMenu); }); } diff --git a/scripts/system/html/js/entityListContextMenu.js b/scripts/system/html/js/entityListContextMenu.js index 59ae2f1f73..62fe951ebe 100644 --- a/scripts/system/html/js/entityListContextMenu.js +++ b/scripts/system/html/js/entityListContextMenu.js @@ -19,6 +19,8 @@ const CONTEXT_MENU_CLASS = "context-menu"; function EntityListContextMenu() { this._elContextMenu = null; this._callback = null; + this._listItems = []; + this._initialize(); } EntityListContextMenu.prototype = { @@ -38,6 +40,11 @@ EntityListContextMenu.prototype = { */ _selectedEntityID: null, + /** + * @private + */ + _listItems: null, + /** * Close the context menu */ @@ -55,10 +62,17 @@ EntityListContextMenu.prototype = { * Open the context menu * @param clickEvent * @param selectedEntityID + * @param enabledOptions */ - open: function(clickEvent, selectedEntityID) { + open: function(clickEvent, selectedEntityID, enabledOptions) { this._selectedEntityID = selectedEntityID; - // If the right-clicked item has a context menu open it. + + this._listItems.forEach(function(listItem) { + let enabled = enabledOptions.includes(listItem.label); + listItem.enabled = enabled; + listItem.element.setAttribute('class', enabled ? '' : 'disabled'); + }); + this._elContextMenu.style.display = "block"; this._elContextMenu.style.left = Math.min(clickEvent.pageX, document.body.offsetWidth - this._elContextMenu.offsetWidth).toString() + "px"; @@ -85,15 +99,21 @@ EntityListContextMenu.prototype = { let elListItem = document.createElement("li"); elListItem.innerText = itemLabel; - if (isEnabled === undefined || isEnabled) { - elListItem.addEventListener("click", function () { - if (this._callback) { - this._callback.call(this, itemLabel, this._selectedEntityID); - } - }.bind(this), false); - } else { - elListItem.setAttribute('class', 'disabled'); - } + let listItem = { + label: itemLabel, + element: elListItem, + enabled: false + }; + + elListItem.addEventListener("click", function () { + if (listItem.enabled && this._callback) { + this._callback.call(this, itemLabel, this._selectedEntityID); + } + }.bind(this), false); + + elListItem.setAttribute('class', 'disabled'); + + this._listItems.push(listItem); this._elContextMenu.appendChild(elListItem); }, @@ -109,8 +129,9 @@ EntityListContextMenu.prototype = { /** * Initialize the context menu. + * @private */ - initialize: function() { + _initialize: function() { this._elContextMenu = document.createElement("ul"); this._elContextMenu.setAttribute("class", CONTEXT_MENU_CLASS); document.body.appendChild(this._elContextMenu); From 4069ca2e0c39a51d1b3eb911d62a19439c3c0b9d Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Sat, 20 Oct 2018 02:43:41 +0200 Subject: [PATCH 211/362] CR feedback --- scripts/system/html/js/entityList.js | 11 ++++---- .../system/html/js/entityListContextMenu.js | 27 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 749f750ecb..663acb758c 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -231,8 +231,11 @@ function loaded() { elRenameInput.select(); } - entityListContextMenu.setCallback(function(optionName, selectedEntityID) { + entityListContextMenu.setOnSelectedCallback(function(optionName, selectedEntityID) { switch (optionName) { + case "Cut": + EventBridge.emitWebEvent(JSON.stringify({ type: 'cut' })); + break; case "Copy": EventBridge.emitWebEvent(JSON.stringify({ type: 'copy' })); break; @@ -263,8 +266,6 @@ function loaded() { focus: false, entityIds: selection, })); - - refreshFooter(); } let enabledContextMenuItems = []; @@ -765,7 +766,7 @@ function loaded() { augmentSpinButtons(); document.addEventListener("contextmenu", function (event) { - entityListContextMenu.close.call(entityListContextMenu); + entityListContextMenu.close(); // Disable default right-click context menu which is not visible in the HMD and makes it seem like the app has locked event.preventDefault(); @@ -773,6 +774,6 @@ function loaded() { // close context menu when switching focus to another window $(window).blur(function() { - entityListContextMenu.close.call(entityListContextMenu); + entityListContextMenu.close(); }); } diff --git a/scripts/system/html/js/entityListContextMenu.js b/scripts/system/html/js/entityListContextMenu.js index 62fe951ebe..d71719f252 100644 --- a/scripts/system/html/js/entityListContextMenu.js +++ b/scripts/system/html/js/entityListContextMenu.js @@ -18,7 +18,7 @@ const CONTEXT_MENU_CLASS = "context-menu"; */ function EntityListContextMenu() { this._elContextMenu = null; - this._callback = null; + this._onSelectedCallback = null; this._listItems = []; this._initialize(); } @@ -33,7 +33,7 @@ EntityListContextMenu.prototype = { /** * @private */ - _callback: null, + _onSelectedCallback: null, /** * @private @@ -82,20 +82,19 @@ EntityListContextMenu.prototype = { }, /** - * Set the event callback - * @param callback + * Set the callback for when a menu item is selected + * @param onSelectedCallback */ - setCallback: function(callback) { - this._callback = callback; + setOnSelectedCallback: function(onSelectedCallback) { + this._onSelectedCallback = onSelectedCallback; }, /** * Add a labeled item to the context menu * @param itemLabel - * @param isEnabled * @private */ - _addListItem: function(itemLabel, isEnabled) { + _addListItem: function(itemLabel) { let elListItem = document.createElement("li"); elListItem.innerText = itemLabel; @@ -106,8 +105,8 @@ EntityListContextMenu.prototype = { }; elListItem.addEventListener("click", function () { - if (listItem.enabled && this._callback) { - this._callback.call(this, itemLabel, this._selectedEntityID); + if (listItem.enabled && this._onSelectedCallback) { + this._onSelectedCallback.call(this, itemLabel, this._selectedEntityID); } }.bind(this), false); @@ -136,12 +135,12 @@ EntityListContextMenu.prototype = { this._elContextMenu.setAttribute("class", CONTEXT_MENU_CLASS); document.body.appendChild(this._elContextMenu); - // TODO: enable Copy, Paste and Duplicate items once implemented - this._addListItem("Copy", false); - this._addListItem("Paste", false); + this._addListItem("Cut"); + this._addListItem("Copy"); + this._addListItem("Paste"); this._addListSeparator(); this._addListItem("Rename"); - this._addListItem("Duplicate", false); + this._addListItem("Duplicate"); this._addListItem("Delete"); // Ignore clicks on context menu background or separator. From 3fa4ee0e1ddede8e7905200a179645c750f8cfc8 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Sat, 20 Oct 2018 03:25:52 +0200 Subject: [PATCH 212/362] enable duplicate copy cut paste functionality of context menu --- scripts/system/html/js/entityList.js | 3 ++- scripts/system/libraries/entityList.js | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 663acb758c..d01e68cff2 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -268,8 +268,9 @@ function loaded() { })); } - let enabledContextMenuItems = []; + let enabledContextMenuItems = ['Copy', 'Paste', 'Duplicate']; if (entitiesByID[entityID] && !entitiesByID[entityID].locked) { + enabledContextMenuItems.push('Cut'); enabledContextMenuItems.push('Rename'); enabledContextMenuItems.push('Delete'); } diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index d3d1d76725..3d73f2f115 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -271,12 +271,15 @@ EntityListTool = function(shouldUseEditTabletApp) { filterInView = data.filterInView === true; } else if (data.type === "radius") { searchRadius = data.radius; + } else if (data.type === "cut") { + cutSelectedEntities(); } else if (data.type === "copy") { - Window.alert("Copy is not yet implemented."); + copySelectedEntities(); } else if (data.type === "paste") { - Window.alert("Paste is not yet implemented."); + pasteEntities(); } else if (data.type === "duplicate") { - Window.alert("Duplicate is not yet implemented."); + SelectionManager.duplicateSelection(); + that.sendUpdate(); } else if (data.type === "rename") { Entities.editEntity(data.entityID, {name: data.name}); // make sure that the name also gets updated in the properties window From 4e9f9b3edf9960af9ea82eb784c7cecd631c94e2 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Mon, 22 Oct 2018 22:39:40 +0200 Subject: [PATCH 213/362] comply with copy/paste update --- scripts/system/libraries/entityList.js | 6 +++--- scripts/system/libraries/entitySelectionTool.js | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 3d73f2f115..da31ef8463 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -272,11 +272,11 @@ EntityListTool = function(shouldUseEditTabletApp) { } else if (data.type === "radius") { searchRadius = data.radius; } else if (data.type === "cut") { - cutSelectedEntities(); + SelectionManager.cutSelectedEntities(); } else if (data.type === "copy") { - copySelectedEntities(); + SelectionManager.copySelectedEntities(); } else if (data.type === "paste") { - pasteEntities(); + SelectionManager.pasteEntities(); } else if (data.type === "duplicate") { SelectionManager.duplicateSelection(); that.sendUpdate(); diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 843d3e986f..a117df6fc7 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -353,12 +353,12 @@ SelectionManager = (function() { } return createdEntityIDs; - } + }; that.cutSelectedEntities = function() { - copySelectedEntities(); + that.copySelectedEntities(); deleteSelectedEntities(); - } + }; that.copySelectedEntities = function() { var entityProperties = Entities.getMultipleEntityProperties(that.selections); @@ -434,7 +434,7 @@ SelectionManager = (function() { z: brn.z + entityClipboard.dimensions.z / 2 }; } - } + }; that.pasteEntities = function() { var dimensions = entityClipboard.dimensions; @@ -442,7 +442,7 @@ SelectionManager = (function() { var pastePosition = getPositionToCreateEntity(maxDimension); var deltaPosition = Vec3.subtract(pastePosition, entityClipboard.position); - var copiedProperties = [] + var copiedProperties = []; var ids = []; entityClipboard.entities.forEach(function(originalProperties) { var properties = deepCopy(originalProperties); @@ -475,7 +475,7 @@ SelectionManager = (function() { redo(copiedProperties); undoHistory.pushCommand(undo, copiedProperties, redo, copiedProperties); - } + }; that._update = function(selectionUpdated) { var properties = null; From 68ab5475feb10472c8d1e8ab612d492e72b077c2 Mon Sep 17 00:00:00 2001 From: Alexia Mandeville Date: Tue, 23 Oct 2018 14:13:09 -0700 Subject: [PATCH 214/362] Remove duplicate display crash options --- interface/src/Menu.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 5e1c9412e8..c4835f9741 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -742,9 +742,6 @@ Menu::Menu() { // Developer > Crash >>> MenuWrapper* crashMenu = developerMenu->addMenu("Crash"); - - // Developer > Crash > Display Crash Options - addCheckableActionToQMenuAndActionHash(crashMenu, MenuOption::DisplayCrashOptions, 0, true); // Developer > Crash > Display Crash Options addCheckableActionToQMenuAndActionHash(crashMenu, MenuOption::DisplayCrashOptions, 0, true); From 10ee5ac5b422144a8ac443233f1dcb3ff75f6b02 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 23 Oct 2018 14:46:13 -0700 Subject: [PATCH 215/362] URL protocol returns the protocol string with the ':' attached (how did this work before?!) --- server-console/src/modules/hf-notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js index 3ee2bd13a4..8a812625b4 100644 --- a/server-console/src/modules/hf-notifications.js +++ b/server-console/src/modules/hf-notifications.js @@ -157,7 +157,7 @@ function HifiNotifications(config, menuNotificationCallback) { var _menuNotificationCallback = menuNotificationCallback; notifier.on('click', function (notifierObject, options) { const optUrl = url.parse(options.url); - if ((optUrl.protocol === "hifi") || (optUrl.protocol === "hifiapp")) { + if ((optUrl.protocol === "hifi:") || (optUrl.protocol === "hifiapp:")) { StartInterface(options.url); _menuNotificationCallback(options.notificationType, false); } else { From 83951328300acdb409d6f4c08e74f11f8b1ee880 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 23 Oct 2018 15:11:02 -0700 Subject: [PATCH 216/362] Attempt to resolve final warnings --- libraries/audio-client/src/AudioClient.cpp | 4 ++-- libraries/audio-client/src/AudioClient.h | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 606763e4ab..1ce6c15951 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -186,11 +186,11 @@ AudioClient::AudioClient() : _networkToOutputResampler(NULL), _localToOutputResampler(NULL), _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), _stats(&_receivedAudioStream), - _positionGetter(DEFAULT_POSITION_GETTER), _TTSTimer(this), + _positionGetter(DEFAULT_POSITION_GETTER), _orientationGetter(DEFAULT_ORIENTATION_GETTER), #if defined(Q_OS_ANDROID) _checkInputTimer(this), _isHeadsetPluggedIn(false), #endif - _orientationGetter(DEFAULT_ORIENTATION_GETTER) { + _TTSTimer(this) { // avoid putting a lock in the device callback assert(_localSamplesAvailable.is_lock_free()); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 2e5ef65473..9b50d3eccb 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -293,12 +293,6 @@ private: bool mixLocalAudioInjectors(float* mixBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); - - Mutex _TTSMutex; - QTimer _TTSTimer; - bool _isProcessingTTS {false}; - QByteArray _TTSAudioBuffer; - int _TTSChunkSize = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * 50; #ifdef Q_OS_ANDROID QTimer _checkInputTimer; @@ -464,6 +458,12 @@ private: QTimer* _checkPeakValuesTimer { nullptr }; bool _isRecording { false }; + + Mutex _TTSMutex; + bool _isProcessingTTS { false }; + QByteArray _TTSAudioBuffer; + int _TTSChunkSize = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * 50; + QTimer _TTSTimer; }; From 7707d92a959657052a26fa4a5bd7afb60477ceb7 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 23 Oct 2018 16:07:35 -0700 Subject: [PATCH 217/362] connect wiring on purchases --- interface/resources/qml/hifi/commerce/wallet/Wallet.qml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 958906a839..c2a8258cca 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -352,7 +352,11 @@ Rectangle { anchors.bottom: tabButtonsContainer.top; anchors.left: parent.left; anchors.right: parent.right; - + Connections { + onSendToScript: { + sendToScript(message); + } + } } @@ -822,7 +826,8 @@ Rectangle { // Because we don't have "channels" for sending messages to a specific QML object, the messages are broadcast to all QML Items. If an Item of yours happens to be visible when some script sends a message with a method you don't expect, you'll get "Unrecognized message..." logs. break; default: - console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); + // HRS FIXME console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); + walletInventory.fromScript(message); } } signal sendToScript(var message); From 2b9dcd0875324a53b67d3cb1b1c2224f6938c7ee Mon Sep 17 00:00:00 2001 From: David Back Date: Tue, 23 Oct 2018 18:38:05 -0700 Subject: [PATCH 218/362] fix particle properties and keylight direction, remove columns, fix orderings, fix sliders --- scripts/system/edit.js | 31 +++- scripts/system/html/css/edit-style.css | 93 +++++----- scripts/system/html/js/entityProperties.js | 197 ++++++++++++--------- 3 files changed, 186 insertions(+), 135 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 53df4176fe..739229e7a7 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2182,10 +2182,14 @@ var PropertiesTool = function (opts) { if (entity.properties.rotation !== undefined) { entity.properties.rotation = Quat.safeEulerAngles(entity.properties.rotation); } + if (entity.properties.emitOrientation !== undefined) { + entity.properties.emitOrientation = Quat.safeEulerAngles(entity.properties.emitOrientation); + } if (entity.properties.keyLight !== undefined && entity.properties.keyLight.direction !== undefined) { - entity.properties.keyLight.direction = Vec3.multiply(RADIANS_TO_DEGREES, - Vec3.toPolar(entity.properties.keyLight.direction)); + print("DBACK TEST entity.properties.keyLight.direction pre " + entity.properties.keyLight.direction.x + " " + entity.properties.keyLight.direction.y + " " + entity.properties.keyLight.direction.z); + entity.properties.keyLight.direction = Vec3.toPolar(entity.properties.keyLight.direction); entity.properties.keyLight.direction.z = 0.0; + print("DBACK TEST entity.properties.keyLight.direction post " + entity.properties.keyLight.direction.x + " " + entity.properties.keyLight.direction.y + " " + entity.properties.keyLight.direction.z); } selections.push(entity); } @@ -2220,18 +2224,27 @@ var PropertiesTool = function (opts) { data.properties.angularVelocity = Vec3.ZERO; } if (data.properties.rotation !== undefined) { - var rotation = data.properties.rotation; - data.properties.rotation = Quat.fromPitchYawRollDegrees(rotation.x, rotation.y, rotation.z); + data.properties.rotation = Quat.fromVec3Degrees(data.properties.rotation); + } + if (data.properties.emitOrientation !== undefined) { + data.properties.emitOrientation = Quat.fromVec3Degrees(data.properties.emitOrientation); } if (data.properties.keyLight !== undefined && data.properties.keyLight.direction !== undefined) { - data.properties.keyLight.direction = Vec3.fromPolar( - data.properties.keyLight.direction.x * DEGREES_TO_RADIANS, - data.properties.keyLight.direction.y * DEGREES_TO_RADIANS - ); + var currentKeyLightDirection = Vec3.toPolar(Entities.getEntityProperties(selectionManager.selections[0], ['keyLight.direction']).keyLight.direction); + print("DBACK TEST data.properties.keyLight.direction pre pre " + data.properties.keyLight.direction.x + " " + data.properties.keyLight.direction.y + " " + data.properties.keyLight.direction.z + " currentKeyLightDirection " + currentKeyLightDirection.x + " " + currentKeyLightDirection.y + " " + currentKeyLightDirection.z); + if (data.properties.keyLight.direction.x === undefined) { + data.properties.keyLight.direction.x = currentKeyLightDirection.x; + } + if (data.properties.keyLight.direction.y === undefined) { + data.properties.keyLight.direction.y = currentKeyLightDirection.y; + } + print("DBACK TEST data.properties.keyLight.direction pre " + data.properties.keyLight.direction.x + " " + data.properties.keyLight.direction.y + " " + data.properties.keyLight.direction.z); + data.properties.keyLight.direction = Vec3.fromPolar(data.properties.keyLight.direction.x, data.properties.keyLight.direction.y); + print("DBACK TEST data.properties.keyLight.direction post " + data.properties.keyLight.direction.x + " " + data.properties.keyLight.direction.y + " " + data.properties.keyLight.direction.z); } Entities.editEntity(selectionManager.selections[0], data.properties); if (data.properties.name !== undefined || data.properties.modelURL !== undefined || data.properties.materialURL !== undefined || - data.properties.visible !== undefined || data.properties.locked !== undefined) { + data.properties.visible !== undefined || data.properties.locked !== undefined) { entityListTool.sendUpdate(); } } diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index c4ab00e689..339b2498f9 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -470,6 +470,11 @@ input[type=checkbox]:checked + label:hover { border: 1.5pt solid black; } +#properties-list { + display: flex; + flex-direction: column; +} + #properties-list fieldset { position: relative; /* 0.1px on the top is to prevent margin collapsing between this and it's first child */ @@ -551,6 +556,16 @@ div.section-header:first-child { margin-bottom: -21px; } +#properties-list .sub-section-header { + border-top: none; + box-shadow: none; + margin-top: 8px; +} + +.sub-section-header + .property { + margin-top: 0; +} + hr { border: none; padding-top: 2px; @@ -634,7 +649,7 @@ hr { height: 1.8rem; } -.text label, .url label, .number label, .textarea label, .xy label, .wh label, .rgb label, .xyz label,.pyr label, .dropdown label, .gen label { +.text label, .url label, .number label, .textarea label, .xy label, .wh label, .rgb label, .xyz label, .pyr label, .dropdown label, .gen label { float: left; margin-left: 1px; margin-bottom: 3px; @@ -648,6 +663,10 @@ hr { margin-top: -2px; } +.xy > div, .wh > div, .xyz > div, .pyr > div, .gen > div { + clear: both; +} + .number > input { clear: both; float: left; @@ -656,9 +675,6 @@ hr { clear: both; float: left; } -.xy > div, .wh > div, .xyz > div, .pyr > div, .gen > div { - clear: both; -} .dropdown { position: relative; @@ -1018,6 +1034,10 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { } +body#entity-list-body { + padding-bottom: 0; +} + #entity-list-header { margin-bottom: 36px; } @@ -1054,16 +1074,6 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { position: relative; /* New positioning context. */ } -#footer-text { - float: right; - padding-top: 12px; - padding-right: 22px; -} - -#entity-list-footer { - padding-top: 9px; -} - #search-area { padding-right: 168px; padding-bottom: 24px; @@ -1091,6 +1101,32 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { width: 120px; } +#entity-list-footer { + padding-top: 9px; +} + +#footer-text { + float: right; + padding-top: 12px; + padding-right: 22px; +} + +input[type=button]#export { + height: 38px; + width: 180px; +} + +#no-entities { + display: none; + position: absolute; + top: 80px; + padding: 12px; + font-family: FiraSans-SemiBold; + font-size: 15px; + font-style: italic; + color: #afafaf; +} + #entity-table-scroll { /* Height is set by JavaScript. */ width: 100%; @@ -1418,33 +1454,6 @@ input#property-scale-button-reset { right: -20px; } -#properties-list #collision-info > fieldset:first-of-type { - border-top: none !important; - box-shadow: none; +#div-property-collisionSoundURL[style*="display: none"] + .property { margin-top: 0; } - -#properties-list { - display: flex; - flex-direction: column; -} - -#no-entities { - display: none; - position: absolute; - top: 80px; - padding: 12px; - font-family: FiraSans-SemiBold; - font-size: 15px; - font-style: italic; - color: #afafaf; -} - -input[type=button]#export { - height: 38px; - width: 180px; -} - -body#entity-list-body { - padding-bottom: 0; -} diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index e5bb80ef2c..a3f986ad53 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -181,6 +181,7 @@ const GROUPS = [ { label: "Light Horizontal Angle", type: "number", + multiplier: DEGREES_TO_RADIANS, decimals: 2, unit: "deg", propertyID: "keyLight.direction.x", @@ -189,6 +190,7 @@ const GROUPS = [ { label: "Light Vertical Angle", type: "number", + multiplier: DEGREES_TO_RADIANS, decimals: 2, unit: "deg", propertyID: "keyLight.direction.y", @@ -301,8 +303,8 @@ const GROUPS = [ { label: "Background Blend", type: "slider", - min: 0.0, - max: 1.0, + min: 0, + max: 1, step: 0.01, decimals: 2, propertyID: "haze.hazeBackgroundBlend", @@ -610,6 +612,7 @@ const GROUPS = [ min: 0.01, max: 10, step: 0.01, + decimals: 2, propertyID: "lifespan", }, { @@ -646,6 +649,7 @@ const GROUPS = [ min: 0, max: 5, step: 0.01, + decimals: 2, propertyID: "emitSpeed", }, { @@ -654,6 +658,7 @@ const GROUPS = [ min: 0, max: 5, step: 0.01, + decimals: 2, propertyID: "speedSpread", }, { @@ -662,6 +667,7 @@ const GROUPS = [ vec3Type: "xyz", min: 0, step: 0.01, + round: 100, subLabels: [ "x", "y", "z" ], propertyID: "emitDimensions", }, @@ -669,8 +675,8 @@ const GROUPS = [ label: "Emit Orientation", type: "vec3", vec3Type: "pyr", - min: 0, step: 0.01, + round: 100, subLabels: [ "pitch", "yaw", "roll" ], unit: "deg", propertyID: "emitOrientation", @@ -689,30 +695,38 @@ const GROUPS = [ { label: "Size", type: "slider", + min: 0, max: 4, step: 0.01, + decimals: 2, propertyID: "particleRadius", }, { label: "Size Spread", type: "slider", + min: 0, max: 4, step: 0.01, + decimals: 2, propertyID: "radiusSpread", }, { label: "Size Start", type: "slider", + min: 0, max: 4, step: 0.01, + decimals: 2, propertyID: "radiusStart", fallbackProperty: "particleRadius", }, { label: "Size Finish", type: "slider", + min: 0, max: 4, step: 0.01, + decimals: 2, propertyID: "radiusFinish", fallbackProperty: "particleRadius", }, @@ -757,6 +771,7 @@ const GROUPS = [ min: 0, max: 1, step: 0.01, + decimals: 2, propertyID: "alpha", }, { @@ -765,6 +780,7 @@ const GROUPS = [ min: 0, max: 1, step: 0.01, + decimals: 2, propertyID: "alphaSpread", }, { @@ -773,6 +789,7 @@ const GROUPS = [ min: 0, max: 1, step: 0.01, + decimals: 2, propertyID: "alphaStart", fallbackProperty: "alpha", }, @@ -782,6 +799,7 @@ const GROUPS = [ min: 0, max: 1, step: 0.01, + decimals: 2, propertyID: "alphaFinish", fallbackProperty: "alpha", }, @@ -796,6 +814,7 @@ const GROUPS = [ type: "vec3", vec3Type: "xyz", step: 0.01, + round: 100, subLabels: [ "x", "y", "z" ], propertyID: "emitAcceleration", }, @@ -804,6 +823,7 @@ const GROUPS = [ type: "vec3", vec3Type: "xyz", step: 0.01, + round: 100, subLabels: [ "x", "y", "z" ], propertyID: "accelerationSpread", }, @@ -819,6 +839,7 @@ const GROUPS = [ min: -360, max: 360, step: 1, + decimals: 0, multiplier: DEGREES_TO_RADIANS, unit: "deg", propertyID: "particleSpin", @@ -829,6 +850,7 @@ const GROUPS = [ min: 0, max: 360, step: 1, + decimals: 0, multiplier: DEGREES_TO_RADIANS, unit: "deg", propertyID: "spinSpread", @@ -839,6 +861,7 @@ const GROUPS = [ min: -360, max: 360, step: 1, + decimals: 0, multiplier: DEGREES_TO_RADIANS, unit: "deg", propertyID: "spinStart", @@ -850,6 +873,7 @@ const GROUPS = [ min: -360, max: 360, step: 1, + decimals: 0, multiplier: DEGREES_TO_RADIANS, unit: "deg", propertyID: "spinFinish", @@ -869,9 +893,10 @@ const GROUPS = [ { label: "Horizontal Angle Start", type: "slider", - min: 0, - max: 180, + min: -180, + max: 0, step: 1, + decimals: 0, multiplier: DEGREES_TO_RADIANS, unit: "deg", propertyID: "azimuthStart", @@ -879,9 +904,10 @@ const GROUPS = [ { label: "Horizontal Angle Finish", type: "slider", - min: -180, - max: 0, + min: 0, + max: 180, step: 1, + decimals: 0, multiplier: DEGREES_TO_RADIANS, unit: "deg", propertyID: "azimuthFinish", @@ -892,6 +918,7 @@ const GROUPS = [ min: 0, max: 180, step: 1, + decimals: 0, multiplier: DEGREES_TO_RADIANS, unit: "deg", propertyID: "polarStart", @@ -902,6 +929,7 @@ const GROUPS = [ min: 0, max: 180, step: 1, + decimals: 0, multiplier: DEGREES_TO_RADIANS, unit: "deg", propertyID: "polarFinish", @@ -935,6 +963,7 @@ const GROUPS = [ label: "Dimension", type: "vec3", vec3Type: "xyz", + min: 0, step: 0.1, decimals: 4, subLabels: [ "x", "y", "z" ], @@ -972,31 +1001,16 @@ const GROUPS = [ { id: "behavior", label: "BEHAVIOR", - twoColumn: true, properties: [ { label: "Grabbable", type: "bool", propertyID: "grab.grabbable", - column: 1, - }, - { - label: "Triggerable", - type: "bool", - propertyID: "grab.triggerable", - column: 2, }, { label: "Cloneable", type: "bool", propertyID: "cloneable", - column: 1, - }, - { - label: "Follow Controller", - type: "bool", - propertyID: "grab.grabFollowsController", - column: 2, }, { label: "Clone Lifetime", @@ -1004,30 +1018,36 @@ const GROUPS = [ unit: "s", propertyID: "cloneLifetime", showPropertyRule: { "cloneable": "true" }, - column: 1, }, { label: "Clone Limit", type: "number", propertyID: "cloneLimit", showPropertyRule: { "cloneable": "true" }, - column: 1, }, { label: "Clone Dynamic", type: "bool", propertyID: "cloneDynamic", showPropertyRule: { "cloneable": "true" }, - column: 1, }, { label: "Clone Avatar Entity", type: "bool", propertyID: "cloneAvatarEntity", showPropertyRule: { "cloneable": "true" }, - column: 1, }, - { // below properties having no column number means place them after two columns div + { + label: "Triggerable", + type: "bool", + propertyID: "grab.triggerable", + }, + { + label: "Follow Controller", + type: "bool", + propertyID: "grab.grabFollowsController", + }, + { label: "Cast shadows", type: "bool", propertyID: "canCastShadow", @@ -1068,34 +1088,18 @@ const GROUPS = [ { id: "collision", label: "COLLISION", - twoColumn: true, properties: [ { label: "Collides", type: "bool", inverse: true, propertyID: "collisionless", - column: -1, // before two columns div - }, - { - label: "Dynamic", - type: "bool", - propertyID: "dynamic", - column: -1, // before two columns div }, { label: "Collides With", type: "sub-header", propertyID: "collidesWithHeader", // not actually a property but used for naming/storing this element showPropertyRule: { "collisionless": "false" }, - column: 1, - }, - { - label: "", - type: "sub-header", - propertyID: "collidesWithHeaderHelper", // not actually a property but used for naming/storing this element - showPropertyRule: { "collisionless": "false" }, - column: 2, }, { label: "Static Entities", @@ -1104,16 +1108,6 @@ const GROUPS = [ propertyName: "static", // actual subProperty name subPropertyOf: "collidesWith", showPropertyRule: { "collisionless": "false" }, - column: 1, - }, - { - label: "Dynamic Entities", - type: "bool", - propertyID: "collidesWithDynamic", - propertyName: "dynamic", // actual subProperty name - subPropertyOf: "collidesWith", - showPropertyRule: { "collisionless": "false" }, - column: 2, }, { label: "Kinematic Entities", @@ -1122,7 +1116,14 @@ const GROUPS = [ propertyName: "kinematic", // actual subProperty name subPropertyOf: "collidesWith", showPropertyRule: { "collisionless": "false" }, - column: 1, + }, + { + label: "Dynamic Entities", + type: "bool", + propertyID: "collidesWithDynamic", + propertyName: "dynamic", // actual subProperty name + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, }, { label: "My Avatar", @@ -1131,7 +1132,6 @@ const GROUPS = [ propertyName: "myAvatar", // actual subProperty name subPropertyOf: "collidesWith", showPropertyRule: { "collisionless": "false" }, - column: 2, }, { label: "Other Avatars", @@ -1140,14 +1140,17 @@ const GROUPS = [ propertyName: "otherAvatar", // actual subProperty name subPropertyOf: "collidesWith", showPropertyRule: { "collisionless": "false" }, - column: 1, }, { label: "Collision sound URL", type: "string", propertyID: "collisionSoundURL", showPropertyRule: { "collisionless": "false" }, - // having no column number means place this after two columns div + }, + { + label: "Dynamic", + type: "bool", + propertyID: "dynamic", }, ] }, @@ -1555,15 +1558,12 @@ function createEmitCheckedPropertyUpdateFunction(propertyName, inverse) { }; } -function createEmitNumberPropertyUpdateFunction(propertyName, multiplier, decimals) { +function createEmitNumberPropertyUpdateFunction(propertyName, multiplier) { return function() { - let value = parseFloat(this.value); - if (multiplier !== undefined) { - value *= multiplier; - } - if (decimals !== undefined) { - value = value.toFixed(decimals); + if (multiplier === undefined) { + multiplier = 1; } + let value = parseFloat(this.value) * multiplier; updateProperty(propertyName, value); }; } @@ -1720,7 +1720,7 @@ function createNumberProperty(property, elProperty, elLabel) { elInput.value = defaultValue; } - elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(propertyName, propertyData.muliplier, propertyData.decimals)); + elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(propertyName, propertyData.multiplier, propertyData.decimals)); elProperty.appendChild(elLabel); elProperty.appendChild(elInput); @@ -1760,24 +1760,31 @@ function createSliderProperty(property, elProperty, elLabel) { elSlider.setAttribute("step", propertyData.step); } - elInput.oninput = function (event) { - let value = event.target.value; - elSlider.value = value; + elInput.onchange = function (event) { + let inputValue = event.target.value; + elSlider.value = inputValue; if (propertyData.multiplier !== undefined) { - value *= propertyData.multiplier; + inputValue *= propertyData.multiplier; } - updateProperty(property.name, value); + updateProperty(property.name, inputValue); }; - elInput.onchange = elInput.oninput; elSlider.oninput = function (event) { - let value = event.target.value; - elInput.value = value; - if (propertyData.multiplier !== undefined) { - value *= propertyData.multiplier; + let sliderValue = event.target.value; + if (propertyData.step === 1) { + if (sliderValue > 0) { + elInput.value = Math.floor(sliderValue); + } else { + elInput.value = Math.ceil(sliderValue); + } + } else { + elInput.value = sliderValue; } - updateProperty(property.name, value); + if (propertyData.multiplier !== undefined) { + sliderValue *= propertyData.multiplier; + } + updateProperty(property.name, sliderValue); }; - + elDiv.appendChild(elLabel); elDiv.appendChild(elSlider); elDiv.appendChild(elInput); @@ -2952,8 +2959,15 @@ function loaded() { case 'number': case 'slider': { let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; - let decimals = propertyData.decimals !== undefined ? propertyData.decimals : 0; - property.elInput.value = (propertyValue / multiplier).toFixed(decimals); + let value = propertyValue / multiplier; + if (propertyData.round !== undefined) { + value = Math.round(value.round) / propertyData.round; + } + if (propertyData.decimals !== undefined) { + property.elInput.value = value.toFixed(propertyData.decimals); + } else { + property.elInput.value = value; + } if (property.elSlider !== undefined) { property.elSlider.value = property.elInput.value; } @@ -2962,11 +2976,26 @@ function loaded() { case 'vec3': case 'vec2': { let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; - let decimals = propertyData.decimals !== undefined ? propertyData.decimals : 0; - property.elInputX.value = (propertyValue.x / multiplier).toFixed(decimals); - property.elInputY.value = (propertyValue.y / multiplier).toFixed(decimals); - if (property.elInputZ !== undefined) { - property.elInputZ.value = (propertyValue.z / multiplier).toFixed(decimals); + let valueX = propertyValue.x / multiplier; + let valueY = propertyValue.y / multiplier; + let valueZ = propertyValue.z / multiplier; + if (propertyData.round !== undefined) { + valueX = Math.round(valueX * propertyData.round) / propertyData.round; + valueY = Math.round(valueY * propertyData.round) / propertyData.round; + valueZ = Math.round(valueZ * propertyData.round) / propertyData.round; + } + if (propertyData.decimals !== undefined) { + property.elInputX.value = valueX.toFixed(propertyData.decimals); + property.elInputY.value = valueY.toFixed(propertyData.decimals); + if (property.elInputZ !== undefined) { + property.elInputZ.value = valueZ.toFixed(propertyData.decimals); + } + } else { + property.elInputX.value = valueX; + property.elInputY.value = valueY; + if (property.elInputZ !== undefined) { + property.elInputZ.value = valueZ; + } } break; } From c7ba386be1c85ccbf266484ab81561057aae216d Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 23 Oct 2018 20:56:53 -0700 Subject: [PATCH 219/362] darn prints somehow slipping past me --- scripts/system/edit.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 739229e7a7..287df1fdca 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2186,10 +2186,8 @@ var PropertiesTool = function (opts) { entity.properties.emitOrientation = Quat.safeEulerAngles(entity.properties.emitOrientation); } if (entity.properties.keyLight !== undefined && entity.properties.keyLight.direction !== undefined) { - print("DBACK TEST entity.properties.keyLight.direction pre " + entity.properties.keyLight.direction.x + " " + entity.properties.keyLight.direction.y + " " + entity.properties.keyLight.direction.z); entity.properties.keyLight.direction = Vec3.toPolar(entity.properties.keyLight.direction); entity.properties.keyLight.direction.z = 0.0; - print("DBACK TEST entity.properties.keyLight.direction post " + entity.properties.keyLight.direction.x + " " + entity.properties.keyLight.direction.y + " " + entity.properties.keyLight.direction.z); } selections.push(entity); } @@ -2231,16 +2229,13 @@ var PropertiesTool = function (opts) { } if (data.properties.keyLight !== undefined && data.properties.keyLight.direction !== undefined) { var currentKeyLightDirection = Vec3.toPolar(Entities.getEntityProperties(selectionManager.selections[0], ['keyLight.direction']).keyLight.direction); - print("DBACK TEST data.properties.keyLight.direction pre pre " + data.properties.keyLight.direction.x + " " + data.properties.keyLight.direction.y + " " + data.properties.keyLight.direction.z + " currentKeyLightDirection " + currentKeyLightDirection.x + " " + currentKeyLightDirection.y + " " + currentKeyLightDirection.z); if (data.properties.keyLight.direction.x === undefined) { data.properties.keyLight.direction.x = currentKeyLightDirection.x; } if (data.properties.keyLight.direction.y === undefined) { data.properties.keyLight.direction.y = currentKeyLightDirection.y; } - print("DBACK TEST data.properties.keyLight.direction pre " + data.properties.keyLight.direction.x + " " + data.properties.keyLight.direction.y + " " + data.properties.keyLight.direction.z); data.properties.keyLight.direction = Vec3.fromPolar(data.properties.keyLight.direction.x, data.properties.keyLight.direction.y); - print("DBACK TEST data.properties.keyLight.direction post " + data.properties.keyLight.direction.x + " " + data.properties.keyLight.direction.y + " " + data.properties.keyLight.direction.z); } Entities.editEntity(selectionManager.selections[0], data.properties); if (data.properties.name !== undefined || data.properties.modelURL !== undefined || data.properties.materialURL !== undefined || From 28c062b842e0883a332568e37cbcdb8e408fc77a Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Wed, 24 Oct 2018 11:57:59 -0300 Subject: [PATCH 220/362] Don't show stats in release builds --- interface/resources/qml/+android/Stats.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/+android/Stats.qml b/interface/resources/qml/+android/Stats.qml index 0dcb07e730..e9a2aa47eb 100644 --- a/interface/resources/qml/+android/Stats.qml +++ b/interface/resources/qml/+android/Stats.qml @@ -10,6 +10,7 @@ Item { property int modality: Qt.NonModal implicitHeight: row.height implicitWidth: row.width + visible: false Component.onCompleted: { stats.parentChanged.connect(fill); From 27e0c85b01d799ac4268b82fad863b21bcf57e88 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 5 Oct 2018 22:13:45 +0200 Subject: [PATCH 221/362] entity list auto scroll --- scripts/system/html/js/entityList.js | 35 +++++++++++++++---- scripts/system/html/js/listView.js | 18 ++++++++++ scripts/system/libraries/entityList.js | 7 ++-- .../system/libraries/entitySelectionTool.js | 8 ++--- 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 0ced016d26..0c87470f57 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -194,7 +194,7 @@ function loaded() { if (selectedIndex >= 0) { selection = []; selection = selection.concat(selectedEntities); - selection.splice(selectedIndex, 1) + selection.splice(selectedIndex, 1); } else { selection = selection.concat(selectedEntities); } @@ -453,7 +453,10 @@ function loaded() { } } - function updateSelectedEntities(selectedIDs) { + function updateSelectedEntities(selectedIDs, autoScroll) { + // force autoScroll to be a boolean + autoScroll = !!autoScroll; + let notFound = false; // reset all currently selected entities and their rows first @@ -482,6 +485,26 @@ function loaded() { } }); + if (autoScroll && selectedIDs.length > 0) { + let firstItem = Number.MAX_VALUE; + let lastItem = -1; + let itemFound = false; + visibleEntities.forEach(function(entity, index) { + if (selectedIDs.indexOf(entity.id) !== -1) { + if (firstItem > index) { + firstItem = index; + } + if (lastItem < index) { + lastItem = index; + } + itemFound = true; + } + }); + if (itemFound) { + entityList.scrollToRow(firstItem, lastItem); + } + } + refreshFooter(); return notFound; @@ -640,8 +663,8 @@ function loaded() { data = JSON.parse(data); if (data.type === "clearEntityList") { clearEntities(); - } else if (data.type === "selectionUpdate") { - let notFound = updateSelectedEntities(data.selectedIDs); + } else if (data.type === "selectionUpdate" && data.caller !== "entityList") { + let notFound = updateSelectedEntities(data.selectedIDs, true); if (notFound) { refreshEntities(); } @@ -653,13 +676,13 @@ function loaded() { clearEntities(); } else { updateEntityData(newEntities); - updateSelectedEntities(data.selectedIDs); + updateSelectedEntities(data.selectedIDs, true); } } }); } else if (data.type === "removeEntities" && data.deletedIDs !== undefined && data.selectedIDs !== undefined) { removeEntities(data.deletedIDs); - updateSelectedEntities(data.selectedIDs); + updateSelectedEntities(data.selectedIDs, true); } else if (data.type === "deleted" && data.ids) { removeEntities(data.ids); } diff --git a/scripts/system/html/js/listView.js b/scripts/system/html/js/listView.js index 10dc37efba..d11452e111 100644 --- a/scripts/system/html/js/listView.js +++ b/scripts/system/html/js/listView.js @@ -152,6 +152,24 @@ ListView.prototype = { this.refresh(); } }, + + scrollToRow(rowIndex, lastRowIndex) { + lastRowIndex = lastRowIndex ? lastRowIndex : rowIndex; + let boundingTop = rowIndex * this.rowHeight; + let boundingBottom = (lastRowIndex * this.rowHeight) + this.rowHeight; + if ((boundingBottom - boundingTop) > this.elTableScroll.clientHeight) { + boundingBottom = boundingTop + this.elTableScroll.clientHeight; + } + + let currentVisibleAreaTop = this.elTableScroll.scrollTop; + let currentVisibleAreaBottom = currentVisibleAreaTop + this.elTableScroll.clientHeight; + + if (boundingTop < currentVisibleAreaTop) { + this.elTableScroll.scrollTop = boundingTop; + } else if (boundingBottom > currentVisibleAreaBottom) { + this.elTableScroll.scrollTop = boundingBottom - (this.elTableScroll.clientHeight); + } + }, refresh: function() { // block refreshing before rows are initialized diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 30e952723f..04b19aa280 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -98,7 +98,7 @@ EntityListTool = function(shouldUseEditTabletApp) { that.setVisible(!visible); }; - selectionManager.addEventListener(function() { + selectionManager.addEventListener(function(isSelectionUpdate, caller) { var selectedIDs = []; for (var i = 0; i < selectionManager.selections.length; i++) { @@ -107,7 +107,8 @@ EntityListTool = function(shouldUseEditTabletApp) { emitJSONScriptEvent({ type: 'selectionUpdate', - selectedIDs: selectedIDs + selectedIDs: selectedIDs, + caller: caller }); }); @@ -224,7 +225,7 @@ EntityListTool = function(shouldUseEditTabletApp) { for (var i = 0; i < ids.length; i++) { entityIDs.push(ids[i]); } - selectionManager.setSelections(entityIDs); + selectionManager.setSelections(entityIDs, "entityList"); if (data.focus) { cameraManager.enable(); cameraManager.focus(selectionManager.worldPosition, diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 843d3e986f..4302a531b5 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -136,7 +136,7 @@ SelectionManager = (function() { return that.selections.length > 0; }; - that.setSelections = function(entityIDs) { + that.setSelections = function(entityIDs, caller) { that.selections = []; for (var i = 0; i < entityIDs.length; i++) { var entityID = entityIDs[i]; @@ -144,7 +144,7 @@ SelectionManager = (function() { Selection.addToSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID); } - that._update(true); + that._update(true, caller); }; that.addEntity = function(entityID, toggleSelection) { @@ -477,7 +477,7 @@ SelectionManager = (function() { undoHistory.pushCommand(undo, copiedProperties, redo, copiedProperties); } - that._update = function(selectionUpdated) { + that._update = function(selectionUpdated, caller) { var properties = null; if (that.selections.length === 0) { that.localDimensions = null; @@ -542,7 +542,7 @@ SelectionManager = (function() { for (var j = 0; j < listeners.length; j++) { try { - listeners[j](selectionUpdated === true); + listeners[j](selectionUpdated === true, caller); } catch (e) { print("ERROR: entitySelectionTool.update got exception: " + JSON.stringify(e)); } From 776b8b8fc7161060b037e12e661dedf122897235 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 9 Oct 2018 00:59:53 +0200 Subject: [PATCH 222/362] CR and style fixes --- scripts/system/edit.js | 42 +++++++++---------- scripts/system/html/js/entityList.js | 29 ++++++------- scripts/system/html/js/listView.js | 14 +++++-- scripts/system/libraries/entityList.js | 9 ++-- .../system/libraries/entitySelectionTool.js | 38 ++++++++--------- 5 files changed, 69 insertions(+), 63 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index b911541f79..0d63b92b63 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -385,7 +385,7 @@ var toolBar = (function () { Entities.editEntity(entityID, { position: position }); - selectionManager._update(); + selectionManager._update(false, this); } else if (dimensionsCheckCount < MAX_DIMENSIONS_CHECKS) { Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL); } @@ -397,9 +397,9 @@ var toolBar = (function () { properties.type + " would be out of bounds."); } - selectionManager.clearSelections(); + selectionManager.clearSelections(this); entityListTool.sendUpdate(); - selectionManager.setSelections([entityID]); + selectionManager.setSelections([entityID], this); Window.setFocus(); @@ -550,7 +550,7 @@ var toolBar = (function () { } deletedEntityTimer = Script.setTimeout(function () { if (entitiesToDelete.length > 0) { - selectionManager.removeEntities(entitiesToDelete); + selectionManager.removeEntities(entitiesToDelete, this); } entityListTool.removeEntities(entitiesToDelete, selectionManager.selections); entitiesToDelete = []; @@ -866,7 +866,7 @@ var toolBar = (function () { gridTool.setVisible(false); grid.setEnabled(false); propertiesTool.setVisible(false); - selectionManager.clearSelections(); + selectionManager.clearSelections(this); cameraManager.disable(); selectionDisplay.disableTriggerMapping(); tablet.landscape = false; @@ -994,7 +994,7 @@ function handleOverlaySelectionToolUpdates(channel, message, sender) { var entity = entityIconOverlayManager.findEntity(data.overlayID); if (entity !== null) { - selectionManager.setSelections([entity]); + selectionManager.setSelections([entity], this); } } } @@ -1141,7 +1141,7 @@ function mouseClickEvent(event) { if (result === null || result === undefined) { if (!event.isShifted) { - selectionManager.clearSelections(); + selectionManager.clearSelections(this); } return; } @@ -1193,9 +1193,9 @@ function mouseClickEvent(event) { } if (!event.isShifted) { - selectionManager.setSelections([foundEntity]); + selectionManager.setSelections([foundEntity], this); } else { - selectionManager.addEntity(foundEntity, true); + selectionManager.addEntity(foundEntity, true, this); } if (wantDebug) { @@ -1493,7 +1493,7 @@ function selectAllEtitiesInCurrentSelectionBox(keepIfTouching) { } } } - selectionManager.setSelections(entities); + selectionManager.setSelections(entities, this); } } @@ -1633,7 +1633,7 @@ function deleteSelectedEntities() { } if (savedProperties.length > 0) { - SelectionManager.clearSelections(); + SelectionManager.clearSelections(this); pushCommandForSelections([], savedProperties); entityListTool.deleteEntities(deletedIDs); } @@ -1650,7 +1650,7 @@ function toggleSelectedEntitiesLocked() { }); } entityListTool.sendUpdate(); - selectionManager._update(); + selectionManager._update(false, this); } } @@ -1664,7 +1664,7 @@ function toggleSelectedEntitiesVisible() { }); } entityListTool.sendUpdate(); - selectionManager._update(); + selectionManager._update(false, this); } } @@ -1861,7 +1861,7 @@ function importSVO(importURL) { } if (isActive) { - selectionManager.setSelections(pastedEntityIDs); + selectionManager.setSelections(pastedEntityIDs, this); } } else { Window.notifyEditError("Can't import entities: entities would be out of bounds."); @@ -1909,7 +1909,7 @@ function deleteKey(value) { } function deselectKey(value) { if (value === 0) { // on release - selectionManager.clearSelections(); + selectionManager.clearSelections(this); } } function toggleKey(value) { @@ -2069,7 +2069,7 @@ function applyEntityProperties(data) { // We might be getting an undo while edit.js is disabled. If that is the case, don't set // our selections, causing the edit widgets to display. if (isActive) { - selectionManager.setSelections(selectedEntityIDs); + selectionManager.setSelections(selectedEntityIDs, this); } } @@ -2272,7 +2272,7 @@ var PropertiesTool = function (opts) { } } pushCommandForSelections(); - selectionManager._update(); + selectionManager._update(false, this); } else if (data.type === 'parent') { parentSelectedEntities(); } else if (data.type === 'unparent') { @@ -2301,7 +2301,7 @@ var PropertiesTool = function (opts) { }); } pushCommandForSelections(); - selectionManager._update(); + selectionManager._update(false, this); } } else if (data.action === "moveAllToGrid") { if (selectionManager.hasSelection()) { @@ -2321,7 +2321,7 @@ var PropertiesTool = function (opts) { }); } pushCommandForSelections(); - selectionManager._update(); + selectionManager._update(false, this); } } else if (data.action === "resetToNaturalDimensions") { if (selectionManager.hasSelection()) { @@ -2342,7 +2342,7 @@ var PropertiesTool = function (opts) { } } pushCommandForSelections(); - selectionManager._update(); + selectionManager._update(false, this); } } else if (data.action === "previewCamera") { if (selectionManager.hasSelection()) { @@ -2360,7 +2360,7 @@ var PropertiesTool = function (opts) { }); } pushCommandForSelections(); - selectionManager._update(); + selectionManager._update(false, this); } } else if (data.action === "reloadClientScripts") { if (selectionManager.hasSelection()) { diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 0c87470f57..a75bd7e4b9 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -13,7 +13,7 @@ const DESCENDING_STRING = '▾'; const LOCKED_GLYPH = ""; const VISIBLE_GLYPH = ""; const TRANSPARENCY_GLYPH = ""; -const BAKED_GLYPH = "" +const BAKED_GLYPH = ""; const SCRIPT_GLYPH = "k"; const BYTES_PER_MEGABYTE = 1024 * 1024; const IMAGE_MODEL_NAME = 'default-image-model.fbx'; @@ -54,10 +54,10 @@ const COMPARE_ASCENDING = function(a, b) { } return 1; -} +}; const COMPARE_DESCENDING = function(a, b) { return COMPARE_ASCENDING(b, a); -} +}; // List of all entities var entities = []; @@ -156,22 +156,22 @@ function loaded() { }; elRefresh.onclick = function() { refreshEntities(); - } + }; elToggleLocked.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' })); - } + }; elToggleVisible.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' })); - } + }; elExport.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'export'})); - } + }; elPal.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' })); - } + }; elDelete.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); - } + }; elFilter.onkeyup = refreshEntityList; elFilter.onpaste = refreshEntityList; elFilter.onchange = onFilterChange; @@ -227,7 +227,7 @@ function loaded() { } } - updateSelectedEntities(selection); + updateSelectedEntities(selection, false); EventBridge.emitWebEvent(JSON.stringify({ type: "selectionUpdate", @@ -289,7 +289,7 @@ function loaded() { hasScript: entity.hasScript, elRow: null, // if this entity has a visible row element assigned to it selected: false // if this entity is selected for edit regardless of having a visible row - } + }; entities.push(entityData); entitiesByID[entityData.id] = entityData; @@ -418,7 +418,7 @@ function loaded() { isBaked: document.querySelector('#entity-isBaked .sort-order'), drawCalls: document.querySelector('#entity-drawCalls .sort-order'), hasScript: document.querySelector('#entity-hasScript .sort-order'), - } + }; function setSortColumn(column) { PROFILE("set-sort-column", function() { if (currentSortColumn === column) { @@ -454,9 +454,6 @@ function loaded() { } function updateSelectedEntities(selectedIDs, autoScroll) { - // force autoScroll to be a boolean - autoScroll = !!autoScroll; - let notFound = false; // reset all currently selected entities and their rows first @@ -663,7 +660,7 @@ function loaded() { data = JSON.parse(data); if (data.type === "clearEntityList") { clearEntities(); - } else if (data.type === "selectionUpdate" && data.caller !== "entityList") { + } else if (data.type === "selectionUpdate") { let notFound = updateSelectedEntities(data.selectedIDs, true); if (notFound) { refreshEntities(); diff --git a/scripts/system/html/js/listView.js b/scripts/system/html/js/listView.js index d11452e111..62163ffe16 100644 --- a/scripts/system/html/js/listView.js +++ b/scripts/system/html/js/listView.js @@ -38,7 +38,7 @@ function ListView(elTableBody, elTableScroll, elTableHeaderRow, createRowFunctio this.lastRowShiftScrollTop = 0; this.initialize(); -}; +} ListView.prototype = { getNumRows: function() { @@ -153,9 +153,15 @@ ListView.prototype = { } }, - scrollToRow(rowIndex, lastRowIndex) { - lastRowIndex = lastRowIndex ? lastRowIndex : rowIndex; - let boundingTop = rowIndex * this.rowHeight; + /** + * Scrolls firstRowIndex with least effort, also tries to make the window include the other selections in case lastRowIndex is set. + * In the case that firstRowIndex and lastRowIndex are already within the visible bounds then nothing will happen. + * @param {number} firstRowIndex - The row that will be scrolled to. + * @param {number} lastRowIndex - The last index of the bound. + */ + scrollToRow: function (firstRowIndex, lastRowIndex) { + lastRowIndex = lastRowIndex ? lastRowIndex : firstRowIndex; + let boundingTop = firstRowIndex * this.rowHeight; let boundingBottom = (lastRowIndex * this.rowHeight) + this.rowHeight; if ((boundingBottom - boundingTop) > this.elTableScroll.clientHeight) { boundingBottom = boundingTop + this.elTableScroll.clientHeight; diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 04b19aa280..045990b99b 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -99,6 +99,10 @@ EntityListTool = function(shouldUseEditTabletApp) { }; selectionManager.addEventListener(function(isSelectionUpdate, caller) { + if (caller === that) { + // ignore events that we emitted from the entity list itself + return; + } var selectedIDs = []; for (var i = 0; i < selectionManager.selections.length; i++) { @@ -107,8 +111,7 @@ EntityListTool = function(shouldUseEditTabletApp) { emitJSONScriptEvent({ type: 'selectionUpdate', - selectedIDs: selectedIDs, - caller: caller + selectedIDs: selectedIDs }); }); @@ -225,7 +228,7 @@ EntityListTool = function(shouldUseEditTabletApp) { for (var i = 0; i < ids.length; i++) { entityIDs.push(ids[i]); } - selectionManager.setSelections(entityIDs, "entityList"); + selectionManager.setSelections(entityIDs, that); if (data.focus) { cameraManager.enable(); cameraManager.focus(selectionManager.worldPosition, diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 4302a531b5..73d84cf293 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -40,7 +40,7 @@ SelectionManager = (function() { Messages.messageReceived.connect(handleEntitySelectionToolUpdates); } - // FUNCTION: HANDLE ENTITY SELECTION TOOL UDPATES + // FUNCTION: HANDLE ENTITY SELECTION TOOL UPDATES function handleEntitySelectionToolUpdates(channel, message, sender) { if (channel !== 'entityToolUpdates') { return; @@ -63,7 +63,7 @@ SelectionManager = (function() { if (wantDebug) { print("setting selection to " + messageParsed.entityID); } - that.setSelections([messageParsed.entityID]); + that.setSelections([messageParsed.entityID], that); } } else if (messageParsed.method === "clearSelection") { if (!SelectionDisplay.triggered() || SelectionDisplay.triggeredHand === messageParsed.hand) { @@ -147,7 +147,7 @@ SelectionManager = (function() { that._update(true, caller); }; - that.addEntity = function(entityID, toggleSelection) { + that.addEntity = function(entityID, toggleSelection, caller) { if (entityID) { var idx = -1; for (var i = 0; i < that.selections.length; i++) { @@ -165,7 +165,7 @@ SelectionManager = (function() { } } - that._update(true); + that._update(true, caller); }; function removeEntityByID(entityID) { @@ -176,21 +176,21 @@ SelectionManager = (function() { } } - that.removeEntity = function (entityID) { + that.removeEntity = function (entityID, caller) { removeEntityByID(entityID); - that._update(true); + that._update(true, caller); }; - that.removeEntities = function(entityIDs) { + that.removeEntities = function(entityIDs, caller) { for (var i = 0, length = entityIDs.length; i < length; i++) { removeEntityByID(entityIDs[i]); } - that._update(true); + that._update(true, caller); }; - that.clearSelections = function() { + that.clearSelections = function(caller) { that.selections = []; - that._update(true); + that._update(true, caller); }; that.addChildrenEntities = function(parentEntityID, entityList) { @@ -985,7 +985,7 @@ SelectionDisplay = (function() { that.pressedHand = NO_HAND; that.triggered = function() { return that.triggeredHand !== NO_HAND; - } + }; function pointingAtDesktopWindowOrTablet(hand) { var pointingAtDesktopWindow = (hand === Controller.Standard.RightHand && SelectionManager.pointingAtDesktopWindowRight) || @@ -1032,7 +1032,7 @@ SelectionDisplay = (function() { that.disableTriggerMapping = function() { that.triggerClickMapping.disable(); that.triggerPressMapping.disable(); - } + }; Script.scriptEnding.connect(that.disableTriggerMapping); // FUNCTION DEF(s): Intersection Check Helpers @@ -1234,7 +1234,7 @@ SelectionDisplay = (function() { if (wantDebug) { print(" Trigger SelectionManager::update"); } - SelectionManager._update(); + SelectionManager._update(false, that); if (wantDebug) { print("=============== eST::MouseMoveEvent END ======================="); @@ -1299,7 +1299,7 @@ SelectionDisplay = (function() { lastMouseEvent.isControl = event.isControl; lastMouseEvent.isAlt = event.isAlt; activeTool.onMove(lastMouseEvent); - SelectionManager._update(); + SelectionManager._update(false, this); } }; @@ -1315,7 +1315,7 @@ SelectionDisplay = (function() { lastMouseEvent.isControl = event.isControl; lastMouseEvent.isAlt = event.isAlt; activeTool.onMove(lastMouseEvent); - SelectionManager._update(); + SelectionManager._update(false, this); } }; @@ -2179,7 +2179,7 @@ SelectionDisplay = (function() { } } - SelectionManager._update(); + SelectionManager._update(false, this); } }); } @@ -2301,7 +2301,7 @@ SelectionDisplay = (function() { previousPickRay = pickRay; - SelectionManager._update(); + SelectionManager._update(false, this); } }); } @@ -2488,7 +2488,7 @@ SelectionDisplay = (function() { previousPickRay = pickRay; - SelectionManager._update(); + SelectionManager._update(false, this); } }); } @@ -2599,7 +2599,7 @@ SelectionDisplay = (function() { previousPickRay = pickRay; - SelectionManager._update(); + SelectionManager._update(false, this); } }); } From 8e1cfb5a067bde0d5676ba3d1024d53cc17ba4ec Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Mon, 22 Oct 2018 23:03:26 +0200 Subject: [PATCH 223/362] fix double click selection --- scripts/system/html/js/entityList.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index a75bd7e4b9..55871533e2 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -239,11 +239,16 @@ function loaded() { } function onRowDoubleClicked() { + let selection = [this.dataset.entityID]; + updateSelectedEntities(selection, false); + EventBridge.emitWebEvent(JSON.stringify({ type: "selectionUpdate", focus: true, - entityIds: [this.dataset.entityID], + entityIds: selection, })); + + refreshFooter(); } function decimalMegabytes(number) { From 5f35a324805d68c97748f5ac65c8b7e97282e15d Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Mon, 22 Oct 2018 23:09:41 +0200 Subject: [PATCH 224/362] remove duplicate calls to refresh footer --- scripts/system/html/js/entityList.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 55871533e2..62c8d6ca6f 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -234,8 +234,6 @@ function loaded() { focus: false, entityIds: selection, })); - - refreshFooter(); } function onRowDoubleClicked() { @@ -247,8 +245,6 @@ function loaded() { focus: true, entityIds: selection, })); - - refreshFooter(); } function decimalMegabytes(number) { From ce20b82408dc4b5f7ac38a032e5d5cd9cff82abe Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 24 Oct 2018 10:41:14 -0700 Subject: [PATCH 225/362] fix view this item in my purchases --- scripts/system/marketplaces/marketplaces.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 3085145176..8f334674cb 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -23,7 +23,7 @@ Script.include("/~/system/libraries/connectionUtils.js"); var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml"; var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; var MARKETPLACE_ITEM_TESTER_QML_PATH = "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"; -var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; +var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; // HRS FIXME "hifi/commerce/purchases/Purchases.qml"; var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); From 8ee6a0ceff9527da0fdd73137a267473f5e5ffc9 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 24 Oct 2018 11:37:02 -0700 Subject: [PATCH 226/362] disable priced items --- scripts/system/html/js/marketplacesInject.js | 25 +++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 0bc0fac13d..f24456cffc 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -284,6 +284,8 @@ $(this).attr('href', '#'); } cost = $(this).closest('.col-xs-3').find('.item-cost').text(); + var costInt = parseInt(cost, 10); + var disable = costInt > 0; // HRS FIXME $(this).closest('.col-xs-3').prev().attr("class", 'col-xs-6'); $(this).closest('.col-xs-3').attr("class", 'col-xs-6'); @@ -292,22 +294,27 @@ priceElement.css({ "padding": "3px 5px", "height": "40px", - "background": "linear-gradient(#00b4ef, #0093C5)", + "background": disable ? "grey" : "linear-gradient(#00b4ef, #0093C5)", "color": "#FFF", "font-weight": "600", "line-height": "34px" }); if (parseInt(cost) > 0) { - priceElement.css({ "width": "auto" }); - priceElement.html(' ' + cost); - priceElement.css({ "min-width": priceElement.width() + 30 }); + if (disable) { + priceElement.html('N/A'); + } else { + priceElement.css({ "width": "auto" }); + priceElement.html(' ' + cost); + priceElement.css({ "min-width": priceElement.width() + 30 }); + } } }); // change pricing to GET/BUY on button hover $('body').on('mouseenter', '#price-or-edit .price', function () { + if (disable) { return; } var $this = $(this); var buyString = "BUY"; var getString = "GET"; @@ -332,12 +339,14 @@ }); $('body').on('mouseleave', '#price-or-edit .price', function () { + if (disable) { return; } var $this = $(this); $this.html($this.data('initialHtml')); }); $('.grid-item').find('#price-or-edit').find('a').on('click', function () { + if (disable) { return false; } if ($(this).closest('.grid-item').find('.price').text() === 'invalidated') { return false; } @@ -420,7 +429,12 @@ var href = purchaseButton.attr('href'); purchaseButton.attr('href', '#'); + var cost = $('.item-cost').text(); + var costInt = parseInt(cost, 10); var availability = $.trim($('.item-availability').text()); + if (costInt > 0) { + availability = ''; // HRS FIXME + } if (availability === 'available') { purchaseButton.css({ "background": "linear-gradient(#00b4ef, #0093C5)", @@ -437,7 +451,6 @@ }); } - var cost = $('.item-cost').text(); var type = $('.item-type').text(); var isUpdating = window.location.href.indexOf('edition=') > -1; var urlParams = new URLSearchParams(window.location.search); From 993cb0cfa0392da8be8f94e34ad261d7f5152b26 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 24 Oct 2018 12:00:17 -0700 Subject: [PATCH 227/362] restore separation of inventory vs proofs. --- interface/resources/qml/hifi/commerce/purchases/Purchases.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 24b365ffc4..11c1ffc8c2 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -476,7 +476,7 @@ Rectangle { anchors.left: parent.left; anchors.leftMargin: 16; width: paintedWidth; - text: "Inventory"; // HRS FIXME isShowingMyItems ? "My Items" : "My Purchases"; + text: isShowingMyItems ? "My Items" : "Inventory"; //"My Purchases"; color: hifi.colors.black; size: 22; } From e1e9b7842785e63a1eb4a4684f64343a363cc011 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 24 Oct 2018 12:22:03 -0700 Subject: [PATCH 228/362] filter priced items from marketplace --- scripts/system/html/js/marketplacesInject.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index f24456cffc..21a2acad7d 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -302,7 +302,8 @@ if (parseInt(cost) > 0) { if (disable) { - priceElement.html('N/A'); + priceElement.html('N/A'); // In case the following fails + $(this).parent().parent().parent().parent().parent().css({"display": "none"}); // HRS FIXME } else { priceElement.css({ "width": "auto" }); priceElement.html(' Date: Wed, 24 Oct 2018 12:43:01 -0700 Subject: [PATCH 230/362] please for the love of god --- .../avatars-renderer/src/avatars-renderer/Avatar.cpp | 10 +++++----- .../avatars-renderer/src/avatars-renderer/Avatar.h | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 4a06a5252a..86f5bd69a5 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -707,10 +707,10 @@ static TextRenderer3D* textRenderer(TextRendererType type) { return displayNameRenderer; } -void Avatar::metaBlendshapeOperator(int blendshapeNumber, const QVector& blendshapeOffsets, const QVector& blendedMeshSizes, - const render::ItemIDs& subItemIDs) { +void Avatar::metaBlendshapeOperator(render::ItemID renderItemID, int blendshapeNumber, const QVector& blendshapeOffsets, + const QVector& blendedMeshSizes, const render::ItemIDs& subItemIDs) { render::Transaction transaction; - transaction.updateItem(_renderItemID, [blendshapeNumber, blendshapeOffsets, blendedMeshSizes, + transaction.updateItem(renderItemID, [blendshapeNumber, blendshapeOffsets, blendedMeshSizes, subItemIDs](AvatarData& avatar) { auto avatarPtr = dynamic_cast(&avatar); if (avatarPtr) { @@ -730,7 +730,7 @@ void Avatar::addToScene(AvatarSharedPointer self, const render::ScenePointer& sc _renderBound = getBounds(); transaction.resetItem(_renderItemID, avatarPayloadPointer); using namespace std::placeholders; - _skeletonModel->addToScene(scene, transaction, std::bind(&Avatar::metaBlendshapeOperator, this, _1, _2, _3, _4)); + _skeletonModel->addToScene(scene, transaction, std::bind(&Avatar::metaBlendshapeOperator, _renderItemID, _1, _2, _3, _4)); _skeletonModel->setTagMask(render::hifi::TAG_ALL_VIEWS); _skeletonModel->setGroupCulled(true); _skeletonModel->setCanCastShadow(true); @@ -954,7 +954,7 @@ void Avatar::fixupModelsInScene(const render::ScenePointer& scene) { if (_skeletonModel->isRenderable() && _skeletonModel->needsFixupInScene()) { _skeletonModel->removeFromScene(scene, transaction); using namespace std::placeholders; - _skeletonModel->addToScene(scene, transaction, std::bind(&Avatar::metaBlendshapeOperator, this, _1, _2, _3, _4)); + _skeletonModel->addToScene(scene, transaction, std::bind(&Avatar::metaBlendshapeOperator, _renderItemID, _1, _2, _3, _4)); _skeletonModel->setTagMask(render::hifi::TAG_ALL_VIEWS); _skeletonModel->setGroupCulled(true); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index ca063a7026..9a4b9bb6b6 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -626,8 +626,8 @@ protected: LoadingStatus _loadingStatus { LoadingStatus::NoModel }; - void metaBlendshapeOperator(int blendshapeNumber, const QVector& blendshapeOffsets, const QVector& blendedMeshSizes, - const render::ItemIDs& subItemIDs); + static void metaBlendshapeOperator(render::ItemID renderItemID, int blendshapeNumber, const QVector& blendshapeOffsets, + const QVector& blendedMeshSizes, const render::ItemIDs& subItemIDs); }; #endif // hifi_Avatar_h From 1ff50bc05ed338c3331329c43aa05c7407bdaf17 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 24 Oct 2018 12:45:17 -0700 Subject: [PATCH 231/362] updates notification applies to assets, and there is no wallet activity notification --- scripts/system/commerce/wallet.js | 32 ++++++++++++++++++--- scripts/system/marketplaces/marketplaces.js | 12 ++++---- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index afdd49c0d1..b1a00809fc 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -497,10 +497,33 @@ function walletClosed() { } function notificationDataProcessPage(data) { - return data.data.history; + return data.data.updates; // HRS FIXME .history; } var shouldShowDot = false; +function notificationPollCallbackUpdates(updatesArray) { + shouldShowDot = shouldShowDot || updatesArray.length > 0; + ui.messagesWaiting(shouldShowDot && !ui.isOpen); + + if (updatesArray.length > 0) { + var message; + if (!ui.notificationInitialCallbackMade) { + message = updatesArray.length + " of your purchased items " + + (updatesArray.length === 1 ? "has an update " : "have updates ") + + "available. Open MARKET to update."; + ui.notificationDisplayBanner(message); + + ui.notificationPollCaresAboutSince = true; + } else { + for (var i = 0; i < updatesArray.length; i++) { + message = "Update available for \"" + + updatesArray[i].base_item_title + "\"." + + "Open MARKET to update."; + ui.notificationDisplayBanner(message); + } + } + } +} function notificationPollCallback(historyArray) { if (!ui.isOpen) { var notificationCount = historyArray.length; @@ -571,12 +594,13 @@ function startup() { onOpened: walletOpened, onClosed: walletClosed, onMessage: fromQml, - notificationPollEndpoint: "/api/v1/commerce/history?per_page=10", + // How are we going to handle two polls when --limitedCommerce is false? + notificationPollEndpoint: "/api/v1/commerce/available_updates?per_page=10", // HRS FIXME "/api/v1/commerce/history?per_page=10", notificationPollTimeoutMs: 300000, notificationDataProcessPage: notificationDataProcessPage, - notificationPollCallback: notificationPollCallback, + notificationPollCallback: notificationPollCallbackUpdates, notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, - notificationPollCaresAboutSince: true + notificationPollCaresAboutSince: false // HRS FIXME true }); GlobalServices.myUsernameChanged.connect(onUsernameChanged); installMarketplaceItemTester(); diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 8f334674cb..15b8c4e7cb 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -1239,12 +1239,12 @@ function startup() { home: MARKETPLACE_URL_INITIAL, onScreenChanged: onTabletScreenChanged, onMessage: onQmlMessageReceived, - notificationPollEndpoint: "/api/v1/commerce/available_updates?per_page=10", - notificationPollTimeoutMs: 300000, - notificationDataProcessPage: notificationDataProcessPage, - notificationPollCallback: notificationPollCallback, - notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, - notificationPollCaresAboutSince: false // Changes to true after first poll + // notificationPollEndpoint: "/api/v1/commerce/available_updates?per_page=10", + // notificationPollTimeoutMs: 300000, + // notificationDataProcessPage: notificationDataProcessPage, + // notificationPollCallback: notificationPollCallback, + // notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, + // notificationPollCaresAboutSince: false // Changes to true after first poll }); ContextOverlay.contextOverlayClicked.connect(openInspectionCertificateQML); Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); From b8cd5045c79c8649e28c2f7a6728dcb350626189 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Wed, 24 Oct 2018 12:54:09 -0700 Subject: [PATCH 232/362] Extra joints on soft entities keep their local transform --- .../avatars-renderer/src/avatars-renderer/Avatar.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 6770cd7f96..36d336f910 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -488,8 +488,8 @@ void Avatar::relayJointDataToChildren() { glm::quat jointRotation; glm::vec3 jointTranslation; if (avatarJointIndex < 0) { - jointRotation = modelEntity->getAbsoluteJointRotationInObjectFrame(jointIndex); - jointTranslation = modelEntity->getAbsoluteJointTranslationInObjectFrame(jointIndex); + jointRotation = modelEntity->getLocalJointRotation(jointIndex); + jointTranslation = modelEntity->getLocalJointTranslation(jointIndex); map.push_back(-1); } else { int jointIndex = getJointIndex(jointName); @@ -512,8 +512,8 @@ void Avatar::relayJointDataToChildren() { jointRotation = getJointRotation(avatarJointIndex); jointTranslation = getJointTranslation(avatarJointIndex); } else { - jointRotation = modelEntity->getAbsoluteJointRotationInObjectFrame(jointIndex); - jointTranslation = modelEntity->getAbsoluteJointTranslationInObjectFrame(jointIndex); + jointRotation = modelEntity->getLocalJointRotation(jointIndex); + jointTranslation = modelEntity->getLocalJointTranslation(jointIndex); } modelEntity->setLocalJointRotation(jointIndex, jointRotation); modelEntity->setLocalJointTranslation(jointIndex, jointTranslation); From b6f259fc42aa7ed03b2001056e46c2b71bfc2d83 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 24 Oct 2018 13:17:45 -0700 Subject: [PATCH 233/362] Fix incorrect merge-resolution --- libraries/avatars/src/AvatarData.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index e08f7fb6f8..7f42289a9b 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -419,11 +419,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent IF_AVATAR_SPACE(PACKET_HAS_AVATAR_GLOBAL_POSITION, sizeof _globalPosition) { auto startSection = destinationBuffer; - if (_overrideGlobalPosition) { - AVATAR_MEMCPY(_globalPositionOverride); - } else { - AVATAR_MEMCPY(_globalPosition); - } + AVATAR_MEMCPY(_globalPosition); int numBytes = destinationBuffer - startSection; From 4d34b12f06c9107e1d5c282711b6a8a6b7942d9b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 21 Oct 2018 18:48:38 -0700 Subject: [PATCH 234/362] rather than do checkDevices to find new audio devices every 2 seconds, do each check 2 seconds after the previous one has completed. This is done to avoid swamping the thread pool when the check takes a long time --- libraries/audio-client/src/AudioClient.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index d00bc29054..bdd6d0edc1 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -244,13 +244,20 @@ AudioClient::AudioClient() : // initialize wasapi; if getAvailableDevices is called from the CheckDevicesThread before this, it will crash getAvailableDevices(QAudio::AudioInput); getAvailableDevices(QAudio::AudioOutput); - + // start a thread to detect any device changes _checkDevicesTimer = new QTimer(this); - connect(_checkDevicesTimer, &QTimer::timeout, this, [this] { - QtConcurrent::run(QThreadPool::globalInstance(), [this] { checkDevices(); }); - }); const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000; + connect(_checkDevicesTimer, &QTimer::timeout, this, [=] { + QtConcurrent::run(QThreadPool::globalInstance(), [=] { + checkDevices(); + // On some systems (Ubuntu) checking all the audio devices can take more than 2 seconds. To + // avoid consuming all of the thread pool, don't start the check interval until the previous + // check has completed. + QMetaObject::invokeMethod(_checkDevicesTimer, "start", Q_ARG(int, DEVICE_CHECK_INTERVAL_MSECS)); + }); + }); + _checkDevicesTimer->setSingleShot(true); _checkDevicesTimer->start(DEVICE_CHECK_INTERVAL_MSECS); // start a thread to detect peak value changes From a88678fb70a124632ae29cbe36980ab3a2608e2d Mon Sep 17 00:00:00 2001 From: David Back Date: Wed, 24 Oct 2018 15:07:47 -0700 Subject: [PATCH 235/362] fix bad css merge, add particle property debounce --- scripts/system/html/css/edit-style.css | 2 +- scripts/system/html/js/entityProperties.js | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 5214dc0c3e..2db7fce065 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1107,7 +1107,7 @@ body#entity-list-body { position: relative; /* New positioning context. */ } -#search-area { +#filter-area { padding-right: 168px; padding-bottom: 24px; } diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index db3424bbd9..676ecaf289 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1304,6 +1304,7 @@ const JSON_EDITOR_ROW_DIV_INDEX = 2; var elGroups = {}; var properties = {}; var colorPickers = {}; +var particlePropertyUpdates = {}; var selectedEntityProperties; var lastEntityID = null; @@ -1534,9 +1535,23 @@ function updateProperty(originalPropertyName, propertyValue) { } else { propertyUpdate[originalPropertyName] = propertyValue; } - updateProperties(propertyUpdate); + // queue up particle property changes with the debounced sync to avoid + // causing particle emitting to reset each frame when updating values + if (properties[originalPropertyName].isParticleProperty) { + Object.keys(propertyUpdate).forEach(function (propertyUpdateKey) { + particlePropertyUpdates[propertyUpdateKey] = propertyUpdate[propertyUpdateKey]; + }); + particleSyncDebounce(); + } else { + updateProperties(propertyUpdate); + } } +var particleSyncDebounce = _.debounce(function () { + updateProperties(particlePropertyUpdates); + particlePropertyUpdates = {}; +}, DEBOUNCE_TIMEOUT); + function updateProperties(propertiesToUpdate) { EventBridge.emitWebEvent(JSON.stringify({ id: lastEntityID, @@ -2717,7 +2732,8 @@ function loaded() { let property = { data: propertyData, elementID: propertyElementID, - name: propertyName, + name: propertyName, + isParticleProperty: group.id.includes("particles"), elProperty: elProperty }; properties[propertyID] = property; From 5c5a5c9cd2a4c25d2c061819b838cee6d8da41f5 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 24 Oct 2018 16:14:05 -0700 Subject: [PATCH 236/362] Add optional default scripts log suppression --- scripts/developer/debugging/debugWindow.js | 36 +++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/scripts/developer/debugging/debugWindow.js b/scripts/developer/debugging/debugWindow.js index 993ca49a40..84bd3c323c 100644 --- a/scripts/developer/debugging/debugWindow.js +++ b/scripts/developer/debugging/debugWindow.js @@ -19,6 +19,23 @@ if (scripts.length >= 2) { return; } +var SUPPRESS_DEFAULT_SCRIPTS_MENU_NAME = "Developer" +var SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME = "Suppress messages from default scripts in Debug Window"; +var DEBUG_WINDOW_SUPPRESS_DEFAULTS_SCRIPTS = 'debugWindowSuppressDefaultScripts'; +var suppressDefaultScripts = Settings.getValue(DEBUG_WINDOW_SUPPRESS_DEFAULTS_SCRIPTS, false) +Menu.addMenuItem({ + menuName: SUPPRESS_DEFAULT_SCRIPTS_MENU_NAME, + menuItemName: SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME, + isCheckable: true, + isChecked: suppressDefaultScripts +}); + +Menu.menuItemEvent.connect(function(menuItem) { + if (menuItem === SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME) { + suppressDefaultScripts = Menu.isOptionChecked(SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME); + } +}); + // Set up the qml ui var qml = Script.resolvePath('debugWindow.qml'); @@ -61,17 +78,24 @@ window.visibleChanged.connect(function() { window.closed.connect(function () { Script.stop(); }); +function shouldLogMessage(scriptFileName) { + return !suppressDefaultScripts + || (scriptFileName !== "defaultScripts.js" && scriptFileName != "controllerScripts.js"); +} + var getFormattedDate = function() { var date = new Date(); return date.getMonth() + "/" + date.getDate() + " " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds(); }; var sendToLogWindow = function(type, message, scriptFileName) { - var typeFormatted = ""; - if (type) { - typeFormatted = type + " - "; + if (shouldLogMessage(scriptFileName)) { + var typeFormatted = ""; + if (type) { + typeFormatted = type + " - "; + } + window.sendToQml("[" + getFormattedDate() + "] " + "[" + scriptFileName + "] " + typeFormatted + message); } - window.sendToQml("[" + getFormattedDate() + "] " + "[" + scriptFileName + "] " + typeFormatted + message); }; ScriptDiscoveryService.printedMessage.connect(function(message, scriptFileName) { @@ -95,6 +119,10 @@ ScriptDiscoveryService.clearDebugWindow.connect(function() { }); Script.scriptEnding.connect(function () { + Settings.setValue(DEBUG_WINDOW_SUPPRESS_DEFAULTS_SCRIPTS, + Menu.isOptionChecked(SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME)); + Menu.removeMenuItem(SUPPRESS_DEFAULT_SCRIPTS_MENU_NAME, SUPPRESS_DEFAULT_SCRIPTS_ITEM_NAME); + var geometry = JSON.stringify({ x: window.position.x, y: window.position.y, From a3b874f9aa77c3c63b106054be3fee8bb6bfe391 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 18 Oct 2018 01:14:13 +0200 Subject: [PATCH 237/362] property tooltips --- .../system/assets/data/createAppTooltips.json | 458 ++++++++++++++++++ scripts/system/edit.js | 5 + scripts/system/html/css/edit-style.css | 22 + scripts/system/html/entityProperties.html | 1 + scripts/system/html/js/createAppTooltip.js | 84 ++++ scripts/system/html/js/entityProperties.js | 8 +- 6 files changed, 577 insertions(+), 1 deletion(-) create mode 100644 scripts/system/assets/data/createAppTooltips.json create mode 100644 scripts/system/html/js/createAppTooltip.js diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json new file mode 100644 index 0000000000..f8c037297f --- /dev/null +++ b/scripts/system/assets/data/createAppTooltips.json @@ -0,0 +1,458 @@ +{ + "shape": { + "tooltip": "The shape of this entity's geometry." + }, + "color": { + "tooltip": "The RGB value of this entity." + }, + "materialURL": { + "tooltip": "The URL of a JSON file containing the material. Use this to change the entity's look. " + }, + "text": { + "tooltip": "The text to display on the entity." + }, + "textColor": { + "tooltip": "The color of the text." + }, + "backgroundColor": { + "tooltip": "The color of the background." + }, + "lineHeight": { + "tooltip": "The height of each line of text. This determines the size of the text." + }, + "faceCamera": { + "tooltip": "If enabled, the entity follows the camera of each user, creating a billboard effect." + }, + "flyingAllowed": { + "tooltip": "If enabled, users can fly in the zone." + }, + "ghostingAllowed": { + "tooltip": "If enabled, users with avatar collisions turned off will not collide with content in the zone." + }, + "filterURL": { + "tooltip": "The URL of a JS file that checks for changes to entity properties within the zone. Runs periodically." + }, + "keyLightMode": { + "tooltip": "Configures the key light in the zone. This light is directional." + }, + "keyLightColor": { + "tooltip": "The color of the key light." + }, + "keyLight.intensity": { + "tooltip": "The intensity of the key light." + }, + "keyLight.direction.y": { + "tooltip": "The angle in deg at which light emits. Starts in the entity's -z direction, and rotates around its y axis." + }, + "keyLight.direction.x": { + "tooltip": "The angle in deg at which light emits. Starts in the entity's -z direction, and rotates around its x axis." + }, + "keyLight.castShadows": { + "tooltip": "If enabled, shadows are cast. The entity or avatar casting the shadow must also have Cast Shadows enabled." + }, + "skyboxMode": { + "tooltip": "Configures the skybox in the zone. The skybox is a cube map image." + }, + "skybox.color": { + "tooltip": "If the URL is blank, this changes the color of the sky, otherwise it modifies the color of the skybox." + }, + "skybox.url": { + "tooltip": "A cube map image that is used to render the sky." + }, + "ambientLightMode": { + "tooltip": "Configures the ambient light in the zone. Use this if you want your skybox to reflect light on the content." + }, + "ambientLight.ambientIntensity": { + "tooltip": "The intensity of the ambient light." + }, + "ambientLight.ambientURL": { + "tooltip": "A cube map image that defines the color of the light coming from each direction." + }, + "hazeMode": { + "tooltip": "Configures the haze in the scene." + }, + "haze.hazeRange": { + "tooltip": "How far the haze extends out. This is measured in meters." + }, + "haze.hazeAltitudeEffect": { + "tooltip": "If enabled, this adjusts the haze intensity as it gets higher." + }, + "haze.hazeBaseRef": { + "tooltip": "The base of the altitude range. Measured in entity space." + }, + "haze.hazeCeiling": { + "tooltip": "The ceiling of the altitude range. Measured in entity space." + }, + "haze.hazeColor": { + "tooltip": "The color of the haze." + }, + "haze.hazeBackgroundBlend": { + "tooltip": "How much the skybox shows through the haze. The higher the value, the more it shows through." + }, + "haze.hazeEnableGlare": { + "tooltip": "If enabled, a glare is enabled on the skybox, based on the key light." + }, + "haze.hazeGlareColor": { + "tooltip": "The color of the glare based on the key light." + }, + "haze.hazeGlareAngle": { + "tooltip": "The angular size of the glare and how much it encompasses the skybox, based on the key light." + }, + "bloomMode": { + "tooltip": "Configures how much bright areas of the scene glow." + }, + "bloom.bloomIntensity": { + "tooltip": "The intensity, or brightness, of the bloom effect." + }, + "bloom.bloomThreshold": { + "tooltip": "The cutoff of the bloom. The higher the value, the more only bright areas of the scene will glow." + }, + "bloom.bloomSize": { + "tooltip": "The radius of bloom. The higher the value, the larger the bloom." + }, + "modelURL": { + "tooltip": "A mesh model from an FBX or OBJ file." + }, + "shapeType": { + "tooltip": "The shape of the collision hull used if collisions are enabled. This affects how an entity collides." + }, + "compoundShapeURL": { + "tooltip": "The OBJ file to use for the compound shape if Collision Shape is \"compound\"." + }, + "animation.url": { + "tooltip": "An animation to play on the model." + }, + "animation.running": { + "tooltip": "If enabled, the animation on the model will play automatically." + }, + "animation.allowTranslation": { + "tooltip": "If enabled, this allows an entity to move in space during an animation." + }, + "animation.loop": { + "tooltip": "If enabled, then the animation will continuously repeat." + }, + "animation.hold": { + "tooltip": "If enabled, then rotations and translations of the last frame played are maintained when the animation stops." + }, + "animation.currentFrame": { + "tooltip": "The current frame being played in the animation." + }, + "animation.firstFrame": { + "tooltip": "The first frame to play in the animation." + }, + "animation.lastFrame": { + "tooltip": "The last frame to play in the animation." + }, + "animation.fps": { + "tooltip": "The speed of the animation." + }, + "textures": { + "tooltip": "The URL of a JPG or PNG image file to display for each particle." + }, + "originalTextures": { + "tooltip": "A JSON string containing the original texture used on the model." + }, + "imageUrl": { + "tooltip": "The URL for the image source.", + "jsPropertyName": "textures" + }, + "sourceUrl": { + "tooltip": "The URL for the web page source." + }, + "dpi": { + "tooltip": "The resolution to display the page at, in pixels per inch. Use this to resize your web source in the frame." + }, + "isEmitting": { + "tooltip": "If enabled, then particles are emitted." + }, + "lifespan": { + "tooltip": "How long each particle lives, measured in seconds." + }, + "maxParticles": { + "tooltip": "The maximum number of particles to render at one time. Older particles are swapped out for new ones." + }, + "emitRate": { + "tooltip": "The number of particles per second to emit." + }, + "emitSpeed": { + "tooltip": "The speed that each particle is emitted at, measured in m/s." + }, + "speedSpread": { + "tooltip": "The spread in speeds at which particles are emitted at, resulting in a variety of speeds." + }, + "emitDimensions": { + "tooltip": "The outer limit radius in dimensions that the particles can be emitted from." + }, + "emitOrientation": { + "tooltip": "The orientation of particle emission relative to the entity's axes." + }, + "emitRadiusStart": { + "tooltip": "The inner limit radius in dimensions that the particles start emitting from." + }, + "emitterShouldTrail": { + "tooltip": "If enabled, then particles are \"left behind\" as the emitter moves, otherwise they are not." + }, + "particleRadius": { + "tooltip": "The size of each particle." + }, + "radiusStart": { + "tooltip": "" + }, + "radiusFinish": { + "tooltip": "" + }, + "radiusSpread": { + "tooltip": "The spread in size that each particle is given, resulting in a variety of sizes." + }, + "particleColor": { + "tooltip": "The color of each particle.", + "jsPropertyName": "color" + }, + "colorSpread": { + "tooltip": "The spread in color that each particle is given, resulting in a variety of colors." + }, + "alpha": { + "tooltip": "The alpha of each particle." + }, + "alphaStart": { + "tooltip": "" + }, + "alphaFinish": { + "tooltip": "" + }, + "alphaSpread": { + "tooltip": "The spread in alpha that each particle is given, resulting in a variety of alphas." + }, + "emitAcceleration": { + "tooltip": "The acceleration that is applied to each particle during its lifetime." + }, + "accelerationSpread": { + "tooltip": "The spread in accelerations that each particle is given, resulting in a variety of accelerations." + }, + "particleSpin": { + "tooltip": "The spin of each particle in the system." + }, + "spinStart": { + "tooltip": "" + }, + "spinFinish": { + "tooltip": "" + }, + "spinSpread": { + "tooltip": "The spread in spin that each particle is given, resulting in a variety of spins." + }, + "rotateWithEntity": { + "tooltip": "If enabled, each particle will spin relative to the roation of the entity as a whole." + }, + "azimuthStart": { + "tooltip": "The angle in deg at which particles are emitted. Starts in the entity's -z direction, and rotates around its y axis." + }, + "azimuthFinish": { + "tooltip": "" + }, + "polarStart": { + "tooltip": "The angle in deg at which particles are emitted. Starts in the entity's -z direction, and rotates around its y axis." + }, + "polarFinish": { + "tooltip": "" + }, + "lightColor": { + "tooltip": "The color of the light emitted.", + "jsPropertyName": "color" + }, + "intensity": { + "tooltip": "The brightness of the light." + }, + "falloffRadius": { + "tooltip": "The distance from the light's center where the intensity is reduced." + }, + "isSpotlight": { + "tooltip": "If enabled, then the light is directional, otherwise the light is a point light which emits light in all directions." + }, + "exponent": { + "tooltip": "Affects the softness of the spotlight beam; the higher the value, the softer the beam." + }, + "cutoff": { + "tooltip": "Affects the size of the spotlight beam; the higher the value, the larger the beam." + }, + "id": { + "tooltip": "The unique identifier of this entity." + }, + "name": { + "tooltip": "The name of this entity." + }, + "description": { + "tooltip": "Use this field to describe the entity." + }, + "position": { + "tooltip": "The global position of this entity." + }, + "rotation": { + "tooltip": "The rotation of the entity with respect to world coordinates." + }, + "dimensions": { + "tooltip": "The global dimensions of this entity." + }, + "scale": { + "tooltip": "The global scaling of this entity,", + "skipJSProperty": true + }, + "registrationPoint": { + "tooltip": "The point in the entity at which the entity is rotated about." + }, + "collisionless": { + "tooltip": "If enabled, this entity will collide with other entities or avatars." + }, + "dynamic": { + "tooltip": "If enabled, this entity has collisions associated with it that can affect its movement." + }, + "collidesWithStatic": { + "tooltip": "If enabled, this entity will collide with other non-moving, static entities.", + "jsPropertyName": "collidesWith" + }, + "collidesWithDynamic": { + "tooltip": "If enabled, this entity will collide with other dynamic entities.", + "jsPropertyName": "collidesWith" + }, + "collidesWithKinematic": { + "tooltip": "If enabled, this entity will collide with other kinematic entities (they have velocity but are not dynamic).", + "jsPropertyName": "collidesWith" + }, + "collidesWithOtherAvatars": { + "tooltip": "If enabled, this entity will collide with other user's avatars.", + "jsPropertyName": "collidesWith" + }, + "collisionSoundURL": { + "tooltip": "The URL of a sound to play when the entity collides with something else." + }, + "grabbable": { + "tooltip": "If enabled, this entity will allow grabbing input and will be moveable.", + "jsPropertyName": "userData[\"grabbableKey\"][\"grabbable\"]" + }, + "triggerable": { + "tooltip": "If enabled, the collider on this entity is used for triggering events.", + "jsPropertyName": "userData[\"grabbableKey\"][\"triggerable\"]" + }, + "cloneable": { + "tooltip": "If enabled, this entity can be duplicated." + }, + "cloneLifetime": { + "tooltip": "The lifetime for clones of this entity." + }, + "cloneLimit": { + "tooltip": "The total number of clones of this entity that can exist in the domain at any given time." + }, + "cloneDynamic": { + "tooltip": "If enabled, then clones created from this entity will be dynamic, allowing the clone to collide." + }, + "cloneAvatarEntity": { + "tooltip": "If enabled, then clones created from this entity will be created as avatar entities." + }, + "ignoreIK": { + "tooltip": "If enabled, grabbed entities will follow the movements of your hand controller instead of your avatar's hand.", + "jsPropertyName": "userData[\"grabbableKey\"][\"ignoreIK\"]" + }, + "canCastShadow": { + "tooltip": "If enabled, this geometry of this entity casts shadows when a shadow-casting light source shines on it." + }, + "parentID": { + "tooltip": "The ID of the entity or avatar that this entity is parented to." + }, + "parentJointIndex": { + "tooltip": "If the entity is parented to an avatar, this joint defines where on the avatar the entity is parented." + }, + "href": { + "tooltip": "The URL that will be opened when a user clicks on this entity. Useful for web pages and portals." + }, + "script": { + "tooltip": "The URL to an external JS file to add behaviors to the client." + }, + "serverScripts": { + "tooltip": "The URL to an external JS file to add behaviors to the server." + }, + "serverScriptsStatus": { + "tooltip": "The status of the server script, if provided. This shows if it's running or has an error.", + "skipJSProperty": true + }, + "hasLifetime": { + "tooltip": "If enabled, the entity will disappear after a certain amount of time specified by Lifetime.", + "jsPropertyName": "lifetime" + }, + "lifetime": { + "tooltip": "The time this entity will exist in the environment for." + }, + "userData": { + "tooltip": "Used to store extra data about the entity in JSON format." + }, + "velocity": { + "tooltip": "The linear velocity vector of the entity. The velocity at which this entity moves forward in space." + }, + "damping": { + "tooltip": "The linear damping to slow down the linear velocity of an entity over time." + }, + "angularVelocity": { + "tooltip": "The angular velocity of the entity in rad/s with respect to its axes, about its pivot point." + }, + "angularDamping": { + "tooltip": "The angular damping to slow down the angular velocity of an entity over time." + }, + "restitution": { + "tooltip": "If enabled, the entity can bounce against other objects that also have Bounciness." + }, + "friction": { + "tooltip": "The friction applied to slow down an entity when it's moving against another entity." + }, + "density": { + "tooltip": "The density of the entity. The higher the density, the harder the entity is to move." + }, + "gravity": { + "tooltip": "The acceleration due to gravity that the entity should move with, in world space." + }, + "acceleration": { + "tooltip": "A acceleration that the entity should move with, in world space." + }, + "createModel": { + "tooltip": "An entity that is based on a custom mesh created from an .OBJ or .FBX.", + "skipJSProperty": true + }, + "createShape": { + "tooltip": "An entity that has many different primitive shapes.", + "skipJSProperty": true + }, + "createLight": { + "tooltip": "An entity that emits light.", + "skipJSProperty": true + }, + "createText": { + "tooltip": "An entity that displays text on a panel.", + "skipJSProperty": true + }, + "createImage": { + "tooltip": "An entity that displays an image on a panel.", + "skipJSProperty": true + }, + "createWeb": { + "tooltip": "An entity that displays a web page on a panel.", + "skipJSProperty": true + }, + "createZone": { + "tooltip": "An entity that can be used for skyboxes, lighting, and can constrain or change avatar behaviors.", + "skipJSProperty": true + }, + "createParticle": { + "tooltip": "An entity that emits particles.", + "skipJSProperty": true + }, + "createMaterial": { + "tooltip": "An entity that creates a material that can be attached to a Shape or Model.", + "skipJSProperty": true + }, + "useAssetServer": { + "tooltip": "A server that hosts content and assets. You can't take items that are hosted here into other domains.", + "skipJSProperty": true + }, + "importNewEntity": { + "tooltip": "Import a local or hosted file that can be used across domains.", + "skipJSProperty": true + } +} diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 9f4ec3c62b..c6432ac2f9 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2481,6 +2481,11 @@ var PropertiesTool = function (opts) { } } else if (data.type === "propertiesPageReady") { updateSelections(true); + } else if (data.type === "tooltipsRequest") { + emitScriptEvent({ + type: 'tooltipsReply', + tooltips: Script.require('./assets/data/createAppTooltips.json') + }); } }; diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 2db7fce065..4ad886f1b7 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1598,3 +1598,25 @@ input.rename-entity { padding-left: 2px; } +.createAppTooltip { + position: absolute; + background: #6a6a6a; + border: 1px solid black; + width: 258px; + min-height: 20px; + padding: 5px; +} + +.createAppTooltip .createAppTooltipDescription { + font-size: 12px; + font-style: italic; + color: #ffffff; +} + +.createAppTooltip .createAppTooltipJSAttribute { + font-size: 10px; + color: #000000; + bottom: 0; + margin-top: 5px; +} + diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 03b034fc83..b56ad346e2 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -21,6 +21,7 @@ + diff --git a/scripts/system/html/js/createAppTooltip.js b/scripts/system/html/js/createAppTooltip.js new file mode 100644 index 0000000000..10ad73e1ee --- /dev/null +++ b/scripts/system/html/js/createAppTooltip.js @@ -0,0 +1,84 @@ +// createAppTooltip.js +// +// Created by Thijs Wenker on 17 Oct 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 + +const CREATE_APP_TOOLTIP_OFFSET = 20; + +function CreateAppTooltip() { + this._tooltipData = null; + this._tooltipDiv = null; +} + +CreateAppTooltip.prototype = { + _tooltipData: null, + _tooltipDiv: null, + + _removeTooltipIfExists: function() { + if (this._tooltipDiv !== null) { + this._tooltipDiv.remove(); + this._tooltipDiv = null; + } + }, + + setTooltipData: function(tooltipData) { + this._tooltipData = tooltipData; + }, + + registerTooltipElement: function(element, tooltipID) { + element.addEventListener("mouseover", function() { + + this._removeTooltipIfExists(); + + let tooltipData = this._tooltipData[tooltipID]; + + if (!tooltipData || tooltipData.tooltip === "") { + return; + } + + let elementRect = element.getBoundingClientRect(); + let elTip = document.createElement("div"); + elTip.className = "createAppTooltip"; + + let elTipDescription = document.createElement("div"); + elTipDescription.className = "createAppTooltipDescription"; + elTipDescription.innerText = tooltipData.tooltip; + elTip.appendChild(elTipDescription); + + let jsAttribute = tooltipID; + if (tooltipData.jsPropertyName) { + jsAttribute = tooltipData.jsPropertyName; + } + + if (!tooltipData.skipJSProperty) { + let elTipJSAttribute = document.createElement("div"); + elTipJSAttribute.className = "createAppTooltipJSAttribute"; + elTipJSAttribute.innerText = `JS Attribute: ${jsAttribute}`; + elTip.appendChild(elTipJSAttribute); + } + + document.body.appendChild(elTip); + + let elementTop = window.pageYOffset + elementRect.top; + + let desiredTooltipTop = elementTop + element.clientHeight + CREATE_APP_TOOLTIP_OFFSET; + + if ((window.innerHeight + window.pageYOffset) < (desiredTooltipTop + elTip.clientHeight)) { + // show above when otherwise out of bounds + elTip.style.top = elementTop - CREATE_APP_TOOLTIP_OFFSET - elTip.clientHeight; + } else { + // show tooltip on below by default + elTip.style.top = desiredTooltipTop; + } + elTip.style.left = window.pageXOffset + elementRect.left; + + this._tooltipDiv = elTip; + }.bind(this), false); + element.addEventListener("mouseout", function() { + this._removeTooltipIfExists(); + }.bind(this), false); + } +}; diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 676ecaf289..59c2ba702b 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1307,6 +1307,7 @@ var colorPickers = {}; var particlePropertyUpdates = {}; var selectedEntityProperties; var lastEntityID = null; +var createAppTooltip = new CreateAppTooltip(); function debugPrint(message) { EventBridge.emitWebEvent( @@ -2659,7 +2660,7 @@ function showParentMaterialNameBox(number, elNumber, elString) { function loaded() { - openEventBridge(function() { + openEventBridge(function() { let elPropertiesList = document.getElementById("properties-list"); GROUPS.forEach(function(group) { @@ -2728,6 +2729,8 @@ function loaded() { let elLabel = document.createElement('label'); elLabel.innerText = propertyData.label; elLabel.setAttribute("for", propertyElementID); + + createAppTooltip.registerTooltipElement(elLabel, propertyID); let property = { data: propertyData, @@ -3150,6 +3153,8 @@ function loaded() { activeElement.select(); } } + } else if (data.type === 'tooltipsReply') { + createAppTooltip.setTooltipData(data.tooltips); } }); } @@ -3381,5 +3386,6 @@ function loaded() { setTimeout(function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'propertiesPageReady' })); + EventBridge.emitWebEvent(JSON.stringify({ type: 'tooltipsRequest' })); }, 1000); } From 358b0b76d1f869882b02eead537d6a20e70f2d41 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 19 Oct 2018 16:32:38 +0200 Subject: [PATCH 238/362] tooltip delay of 500ms --- .../system/assets/data/createAppTooltips.json | 2 +- scripts/system/html/js/createAppTooltip.js | 76 +++++++++++-------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json index f8c037297f..7cc9e0a97e 100644 --- a/scripts/system/assets/data/createAppTooltips.json +++ b/scripts/system/assets/data/createAppTooltips.json @@ -294,7 +294,7 @@ "tooltip": "The global dimensions of this entity." }, "scale": { - "tooltip": "The global scaling of this entity,", + "tooltip": "The global scaling of this entity.", "skipJSProperty": true }, "registrationPoint": { diff --git a/scripts/system/html/js/createAppTooltip.js b/scripts/system/html/js/createAppTooltip.js index 10ad73e1ee..edd0f6366a 100644 --- a/scripts/system/html/js/createAppTooltip.js +++ b/scripts/system/html/js/createAppTooltip.js @@ -7,17 +7,25 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html const CREATE_APP_TOOLTIP_OFFSET = 20; +const TOOLTIP_DELAY = 500; // ms function CreateAppTooltip() { this._tooltipData = null; this._tooltipDiv = null; + this._delayTimeout = null; } CreateAppTooltip.prototype = { _tooltipData: null, _tooltipDiv: null, + _delayTimeout: null, _removeTooltipIfExists: function() { + if (this._delayTimeout !== null) { + window.clearTimeout(this._delayTimeout); + this._delayTimeout = null; + } + if (this._tooltipDiv !== null) { this._tooltipDiv.remove(); this._tooltipDiv = null; @@ -33,49 +41,51 @@ CreateAppTooltip.prototype = { this._removeTooltipIfExists(); - let tooltipData = this._tooltipData[tooltipID]; + this._delayTimeout = window.setTimeout(function() { + let tooltipData = this._tooltipData[tooltipID]; - if (!tooltipData || tooltipData.tooltip === "") { - return; - } + if (!tooltipData || tooltipData.tooltip === "") { + return; + } - let elementRect = element.getBoundingClientRect(); - let elTip = document.createElement("div"); - elTip.className = "createAppTooltip"; + let elementRect = element.getBoundingClientRect(); + let elTip = document.createElement("div"); + elTip.className = "createAppTooltip"; - let elTipDescription = document.createElement("div"); - elTipDescription.className = "createAppTooltipDescription"; - elTipDescription.innerText = tooltipData.tooltip; - elTip.appendChild(elTipDescription); + let elTipDescription = document.createElement("div"); + elTipDescription.className = "createAppTooltipDescription"; + elTipDescription.innerText = tooltipData.tooltip; + elTip.appendChild(elTipDescription); - let jsAttribute = tooltipID; - if (tooltipData.jsPropertyName) { - jsAttribute = tooltipData.jsPropertyName; - } + let jsAttribute = tooltipID; + if (tooltipData.jsPropertyName) { + jsAttribute = tooltipData.jsPropertyName; + } - if (!tooltipData.skipJSProperty) { - let elTipJSAttribute = document.createElement("div"); - elTipJSAttribute.className = "createAppTooltipJSAttribute"; - elTipJSAttribute.innerText = `JS Attribute: ${jsAttribute}`; - elTip.appendChild(elTipJSAttribute); - } + if (!tooltipData.skipJSProperty) { + let elTipJSAttribute = document.createElement("div"); + elTipJSAttribute.className = "createAppTooltipJSAttribute"; + elTipJSAttribute.innerText = `JS Attribute: ${jsAttribute}`; + elTip.appendChild(elTipJSAttribute); + } - document.body.appendChild(elTip); + document.body.appendChild(elTip); - let elementTop = window.pageYOffset + elementRect.top; + let elementTop = window.pageYOffset + elementRect.top; - let desiredTooltipTop = elementTop + element.clientHeight + CREATE_APP_TOOLTIP_OFFSET; + let desiredTooltipTop = elementTop + element.clientHeight + CREATE_APP_TOOLTIP_OFFSET; - if ((window.innerHeight + window.pageYOffset) < (desiredTooltipTop + elTip.clientHeight)) { - // show above when otherwise out of bounds - elTip.style.top = elementTop - CREATE_APP_TOOLTIP_OFFSET - elTip.clientHeight; - } else { - // show tooltip on below by default - elTip.style.top = desiredTooltipTop; - } - elTip.style.left = window.pageXOffset + elementRect.left; + if ((window.innerHeight + window.pageYOffset) < (desiredTooltipTop + elTip.clientHeight)) { + // show above when otherwise out of bounds + elTip.style.top = elementTop - CREATE_APP_TOOLTIP_OFFSET - elTip.clientHeight; + } else { + // show tooltip on below by default + elTip.style.top = desiredTooltipTop; + } + elTip.style.left = window.pageXOffset + elementRect.left; - this._tooltipDiv = elTip; + this._tooltipDiv = elTip; + }.bind(this), TOOLTIP_DELAY); }.bind(this), false); element.addEventListener("mouseout", function() { this._removeTooltipIfExists(); From 83051940a854d8bee91e23f6542fa45554503e98 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 23 Oct 2018 04:49:45 +0200 Subject: [PATCH 239/362] Fix QA feedback --- .../system/assets/data/createAppTooltips.json | 45 ++++++++++++------- scripts/system/edit.js | 3 +- scripts/system/html/js/createAppTooltip.js | 26 ++++++++++- scripts/system/html/js/entityProperties.js | 2 + 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json index 7cc9e0a97e..0e6818c512 100644 --- a/scripts/system/assets/data/createAppTooltips.json +++ b/scripts/system/assets/data/createAppTooltips.json @@ -147,7 +147,7 @@ "tooltip": "The speed of the animation." }, "textures": { - "tooltip": "The URL of a JPG or PNG image file to display for each particle." + "tooltip": "A JSON string containing a texture. Use a name from the Original Texture property to override it." }, "originalTextures": { "tooltip": "A JSON string containing the original texture used on the model." @@ -171,6 +171,10 @@ "maxParticles": { "tooltip": "The maximum number of particles to render at one time. Older particles are swapped out for new ones." }, + "particleTextures": { + "tooltip": "The URL of a JPG or PNG image file to display for each particle.", + "jsPropertyName": "textures" + }, "emitRate": { "tooltip": "The number of particles per second to emit." }, @@ -244,16 +248,16 @@ "rotateWithEntity": { "tooltip": "If enabled, each particle will spin relative to the roation of the entity as a whole." }, - "azimuthStart": { + "polarStart": { "tooltip": "The angle in deg at which particles are emitted. Starts in the entity's -z direction, and rotates around its y axis." }, - "azimuthFinish": { + "polarFinish": { "tooltip": "" }, - "polarStart": { + "azimuthStart": { "tooltip": "The angle in deg at which particles are emitted. Starts in the entity's -z direction, and rotates around its y axis." }, - "polarFinish": { + "azimuthFinish": { "tooltip": "" }, "lightColor": { @@ -300,6 +304,12 @@ "registrationPoint": { "tooltip": "The point in the entity at which the entity is rotated about." }, + "visible": { + "tooltip": "If enabled, this entity will be visible." + }, + "locked": { + "tooltip": "If enabled, this entity will be locked." + }, "collisionless": { "tooltip": "If enabled, this entity will collide with other entities or avatars." }, @@ -318,20 +328,22 @@ "tooltip": "If enabled, this entity will collide with other kinematic entities (they have velocity but are not dynamic).", "jsPropertyName": "collidesWith" }, - "collidesWithOtherAvatars": { + "collidesWithOtherAvatar": { "tooltip": "If enabled, this entity will collide with other user's avatars.", "jsPropertyName": "collidesWith" }, + "collidesWithMyAvatar": { + "tooltip": "If enabled, this entity will collide with your own avatar.", + "jsPropertyName": "collidesWith" + }, "collisionSoundURL": { "tooltip": "The URL of a sound to play when the entity collides with something else." }, - "grabbable": { - "tooltip": "If enabled, this entity will allow grabbing input and will be moveable.", - "jsPropertyName": "userData[\"grabbableKey\"][\"grabbable\"]" + "grab.grabbable": { + "tooltip": "If enabled, this entity will allow grabbing input and will be moveable." }, - "triggerable": { - "tooltip": "If enabled, the collider on this entity is used for triggering events.", - "jsPropertyName": "userData[\"grabbableKey\"][\"triggerable\"]" + "grab.triggerable": { + "tooltip": "If enabled, the collider on this entity is used for triggering events." }, "cloneable": { "tooltip": "If enabled, this entity can be duplicated." @@ -348,9 +360,8 @@ "cloneAvatarEntity": { "tooltip": "If enabled, then clones created from this entity will be created as avatar entities." }, - "ignoreIK": { - "tooltip": "If enabled, grabbed entities will follow the movements of your hand controller instead of your avatar's hand.", - "jsPropertyName": "userData[\"grabbableKey\"][\"ignoreIK\"]" + "grab.grabFollowsController": { + "tooltip": "If enabled, grabbed entities will follow the movements of your hand controller instead of your avatar's hand." }, "canCastShadow": { "tooltip": "If enabled, this geometry of this entity casts shadows when a shadow-casting light source shines on it." @@ -411,6 +422,10 @@ "acceleration": { "tooltip": "A acceleration that the entity should move with, in world space." }, + "alignToGrid": { + "tooltip": "Used to align entities to the grid, or floor of the environment.", + "skipJSProperty": true + }, "createModel": { "tooltip": "An entity that is based on a custom mesh created from an .OBJ or .FBX.", "skipJSProperty": true diff --git a/scripts/system/edit.js b/scripts/system/edit.js index c6432ac2f9..6425806771 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2484,7 +2484,8 @@ var PropertiesTool = function (opts) { } else if (data.type === "tooltipsRequest") { emitScriptEvent({ type: 'tooltipsReply', - tooltips: Script.require('./assets/data/createAppTooltips.json') + tooltips: Script.require('./assets/data/createAppTooltips.json'), + hmdActive: HMD.active, }); } }; diff --git a/scripts/system/html/js/createAppTooltip.js b/scripts/system/html/js/createAppTooltip.js index edd0f6366a..a42e5efe05 100644 --- a/scripts/system/html/js/createAppTooltip.js +++ b/scripts/system/html/js/createAppTooltip.js @@ -8,17 +8,20 @@ const CREATE_APP_TOOLTIP_OFFSET = 20; const TOOLTIP_DELAY = 500; // ms +const TOOLTIP_DEBUG = false; function CreateAppTooltip() { this._tooltipData = null; this._tooltipDiv = null; this._delayTimeout = null; + this._isEnabled = false; } CreateAppTooltip.prototype = { _tooltipData: null, _tooltipDiv: null, _delayTimeout: null, + _isEnabled: null, _removeTooltipIfExists: function() { if (this._delayTimeout !== null) { @@ -32,12 +35,19 @@ CreateAppTooltip.prototype = { } }, + setIsEnabled: function(isEnabled) { + this._isEnabled = isEnabled; + }, + setTooltipData: function(tooltipData) { this._tooltipData = tooltipData; }, registerTooltipElement: function(element, tooltipID) { element.addEventListener("mouseover", function() { + if (!this._isEnabled) { + return; + } this._removeTooltipIfExists(); @@ -45,7 +55,10 @@ CreateAppTooltip.prototype = { let tooltipData = this._tooltipData[tooltipID]; if (!tooltipData || tooltipData.tooltip === "") { - return; + if (!TOOLTIP_DEBUG) { + return; + } + tooltipData = {tooltip: 'PLEASE SET THIS TOOLTIP'}; } let elementRect = element.getBoundingClientRect(); @@ -74,6 +87,7 @@ CreateAppTooltip.prototype = { let elementTop = window.pageYOffset + elementRect.top; let desiredTooltipTop = elementTop + element.clientHeight + CREATE_APP_TOOLTIP_OFFSET; + let desiredTooltipLeft = window.pageXOffset + elementRect.left; if ((window.innerHeight + window.pageYOffset) < (desiredTooltipTop + elTip.clientHeight)) { // show above when otherwise out of bounds @@ -82,12 +96,20 @@ CreateAppTooltip.prototype = { // show tooltip on below by default elTip.style.top = desiredTooltipTop; } - elTip.style.left = window.pageXOffset + elementRect.left; + if ((window.innerWidth + window.pageXOffset) < (desiredTooltipLeft + elTip.clientWidth)) { + elTip.style.left = document.body.clientWidth + window.pageXOffset - elTip.offsetWidth; + } else { + elTip.style.left = desiredTooltipLeft; + } this._tooltipDiv = elTip; }.bind(this), TOOLTIP_DELAY); }.bind(this), false); element.addEventListener("mouseout", function() { + if (!this._isEnabled) { + return; + } + this._removeTooltipIfExists(); }.bind(this), false); } diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 59c2ba702b..173d806d59 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -3154,6 +3154,7 @@ function loaded() { } } } else if (data.type === 'tooltipsReply') { + createAppTooltip.setIsEnabled(!data.hmdActive); createAppTooltip.setTooltipData(data.tooltips); } }); @@ -3170,6 +3171,7 @@ function loaded() { let elLabel = document.createElement('label'); elLabel.setAttribute("for", serverScriptStatusElementID); elLabel.innerText = "Server Script Status"; + createAppTooltip.registerTooltipElement(elLabel, "serverScriptsStatus"); let elServerScriptStatus = document.createElement('span'); elServerScriptStatus.setAttribute("id", serverScriptStatusElementID); elDiv.appendChild(elLabel); From bb7d1f8afb2cef4a47a7b21ebc75f60742235161 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 23 Oct 2018 22:37:21 +0200 Subject: [PATCH 240/362] fix broken and missing tooltips --- .../system/assets/data/createAppTooltips.json | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json index 0e6818c512..83ddcaa34b 100644 --- a/scripts/system/assets/data/createAppTooltips.json +++ b/scripts/system/assets/data/createAppTooltips.json @@ -5,9 +5,6 @@ "color": { "tooltip": "The RGB value of this entity." }, - "materialURL": { - "tooltip": "The URL of a JSON file containing the material. Use this to change the entity's look. " - }, "text": { "tooltip": "The text to display on the entity." }, @@ -35,7 +32,7 @@ "keyLightMode": { "tooltip": "Configures the key light in the zone. This light is directional." }, - "keyLightColor": { + "keyLight.color": { "tooltip": "The color of the key light." }, "keyLight.intensity": { @@ -152,7 +149,7 @@ "originalTextures": { "tooltip": "A JSON string containing the original texture used on the model." }, - "imageUrl": { + "image": { "tooltip": "The URL for the image source.", "jsPropertyName": "textures" }, @@ -279,6 +276,36 @@ "cutoff": { "tooltip": "Affects the size of the spotlight beam; the higher the value, the larger the beam." }, + "materialURL": { + "tooltip": "The URL to an external JSON file or \"materialData\", \"materialData? to use Material Data." + }, + "materialData": { + "tooltip": "Can be used instead of a JSON file when material set to materialData." + }, + "materialNameToReplace": { + "tooltip": "Material name of parent entity to map this material entity on.", + "jsPropertyName": "parentMaterialName" + }, + "submeshToReplace": { + "tooltip": "Submesh index of the parent entity to map this material on.", + "jsPropertyName": "parentMaterialName" + }, + "selectSubmesh": { + "tooltip": "If enabled, \"Select Submesh\" property will show up, otherwise \"Material Name to Replace\" will be shown.", + "skipJSProperty": true + }, + "priority": { + "tooltip": "The priority of the material, where a larger number means higher priority. Original materials = 0." + }, + "materialMappingPos": { + "tooltip": "The offset position of the bottom left of the material within the parent's UV space." + }, + "materialMappingScale": { + "tooltip": "How many times the material will repeat in each direction within the parent's UV space." + }, + "materialMappingRot": { + "tooltip": "How much to rotate the material within the parent's UV-space, in degrees." + }, "id": { "tooltip": "The unique identifier of this entity." }, From 13cad17793d8d5d2751ac3b8dae83d290838a7b3 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 24 Oct 2018 21:35:47 +0200 Subject: [PATCH 241/362] better style js attributes --- scripts/system/html/css/edit-style.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 4ad886f1b7..cf7124d9eb 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1614,7 +1614,8 @@ input.rename-entity { } .createAppTooltip .createAppTooltipJSAttribute { - font-size: 10px; + font-family: Raleway-SemiBold; + font-size: 11px; color: #000000; bottom: 0; margin-top: 5px; From 252e504ab91e9e53bc963f386c4ee122e205343d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 20 Oct 2018 10:12:52 -0700 Subject: [PATCH 242/362] attempt to keep userData and new grab properties in sync --- .../entities/src/EntityItemProperties.cpp | 10 + libraries/entities/src/EntityItemProperties.h | 1 + .../entities/src/EntityScriptingInterface.cpp | 232 ++++++++++++++++++ libraries/entities/src/EntityTree.cpp | 211 ++++++++-------- libraries/entities/src/EntityTree.h | 2 + libraries/entities/src/GrabPropertyGroup.cpp | 30 +++ 6 files changed, 388 insertions(+), 98 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 37d8dc0bdc..d901592759 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -3803,6 +3803,16 @@ bool EntityItemProperties::queryAACubeRelatedPropertyChanged() const { return parentRelatedPropertyChanged() || dimensionsChanged(); } +bool EntityItemProperties::grabbingRelatedPropertyChanged() const { + const GrabPropertyGroup& grabProperties = getGrab(); + return grabProperties.triggerableChanged() || grabProperties.grabbableChanged() || + grabProperties.grabFollowsControllerChanged() || grabProperties.grabKinematicChanged() || + grabProperties.equippableChanged() || grabProperties.equippableLeftPositionChanged() || + grabProperties.equippableRightPositionChanged() || grabProperties.equippableLeftRotationChanged() || + grabProperties.equippableRightRotationChanged() || grabProperties.equippableIndicatorURLChanged() || + grabProperties.equippableIndicatorScaleChanged() || grabProperties.equippableIndicatorOffsetChanged(); +} + // Checking Certifiable Properties #define ADD_STRING_PROPERTY(n, N) if (!get##N().isEmpty()) json[#n] = get##N() #define ADD_ENUM_PROPERTY(n, N) json[#n] = get##N##AsString() diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index ae2c402d22..c91ccda5aa 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -108,6 +108,7 @@ public: bool getScalesWithParent() const; bool parentRelatedPropertyChanged() const; bool queryAACubeRelatedPropertyChanged() const; + bool grabbingRelatedPropertyChanged() const; AABox getAABox() const; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index f8b22fdbae..ae951c7862 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -16,6 +16,9 @@ #include #include +#include +#include +#include #include #include @@ -37,6 +40,7 @@ #include "WebEntityItem.h" #include #include +#include "GrabPropertyGroup.h" const QString GRABBABLE_USER_DATA = "{\"grabbableKey\":{\"grabbable\":true}}"; const QString NOT_GRABBABLE_USER_DATA = "{\"grabbableKey\":{\"grabbable\":false}}"; @@ -237,6 +241,229 @@ EntityItemProperties convertPropertiesFromScriptSemantics(const EntityItemProper } +void synchronizeSpatialKey(const GrabPropertyGroup& grabProperties, QJsonObject& grabbableKey, bool& userDataChanged) { + if (grabProperties.equippableLeftPositionChanged() || + grabProperties.equippableRightPositionChanged() || + grabProperties.equippableRightRotationChanged() || + grabProperties.equippableIndicatorURLChanged() || + grabProperties.equippableIndicatorScaleChanged() || + grabProperties.equippableIndicatorOffsetChanged()) { + + QJsonObject spatialKey = grabbableKey["spatialKey"].toObject(); + + if (grabProperties.equippableLeftPositionChanged()) { + if (grabProperties.getEquippableLeftPosition() == INITIAL_LEFT_EQUIPPABLE_POSITION) { + spatialKey.remove("leftRelativePosition"); + } else { + spatialKey["leftRelativePosition"] = + QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableLeftPosition())); + } + } + if (grabProperties.equippableRightPositionChanged()) { + if (grabProperties.getEquippableRightPosition() == INITIAL_RIGHT_EQUIPPABLE_POSITION) { + spatialKey.remove("rightRelativePosition"); + } else { + spatialKey["rightRelativePosition"] = + QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableRightPosition())); + } + } + if (grabProperties.equippableLeftRotationChanged()) { + spatialKey["relativeRotation"] = + QJsonValue::fromVariant(quatToQMap(grabProperties.getEquippableLeftRotation())); + } else if (grabProperties.equippableRightRotationChanged()) { + spatialKey["relativeRotation"] = + QJsonValue::fromVariant(quatToQMap(grabProperties.getEquippableRightRotation())); + } + + grabbableKey["spatialKey"] = spatialKey; + userDataChanged = true; + } +} + + +void synchronizeGrabbableKey(const GrabPropertyGroup& grabProperties, QJsonObject& userData, bool& userDataChanged) { + if (grabProperties.triggerableChanged() || + grabProperties.grabbableChanged() || + grabProperties.grabFollowsControllerChanged() || + grabProperties.grabKinematicChanged() || + grabProperties.equippableChanged()) { + + QJsonObject grabbableKey = userData["grabbableKey"].toObject(); + + if (grabProperties.triggerableChanged()) { + if (grabProperties.getTriggerable()) { + grabbableKey["triggerable"] = true; + } else { + grabbableKey.remove("triggerable"); + } + } + if (grabProperties.grabbableChanged()) { + if (grabProperties.getGrabbable()) { + grabbableKey.remove("grabbable"); + } else { + grabbableKey["grabbable"] = false; + } + } + if (grabProperties.grabFollowsControllerChanged()) { + if (grabProperties.getGrabFollowsController()) { + grabbableKey.remove("ignoreIK"); + } else { + grabbableKey["ignoreIK"] = false; + } + } + if (grabProperties.grabKinematicChanged()) { + if (grabProperties.getGrabKinematic()) { + grabbableKey.remove("kinematic"); + } else { + grabbableKey["kinematic"] = false; + } + } + if (grabProperties.equippableChanged()) { + if (grabProperties.getEquippable()) { + grabbableKey["equippable"] = true; + } else { + grabbableKey.remove("equippable"); + } + } + + if (grabbableKey.contains("spatialKey")) { + synchronizeSpatialKey(grabProperties, grabbableKey, userDataChanged); + } + + userData["grabbableKey"] = grabbableKey; + userDataChanged = true; + } +} + +void synchronizeGrabJoints(const GrabPropertyGroup& grabProperties, QJsonObject& joints) { + QJsonArray rightHand = joints["RightHand"].toArray(); + QJsonObject rightHandPosition = rightHand.size() > 0 ? rightHand[0].toObject() : QJsonObject(); + QJsonObject rightHandRotation = rightHand.size() > 1 ? rightHand[1].toObject() : QJsonObject(); + QJsonArray leftHand = joints["LeftHand"].toArray(); + QJsonObject leftHandPosition = leftHand.size() > 0 ? leftHand[0].toObject() : QJsonObject(); + QJsonObject leftHandRotation = leftHand.size() > 1 ? leftHand[1].toObject() : QJsonObject(); + + if (grabProperties.equippableLeftPositionChanged()) { + leftHandPosition = + QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableLeftPosition())).toObject(); + } + if (grabProperties.equippableRightPositionChanged()) { + rightHandPosition = + QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableRightPosition())).toObject(); + } + if (grabProperties.equippableLeftRotationChanged()) { + leftHandRotation = + QJsonValue::fromVariant(quatToQMap(grabProperties.getEquippableLeftRotation())).toObject(); + } + if (grabProperties.equippableRightRotationChanged()) { + rightHandRotation = + QJsonValue::fromVariant(quatToQMap(grabProperties.getEquippableRightRotation())).toObject(); + } + + rightHand[0] = rightHandPosition; + rightHand[1] = rightHandRotation; + joints["RightHand"] = rightHand; + leftHand[0] = leftHandPosition; + leftHand[1] = leftHandRotation; + joints["LeftHand"] = leftHand; +} + +void synchronizeEquipHotspot(const GrabPropertyGroup& grabProperties, QJsonObject& userData, bool& userDataChanged) { + if (grabProperties.equippableLeftPositionChanged() || + grabProperties.equippableRightPositionChanged() || + grabProperties.equippableRightRotationChanged() || + grabProperties.equippableIndicatorURLChanged() || + grabProperties.equippableIndicatorScaleChanged() || + grabProperties.equippableIndicatorOffsetChanged()) { + + QJsonArray equipHotspots = userData["equipHotspots"].toArray(); + QJsonObject equipHotspot = equipHotspots[0].toObject(); + QJsonObject joints = equipHotspot["joints"].toObject(); + + synchronizeGrabJoints(grabProperties, joints); + + if (grabProperties.equippableIndicatorURLChanged()) { + equipHotspot["modelURL"] = grabProperties.getEquippableIndicatorURL(); + } + if (grabProperties.equippableIndicatorScaleChanged()) { + QJsonObject scale = + QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableIndicatorScale())).toObject(); + equipHotspot["radius"] = scale; + equipHotspot["modelScale"] = scale; + + } + if (grabProperties.equippableIndicatorOffsetChanged()) { + equipHotspot["position"] = + QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableIndicatorOffset())).toObject(); + } + + equipHotspot["joints"] = joints; + equipHotspots[0] = equipHotspot; + userData["equipHotspots"] = equipHotspots; + userDataChanged = true; + } +} + +void synchronizeWearable(const GrabPropertyGroup& grabProperties, QJsonObject& userData, bool& userDataChanged) { + if (grabProperties.equippableLeftPositionChanged() || + grabProperties.equippableRightPositionChanged() || + grabProperties.equippableRightRotationChanged() || + grabProperties.equippableIndicatorURLChanged() || + grabProperties.equippableIndicatorScaleChanged() || + grabProperties.equippableIndicatorOffsetChanged()) { + + QJsonObject wearable = userData["wearable"].toObject(); + QJsonObject joints = wearable["joints"].toObject(); + + synchronizeGrabJoints(grabProperties, joints); + + wearable["joints"] = joints; + userData["wearable"] = wearable; + userDataChanged = true; + } +} + +void synchronizeEditedGrabProperties(EntityItemProperties& properties, const QString& previousUserdata) { + // After sufficient warning to content creators, we should be able to remove this. + + if (properties.grabbingRelatedPropertyChanged()) { + // This edit touches a new-style grab property, so make userData agree... + GrabPropertyGroup& grabProperties = properties.getGrab(); + + bool userDataChanged { false }; + + // if the edit changed userData, use the updated version coming along with the edit. If not, use + // what was already in the entity. + QByteArray userDataString; + if (properties.userDataChanged()) { + userDataString = properties.getUserData().toUtf8(); + } else { + userDataString = previousUserdata.toUtf8();; + } + QJsonObject userData = QJsonDocument::fromJson(userDataString).object(); + + if (userData.contains("grabbableKey")) { + synchronizeGrabbableKey(grabProperties, userData, userDataChanged); + } + if (userData.contains("equipHotspots")) { + synchronizeEquipHotspot(grabProperties, userData, userDataChanged); + } + if (userData.contains("wearable")) { + synchronizeWearable(grabProperties, userData, userDataChanged); + } + + if (userDataChanged) { + properties.setUserData(QJsonDocument(userData).toJson()); + } + + } else if (properties.userDataChanged()) { + // This edit touches userData (and doesn't touch a new-style grab property). Check for grabbableKey in the + // userdata and make the new-style grab properties agree + convertGrabUserDataToProperties(properties); + } +} + + QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties, bool clientOnly) { PROFILE_RANGE(script_entities, __FUNCTION__); @@ -257,6 +484,7 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties propertiesWithSimID = convertPropertiesFromScriptSemantics(propertiesWithSimID, scalesWithParent); propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged()); + synchronizeEditedGrabProperties(propertiesWithSimID, QString()); EntityItemID id; // If we have a local entity tree set, then also update it. @@ -559,6 +787,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& simulationOwner = entity->getSimulationOwner(); }); + QString previousUserdata; if (entity) { if (properties.hasSimulationRestrictedChanges()) { if (_bidOnSimulationOwnership) { @@ -597,6 +826,8 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& // make sure the properties has a type, so that the encode can know which properties to include properties.setType(entity->getType()); + + previousUserdata = entity->getUserData(); } else if (_bidOnSimulationOwnership) { // bail when simulation participants don't know about entity return QUuid(); @@ -605,6 +836,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& // How to check for this cheaply? properties = convertPropertiesFromScriptSemantics(properties, properties.getScalesWithParent()); + synchronizeEditedGrabProperties(properties, previousUserdata); properties.setLastEditedBy(sessionID); // done reading and modifying properties --> start write diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 0b3b8abba2..fdd92eb11c 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2493,6 +2493,118 @@ bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer return true; } +void convertGrabUserDataToProperties(EntityItemProperties& properties) { + GrabPropertyGroup& grabProperties = properties.getGrab(); + QJsonObject userData = QJsonDocument::fromJson(properties.getUserData().toUtf8()).object(); + + QJsonValue grabbableKeyValue = userData["grabbableKey"]; + if (grabbableKeyValue.isObject()) { + QJsonObject grabbableKey = grabbableKeyValue.toObject(); + + QJsonValue wantsTrigger = grabbableKey["wantsTrigger"]; + if (wantsTrigger.isBool()) { + grabProperties.setTriggerable(wantsTrigger.toBool()); + } + QJsonValue triggerable = grabbableKey["triggerable"]; + if (triggerable.isBool()) { + grabProperties.setTriggerable(triggerable.toBool()); + } + QJsonValue grabbable = grabbableKey["grabbable"]; + if (grabbable.isBool()) { + grabProperties.setGrabbable(grabbable.toBool()); + } + QJsonValue ignoreIK = grabbableKey["ignoreIK"]; + if (ignoreIK.isBool()) { + grabProperties.setGrabFollowsController(ignoreIK.toBool()); + } + QJsonValue kinematic = grabbableKey["kinematic"]; + if (kinematic.isBool()) { + grabProperties.setGrabKinematic(kinematic.toBool()); + } + QJsonValue equippable = grabbableKey["equippable"]; + if (equippable.isBool()) { + grabProperties.setEquippable(equippable.toBool()); + } + + if (grabbableKey["spatialKey"].isObject()) { + QJsonObject spatialKey = grabbableKey["spatialKey"].toObject(); + grabProperties.setEquippable(true); + if (spatialKey["leftRelativePosition"].isObject()) { + grabProperties.setEquippableLeftPosition(qMapToVec3(spatialKey["leftRelativePosition"].toVariant())); + } + if (spatialKey["rightRelativePosition"].isObject()) { + grabProperties.setEquippableRightPosition(qMapToVec3(spatialKey["rightRelativePosition"].toVariant())); + } + if (spatialKey["relativeRotation"].isObject()) { + grabProperties.setEquippableLeftRotation(qMapToQuat(spatialKey["relativeRotation"].toVariant())); + grabProperties.setEquippableRightRotation(qMapToQuat(spatialKey["relativeRotation"].toVariant())); + } + } + } + + QJsonValue wearableValue = userData["wearable"]; + if (wearableValue.isObject()) { + QJsonObject wearable = wearableValue.toObject(); + QJsonObject joints = wearable["joints"].toObject(); + if (joints["LeftHand"].isArray()) { + QJsonArray leftHand = joints["LeftHand"].toArray(); + if (leftHand.size() == 2) { + grabProperties.setEquippable(true); + grabProperties.setEquippableLeftPosition(qMapToVec3(leftHand[0].toVariant())); + grabProperties.setEquippableLeftRotation(qMapToQuat(leftHand[1].toVariant())); + } + } + if (joints["RightHand"].isArray()) { + QJsonArray rightHand = joints["RightHand"].toArray(); + if (rightHand.size() == 2) { + grabProperties.setEquippable(true); + grabProperties.setEquippableRightPosition(qMapToVec3(rightHand[0].toVariant())); + grabProperties.setEquippableRightRotation(qMapToQuat(rightHand[1].toVariant())); + } + } + } + + QJsonValue equipHotspotsValue = userData["equipHotspots"]; + if (equipHotspotsValue.isArray()) { + QJsonArray equipHotspots = equipHotspotsValue.toArray(); + if (equipHotspots.size() > 0) { + // just take the first one + QJsonObject firstHotSpot = equipHotspots[0].toObject(); + QJsonObject joints = firstHotSpot["joints"].toObject(); + if (joints["LeftHand"].isArray()) { + QJsonArray leftHand = joints["LeftHand"].toArray(); + if (leftHand.size() == 2) { + grabProperties.setEquippableLeftPosition(qMapToVec3(leftHand[0].toVariant())); + grabProperties.setEquippableLeftRotation(qMapToQuat(leftHand[1].toVariant())); + } + } + if (joints["RightHand"].isArray()) { + QJsonArray rightHand = joints["RightHand"].toArray(); + if (rightHand.size() == 2) { + grabProperties.setEquippable(true); + grabProperties.setEquippableRightPosition(qMapToVec3(rightHand[0].toVariant())); + grabProperties.setEquippableRightRotation(qMapToQuat(rightHand[1].toVariant())); + } + } + QJsonValue indicatorURL = firstHotSpot["modelURL"]; + if (indicatorURL.isString()) { + grabProperties.setEquippableIndicatorURL(indicatorURL.toString()); + } + QJsonValue indicatorScale = firstHotSpot["modelScale"]; + if (indicatorScale.isDouble()) { + grabProperties.setEquippableIndicatorScale(glm::vec3((float)indicatorScale.toDouble())); + } else if (indicatorScale.isObject()) { + grabProperties.setEquippableIndicatorScale(qMapToVec3(indicatorScale.toVariant())); + } + QJsonValue indicatorOffset = firstHotSpot["position"]; + if (indicatorOffset.isObject()) { + grabProperties.setEquippableIndicatorOffset(qMapToVec3(indicatorOffset.toVariant())); + } + } + } +} + + bool EntityTree::readFromMap(QVariantMap& map) { // These are needed to deal with older content (before adding inheritance modes) int contentVersion = map["Version"].toInt(); @@ -2639,104 +2751,7 @@ bool EntityTree::readFromMap(QVariantMap& map) { // convert old grab-related userData to new grab properties if (contentVersion < (int)EntityVersion::GrabProperties) { - QJsonObject userData = QJsonDocument::fromJson(properties.getUserData().toUtf8()).object(); - QJsonObject grabbableKey = userData["grabbableKey"].toObject(); - QJsonValue wantsTrigger = grabbableKey["wantsTrigger"]; - - GrabPropertyGroup& grabProperties = properties.getGrab(); - - if (wantsTrigger.isBool()) { - grabProperties.setTriggerable(wantsTrigger.toBool()); - } - QJsonValue triggerable = grabbableKey["triggerable"]; - if (triggerable.isBool()) { - grabProperties.setTriggerable(triggerable.toBool()); - } - QJsonValue grabbable = grabbableKey["grabbable"]; - if (grabbable.isBool()) { - grabProperties.setGrabbable(grabbable.toBool()); - } - QJsonValue ignoreIK = grabbableKey["ignoreIK"]; - if (ignoreIK.isBool()) { - grabProperties.setGrabFollowsController(ignoreIK.toBool()); - } - QJsonValue kinematic = grabbableKey["kinematic"]; - if (kinematic.isBool()) { - grabProperties.setGrabKinematic(kinematic.toBool()); - } - - if (grabbableKey["spatialKey"].isObject()) { - QJsonObject spatialKey = grabbableKey["spatialKey"].toObject(); - grabProperties.setEquippable(true); - if (spatialKey["leftRelativePosition"].isObject()) { - grabProperties.setEquippableLeftPosition(qMapToVec3(spatialKey["leftRelativePosition"].toVariant())); - } - if (spatialKey["rightRelativePosition"].isObject()) { - grabProperties.setEquippableRightPosition(qMapToVec3(spatialKey["rightRelativePosition"].toVariant())); - } - if (spatialKey["relativeRotation"].isObject()) { - grabProperties.setEquippableLeftRotation(qMapToQuat(spatialKey["relativeRotation"].toVariant())); - grabProperties.setEquippableRightRotation(qMapToQuat(spatialKey["relativeRotation"].toVariant())); - } - } - - QJsonObject wearable = userData["wearable"].toObject(); - QJsonObject joints = wearable["joints"].toObject(); - if (joints["LeftHand"].isArray()) { - QJsonArray leftHand = joints["LeftHand"].toArray(); - if (leftHand.size() == 2) { - grabProperties.setEquippable(true); - grabProperties.setEquippableLeftPosition(qMapToVec3(leftHand[0].toVariant())); - grabProperties.setEquippableLeftRotation(qMapToQuat(leftHand[1].toVariant())); - } - } - if (joints["RightHand"].isArray()) { - QJsonArray rightHand = joints["RightHand"].toArray(); - if (rightHand.size() == 2) { - grabProperties.setEquippable(true); - grabProperties.setEquippableRightPosition(qMapToVec3(rightHand[0].toVariant())); - grabProperties.setEquippableRightRotation(qMapToQuat(rightHand[1].toVariant())); - } - } - - if (userData["equipHotspots"].isArray()) { - QJsonArray equipHotspots = userData["equipHotspots"].toArray(); - if (equipHotspots.size() > 0) { - // just take the first one - QJsonObject firstHotSpot = equipHotspots[0].toObject(); - QJsonObject joints = firstHotSpot["joints"].toObject(); - if (joints["LeftHand"].isArray()) { - QJsonArray leftHand = joints["LeftHand"].toArray(); - if (leftHand.size() == 2) { - grabProperties.setEquippable(true); - grabProperties.setEquippableLeftPosition(qMapToVec3(leftHand[0].toVariant())); - grabProperties.setEquippableLeftRotation(qMapToQuat(leftHand[1].toVariant())); - } - } - if (joints["RightHand"].isArray()) { - QJsonArray rightHand = joints["RightHand"].toArray(); - if (rightHand.size() == 2) { - grabProperties.setEquippable(true); - grabProperties.setEquippableRightPosition(qMapToVec3(rightHand[0].toVariant())); - grabProperties.setEquippableRightRotation(qMapToQuat(rightHand[1].toVariant())); - } - } - QJsonValue indicatorURL = firstHotSpot["modelURL"]; - if (indicatorURL.isString()) { - grabProperties.setEquippableIndicatorURL(indicatorURL.toString()); - } - QJsonValue indicatorScale = firstHotSpot["modelScale"]; - if (indicatorScale.isDouble()) { - grabProperties.setEquippableIndicatorScale(glm::vec3((float)indicatorScale.toDouble())); - } else if (indicatorScale.isObject()) { - grabProperties.setEquippableIndicatorScale(qMapToVec3(indicatorScale.toVariant())); - } - QJsonValue indicatorOffset = firstHotSpot["position"]; - if (indicatorOffset.isObject()) { - grabProperties.setEquippableIndicatorOffset(qMapToVec3(indicatorOffset.toVariant())); - } - } - } + convertGrabUserDataToProperties(properties); } // Zero out the spread values that were fixed in version ParticleEntityFix so they behave the same as before diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 2f971b8566..634ffcc1f3 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -424,4 +424,6 @@ private: std::map _namedPaths; }; +void convertGrabUserDataToProperties(EntityItemProperties& properties); + #endif // hifi_EntityTree_h diff --git a/libraries/entities/src/GrabPropertyGroup.cpp b/libraries/entities/src/GrabPropertyGroup.cpp index c433043e31..996eed4720 100644 --- a/libraries/entities/src/GrabPropertyGroup.cpp +++ b/libraries/entities/src/GrabPropertyGroup.cpp @@ -51,6 +51,9 @@ void GrabPropertyGroup::copyFromScriptValue(const QScriptValue& object, bool& _d COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(grab, equippableLeftRotation, quat, setEquippableLeftRotation); COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(grab, equippableRightPosition, vec3, setEquippableRightPosition); COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(grab, equippableRightRotation, quat, setEquippableRightRotation); + COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(grab, equippableIndicatorURL, QString, setEquippableIndicatorURL); + COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(grab, equippableIndicatorScale, vec3, setEquippableIndicatorScale); + COPY_GROUP_PROPERTY_FROM_QSCRIPTVALUE(grab, equippableIndicatorOffset, vec3, setEquippableIndicatorOffset); } void GrabPropertyGroup::merge(const GrabPropertyGroup& other) { @@ -63,6 +66,9 @@ void GrabPropertyGroup::merge(const GrabPropertyGroup& other) { COPY_PROPERTY_IF_CHANGED(equippableLeftRotation); COPY_PROPERTY_IF_CHANGED(equippableRightPosition); COPY_PROPERTY_IF_CHANGED(equippableRightRotation); + COPY_PROPERTY_IF_CHANGED(equippableIndicatorURL); + COPY_PROPERTY_IF_CHANGED(equippableIndicatorScale); + COPY_PROPERTY_IF_CHANGED(equippableIndicatorOffset); } void GrabPropertyGroup::debugDump() const { @@ -77,6 +83,9 @@ void GrabPropertyGroup::debugDump() const { qCDebug(entities) << " _equippableLeftRotation:" << _equippableLeftRotation; qCDebug(entities) << " _equippableRightPosition:" << _equippableRightPosition; qCDebug(entities) << " _equippableRightRotation:" << _equippableRightRotation; + qCDebug(entities) << " _equippableIndicatorURL:" << _equippableIndicatorURL; + qCDebug(entities) << " _equippableIndicatorScale:" << _equippableIndicatorScale; + qCDebug(entities) << " _equippableIndicatorOffset:" << _equippableIndicatorOffset; } void GrabPropertyGroup::listChangedProperties(QList& out) { @@ -107,6 +116,15 @@ void GrabPropertyGroup::listChangedProperties(QList& out) { if (equippableRightRotationChanged()) { out << "grab-equippableRightRotation"; } + if (equippableIndicatorURLChanged()) { + out << "grab-equippableIndicatorURL"; + } + if (equippableIndicatorScaleChanged()) { + out << "grab-equippableIndicatorScale"; + } + if (equippableIndicatorOffsetChanged()) { + out << "grab-equippableIndicatorOffset"; + } } bool GrabPropertyGroup::appendToEditPacket(OctreePacketData* packetData, @@ -184,6 +202,9 @@ void GrabPropertyGroup::markAllChanged() { _equippableLeftRotationChanged = true; _equippableRightPositionChanged = true; _equippableRightRotationChanged = true; + _equippableIndicatorURLChanged = true; + _equippableIndicatorScaleChanged = true; + _equippableIndicatorOffsetChanged = true; } EntityPropertyFlags GrabPropertyGroup::getChangedProperties() const { @@ -215,6 +236,9 @@ void GrabPropertyGroup::getProperties(EntityItemProperties& properties) const { COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(Grab, EquippableLeftRotation, getEquippableLeftRotation); COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(Grab, EquippableRightPosition, getEquippableRightPosition); COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(Grab, EquippableRightRotation, getEquippableRightRotation); + COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(Grab, EquippableIndicatorURL, getEquippableIndicatorURL); + COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(Grab, EquippableIndicatorScale, getEquippableIndicatorScale); + COPY_ENTITY_GROUP_PROPERTY_TO_PROPERTIES(Grab, EquippableIndicatorOffset, getEquippableIndicatorOffset); } bool GrabPropertyGroup::setProperties(const EntityItemProperties& properties) { @@ -231,6 +255,12 @@ bool GrabPropertyGroup::setProperties(const EntityItemProperties& properties) { setEquippableRightPosition); SET_ENTITY_GROUP_PROPERTY_FROM_PROPERTIES(Grab, EquippableRightRotation, equippableRightRotation, setEquippableRightRotation); + SET_ENTITY_GROUP_PROPERTY_FROM_PROPERTIES(Grab, EquippableIndicatorURL, equippableIndicatorURL, + setEquippableIndicatorURL); + SET_ENTITY_GROUP_PROPERTY_FROM_PROPERTIES(Grab, EquippableIndicatorScale, equippableIndicatorScale, + setEquippableIndicatorScale); + SET_ENTITY_GROUP_PROPERTY_FROM_PROPERTIES(Grab, EquippableIndicatorOffset, equippableIndicatorOffset, + setEquippableIndicatorOffset); return somethingChanged; } From 387a8ce933b2a9d45e7fb818a894a7761f368074 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 24 Oct 2018 10:21:34 -0700 Subject: [PATCH 243/362] fix QJsonArray handling --- libraries/entities/src/EntityScriptingInterface.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index ae951c7862..a920ed92ff 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -360,11 +360,13 @@ void synchronizeGrabJoints(const GrabPropertyGroup& grabProperties, QJsonObject& QJsonValue::fromVariant(quatToQMap(grabProperties.getEquippableRightRotation())).toObject(); } - rightHand[0] = rightHandPosition; - rightHand[1] = rightHandRotation; + rightHand = QJsonArray(); + rightHand.append(rightHandPosition); + rightHand.append(rightHandRotation); joints["RightHand"] = rightHand; - leftHand[0] = leftHandPosition; - leftHand[1] = leftHandRotation; + leftHand = QJsonArray(); + leftHand.append(leftHandPosition); + leftHand.append(leftHandRotation); joints["LeftHand"] = leftHand; } @@ -398,7 +400,8 @@ void synchronizeEquipHotspot(const GrabPropertyGroup& grabProperties, QJsonObjec } equipHotspot["joints"] = joints; - equipHotspots[0] = equipHotspot; + equipHotspots = QJsonArray(); + equipHotspots.append(equipHotspot); userData["equipHotspots"] = equipHotspots; userDataChanged = true; } From c5aafd4644be96280bffb98d4bb06f1717ab9c3f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 24 Oct 2018 12:24:06 -0700 Subject: [PATCH 244/362] fix spatialKey conversion --- libraries/entities/src/EntityScriptingInterface.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index a920ed92ff..3491688588 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -286,7 +286,10 @@ void synchronizeGrabbableKey(const GrabPropertyGroup& grabProperties, QJsonObjec grabProperties.grabbableChanged() || grabProperties.grabFollowsControllerChanged() || grabProperties.grabKinematicChanged() || - grabProperties.equippableChanged()) { + grabProperties.equippableChanged() || + grabProperties.equippableLeftPositionChanged() || + grabProperties.equippableRightPositionChanged() || + grabProperties.equippableRightRotationChanged()) { QJsonObject grabbableKey = userData["grabbableKey"].toObject(); From b93807b1a766526a16d8c2c8427e450fdb686a21 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 24 Oct 2018 12:32:38 -0700 Subject: [PATCH 245/362] remove conversion code from equipEntity.js --- .../controllerModules/equipEntity.js | 93 ++----------------- 1 file changed, 9 insertions(+), 84 deletions(-) diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index f4d9c731b7..9a78c706a9 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -167,16 +167,6 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa }; -var alreadyWarned = {}; -function warnAboutUserData(props) { - if (alreadyWarned[props.id]) { - return; - } - print("Warning -- overriding grab properties with userData for " + props.id + " / " + props.name); - alreadyWarned[props.id] = true; -} - - (function() { var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; @@ -199,80 +189,15 @@ function warnAboutUserData(props) { var UNEQUIP_KEY = "u"; function getWearableData(props) { - if (props.grab.equippable) { - // if equippable is true, we know this was already converted from the old userData style to properties - 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 { - // check for old userData equippability. The JSON reader will convert userData to properties - // in EntityTree.cpp, but this won't catch things created from scripts or some items in - // the market. Eventually we'll remove this section. - try { - if (!props.userDataParsed) { - props.userDataParsed = JSON.parse(props.userData); - } - var userDataParsed = props.userDataParsed; - - // userData: { wearable: { joints: { LeftHand: {...}, RightHand: {...} } } } - if (userDataParsed.wearable && userDataParsed.wearable.joints) { - warnAboutUserData(props); - userDataParsed.wearable.indicatorURL = ""; - userDataParsed.wearable.indicatorScale = { x: 1, y: 1, z: 1 }; - userDataParsed.wearable.indicatorOffset = { x: 0, y: 0, z: 0 }; - return userDataParsed.wearable; - } - - // userData: { equipHotspots: { joints: { LeftHand: {...}, RightHand: {...} } } } - // https://highfidelity.atlassian.net/wiki/spaces/HOME/pages/51085337/Authoring+Equippable+Entities - if (userDataParsed.equipHotspots && - userDataParsed.equipHotspots.length > 0 && - userDataParsed.equipHotspots[0].joints) { - warnAboutUserData(props); - var hotSpot = userDataParsed.equipHotspots[0]; - - var indicatorScale = { x: hotSpot.radius, y: hotSpot.radius, z: hotSpot.radius }; - if (hotSpot.modelURL && hotSpot.modelURL !== "") { - indicatorScale = hotSpot.modelScale; - } - - return { - joints: hotSpot.joints, - indicatorURL: hotSpot.modelURL, - indicatorScale: indicatorScale, - indicatorOffset: hotSpot.position, - }; - } - - // userData:{grabbableKey:{spatialKey:{leftRelativePosition:{...},rightRelativePosition:{...}}}} - if (userDataParsed.grabbableKey && - userDataParsed.grabbableKey.spatialKey) { - warnAboutUserData(props); - var joints = {}; - joints.LeftHand = [ { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0, w: 1 } ]; - joints.RightHand = [ { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0, w: 1 } ]; - if (userDataParsed.grabbableKey.spatialKey.leftRelativePosition) { - joints.LeftHand = [userDataParsed.grabbableKey.spatialKey.leftRelativePosition, - userDataParsed.grabbableKey.spatialKey.relativeRotation]; - } - if (userDataParsed.grabbableKey.spatialKey.rightRelativePosition) { - joints.RightHand = [userDataParsed.grabbableKey.spatialKey.rightRelativePosition, - userDataParsed.grabbableKey.spatialKey.relativeRotation]; - } - return { joints: joints }; - } - - } catch (err) { - // don't spam logs - } - return null; - } + 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 + }; } function getAttachPointSettings() { From b8f1c966852d5dfaf3e6ca1083aabcafcf768e96 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 25 Oct 2018 07:46:46 -0700 Subject: [PATCH 246/362] rats --- .../controllerModules/equipEntity.js | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index 9a78c706a9..2f343ff84b 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -189,15 +189,19 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var UNEQUIP_KEY = "u"; function getWearableData(props) { - 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 - }; + 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() { From c85dbb823327a66c0c1290eedfb9566ce8c4947f Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 25 Oct 2018 11:25:56 -0700 Subject: [PATCH 247/362] limitedCommerce switch --- interface/src/Application.cpp | 4 +++- interface/src/scripting/WalletScriptingInterface.cpp | 4 ---- interface/src/scripting/WalletScriptingInterface.h | 8 ++++++-- interface/src/ui/overlays/Web3DOverlay.cpp | 3 +++ 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e515a22403..d053bb2d2d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1144,7 +1144,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // set the OCULUS_STORE property so the oculus plugin can know if we ran from the Oculus Store static const QString OCULUS_STORE_ARG = "--oculus-store"; - setProperty(hifi::properties::OCULUS_STORE, arguments().indexOf(OCULUS_STORE_ARG) != -1); + bool isStore = arguments().indexOf(OCULUS_STORE_ARG) != -1; + setProperty(hifi::properties::OCULUS_STORE, isStore); + DependencyManager::get()->setLimitedCommerce(isStore); // Or we could make it a separate arg, or if either arg is set, etc. And should this instead by a hifi::properties? updateHeartbeat(); diff --git a/interface/src/scripting/WalletScriptingInterface.cpp b/interface/src/scripting/WalletScriptingInterface.cpp index e2158b9fd7..77ca232712 100644 --- a/interface/src/scripting/WalletScriptingInterface.cpp +++ b/interface/src/scripting/WalletScriptingInterface.cpp @@ -15,10 +15,6 @@ CheckoutProxy::CheckoutProxy(QObject* qmlObject, QObject* parent) : QmlWrapper(q Q_ASSERT(QThread::currentThread() == qApp->thread()); } -WalletScriptingInterface::WalletScriptingInterface() { - -} - void WalletScriptingInterface::refreshWalletStatus() { auto wallet = DependencyManager::get(); wallet->getWalletStatus(); diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h index 25955ca7a3..c4ab0da851 100644 --- a/interface/src/scripting/WalletScriptingInterface.h +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -29,7 +29,6 @@ public: CheckoutProxy(QObject* qmlObject, QObject* parent = nullptr); }; - /**jsdoc * @namespace Wallet * @@ -40,11 +39,12 @@ public: */ class WalletScriptingInterface : public QObject, public Dependency { Q_OBJECT + SINGLETON_DEPENDENCY Q_PROPERTY(uint walletStatus READ getWalletStatus WRITE setWalletStatus NOTIFY walletStatusChanged) + Q_PROPERTY(bool limitedCommerce READ getLimitedCommerce WRITE setLimitedCommerce) public: - WalletScriptingInterface(); /**jsdoc * @function Wallet.refreshWalletStatus @@ -67,6 +67,9 @@ public: // scripts could cause the Wallet to incorrectly report its status. void setWalletStatus(const uint& status); + bool getLimitedCommerce() { return _limitedCommerce; } + void setLimitedCommerce(bool isLimited) { _limitedCommerce = isLimited; } + signals: /**jsdoc @@ -97,6 +100,7 @@ signals: private: uint _walletStatus; + bool _limitedCommerce = false; }; #endif // hifi_WalletScriptingInterface_h diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 084615cae2..1a1bd00655 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -53,6 +53,7 @@ #include "ui/AvatarInputs.h" #include "avatar/AvatarManager.h" #include "scripting/AccountServicesScriptingInterface.h" +#include "scripting/WalletScriptingInterface.h" #include #include "ui/Snapshot.h" #include "SoundCacheScriptingInterface.h" @@ -95,6 +96,7 @@ Web3DOverlay::Web3DOverlay() { _webSurface->getSurfaceContext()->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED _webSurface->getSurfaceContext()->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); _webSurface->getSurfaceContext()->setContextProperty("AddressManager", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Wallet", DependencyManager::get().data()); } Web3DOverlay::Web3DOverlay(const Web3DOverlay* Web3DOverlay) : @@ -269,6 +271,7 @@ void Web3DOverlay::setupQmlSurface(bool isTablet) { _webSurface->getSurfaceContext()->setContextProperty("Web3DOverlay", this); _webSurface->getSurfaceContext()->setContextProperty("Window", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); + _webSurface->getSurfaceContext()->setContextProperty("Wallet", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("HiFiAbout", AboutUtil::getInstance()); _webSurface->getSurfaceContext()->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); From 6284c902be777e5342ee6c8579caeab7646a0027 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 25 Oct 2018 11:29:10 -0700 Subject: [PATCH 248/362] use the switch --- .../qml/hifi/commerce/purchases/Purchases.qml | 6 +-- .../qml/hifi/commerce/wallet/Wallet.qml | 51 +++++++++++-------- scripts/system/commerce/wallet.js | 3 +- scripts/system/html/js/marketplacesInject.js | 17 ++++--- scripts/system/marketplaces/marketplaces.js | 3 +- 5 files changed, 45 insertions(+), 35 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 11c1ffc8c2..b0d976b499 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -180,8 +180,8 @@ Rectangle { HifiCommerceCommon.EmulatedMarketplaceHeader { id: titleBarContainer; z: 997; - visible: false; //HRS FIXME !needsLogIn.visible; - height: 100; // HRS FIXME + visible: false; + height: 100; // HRS FIXME: get rid of the header and associated code entirely? // Size width: parent.width; // Anchors @@ -476,7 +476,7 @@ Rectangle { anchors.left: parent.left; anchors.leftMargin: 16; width: paintedWidth; - text: isShowingMyItems ? "My Items" : "Inventory"; //"My Purchases"; + text: isShowingMyItems ? "My Items" : "Inventory"; color: hifi.colors.black; size: 22; } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index c2a8258cca..c8fb346711 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -30,6 +30,7 @@ Rectangle { property string activeView: "initialize"; property bool keyboardRaised: false; property bool isPassword: false; + property bool limitedCommerce: true; anchors.fill: (typeof parent === undefined) ? undefined : parent; @@ -65,7 +66,7 @@ Rectangle { } } else if (walletStatus === 5) { if (root.activeView !== "walletSetup") { - root.activeView = "walletInventory"; // HRS FIXME "walletHome"; + root.activeView = "walletInventory"; Commerce.getSecurityImage(); } } else { @@ -125,7 +126,7 @@ Rectangle { // Title Bar text RalewaySemiBold { id: titleBarText; - text: "ASSETS"; //"WALLET"; + text: "ASSETS"; // Text size size: hifi.fontSizes.overlayTitle; // Anchors @@ -370,6 +371,7 @@ Rectangle { listModelName: "Send Money Connections"; z: 997; visible: root.activeView === "sendMoney"; + keyboardContainer: root; anchors.fill: parent; parentAppTitleBarHeight: titleBarContainer.height; parentAppNavBarHeight: tabButtonsContainer.height; @@ -464,13 +466,13 @@ Rectangle { visible: !walletSetup.visible; color: root.activeView === "walletHome" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; - anchors.left: exchangeMoneyButtonContainer.right; // HRS FIXME parent.left; + anchors.left: exchangeMoneyButtonContainer.right; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; HiFiGlyphs { id: homeTabIcon; - text: hifi.glyphs.leftRightArrows; // HRS FIXME hifi.glyphs.home2; + text: hifi.glyphs.leftRightArrows; // Size size: 50; // Anchors @@ -478,11 +480,11 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: -2; // Style - color: hifi.colors.lightGray50; // HRS FIXME root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + color: root.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); } RalewaySemiBold { - text: "RECENT ACTIVITY"; //"WALLET HOME"; + text: "RECENT ACTIVITY"; // Text size size: 16; // Anchors @@ -493,15 +495,16 @@ Rectangle { anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: hifi.colors.lightGray50; // HRS FIXME root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + color: root.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignTop; } - /* HRS FIXME MouseArea { + MouseArea { id: walletHomeTabMouseArea; anchors.fill: parent; + enabled: !root.limitedCommerce; hoverEnabled: enabled; onClicked: { root.activeView = "walletHome"; @@ -509,22 +512,22 @@ Rectangle { } onEntered: parent.color = hifi.colors.blueHighlight; onExited: parent.color = root.activeView === "walletHome" ? hifi.colors.blueAccent : hifi.colors.black; - }*/ + } } // "EXCHANGE MONEY" tab button Rectangle { id: exchangeMoneyButtonContainer; visible: !walletSetup.visible; - color: root.activeView === "walletInventory" ? hifi.colors.blueAccent : hifi.colors.black; // HRS FIXME hifi.colors.black; + color: root.activeView === "walletInventory" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; - anchors.left: parent.left; // FIXME walletHomeButtonContainer.right; + anchors.left: parent.left; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; HiFiGlyphs { id: exchangeMoneyTabIcon; - text: hifi.glyphs.home2; // HRS FIXME hifi.glyphs.leftRightArrows; + text: hifi.glyphs.home2; // Size size: 50; // Anchors @@ -532,11 +535,11 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: -2; // Style - color: root.activeView === "walletInventory" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; // HRS FIXMEhifi.colors.lightGray50; + color: root.activeView === "walletInventory" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; } RalewaySemiBold { - text: "INVENTORY"; // HRS FIXME "EXCHANGE MONEY"; + text: "INVENTORY"; // Text size size: 16; // Anchors @@ -547,7 +550,7 @@ Rectangle { anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: root.activeView === "walletInventory" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; // HRS FIXME hifi.colors.lightGray50; + color: root.activeView === "walletInventory" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; @@ -574,7 +577,7 @@ Rectangle { visible: !walletSetup.visible; color: root.activeView === "sendMoney" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; - anchors.left: walletHomeButtonContainer.right; // HRS FIXME exchangeMoneyButtonContainer.right; + anchors.left: walletHomeButtonContainer.right; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; @@ -588,7 +591,7 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: -2; // Style - color: hifi.colors.lightGray50; // HRS FIXME root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + color: root.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); } RalewaySemiBold { @@ -603,16 +606,17 @@ Rectangle { anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: hifi.colors.lightGray50; // HRS FIXME root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + color: root.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignTop; } - /* HRS FIXME MouseArea { + MouseArea { id: sendMoneyTabMouseArea; anchors.fill: parent; + enabled: !root.limitedCommerce; hoverEnabled: enabled; onClicked: { root.activeView = "sendMoney"; @@ -620,7 +624,7 @@ Rectangle { } onEntered: parent.color = hifi.colors.blueHighlight; onExited: parent.color = root.activeView === "sendMoney" ? hifi.colors.blueAccent : hifi.colors.black; - } */ + } } // "SECURITY" tab button @@ -737,7 +741,7 @@ Rectangle { sendMoneyButtonContainer.color = hifi.colors.black; securityButtonContainer.color = hifi.colors.black; helpButtonContainer.color = hifi.colors.black; - exchangeMoneyButtonContainer.color = hifi.colors.black; // HRS FIXME + exchangeMoneyButtonContainer.color = hifi.colors.black; if (root.activeView === "walletHome") { walletHomeButtonContainer.color = hifi.colors.blueAccent; } else if (root.activeView === "sendMoney") { @@ -746,7 +750,7 @@ Rectangle { securityButtonContainer.color = hifi.colors.blueAccent; } else if (root.activeView === "help") { helpButtonContainer.color = hifi.colors.blueAccent; - } else if (root.activeView == "walletInventory") { // HRS FIXME + } else if (root.activeView == "walletInventory") { exchangeMoneyButtonContainer.color = hifi.colors.blueAccent; } } @@ -825,6 +829,9 @@ Rectangle { case 'avatarDisconnected': // Because we don't have "channels" for sending messages to a specific QML object, the messages are broadcast to all QML Items. If an Item of yours happens to be visible when some script sends a message with a method you don't expect, you'll get "Unrecognized message..." logs. break; + case 'setLimitedCommerce': + root.limitedCommerce = message.limitedCommerce; + break; default: // HRS FIXME console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); walletInventory.fromScript(message); diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index b1a00809fc..58f39ca54c 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -490,6 +490,7 @@ function walletOpened() { triggerPressMapping.enable(); shouldShowDot = false; ui.messagesWaiting(shouldShowDot); + ui.sendMessage({method: 'setLimitedCommerce', limitedCommerce: Wallet.limitedCommerce}); // HRS FIXME Wallet should be accessible in qml. Why isn't it? } function walletClosed() { @@ -582,7 +583,7 @@ function uninstallMarketplaceItemTester() { } } -var BUTTON_NAME = "ASSETS"; //HRS FIXME "WALLET"; +var BUTTON_NAME = "ASSETS"; var WALLET_QML_SOURCE = "hifi/commerce/wallet/Wallet.qml"; var ui; function startup() { diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 21a2acad7d..e1863f791a 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -27,6 +27,7 @@ var xmlHttpRequest = null; var isPreparing = false; // Explicitly track download request status. + var limitedCommerce = false; var commerceMode = false; var userIsLoggedIn = false; var walletNeedsSetup = false; @@ -219,7 +220,9 @@ purchasesElement.style = "height:100%;margin-top:18px;font-weight:bold;float:right;margin-right:" + (dropDownElement.offsetWidth + 30) + "px;position:relative;z-index:999;"; navbarBrandElement.parentNode.insertAdjacentElement('beforeend', purchasesElement); - $('#purchasesButton').css('display', 'none'); // HRS FIXME + if (limitedCommerce) { + $('#purchasesButton').css('display', 'none'); + } $('#purchasesButton').on('click', function () { EventBridge.emitWebEvent(JSON.stringify({ type: "PURCHASES", @@ -285,7 +288,7 @@ } cost = $(this).closest('.col-xs-3').find('.item-cost').text(); var costInt = parseInt(cost, 10); - var disable = costInt > 0; // HRS FIXME + var disable = limitedCommerce && (costInt > 0); $(this).closest('.col-xs-3').prev().attr("class", 'col-xs-6'); $(this).closest('.col-xs-3').attr("class", 'col-xs-6'); @@ -303,7 +306,7 @@ if (parseInt(cost) > 0) { if (disable) { priceElement.html('N/A'); // In case the following fails - $(this).parent().parent().parent().parent().parent().css({"display": "none"}); // HRS FIXME + $(this).parent().parent().parent().parent().parent().css({"display": "none"}); // HRS FIXME, oh and do I have to set display non-none in the other branch? } else { priceElement.css({ "width": "auto" }); priceElement.html(' 0) { - availability = ''; // HRS FIXME + if (limitedCommerce && (costInt > 0)) { + availability = ''; } if (availability === 'available') { purchaseButton.css({ @@ -757,6 +757,7 @@ cancelClaraDownload(); } else if (message.type === "marketplaces") { if (message.action === "commerceSetting") { + limitedCommerce = !!message.data.limitedCommerce; commerceMode = !!message.data.commerceMode; userIsLoggedIn = !!message.data.userIsLoggedIn; walletNeedsSetup = !!message.data.walletNeedsSetup; diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 15b8c4e7cb..5b1d5c8897 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -230,7 +230,8 @@ function sendCommerceSettings() { userIsLoggedIn: Account.loggedIn, walletNeedsSetup: walletNeedsSetup(), metaverseServerURL: Account.metaverseServerURL, - messagesWaiting: shouldShowDot + messagesWaiting: shouldShowDot, + limitedCommerce: Wallet.limitedCommerce } }); } From eb347c55963dbf23b6f17cf2586b02f8b60da97e Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 25 Oct 2018 11:30:18 -0700 Subject: [PATCH 249/362] remove particle explorer --- .../particle_explorer/hifi-entity-ui.js | 709 ------------------ .../particle_explorer/particle-style.css | 140 ---- .../particle_explorer/particleExplorer.html | 43 -- .../particle_explorer/particleExplorer.js | 485 ------------ .../particle_explorer/particleExplorerTool.js | 144 ---- .../particle_explorer/underscore-min.js | 6 - 6 files changed, 1527 deletions(-) delete mode 100644 scripts/system/particle_explorer/hifi-entity-ui.js delete mode 100644 scripts/system/particle_explorer/particle-style.css delete mode 100644 scripts/system/particle_explorer/particleExplorer.html delete mode 100644 scripts/system/particle_explorer/particleExplorer.js delete mode 100644 scripts/system/particle_explorer/particleExplorerTool.js delete mode 100644 scripts/system/particle_explorer/underscore-min.js diff --git a/scripts/system/particle_explorer/hifi-entity-ui.js b/scripts/system/particle_explorer/hifi-entity-ui.js deleted file mode 100644 index 62a0aadc86..0000000000 --- a/scripts/system/particle_explorer/hifi-entity-ui.js +++ /dev/null @@ -1,709 +0,0 @@ -/* global window, document, print, alert, console,setTimeout, clearTimeout, _ $ */ -/* eslint no-console: 0 */ - -/** -UI Builder V1.0 - -Created by Matti 'Menithal' Lahtinen -24/5/2017 -Copyright 2017 High Fidelity, Inc. - -This can eventually be expanded to all of Edit, for now, starting -with Particles Only. - -This is created for the sole purpose of streamliming the bridge, and to simplify -the logic between an inputfield in WebView and Entities in High Fidelity. - -We also do not need anything as heavy as jquery or any other platform, -as we are mostly only building for QT (while, all the other JS frameworks usually do alot of polyfilling) - -Available Types: - - JSONInputField - Accepts JSON input, once one presses Save, it will be propegated. - Button- A Button that listens for a custom event as defined by callback - Boolean - Creates a checkbox that the user can either check or uncheck - SliderFloat - Creates a slider (with input) that has Float values from min to max. - Default is min 0, max 1 - SliderInteger - Creates a slider (with input) that has a Integer value from min to max. - Default is min 1, max 10000 - SliderRadian - Creates a slider (with input) that has Float values in degrees, - that are converted to radians. default is min 0, max Math.PI. - Texture - Creates a Image with an url input field that points to texture. - If image cannot form, show "cannot find image" - VecQuaternion - Creates a 3D Vector field that converts to quaternions. - Checkbox exists to show quaternions instead. - Color - Create field color button, that when pressed, opens the color picker. - Vector - Create a 3D Vector field that has one to one correspondence. - -The script will use this structure to build a UI that is connected The -id fields within High Fidelity - -This should make editing, and everything related much more simpler to maintain, -and If there is any changes to either the Entities or properties of - -**/ - -var RADIANS_PER_DEGREE = Math.PI / 180; -var DEBOUNCE_TIMEOUT = 125; - -var roundFloat = function (input, round) { - round = round ? round : 1000; - var sanitizedInput; - if (typeof input === "string") { - sanitizedInput = parseFloat(input); - } else { - sanitizedInput = input; - } - return Math.round(sanitizedInput * round) / round; -}; - -function HifiEntityUI(parent) { - this.parent = parent; - - var self = this; - this.sendPackage = {}; - this.settingsUpdateLock = false; - this.webBridgeSync = function(id, val) { - if (!this.settingsUpdateLock) { - this.sendPackage[id] = val; - this.webBridgeSyncDebounce(); - } - }; - this.webBridgeSyncDebounce = _.debounce(function () { - if (self.EventBridge) { - self.submitChanges(self.sendPackage); - self.sendPackage = {}; - } - }, DEBOUNCE_TIMEOUT); -} - -HifiEntityUI.prototype = { - setOnSelect: function (callback) { - this.onSelect = callback; - }, - submitChanges: function (structure) { - var message = { - messageType: "settings_update", - updatedSettings: structure - }; - this.EventBridge.emitWebEvent(JSON.stringify(message)); - }, - setUI: function (structure) { - this.structure = structure; - }, - disableFields: function () { - var fields = document.getElementsByTagName("input"); - for (var i = 0; i < fields.length; i++) { - if (fields[i].getAttribute("type") !== "button") { - fields[i].value = ""; - } - - fields[i].setAttribute("disabled", true); - } - var textures = document.getElementsByTagName("img"); - for (i = 0; i < textures.length; i++) { - textures[i].src = ""; - } - - textures = document.getElementsByClassName("with-texture"); - for (i = 0; i < textures.length; i++) { - textures[i].classList.remove("with-textures"); - textures[i].classList.add("no-texture"); - } - - var textareas = document.getElementsByTagName("textarea"); - for (var x = 0; x < textareas.length; x++) { - textareas[x].remove(); - } - }, - getSettings: function () { - var self = this; - var json = {}; - var keys = Object.keys(self.builtRows); - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var el = self.builtRows[key]; - if (el.className.indexOf("checkbox") !== -1) { - json[key] = document.getElementById(key) - .checked ? true : false; - } else if (el.className.indexOf("vector-section") !== -1) { - var vector = {}; - if (el.className.indexOf("rgb") !== -1) { - var red = document.getElementById(key + "-red"); - var blue = document.getElementById(key + "-blue"); - var green = document.getElementById(key + "-green"); - vector.red = red.value; - vector.blue = blue.value; - vector.green = green.value; - } else if (el.className.indexOf("pyr") !== -1) { - var p = document.getElementById(key + "-Pitch"); - var y = document.getElementById(key + "-Yaw"); - var r = document.getElementById(key + "-Roll"); - vector.x = p.value; - vector.y = y.value; - vector.z = r.value; - } else { - var x = document.getElementById(key + "-x"); - var ey = document.getElementById(key + "-y"); - var z = document.getElementById(key + "-z"); - vector.x = x.value; - vector.y = ey.value; - vector.z = z.value; - } - json[key] = vector; - } else if (el.className.indexOf("radian") !== -1) { - json[key] = document.getElementById(key).value * RADIANS_PER_DEGREE; - } else if (el.className.length > 0) { - json[key] = document.getElementById(key).value; - } - } - - - return json; - }, - fillFields: function (currentProperties) { - var self = this; - var fields = document.getElementsByTagName("input"); - - if (!currentProperties.locked) { - for (var i = 0; i < fields.length; i++) { - fields[i].removeAttribute("disabled"); - if (fields[i].hasAttribute("data-max")) { - // Reset Max to original max - fields[i].setAttribute("max", fields[i].getAttribute("data-max")); - } - } - } - - if (self.onSelect) { - self.onSelect(); - } - var keys = Object.keys(currentProperties); - - - for (var e in keys) { - if (keys.hasOwnProperty(e)) { - var value = keys[e]; - - var property = currentProperties[value]; - var field = self.builtRows[value]; - if (field) { - var el = document.getElementById(value); - - if (field.className.indexOf("radian") !== -1) { - el.value = property / RADIANS_PER_DEGREE; - el.onchange({ - target: el - }); - } else if (field.className.indexOf("range") !== -1 || field.className.indexOf("texture") !== -1) { - el.value = property; - el.onchange({ - target: el - }); - } else if (field.className.indexOf("checkbox") !== -1) { - if (property) { - el.setAttribute("checked", property); - } else { - el.removeAttribute("checked"); - } - } else if (field.className.indexOf("vector-section") !== -1) { - if (field.className.indexOf("rgb") !== -1) { - var red = document.getElementById(value + "-red"); - var blue = document.getElementById(value + "-blue"); - var green = document.getElementById(value + "-green"); - red.value = parseInt(property.red); - blue.value = parseInt(property.blue); - green.value = parseInt(property.green); - - red.oninput({ - target: red - }); - } else if (field.className.indexOf("xyz") !== -1) { - var x = document.getElementById(value + "-x"); - var y = document.getElementById(value + "-y"); - var z = document.getElementById(value + "-z"); - - x.value = roundFloat(property.x, 100); - y.value = roundFloat(property.y, 100); - z.value = roundFloat(property.z, 100); - } else if (field.className.indexOf("pyr") !== -1) { - var pitch = document.getElementById(value + "-Pitch"); - var yaw = document.getElementById(value + "-Yaw"); - var roll = document.getElementById(value + "-Roll"); - - pitch.value = roundFloat(property.x, 100); - yaw.value = roundFloat(property.y, 100); - roll.value = roundFloat(property.z, 100); - - } - } - } - } - } - }, - connect: function (EventBridge) { - this.EventBridge = EventBridge; - - var self = this; - - EventBridge.emitWebEvent(JSON.stringify({ - messageType: 'page_loaded' - })); - - EventBridge.scriptEventReceived.connect(function (data) { - data = JSON.parse(data); - - if (data.messageType === 'particle_settings') { - self.settingsUpdateLock = true; - self.fillFields(data.currentProperties); - self.settingsUpdateLock = false; - // Do expected property match with structure; - } else if (data.messageType === 'particle_close') { - self.disableFields(); - } - }); - }, - build: function () { - var self = this; - var sections = Object.keys(this.structure); - this.builtRows = {}; - sections.forEach(function (section, index) { - var properties = self.structure[section]; - self.addSection(self.parent, section, properties, index); - }); - }, - addSection: function (parent, section, properties, index) { - var self = this; - - var sectionDivHeader = document.createElement("fieldset"); - var title = document.createElement("legend"); - var dropDown = document.createElement("span"); - - dropDown.className = "arrow"; - sectionDivHeader.className = "major"; - title.className = "section-header"; - title.id = section + "-section"; - title.innerHTML = section; - title.appendChild(dropDown); - sectionDivHeader.appendChild(title); - - var collapsed = index !== 0; - - dropDown.innerHTML = collapsed ? "L" : "M"; - sectionDivHeader.setAttribute("collapsed", collapsed); - parent.appendChild(sectionDivHeader); - - var sectionDivBody = document.createElement("div"); - sectionDivBody.className = "property-group"; - - var animationWrapper = document.createElement("div"); - animationWrapper.className = "section-wrap"; - - for (var property in properties) { - if (properties.hasOwnProperty(property)) { - var builtRow = self.addElement(animationWrapper, properties[property]); - var id = properties[property].id; - if (id) { - self.builtRows[id] = builtRow; - } - } - } - sectionDivBody.appendChild(animationWrapper); - sectionDivHeader.appendChild(sectionDivBody); - _.defer(function () { - var height = (animationWrapper.clientHeight) + "px"; - if (collapsed) { - sectionDivBody.classList.remove("visible"); - sectionDivBody.style.maxHeight = "0px"; - } else { - sectionDivBody.classList.add("visible"); - sectionDivBody.style.maxHeight = height; - } - - title.onclick = function () { - collapsed = !collapsed; - if (collapsed) { - sectionDivBody.classList.remove("visible"); - sectionDivBody.style.maxHeight = "0px"; - } else { - sectionDivBody.classList.add("visible"); - sectionDivBody.style.maxHeight = (animationWrapper.clientHeight) + "px"; - } - // sectionDivBody.style.display = collapsed ? "none": "block"; - dropDown.innerHTML = collapsed ? "L" : "M"; - title.setAttribute("collapsed", collapsed); - }; - }); - }, - addLabel: function (parent, group) { - var label = document.createElement("label"); - label.innerHTML = group.name; - parent.appendChild(label); - if (group.unit) { - var span = document.createElement("span"); - span.innerHTML = group.unit; - span.className = "unit"; - label.appendChild(span); - } - return label; - }, - addVector: function (parent, group, labels, domArray) { - var self = this; - var inputs = labels ? labels : ["x", "y", "z"]; - domArray = domArray ? domArray : []; - parent.id = group.id; - for (var index in inputs) { - var element = document.createElement("input"); - - element.setAttribute("type", "number"); - element.className = inputs[index]; - element.id = group.id + "-" + inputs[index]; - - if (group.defaultRange) { - if (group.defaultRange.min) { - element.setAttribute("min", group.defaultRange.min); - } - if (group.defaultRange.max) { - element.setAttribute("max", group.defaultRange.max); - } - if (group.defaultRange.step) { - element.setAttribute("step", group.defaultRange.step); - } - } - if (group.oninput) { - element.oninput = group.oninput; - } else { - element.oninput = function (event) { - self.webBridgeSync(group.id, { - x: domArray[0].value, - y: domArray[1].value, - z: domArray[2].value - }); - }; - } - element.onchange = element.oninput; - domArray.push(element); - } - - this.addLabel(parent, group); - var className = ""; - for (var i = 0; i < inputs.length; i++) { - className += inputs[i].charAt(0) - .toLowerCase(); - } - parent.className += " property vector-section " + className; - - // Add Tuple and the rest - var tupleContainer = document.createElement("div"); - tupleContainer.className = "tuple"; - for (var domIndex in domArray) { - var container = domArray[domIndex]; - var div = document.createElement("div"); - var label = document.createElement("label"); - label.innerHTML = inputs[domIndex] + ":"; - label.setAttribute("for", container.id); - div.appendChild(container); - div.appendChild(label); - tupleContainer.appendChild(div); - } - parent.appendChild(tupleContainer); - }, - addVectorQuaternion: function (parent, group) { - this.addVector(parent, group, ["Pitch", "Yaw", "Roll"]); - }, - addColorPicker: function (parent, group) { - var self = this; - var $colPickContainer = $('
', { - id: group.id, - class: "color-picker" - }); - var updateColors = function (red, green, blue) { - $colPickContainer.css('background-color', "rgb(" + - red + "," + - green + "," + - blue + ")"); - }; - - var inputs = ["red", "green", "blue"]; - var domArray = []; - group.oninput = function (event) { - $colPickContainer.colpickSetColor( - { - r: domArray[0].value, - g: domArray[1].value, - b: domArray[2].value - }, - true); - }; - group.defaultRange = { - min: 0, - max: 255, - step: 1 - }; - - parent.appendChild($colPickContainer[0]); - self.addVector(parent, group, inputs, domArray); - - updateColors(domArray[0].value, domArray[1].value, domArray[2].value); - - // Could probably write a custom one for this to completely write out jquery, - // but for now, using the same as earlier. - - /* Color Picker Logic Here */ - - - $colPickContainer.colpick({ - colorScheme: (group.layoutColorScheme === undefined ? 'dark' : group.layoutColorScheme), - layout: (group.layoutType === undefined ? 'hex' : group.layoutType), - submit: (group.useSubmitButton === undefined ? true : group.useSubmitButton), - color: { - r: domArray[0].value, - g: domArray[1].value, - b: domArray[2].value - }, - onChange: function (hsb, hex, rgb, el) { - updateColors(rgb.r, rgb.g, rgb.b); - - domArray[0].value = rgb.r; - domArray[1].value = rgb.g; - domArray[2].value = rgb.b; - self.webBridgeSync(group.id, { - red: rgb.r, - green: rgb.g, - blue: rgb.b - }); - }, - onSubmit: function (hsb, hex, rgb, el) { - $(el) - .css('background-color', '#' + hex); - $(el) - .colpickHide(); - domArray[0].value = rgb.r; - domArray[1].value = rgb.g; - domArray[2].value = rgb.b; - self.webBridgeSync(group.id, { - red: rgb.r, - green: rgb.g, - blue: rgb.b - }); - } - }); - }, - addTextureField: function (parent, group) { - var self = this; - this.addLabel(parent, group); - parent.className += " property texture"; - var textureImage = document.createElement("div"); - var textureUrl = document.createElement("input"); - textureUrl.setAttribute("type", "text"); - textureUrl.id = group.id; - textureImage.className = "texture-image no-texture"; - var image = document.createElement("img"); - var imageLoad = _.debounce(function (url) { - if (url.slice(0, 5).toLowerCase() === "atp:/") { - image.src = ""; - image.style.display = "none"; - textureImage.classList.remove("with-texture"); - textureImage.classList.remove("no-texture"); - textureImage.classList.add("no-preview"); - } else if (url.length > 0) { - textureImage.classList.remove("no-texture"); - textureImage.classList.remove("no-preview"); - textureImage.classList.add("with-texture"); - image.src = url; - image.style.display = "block"; - } else { - image.src = ""; - image.style.display = "none"; - textureImage.classList.remove("with-texture"); - textureImage.classList.remove("no-preview"); - textureImage.classList.add("no-texture"); - } - }, DEBOUNCE_TIMEOUT * 2); - - textureUrl.oninput = function (event) { - // Add throttle - var url = event.target.value; - imageLoad(url); - self.webBridgeSync(group.id, url); - }; - textureUrl.onchange = textureUrl.oninput; - textureImage.appendChild(image); - parent.appendChild(textureImage); - parent.appendChild(textureUrl); - }, - addSlider: function (parent, group) { - var self = this; - this.addLabel(parent, group); - parent.className += " property range"; - var container = document.createElement("div"); - container.className = "slider-wrapper"; - var slider = document.createElement("input"); - slider.setAttribute("type", "range"); - - var inputField = document.createElement("input"); - inputField.setAttribute("type", "number"); - - container.appendChild(slider); - container.appendChild(inputField); - parent.appendChild(container); - - if (group.type === "SliderInteger") { - inputField.setAttribute("min", group.min !== undefined ? group.min : 0); - inputField.setAttribute("step", 1); - - slider.setAttribute("min", group.min !== undefined ? group.min : 0); - slider.setAttribute("max", group.max !== undefined ? group.max : 10000); - slider.setAttribute("data-max", group.max !== undefined ? group.max : 10000); - slider.setAttribute("step", 1); - - inputField.oninput = function (event) { - // TODO: Remove this functionality? Alan finds it confusing - if (parseInt(event.target.value) > parseInt(slider.getAttribute("max")) && group.max !== 1) { - slider.setAttribute("max", event.target.value); - } - slider.value = event.target.value; - self.webBridgeSync(group.id, slider.value); - }; - inputField.onchange = inputField.oninput; - slider.oninput = function (event) { - inputField.value = event.target.value; - self.webBridgeSync(group.id, inputField.value); - }; - - inputField.id = group.id; - } else if (group.type === "SliderRadian") { - slider.setAttribute("min", group.min !== undefined ? group.min : 0); - slider.setAttribute("max", group.max !== undefined ? group.max : 180); - slider.setAttribute("step", 1); - parent.className += " radian"; - inputField.setAttribute("min", (group.min !== undefined ? group.min : 0)); - inputField.setAttribute("max", (group.max !== undefined ? group.max : 180)); - - inputField.oninput = function (event) { - slider.value = event.target.value; - self.webBridgeSync(group.id, slider.value * RADIANS_PER_DEGREE); - }; - inputField.onchange = inputField.oninput; - - inputField.id = group.id; - slider.oninput = function (event) { - if (event.target.value > 0) { - inputField.value = Math.floor(event.target.value); - } else { - inputField.value = Math.ceil(event.target.value); - } - self.webBridgeSync(group.id, inputField.value * RADIANS_PER_DEGREE); - }; - var degrees = document.createElement("label"); - degrees.innerHTML = "°"; - degrees.style.fontSize = "1.4rem"; - degrees.style.display = "inline"; - degrees.style.verticalAlign = "top"; - degrees.style.paddingLeft = "0.4rem"; - container.appendChild(degrees); - - } else { - // Must then be Float - inputField.setAttribute("min", group.min !== undefined ? group.min : 0); - slider.setAttribute("step", 0.01); - - slider.setAttribute("min", group.min !== undefined ? group.min : 0); - slider.setAttribute("max", group.max !== undefined ? group.max : 1); - slider.setAttribute("data-max", group.max !== undefined ? group.max : 1); - slider.setAttribute("step", 0.01); - - inputField.oninput = function (event) { - // TODO: Remove this functionality? Alan finds it confusing - if (parseFloat(event.target.value) > parseFloat(slider.getAttribute("max")) && group.max !== 1) { - slider.setAttribute("max", event.target.value); - } - - slider.value = event.target.value; - self.webBridgeSync(group.id, slider.value); - // bind web sock update here. - }; - inputField.onchange = inputField.oninput; - slider.oninput = function (event) { - inputField.value = event.target.value; - self.webBridgeSync(group.id, inputField.value); - }; - - inputField.id = group.id; - } - - // UpdateBinding - }, - addCheckBox: function (parent, group) { - var checkBox = document.createElement("input"); - checkBox.setAttribute("type", "checkbox"); - var self = this; - checkBox.onchange = function (event) { - self.webBridgeSync(group.id, event.target.checked); - }; - checkBox.id = group.id; - parent.appendChild(checkBox); - var label = this.addLabel(parent, group); - label.setAttribute("for", checkBox.id); - parent.className += " property checkbox"; - }, - addElement: function (parent, group) { - var self = this; - var property = document.createElement("div"); - property.id = group.id; - - var row = document.createElement("div"); - switch (group.type) { - case "Button": - var button = document.createElement("input"); - button.setAttribute("type", "button"); - button.id = group.id; - if (group.disabled) { - button.disabled = group.disabled; - } - button.className = group.class; - button.value = group.name; - - button.onclick = group.callback; - parent.appendChild(button); - break; - case "Row": - var hr = document.createElement("hr"); - hr.className = "splitter"; - if (group.id) { - hr.id = group.id; - } - parent.appendChild(hr); - break; - case "Boolean": - self.addCheckBox(row, group); - parent.appendChild(row); - break; - case "SliderFloat": - case "SliderInteger": - case "SliderRadian": - self.addSlider(row, group); - parent.appendChild(row); - break; - case "Texture": - self.addTextureField(row, group); - parent.appendChild(row); - break; - case "Color": - self.addColorPicker(row, group); - parent.appendChild(row); - break; - case "Vector": - self.addVector(row, group); - parent.appendChild(row); - break; - case "VectorQuaternion": - self.addVectorQuaternion(row, group); - parent.appendChild(row); - break; - default: - console.log("not defined"); - } - return row; - } -}; \ No newline at end of file diff --git a/scripts/system/particle_explorer/particle-style.css b/scripts/system/particle_explorer/particle-style.css deleted file mode 100644 index cde325f6c6..0000000000 --- a/scripts/system/particle_explorer/particle-style.css +++ /dev/null @@ -1,140 +0,0 @@ -/* -// particle-style.css -// -// Created by Matti 'Menithal' Lahtinen on 21 May 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 -*/ - - -.property-group { - max-height: 0; - -webkit-transition: max-height 0.15s ease-out; - transition: max-height 0.15s ease-out; - overflow: hidden; -} -.property-group.visible { - transition: max-height 0.25s ease-in; -} -.section-wrap { - width: 100%; -} -.property { - padding: 0.4rem 0; - margin: 0; -} -.property.checkbox { - margin: 0; -} -.property.range label{ - display: block; -} - -input[type="button"] { - margin: 0.4rem; - min-width: 6rem; -} -input[type="text"] { - margin: 0; -} -.property.range input[type=number]{ - margin-left: 0.8rem; - width: 5.4rem; - height: 1.8rem; -} -input[type=range] { - -webkit-appearance: none; - background: #2e2e2e; - height: 1.8rem; - border-radius: 1rem; -} -input[type=range]::-webkit-slider-thumb { - -webkit-appearance:none; - width: 0.6rem; - height: 1.8rem; - padding:0; - margin: 0; - background-color: #696969; - border-radius: 1rem; -} -input[type=range]::-webkit-slider-thumb:hover { - background-color: white; -} -input[type=range]:focus { /*#252525*/ - outline: none; -} -.tuple label { - text-transform: capitalize; -} -.slider-wrapper { - display: table; - padding: 0.4rem 0; -} -hr.splitter{ - width: 100%; - padding: 0.2rem 0 0 0; - margin: 0; - position: relative; - clear: both; -} -hr.splitter:last-of-type{ - padding:0; -} -#rem { - height: 1rem; - width: 1rem; -} -.property { - min-height: 2rem; -} -.property.vector-section{ - - width: 24rem; -} - -.property.texture { - display: block; -} -.property.texture input{ - margin: 0.4rem 0; -} -.texture-image img{ - padding: 0; - margin: 0; - width: 100%; - height: 100%; - display: none; -} -.texture-image { - display: block; - position: relative; - background-repeat: no-repeat; - background-position: center; - background-size: 100% 100%; - margin-top: 0.4rem; - height:128px; - width: 128px; - background-image: url('data:image/png;base64,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'); -} - -.texture-image.no-texture { - background-image: url('data:image/png;base64,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'); -} - -.texture-image.no-preview { - background-image: url('data:image/png;base64,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'); -} - -#properties-list > fieldset { - margin-top: 0px; -} - -#main-header { - margin-bottom: 21px; -} - -.section-wrap { - padding: 21px 0px; -} \ No newline at end of file diff --git a/scripts/system/particle_explorer/particleExplorer.html b/scripts/system/particle_explorer/particleExplorer.html deleted file mode 100644 index ab4c249cc3..0000000000 --- a/scripts/system/particle_explorer/particleExplorer.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - -
-
- -
- -
-
- - - diff --git a/scripts/system/particle_explorer/particleExplorer.js b/scripts/system/particle_explorer/particleExplorer.js deleted file mode 100644 index f1b7c8600f..0000000000 --- a/scripts/system/particle_explorer/particleExplorer.js +++ /dev/null @@ -1,485 +0,0 @@ -// -// particleExplorer.js -// -// Created by James B. Pollack @imgntn on 9/26/2015 -// Copyright 2017 High Fidelity, Inc. -// -// Reworked by Menithal on 20/5/2017 -// Reworked by Daniela Fontes and Artur Gomes (Mimicry) on 12/18/2017 -// -// Web app side of the App - contains GUI. -// This is an example of a new, easy way to do two way bindings between dynamically created GUI and in-world entities. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -/* global HifiEntityUI, openEventBridge, console, EventBridge, document, window */ -/* eslint no-console: 0, no-global-assign: 0 */ - -(function () { - - var root = document.getElementById("properties-list"); - - window.onload = function () { - var ui = new HifiEntityUI(root); - var textarea = document.createElement("textarea"); - var properties = ""; - var menuStructure = { - General: [ - { - type: "Row", - id: "export-import-field" - }, - { - id: "show-properties-button", - name: "Show Properties", - type: "Button", - class: "blue", - disabled: true, - callback: function (event) { - var insertZone = document.getElementById("export-import-field"); - var json = ui.getSettings(); - properties = JSON.stringify(json); - textarea.value = properties; - if (!insertZone.contains(textarea)) { - insertZone.appendChild(textarea); - insertZone.parentNode.parentNode.style.maxHeight = - insertZone.parentNode.clientHeight + "px"; - document.getElementById("export-properties-button").removeAttribute("disabled"); - textarea.onchange = function (e) { - if (e.target.value !== properties) { - document.getElementById("import-properties-button").removeAttribute("disabled"); - } - }; - textarea.oninput = textarea.onchange; - document.getElementById("show-properties-button").value = "Hide Properties"; - } else { - textarea.onchange = function () {}; - textarea.oninput = textarea.onchange; - textarea.value = ""; - textarea.remove(); - insertZone.parentNode.parentNode.style.maxHeight = - insertZone.parentNode.clientHeight + "px"; - document.getElementById("export-properties-button").setAttribute("disabled", true); - document.getElementById("import-properties-button").setAttribute("disabled", true); - document.getElementById("show-properties-button").value = "Show Properties"; - } - } - }, - { - id: "import-properties-button", - name: "Import", - type: "Button", - class: "blue", - disabled: true, - callback: function (event) { - ui.fillFields(JSON.parse(textarea.value)); - ui.submitChanges(JSON.parse(textarea.value)); - } - }, - { - id: "export-properties-button", - name: "Export", - type: "Button", - class: "red", - disabled: true, - callback: function (event) { - textarea.select(); - try { - var success = document.execCommand('copy'); - if (!success) { - throw "Not success :("; - } - } catch (e) { - print("couldnt copy field"); - } - } - }, - { - type: "Row" - }, - { - id: "isEmitting", - name: "Is Emitting", - type: "Boolean" - }, - { - type: "Row" - }, - { - id: "lifespan", - name: "Lifespan", - type: "SliderFloat", - min: 0.01, - max: 10 - }, - { - type: "Row" - }, - { - id: "maxParticles", - name: "Max Particles", - type: "SliderInteger", - min: 1, - max: 10000 - }, - { - type: "Row" - }, - { - id: "textures", - name: "Textures", - type: "Texture" - }, - { - type: "Row" - } - ], - Emit: [ - { - id: "emitRate", - name: "Emit Rate", - type: "SliderInteger", - max: 1000, - min: 1 - }, - { - type: "Row" - }, - { - id: "emitSpeed", - name: "Emit Speed", - type: "SliderFloat", - max: 5 - }, - { - id: "speedSpread", - name: "Speed Spread", - type: "SliderFloat", - max: 5 - }, - { - type: "Row" - }, - { - id: "emitDimensions", - name: "Emit Dimension", - type: "Vector", - defaultRange: { - min: 0, - step: 0.01 - } - }, - { - type: "Row" - }, - { - id: "emitOrientation", - unit: "deg", - name: "Emit Orientation", - type: "VectorQuaternion", - defaultRange: { - min: 0, - step: 0.01 - } - }, - { - type: "Row" - }, - { - id: "emitterShouldTrail", - name: "Emitter Should Trail", - type: "Boolean" - }, - { - type: "Row" - } - ], - Radius: [ - { - id: "particleRadius", - name: "Particle Radius", - type: "SliderFloat", - max: 4.0 - }, - { - type: "Row" - }, - { - id: "radiusSpread", - name: "Radius Spread", - type: "SliderFloat", - max: 4.0 - }, - { - type: "Row" - }, - { - id: "radiusStart", - name: "Radius Start", - type: "SliderFloat", - max: 4.0 - }, - { - type: "Row" - }, - { - id: "radiusFinish", - name: "Radius Finish", - type: "SliderFloat", - max: 4.0 - }, - { - type: "Row" - } - ], - Color: [ - { - id: "color", - name: "Color", - type: "Color", - defaultColor: { - red: 255, - green: 255, - blue: 255 - }, - layoutType: "hex", - layoutColorScheme: "dark", - useSubmitButton: false - }, - { - type: "Row" - }, - { - id: "colorSpread", - name: "Color Spread", - type: "Color", - defaultColor: { - red: 0, - green: 0, - blue: 0 - }, - layoutType: "hex", - layoutColorScheme: "dark", - useSubmitButton: false - }, - { - type: "Row" - }, - { - id: "colorStart", - name: "Color Start", - type: "Color", - defaultColor: { - red: 255, - green: 255, - blue: 255 - }, - layoutType: "hex", - layoutColorScheme: "dark", - useSubmitButton: false - }, - { - type: "Row" - }, - { - id: "colorFinish", - name: "Color Finish", - type: "Color", - defaultColor: { - red: 255, - green: 255, - blue: 255 - }, - layoutType: "hex", - layoutColorScheme: "dark", - useSubmitButton: false - }, - { - type: "Row" - } - ], - Acceleration: [ - { - id: "emitAcceleration", - name: "Emit Acceleration", - type: "Vector", - defaultRange: { - step: 0.01 - } - }, - { - type: "Row" - }, - { - id: "accelerationSpread", - name: "Acceleration Spread", - type: "Vector", - defaultRange: { - step: 0.01 - } - }, - { - type: "Row" - } - ], - Alpha: [ - { - id: "alpha", - name: "Alpha", - type: "SliderFloat", - max: 1.0 - }, - { - type: "Row" - }, - { - id: "alphaSpread", - name: "Alpha Spread", - type: "SliderFloat", - max: 1.0 - }, - { - type: "Row" - }, - { - id: "alphaStart", - name: "Alpha Start", - type: "SliderFloat", - max: 1.0 - }, - { - type: "Row" - }, - { - id: "alphaFinish", - name: "Alpha Finish", - type: "SliderFloat", - max: 1.0 - }, - { - type: "Row" - } - ], - Spin: [ - { - id: "particleSpin", - name: "Particle Spin", - type: "SliderRadian", - min: -360.0, - max: 360.0 - }, - { - type: "Row" - }, - { - id: "spinSpread", - name: "Spin Spread", - type: "SliderRadian", - max: 360.0 - }, - { - type: "Row" - }, - { - id: "spinStart", - name: "Spin Start", - type: "SliderRadian", - min: -360.0, - max: 360.0 - }, - { - type: "Row" - }, - { - id: "spinFinish", - name: "Spin Finish", - type: "SliderRadian", - min: -360.0, - max: 360.0 - }, - { - type: "Row" - }, - { - id: "rotateWithEntity", - name: "Rotate with Entity", - type: "Boolean" - }, - { - type: "Row" - } - ], - Polar: [ - { - id: "polarStart", - name: "Polar Start", - unit: "deg", - type: "SliderRadian" - }, - { - type: "Row" - }, - { - id: "polarFinish", - name: "Polar Finish", - unit: "deg", - type: "SliderRadian" - }, - { - type: "Row" - } - ], - Azimuth: [ - { - id: "azimuthStart", - name: "Azimuth Start", - unit: "deg", - type: "SliderRadian", - min: -180, - max: 0 - }, - { - type: "Row" - }, - { - id: "azimuthFinish", - name: "Azimuth Finish", - unit: "deg", - type: "SliderRadian" - }, - { - type: "Row" - } - ] - }; - ui.setUI(menuStructure); - ui.setOnSelect(function () { - document.getElementById("show-properties-button").removeAttribute("disabled"); - document.getElementById("export-properties-button").setAttribute("disabled", true); - document.getElementById("import-properties-button").setAttribute("disabled", true); - }); - ui.build(); - var overrideLoad = false; - if (openEventBridge === undefined) { - overrideLoad = true, - openEventBridge = function (callback) { - callback({ - emitWebEvent: function () {}, - submitChanges: function () {}, - scriptEventReceived: { - connect: function () { - - } - } - }); - }; - } - openEventBridge(function (EventBridge) { - ui.connect(EventBridge); - }); - if (overrideLoad) { - openEventBridge(); - } - }; -})(); diff --git a/scripts/system/particle_explorer/particleExplorerTool.js b/scripts/system/particle_explorer/particleExplorerTool.js deleted file mode 100644 index a3be004329..0000000000 --- a/scripts/system/particle_explorer/particleExplorerTool.js +++ /dev/null @@ -1,144 +0,0 @@ -// -// particleExplorerTool.js -// -// Created by Eric Levin on 2/15/16 -// Copyright 2016 High Fidelity, Inc. -// Adds particleExplorer tool to the edit panel when a user selects a particle entity from the edit tool window -// This is an example of a new, easy way to do two way bindings between dynamically created GUI and in-world entities. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -/* global ParticleExplorerTool */ - - -var PARTICLE_EXPLORER_HTML_URL = Script.resolvePath('particleExplorer.html'); - -ParticleExplorerTool = function(createToolsWindow) { - var that = {}; - that.activeParticleEntity = 0; - that.updatedActiveParticleProperties = {}; - - that.createWebView = function() { - that.webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - that.webView.setVisible = function(value) {}; - that.webView.webEventReceived.connect(that.webEventReceived); - createToolsWindow.webEventReceived.addListener(this, that.webEventReceived); - }; - - function emitScriptEvent(data) { - var messageData = JSON.stringify(data); - that.webView.emitScriptEvent(messageData); - createToolsWindow.emitScriptEvent(messageData); - } - - that.destroyWebView = function() { - if (!that.webView) { - return; - } - that.activeParticleEntity = 0; - that.updatedActiveParticleProperties = {}; - - emitScriptEvent({ - messageType: "particle_close" - }); - }; - - function sendParticleProperties(properties) { - emitScriptEvent({ - messageType: "particle_settings", - currentProperties: properties - }); - } - - function sendActiveParticleProperties() { - var properties = Entities.getEntityProperties(that.activeParticleEntity); - if (properties.emitOrientation) { - properties.emitOrientation = Quat.safeEulerAngles(properties.emitOrientation); - } - // Update uninitialized variables - if (isNaN(properties.alphaStart)) { - properties.alphaStart = properties.alpha; - } - if (isNaN(properties.alphaFinish)) { - properties.alphaFinish = properties.alpha; - } - if (isNaN(properties.radiusStart)) { - properties.radiusStart = properties.particleRadius; - } - if (isNaN(properties.radiusFinish)) { - properties.radiusFinish = properties.particleRadius; - } - if (isNaN(properties.colorStart.red)) { - properties.colorStart = properties.color; - } - if (isNaN(properties.colorFinish.red)) { - properties.colorFinish = properties.color; - } - if (isNaN(properties.spinStart)) { - properties.spinStart = properties.particleSpin; - } - if (isNaN(properties.spinFinish)) { - properties.spinFinish = properties.particleSpin; - } - sendParticleProperties(properties); - } - - function sendUpdatedActiveParticleProperties() { - sendParticleProperties(that.updatedActiveParticleProperties); - that.updatedActiveParticleProperties = {}; - } - - that.webEventReceived = function(message) { - var data = JSON.parse(message); - if (data.messageType === "settings_update") { - var updatedSettings = data.updatedSettings; - - var optionalProps = ["alphaStart", "alphaFinish", "radiusStart", "radiusFinish", "colorStart", "colorFinish", "spinStart", "spinFinish"]; - var fallbackProps = ["alpha", "particleRadius", "color", "particleSpin"]; - for (var i = 0; i < optionalProps.length; i++) { - var fallbackProp = fallbackProps[Math.floor(i / 2)]; - var optionalValue = updatedSettings[optionalProps[i]]; - var fallbackValue = updatedSettings[fallbackProp]; - if (optionalValue && fallbackValue) { - delete updatedSettings[optionalProps[i]]; - } - } - - if (updatedSettings.emitOrientation) { - updatedSettings.emitOrientation = Quat.fromVec3Degrees(updatedSettings.emitOrientation); - } - - Entities.editEntity(that.activeParticleEntity, updatedSettings); - - var entityProps = Entities.getEntityProperties(that.activeParticleEntity, optionalProps); - - var needsUpdate = false; - for (var i = 0; i < optionalProps.length; i++) { - var fallbackProp = fallbackProps[Math.floor(i / 2)]; - var fallbackValue = updatedSettings[fallbackProp]; - if (fallbackValue) { - var optionalProp = optionalProps[i]; - if ((fallbackProp !== "color" && isNaN(entityProps[optionalProp])) || (fallbackProp === "color" && isNaN(entityProps[optionalProp].red))) { - that.updatedActiveParticleProperties[optionalProp] = fallbackValue; - needsUpdate = true; - } - } - } - - if (needsUpdate) { - sendUpdatedActiveParticleProperties(); - } - - } else if (data.messageType === "page_loaded") { - sendActiveParticleProperties(); - } - }; - - that.setActiveParticleEntity = function(id) { - that.activeParticleEntity = id; - sendActiveParticleProperties(); - }; - - return that; -}; diff --git a/scripts/system/particle_explorer/underscore-min.js b/scripts/system/particle_explorer/underscore-min.js deleted file mode 100644 index f01025b7bc..0000000000 --- a/scripts/system/particle_explorer/underscore-min.js +++ /dev/null @@ -1,6 +0,0 @@ -// Underscore.js 1.8.3 -// http://underscorejs.org -// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors -// Underscore may be freely distributed under the MIT license. -(function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); -//# sourceMappingURL=underscore-min.map \ No newline at end of file From 8b797952479eb1b5db73d20ac15fedf5926bc7de Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 25 Oct 2018 12:31:01 -0700 Subject: [PATCH 250/362] nix the word "purchase" where possible --- .../qml/hifi/commerce/checkout/Checkout.qml | 26 +++++++++---------- .../common/EmulatedMarketplaceHeader.qml | 2 +- .../hifi/commerce/common/FirstUseTutorial.qml | 4 +-- .../qml/hifi/commerce/purchases/Purchases.qml | 2 +- .../qml/hifi/commerce/wallet/Wallet.qml | 2 +- .../qml/hifi/commerce/wallet/WalletHome.qml | 2 +- scripts/system/html/js/marketplacesInject.js | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index b13f23f17d..19cc0d2d9d 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -383,7 +383,7 @@ Rectangle { anchors.leftMargin: 16; width: paintedWidth; height: paintedHeight; - text: "Review Purchase:"; + text: "Review:"; color: hifi.colors.black; size: 28; } @@ -570,7 +570,7 @@ Rectangle { height: 50; anchors.left: parent.left; anchors.right: parent.right; - text: root.isUpdating ? "UPDATE TO THIS ITEM FOR FREE" : "VIEW THIS ITEM IN MY PURCHASES"; + text: root.isUpdating ? "UPDATE TO THIS ITEM FOR FREE" : "VIEW THIS ITEM IN YOUR INVENTORY"; onClicked: { if (root.isUpdating) { sendToScript({method: 'checkout_goToPurchases', filterText: root.baseItemName}); @@ -608,9 +608,9 @@ Rectangle { } else if (root.isCertified) { if (!root.shouldBuyWithControlledFailure) { if (root.itemType === "contentSet" && !Entities.canReplaceContent()) { - lightboxPopup.titleText = "Purchase Content Set"; + lightboxPopup.titleText = "Get Content Set"; lightboxPopup.bodyText = "You will not be able to replace this domain's content with " + root.itemName + - " until the server owner gives you 'Replace Content' permissions.

Are you sure you want to purchase this content set?"; + "
until the server owner gives you 'Replace Content' permissions.

Are you sure you want to get this content set?"; lightboxPopup.button1text = "CANCEL"; lightboxPopup.button1method = function() { lightboxPopup.visible = false; @@ -694,7 +694,7 @@ Rectangle { id: completeText2; text: "The " + (root.itemTypesText)[itemTypesArray.indexOf(root.itemType)] + '
' + root.itemName + '' + - " has been added to your Purchases and a receipt will appear in your Wallet's transaction history."; + " has been added to your Inventory."; // Text size size: 18; // Anchors @@ -864,7 +864,7 @@ Rectangle { RalewaySemiBold { id: myPurchasesLink; - text: 'View this item in My Purchases'; + text: 'View this item in your Inventory'; // Text size size: 18; // Anchors @@ -908,7 +908,7 @@ Rectangle { RalewayRegular { id: pendingText; - text: 'Your item is marked "pending" while your purchase is being confirmed. ' + + text: 'Your item is marked "pending" while it is being confirmed. ' + 'Learn More'; // Text size size: 18; @@ -925,8 +925,8 @@ Rectangle { horizontalAlignment: Text.AlignLeft; verticalAlignment: Text.AlignVCenter; onLinkActivated: { - lightboxPopup.titleText = "Purchase Confirmations"; - lightboxPopup.bodyText = 'Your item is marked "pending" while your purchase is being confirmed.

' + + lightboxPopup.titleText = "Confirmations"; + lightboxPopup.bodyText = 'Your item is marked "pending" while it is being confirmed.

' + 'Confirmations usually take about 90 seconds.'; lightboxPopup.button1text = "CLOSE"; lightboxPopup.button1method = function() { @@ -971,7 +971,7 @@ Rectangle { RalewayRegular { id: failureHeaderText; - text: "Purchase Failed.
Your Purchases and HFC balance haven't changed."; + text: "Purchase Failed.
Your Inventory and HFC balance haven't changed."; // Text size size: 24; // Anchors @@ -1122,10 +1122,10 @@ Rectangle { if (root.balanceAfterPurchase < 0) { // If you already own the item... if (!root.alreadyOwned) { - buyText.text = "Your Wallet does not have sufficient funds to purchase this item."; + buyText.text = "You do not have sufficient funds to purchase this item."; // Else if you don't already own the item... } else if (canBuyAgain()) { - buyText.text = "Your Wallet does not have sufficient funds to purchase this item again."; + buyText.text = "You do not have sufficient funds to purchase this item again."; } else { buyText.text = "While you do not have sufficient funds to buy this, you already have this item." } @@ -1171,7 +1171,7 @@ Rectangle { buyText.text = ""; } } else { - buyText.text = 'This type of item cannot currently be certified, so it will not show up in "My Purchases". You can access it again for free from the Marketplace.'; + buyText.text = 'This type of item cannot currently be certified, so it will not show up in "Inventory". You can access it again for free from the Marketplace.'; buyTextContainer.color = hifi.colors.white; buyTextContainer.border.color = hifi.colors.white; buyGlyph.text = ""; diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index 1b77dcd3e9..de9c62c197 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -128,7 +128,7 @@ Item { RalewaySemiBold { id: myPurchasesText; - text: "My Purchases"; + text: "Inventory"; // Text size size: 18; // Style diff --git a/interface/resources/qml/hifi/commerce/common/FirstUseTutorial.qml b/interface/resources/qml/hifi/commerce/common/FirstUseTutorial.qml index 5f874d3f04..0b982893f1 100644 --- a/interface/resources/qml/hifi/commerce/common/FirstUseTutorial.qml +++ b/interface/resources/qml/hifi/commerce/common/FirstUseTutorial.qml @@ -87,7 +87,7 @@ Rectangle { } RalewayRegular { id: introText2; - text: "My Purchases"; + text: "Inventory"; // Text size size: 22; // Anchors @@ -116,7 +116,7 @@ Rectangle { RalewayRegular { id: step1text; - text: "The 'REZ IT' button makes your purchase appear in front of you."; + text: "The 'REZ IT' button makes your item appear in front of you."; // Text size size: 20; // Anchors diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index b0d976b499..655af79e68 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -946,7 +946,7 @@ Rectangle { // Explanitory text RalewayRegular { id: haventPurchasedYet; - text: "You haven't purchased anything yet!

Get an item from Marketplace to add it to My Purchases."; + text: "You haven't gotten anything yet!

Get an item from Marketplace to add it to your Inventory."; // Text size size: 22; // Anchors diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index c8fb346711..d66ed62768 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -404,7 +404,7 @@ Rectangle { lightboxPopup.titleText = "Automatically Log Out"; lightboxPopup.bodyText = "By default, after you log in to High Fidelity, you will stay logged in to your High Fidelity " + "account even after you close and re-open Interface. This means anyone who opens Interface on your computer " + - "could make purchases with your Wallet.\n\n" + + "could make purchases with your HFC.\n\n" + "If you do not want to stay logged in across Interface sessions, check this box."; lightboxPopup.button1text = "CLOSE"; lightboxPopup.button1method = function() { diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 627da1d43f..246773ff07 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -181,7 +181,7 @@ Item { RalewaySemiBold { id: myPurchasesLink; - text: 'My Purchases'; + text: 'Inventory'; // Anchors anchors.top: parent.top; anchors.topMargin: 26; diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index e1863f791a..43fd9ab935 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -458,7 +458,7 @@ if (isUpdating) { purchaseButton.html('UPDATE FOR FREE'); } else if (availability !== 'available') { - purchaseButton.html('UNAVAILABLE (' + availability + ')'); + purchaseButton.html('UNAVAILABLE' + (availability ? ('(' + availability + ')') : '')); } else if (parseInt(cost) > 0 && $('#side-info').find('#buyItemButton').size() === 0) { purchaseButton.html('PURCHASE ' + cost); From e3b1e14db6ffc94470f1ee2fabfb6a587816d04c Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 25 Oct 2018 12:37:08 -0700 Subject: [PATCH 251/362] nix "buy" where possible. --- interface/resources/qml/LoginDialog/LinkAccountBody.qml | 2 +- interface/resources/qml/hifi/avatarapp/MessageBoxes.qml | 8 ++++---- .../resources/qml/hifi/commerce/wallet/NeedsLogIn.qml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 48cf124127..00980b0876 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -96,7 +96,7 @@ Item { topMargin: hifi.dimensions.contentSpacing.y } - text: qsTr("Sign in to High Fidelity to make friends, get HFC, and buy interesting things on the Marketplace!") + text: qsTr("Sign in to High Fidelity to make friends, get HFC, and get interesting things on the Marketplace!") width: parent.width wrapMode: Text.WordWrap lineHeight: 1 diff --git a/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml b/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml index b7782c697d..2c02272b46 100644 --- a/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml +++ b/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml @@ -63,8 +63,8 @@ MessageBox { popup.dialogButtons.yesButton.fontCapitalization = Font.MixedCase; popup.button1text = 'CANCEL' popup.titleText = 'Get Wearables' - popup.bodyText = 'Buy wearables from Marketplace.' + '
' + - 'Wear wearable from My Purchases.' + '
' + '
' + + popup.bodyText = 'Get wearables from Marketplace.' + '
' + + 'Wear wearable from Inventory.' + '
' + '
' + 'Visit “AvatarIsland” to get wearables' popup.imageSource = getWearablesUrl; @@ -128,8 +128,8 @@ MessageBox { popup.button1text = 'CANCEL' popup.titleText = 'Get Avatars' - popup.bodyText = 'Buy avatars from Marketplace.' + '
' + - 'Wear avatars in My Purchases.' + '
' + '
' + + popup.bodyText = 'Get avatars from Marketplace.' + '
' + + 'Wear avatars in Inventory.' + '
' + '
' + 'Visit “BodyMart” to get free avatars.' popup.imageSource = getAvatarsUrl; diff --git a/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml b/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml index eadf1ca8a2..03af964830 100644 --- a/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml +++ b/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml @@ -93,7 +93,7 @@ Item { // Text below helper text RalewayRegular { id: loginDetailText; - text: "To buy/sell items on the Marketplace, or to use your Wallet, you must first log in to High Fidelity."; + text: "To get items on the Marketplace, or to use your Assets, you must first log in to High Fidelity."; // Text size size: 18; // Anchors From 3503d79b052f3aa174ab570b36162ebd7e32f0be Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 25 Oct 2018 12:39:33 -0700 Subject: [PATCH 252/362] missed a buy/purchase --- interface/resources/qml/hifi/commerce/checkout/Checkout.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 19cc0d2d9d..6ff7c1ef1c 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -594,7 +594,7 @@ Rectangle { anchors.left: parent.left; anchors.right: parent.right; text: (root.isUpdating && root.itemEdition > 0) ? "CONFIRM UPDATE" : (((root.isCertified) ? ((ownershipStatusReceived && balanceReceived && availableUpdatesReceived) ? - ((viewInMyPurchasesButton.visible && !root.isUpdating) ? "Buy It Again" : "Confirm Purchase") : "--") : "Get Item")); + ((viewInMyPurchasesButton.visible && !root.isUpdating) ? "Get It Again" : "Confirm") : "--") : "Get Item")); onClicked: { if (root.isUpdating && root.itemEdition > 0) { // If we're updating an app, the existing app needs to be uninstalled. From 936c2b2cab7a12f675e797a57e587a73665cafe7 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 25 Oct 2018 12:46:30 -0700 Subject: [PATCH 253/362] don't show price for free items --- interface/resources/qml/hifi/commerce/checkout/Checkout.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 6ff7c1ef1c..f88a70ff05 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -448,7 +448,7 @@ Rectangle { // "HFC" balance label HiFiGlyphs { id: itemPriceTextLabel; - visible: !(root.isUpdating && root.itemEdition > 0); + visible: !(root.isUpdating && root.itemEdition > 0) && (root.itemPrice > 0); text: hifi.glyphs.hfc; // Size size: 30; @@ -464,7 +464,7 @@ Rectangle { } FiraSansSemiBold { id: itemPriceText; - text: (root.isUpdating && root.itemEdition > 0) ? "FREE\nUPDATE" : ((root.itemPrice === -1) ? "--" : root.itemPrice); + text: (root.isUpdating && root.itemEdition > 0) ? "FREE\nUPDATE" : ((root.itemPrice === -1) ? "--" : ((root.itemPrice > 0) ? root.itemPrice : "FREE")); // Text size size: (root.isUpdating && root.itemEdition > 0) ? 20 : 26; // Anchors From 9095df46416c51874aeb2cc1d9028aaddbea5a9e Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 25 Oct 2018 12:59:49 -0700 Subject: [PATCH 254/362] fix getting inventory back after showing my items --- interface/resources/qml/hifi/commerce/wallet/Wallet.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index d66ed62768..ee86844aaa 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -563,6 +563,7 @@ Rectangle { hoverEnabled: enabled; onClicked: { root.activeView = "walletInventory"; + walletInventory.isShowingMyItems = false; tabButtonsContainer.resetTabButtonColors(); } onEntered: parent.color = hifi.colors.blueHighlight; From 7dff0155851bca5e23202a5c8c9704e73f54b6db Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 25 Oct 2018 10:09:38 -0700 Subject: [PATCH 255/362] maybe? --- libraries/render-utils/src/MeshPartPayload.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 2fe0368db2..4ebd92bb05 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -242,9 +242,13 @@ ModelMeshPartPayload::ModelMeshPartPayload(ModelPointer model, int meshIndex, in #ifdef Q_OS_MAC // On mac AMD, we specifically need to have a _meshBlendshapeBuffer bound when using a deformed mesh pipeline // it cannot be null otherwise we crash in the drawcall using a deformed pipeline with a skinned only (not blendshaped) mesh - if ((_isBlendShaped || _isSkinned)) { - glm::vec4 data; - _meshBlendshapeBuffer = std::make_shared(sizeof(glm::vec4), reinterpret_cast(&data)); + if (_isBlendShaped) { + std::vector data(_meshNumVertices); + const auto blendShapeBufferSize = _meshNumVertices * sizeof(BlendshapeOffset); + _meshBlendshapeBuffer = std::make_shared(blendShapeBufferSize, reinterpret_cast(data.data()), blendShapeBufferSize); + } else if (_isSkinned) { + BlendshapeOffset data; + _meshBlendshapeBuffer = std::make_shared(sizeof(BlendshapeOffset), reinterpret_cast(&data), sizeof(BlendshapeOffset)); } #endif From 13acfb325ae55db7ffde438c5e344a6658431f60 Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 25 Oct 2018 15:06:15 -0700 Subject: [PATCH 256/362] fix collides with properties, fix ordering for start/finish/spread properties --- scripts/system/html/js/entityProperties.js | 216 ++++++++++----------- 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index d564a59a43..f73d634af9 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -710,15 +710,6 @@ const GROUPS = [ decimals: 2, propertyID: "particleRadius", }, - { - label: "Size Spread", - type: "slider", - min: 0, - max: 4, - step: 0.01, - decimals: 2, - propertyID: "radiusSpread", - }, { label: "Size Start", type: "slider", @@ -739,6 +730,15 @@ const GROUPS = [ propertyID: "radiusFinish", fallbackProperty: "particleRadius", }, + { + label: "Size Spread", + type: "slider", + min: 0, + max: 4, + step: 0.01, + decimals: 2, + propertyID: "radiusSpread", + }, ] }, { @@ -783,15 +783,6 @@ const GROUPS = [ decimals: 2, propertyID: "alpha", }, - { - label: "Alpha Spread", - type: "slider", - min: 0, - max: 1, - step: 0.01, - decimals: 2, - propertyID: "alphaSpread", - }, { label: "Alpha Start", type: "slider", @@ -812,6 +803,15 @@ const GROUPS = [ propertyID: "alphaFinish", fallbackProperty: "alpha", }, + { + label: "Alpha Spread", + type: "slider", + min: 0, + max: 1, + step: 0.01, + decimals: 2, + propertyID: "alphaSpread", + }, ] }, { @@ -853,17 +853,6 @@ const GROUPS = [ unit: "deg", propertyID: "particleSpin", }, - { - label: "Spin Spread", - type: "slider", - min: 0, - max: 360, - step: 1, - decimals: 0, - multiplier: DEGREES_TO_RADIANS, - unit: "deg", - propertyID: "spinSpread", - }, { label: "Spin Start", type: "slider", @@ -888,6 +877,17 @@ const GROUPS = [ propertyID: "spinFinish", fallbackProperty: "particleSpin", }, + { + label: "Spin Spread", + type: "slider", + min: 0, + max: 360, + step: 1, + decimals: 0, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "spinSpread", + }, { label: "Rotate with Entity", type: "bool", @@ -1355,15 +1355,15 @@ function getPropertyInputElement(propertyID) { } function enableChildren(el, selector) { - var elSelectors = el.querySelectorAll(selector); - for (var selectorIndex = 0; selectorIndex < elSelectors.length; ++selectorIndex) { + let elSelectors = el.querySelectorAll(selector); + for (let selectorIndex = 0; selectorIndex < elSelectors.length; ++selectorIndex) { elSelectors[selectorIndex].removeAttribute('disabled'); } } function disableChildren(el, selector) { - var elSelectors = el.querySelectorAll(selector); - for (var selectorIndex = 0; selectorIndex < elSelectors.length; ++selectorIndex) { + let elSelectors = el.querySelectorAll(selector); + for (let selectorIndex = 0; selectorIndex < elSelectors.length; ++selectorIndex) { elSelectors[selectorIndex].setAttribute('disabled', 'disabled'); } } @@ -1371,7 +1371,7 @@ function disableChildren(el, selector) { function enableProperties() { enableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); enableChildren(document, ".colpick"); - var elLocked = getPropertyInputElement("locked"); + let elLocked = getPropertyInputElement("locked"); if (elLocked.checked === false) { removeStaticUserData(); @@ -1382,10 +1382,10 @@ function enableProperties() { function disableProperties() { disableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); disableChildren(document, ".colpick"); - for (var pickKey in colorPickers) { + for (let pickKey in colorPickers) { colorPickers[pickKey].colpickHide(); } - var elLocked = getPropertyInputElement("locked"); + let elLocked = getPropertyInputElement("locked"); if (elLocked.checked === true) { if ($('#property-userData-editor').css('display') === "block") { @@ -1526,7 +1526,7 @@ function getPropertyValue(originalPropertyName) { * PROPERTY UPDATE FUNCTIONS */ -function updateProperty(originalPropertyName, propertyValue) { +function updateProperty(originalPropertyName, propertyValue, isParticleProperty) { let propertyUpdate = {}; // if this is a compound property name (i.e. animation.running) then split it by . up to 3 times let splitPropertyName = originalPropertyName.split('.'); @@ -1545,8 +1545,8 @@ function updateProperty(originalPropertyName, propertyValue) { propertyUpdate[originalPropertyName] = propertyValue; } // queue up particle property changes with the debounced sync to avoid - // causing particle emitting to reset each frame when updating values - if (properties[originalPropertyName].isParticleProperty) { + // causing particle emitting to reset excessively with each value change + if (isParticleProperty) { Object.keys(propertyUpdate).forEach(function (propertyUpdateKey) { particlePropertyUpdates[propertyUpdateKey] = propertyUpdate[propertyUpdateKey]; }); @@ -1569,30 +1569,29 @@ function updateProperties(propertiesToUpdate) { })); } - -function createEmitTextPropertyUpdateFunction(propertyName) { +function createEmitTextPropertyUpdateFunction(propertyName, isParticleProperty) { return function() { - updateProperty(propertyName, this.value); + updateProperty(propertyName, this.value, isParticleProperty); }; } -function createEmitCheckedPropertyUpdateFunction(propertyName, inverse) { +function createEmitCheckedPropertyUpdateFunction(propertyName, inverse, isParticleProperty) { return function() { - updateProperty(propertyName, inverse ? !this.checked : this.checked); + updateProperty(propertyName, inverse ? !this.checked : this.checked, isParticleProperty); }; } -function createEmitNumberPropertyUpdateFunction(propertyName, multiplier) { +function createEmitNumberPropertyUpdateFunction(propertyName, multiplier, isParticleProperty) { return function() { if (multiplier === undefined) { multiplier = 1; } let value = parseFloat(this.value) * multiplier; - updateProperty(propertyName, value); + updateProperty(propertyName, value, isParticleProperty); }; } -function createEmitVec2PropertyUpdateFunction(propertyName, elX, elY, multiplier) { +function createEmitVec2PropertyUpdateFunction(propertyName, elX, elY, multiplier, isParticleProperty) { return function () { if (multiplier === undefined) { multiplier = 1; @@ -1601,11 +1600,11 @@ function createEmitVec2PropertyUpdateFunction(propertyName, elX, elY, multiplier x: elX.value * multiplier, y: elY.value * multiplier }; - updateProperty(propertyName, newValue); + updateProperty(propertyName, newValue, isParticleProperty); }; } -function createEmitVec3PropertyUpdateFunction(propertyName, elX, elY, elZ, multiplier) { +function createEmitVec3PropertyUpdateFunction(propertyName, elX, elY, elZ, multiplier, isParticleProperty) { return function() { if (multiplier === undefined) { multiplier = 1; @@ -1615,26 +1614,26 @@ function createEmitVec3PropertyUpdateFunction(propertyName, elX, elY, elZ, multi y: elY.value * multiplier, z: elZ.value * multiplier }; - updateProperty(propertyName, newValue); + updateProperty(propertyName, newValue, isParticleProperty); }; } -function createEmitColorPropertyUpdateFunction(propertyName, elRed, elGreen, elBlue) { +function createEmitColorPropertyUpdateFunction(propertyName, elRed, elGreen, elBlue, isParticleProperty) { return function() { - emitColorPropertyUpdate(propertyName, elRed.value, elGreen.value, elBlue.value); + emitColorPropertyUpdate(propertyName, elRed.value, elGreen.value, elBlue.value, isParticleProperty); }; } -function emitColorPropertyUpdate(propertyName, red, green, blue) { +function emitColorPropertyUpdate(propertyName, red, green, blue, isParticleProperty) { let newValue = { red: red, green: green, blue: blue }; - updateProperty(propertyName, newValue); + updateProperty(propertyName, newValue, isParticleProperty); } -function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString) { +function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString, isParticleProperty) { if (subPropertyElement.checked) { if (propertyValue.indexOf(subPropertyString)) { propertyValue += subPropertyString + ','; @@ -1643,13 +1642,13 @@ function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElemen // We've unchecked, so remove propertyValue = propertyValue.replace(subPropertyString + ",", ""); } - updateProperty(propertyName, propertyValue); + updateProperty(propertyName, propertyValue, isParticleProperty); } -function createImageURLUpdateFunction(propertyName) { +function createImageURLUpdateFunction(propertyName, isParticleProperty) { return function () { - var newTextures = JSON.stringify({ "tex.picture": this.value }); - updateProperty(propertyName, newTextures); + let newTextures = JSON.stringify({ "tex.picture": this.value }); + updateProperty(propertyName, newTextures, isParticleProperty); }; } @@ -1672,7 +1671,7 @@ function createStringProperty(property, elProperty, elLabel) { elInput.readOnly = true; } - elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName, property.isParticleProperty)); elProperty.appendChild(elLabel); elProperty.appendChild(elInput); @@ -1708,10 +1707,10 @@ function createBoolProperty(property, elProperty, elLabel) { let subPropertyOf = propertyData.subPropertyOf; if (subPropertyOf !== undefined) { elInput.addEventListener('change', function() { - updateCheckedSubProperty(subPropertyOf, selectedEntityProperties[subPropertyOf], elInput, propertyName); + updateCheckedSubProperty(subPropertyOf, selectedEntityProperties[subPropertyOf], elInput, propertyName, property.isParticleProperty); }); } else { - elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(propertyName, propertyData.inverse)); + elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(propertyName, propertyData.inverse, property.isParticleProperty)); } return elInput; @@ -1744,7 +1743,7 @@ function createNumberProperty(property, elProperty, elLabel) { elInput.value = defaultValue; } - elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(propertyName, propertyData.multiplier, propertyData.decimals)); + elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(propertyName, propertyData.multiplier, propertyData.decimals, property.isParticleProperty)); elProperty.appendChild(elLabel); elProperty.appendChild(elInput); @@ -1790,7 +1789,7 @@ function createSliderProperty(property, elProperty, elLabel) { if (propertyData.multiplier !== undefined) { inputValue *= propertyData.multiplier; } - updateProperty(property.name, inputValue); + updateProperty(property.name, inputValue, property.isParticleProperty); }; elSlider.oninput = function (event) { let sliderValue = event.target.value; @@ -1806,7 +1805,7 @@ function createSliderProperty(property, elProperty, elLabel) { if (propertyData.multiplier !== undefined) { sliderValue *= propertyData.multiplier; } - updateProperty(property.name, sliderValue); + updateProperty(property.name, sliderValue, property.isParticleProperty); }; elDiv.appendChild(elLabel); @@ -1842,8 +1841,8 @@ function createVec3Property(property, elProperty, elLabel) { let elInputZ = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Z_INPUT], propertyData.min, propertyData.max, propertyData.step); - let inputChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elInputX, elInputY, - elInputZ, propertyData.multiplier); + let inputChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elInputX, elInputY, elInputZ, + propertyData.multiplier, property.isParticleProperty); elInputX.addEventListener('change', inputChangeFunction); elInputY.addEventListener('change', inputChangeFunction); elInputZ.addEventListener('change', inputChangeFunction); @@ -1875,8 +1874,8 @@ function createVec2Property(property, elProperty, elLabel) { let elInputY = createTupleNumberInput(elTuple, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Y_INPUT], propertyData.min, propertyData.max, propertyData.step); - let inputChangeFunction = createEmitVec2PropertyUpdateFunction(propertyName, elInputX, - elInputY, propertyData.multiplier); + let inputChangeFunction = createEmitVec2PropertyUpdateFunction(propertyName, elInputX, elInputY, + propertyData.multiplier, property.isParticleProperty); elInputX.addEventListener('change', inputChangeFunction); elInputY.addEventListener('change', inputChangeFunction); @@ -1907,7 +1906,8 @@ function createColorProperty(property, elProperty, elLabel) { let elInputG = createTupleNumberInput(elTuple, elementID, "green", COLOR_MIN, COLOR_MAX, COLOR_STEP); let elInputB = createTupleNumberInput(elTuple, elementID, "blue", COLOR_MIN, COLOR_MAX, COLOR_STEP); - let inputChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elInputR, elInputG, elInputB); + let inputChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elInputR, elInputG, elInputB, + property.isParticleProperty); elInputR.addEventListener('change', inputChangeFunction); elInputG.addEventListener('change', inputChangeFunction); elInputB.addEventListener('change', inputChangeFunction); @@ -1963,7 +1963,7 @@ function createDropdownProperty(property, propertyID, elProperty, elLabel) { elInput.add(option); } - elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName, property.isParticleProperty)); elProperty.appendChild(elLabel); elProperty.appendChild(elInput); @@ -1990,7 +1990,7 @@ function createTextareaProperty(property, elProperty, elLabel) { elInput.readOnly = true; } - elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName)); + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName, property.isParticleProperty)); elProperty.appendChild(elInput); @@ -2056,9 +2056,9 @@ function createTextureProperty(property, elProperty, elLabel) { elInput.imageLoad = imageLoad; elInput.oninput = function (event) { // Add throttle - var url = event.target.value; + let url = event.target.value; imageLoad(url); - updateProperty(property.name, url) + updateProperty(property.name, url, property.isParticleProperty) }; elInput.onchange = elInput.oninput; @@ -2199,7 +2199,7 @@ function reloadServerScripts() { function copySkyboxURLToAmbientURL() { let skyboxURL = getPropertyInputElement("skybox.url").value; getPropertyInputElement("ambientLight.ambientURL").value = skyboxURL; - updateProperty("ambientLight.ambientURL", skyboxURL); + updateProperty("ambientLight.ambientURL", skyboxURL, false); } @@ -2214,13 +2214,13 @@ function clearUserData() { showUserDataTextArea(); showNewJSONEditorButton(); hideSaveUserDataButton(); - updateProperty('userData', elUserData.value); + updateProperty('userData', elUserData.value, false); } function newJSONEditor() { deleteJSONEditor(); createJSONEditor(); - var data = {}; + let data = {}; setEditorJSON(data); hideUserDataTextArea(); hideNewJSONEditorButton(); @@ -2232,7 +2232,7 @@ function saveUserData() { } function setUserDataFromEditor(noUpdate) { - var json = null; + let json = null; try { json = editor.get(); } catch (e) { @@ -2241,7 +2241,7 @@ function setUserDataFromEditor(noUpdate) { if (json === null) { return; } else { - var text = editor.getText(); + let text = editor.getText(); if (noUpdate === true) { EventBridge.emitWebEvent( JSON.stringify({ @@ -2254,15 +2254,15 @@ function setUserDataFromEditor(noUpdate) { ); return; } else { - updateProperty('userData', text); + updateProperty('userData', text, false); } } } function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults, removeKeys) { - var propertyUpdate = {}; - var parsedData = {}; - var keysToBeRemoved = removeKeys ? removeKeys : []; + let propertyUpdate = {}; + let parsedData = {}; + let keysToBeRemoved = removeKeys ? removeKeys : []; try { if ($('#property-userData-editor').css('height') !== "0px") { // if there is an expanded, we want to use its json. @@ -2277,14 +2277,14 @@ function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults, r if (!(groupName in parsedData)) { parsedData[groupName] = {}; } - var keys = Object.keys(updateKeyPair); + let keys = Object.keys(updateKeyPair); keys.forEach(function (key) { if (updateKeyPair[key] !== null && updateKeyPair[key] !== "null") { if (updateKeyPair[key] instanceof Element) { if (updateKeyPair[key].type === "checkbox") { parsedData[groupName][key] = updateKeyPair[key].checked; } else { - var val = isNaN(updateKeyPair[key].value) ? updateKeyPair[key].value : parseInt(updateKeyPair[key].value); + let val = isNaN(updateKeyPair[key].value) ? updateKeyPair[key].value : parseInt(updateKeyPair[key].value); parsedData[groupName][key] = val; } } else { @@ -2317,8 +2317,8 @@ function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults, r var editor = null; function createJSONEditor() { - var container = document.getElementById("property-userData-editor"); - var options = { + let container = document.getElementById("property-userData-editor"); + let options = { search: false, mode: 'tree', modes: ['code', 'tree'], @@ -2330,7 +2330,7 @@ function createJSONEditor() { alert('JSON editor:' + e); }, onChange: function() { - var currentJSONString = editor.getText(); + let currentJSONString = editor.getText(); if (currentJSONString === '{"":""}') { return; @@ -2431,13 +2431,13 @@ function clearMaterialData() { showMaterialDataTextArea(); showNewJSONMaterialEditorButton(); hideSaveMaterialDataButton(); - updateProperty('materialData', elMaterialData.value); + updateProperty('materialData', elMaterialData.value, false); } function newJSONMaterialEditor() { deleteJSONMaterialEditor(); createJSONMaterialEditor(); - var data = {}; + let data = {}; setMaterialEditorJSON(data); hideMaterialDataTextArea(); hideNewJSONMaterialEditorButton(); @@ -2449,7 +2449,7 @@ function saveMaterialData() { } function setMaterialDataFromEditor(noUpdate) { - var json = null; + let json = null; try { json = materialEditor.get(); } catch (e) { @@ -2458,7 +2458,7 @@ function setMaterialDataFromEditor(noUpdate) { if (json === null) { return; } else { - var text = materialEditor.getText(); + let text = materialEditor.getText(); if (noUpdate === true) { EventBridge.emitWebEvent( JSON.stringify({ @@ -2471,7 +2471,7 @@ function setMaterialDataFromEditor(noUpdate) { ); return; } else { - updateProperty('materialData', text); + updateProperty('materialData', text, false); } } } @@ -2479,8 +2479,8 @@ function setMaterialDataFromEditor(noUpdate) { var materialEditor = null; function createJSONMaterialEditor() { - var container = document.getElementById("property-materialData-editor"); - var options = { + let container = document.getElementById("property-materialData-editor"); + let options = { search: false, mode: 'tree', modes: ['code', 'tree'], @@ -2492,7 +2492,7 @@ function createJSONMaterialEditor() { alert('JSON editor:' + e); }, onChange: function() { - var currentJSONString = materialEditor.getText(); + let currentJSONString = materialEditor.getText(); if (currentJSONString === '{"":""}') { return; @@ -2582,11 +2582,11 @@ function saveJSONMaterialData(noUpdate) { } function bindAllNonJSONEditorElements() { - var inputs = $('input'); - var i; + let inputs = $('input'); + let i; for (i = 0; i < inputs.length; ++i) { - var input = inputs[i]; - var field = $(input); + let input = inputs[i]; + let field = $(input); // TODO FIXME: (JSHint) Functions declared within loops referencing // an outer scoped variable may lead to confusing semantics. field.on('focus', function(e) { @@ -2649,7 +2649,7 @@ function setDropdownValue(event) { */ function setTextareaScrolling(element) { - var isScrolling = element.scrollHeight > element.offsetHeight; + let isScrolling = element.scrollHeight > element.offsetHeight; element.setAttribute("scrolling", isScrolling ? "true" : "false"); } @@ -3229,22 +3229,22 @@ function loaded() { let elParentMaterialNameNumber = getPropertyInputElement("submeshToReplace"); let elParentMaterialNameCheckbox = getPropertyInputElement("selectSubmesh"); elParentMaterialNameString.addEventListener('change', function () { - updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + this.value); + updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + this.value, false); }); elParentMaterialNameNumber.addEventListener('change', function () { - updateProperty("parentMaterialName", this.value); + updateProperty("parentMaterialName", this.value, false); }); elParentMaterialNameCheckbox.addEventListener('change', function () { if (this.checked) { - updateProperty("parentMaterialName", elParentMaterialNameNumber.value); + updateProperty("parentMaterialName", elParentMaterialNameNumber.value, false); showParentMaterialNameBox(true, elParentMaterialNameNumber, elParentMaterialNameString); } else { - updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + elParentMaterialNameString.value); + updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + elParentMaterialNameString.value, false); showParentMaterialNameBox(false, elParentMaterialNameNumber, elParentMaterialNameString); } }); - getPropertyInputElement("image").addEventListener('change', createImageURLUpdateFunction('textures')); + getPropertyInputElement("image").addEventListener('change', createImageURLUpdateFunction('textures', false)); // Collapsible sections let elCollapsible = document.getElementsByClassName("section-header"); @@ -3339,12 +3339,12 @@ function loaded() { let propertyID = elDropdown.getAttribute("propertyID"); let property = properties[propertyID]; property.elInput = dt; - dt.addEventListener('change', createEmitTextPropertyUpdateFunction(property.name)); + dt.addEventListener('change', createEmitTextPropertyUpdateFunction(property.name, property.isParticleProperty)); } elDropdowns = document.getElementsByTagName("select"); while (elDropdowns.length > 0) { - var el = elDropdowns[0]; + let el = elDropdowns[0]; el.parentNode.removeChild(el); elDropdowns = document.getElementsByTagName("select"); } From 95d160a1701fe1ea3b290f1fe2b090dca0838111 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Fri, 28 Sep 2018 15:59:04 -0700 Subject: [PATCH 257/362] Working on mac GL issues --- CMakeLists.txt | 2 +- interface/src/Application.cpp | 45 ++-- interface/src/main.cpp | 13 ++ interface/src/ui/ApplicationOverlay.cpp | 1 + .../src/RenderableWebEntityItem.cpp | 1 + libraries/gl/src/gl/Context.cpp | 30 +-- libraries/gl/src/gl/Context.h | 11 +- libraries/gl/src/gl/ContextQt.cpp | 52 ++++- libraries/gl/src/gl/GLHelpers.cpp | 7 +- libraries/gl/src/gl/GLWidget.cpp | 4 +- libraries/gl/src/gl/GLWidget.h | 2 +- libraries/gl/src/gl/GLWindow.cpp | 2 +- libraries/gl/src/gl/OffscreenGLCanvas.cpp | 23 -- libraries/gl/src/gl/OffscreenGLCanvas.h | 3 - libraries/gl/src/gl/QOpenGLContextWrapper.cpp | 17 ++ libraries/gl/src/gl/QOpenGLContextWrapper.h | 5 + .../gpu-gl-common/src/gpu/gl/GLBackend.cpp | 199 ++++++++--------- .../gpu-gl-common/src/gpu/gl/GLBackend.h | 36 ++- .../qml/src/qml/impl/RenderEventHandler.cpp | 5 +- libraries/qml/src/qml/impl/TextureCache.cpp | 14 +- tests-manual/qml/qml/MacQml.qml | 77 +++++++ tests-manual/qml/src/MacQml.cpp | 78 +++++++ tests-manual/qml/src/MacQml.h | 16 ++ tests-manual/qml/src/StressWeb.cpp | 131 +++++++++++ tests-manual/qml/src/StressWeb.h | 34 +++ tests-manual/qml/src/TestCase.cpp | 25 +++ tests-manual/qml/src/TestCase.h | 23 ++ tests-manual/qml/src/main.cpp | 205 +++++------------- 28 files changed, 706 insertions(+), 355 deletions(-) create mode 100644 tests-manual/qml/qml/MacQml.qml create mode 100644 tests-manual/qml/src/MacQml.cpp create mode 100644 tests-manual/qml/src/MacQml.h create mode 100644 tests-manual/qml/src/StressWeb.cpp create mode 100644 tests-manual/qml/src/StressWeb.h create mode 100644 tests-manual/qml/src/TestCase.cpp create mode 100644 tests-manual/qml/src/TestCase.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d52a557cb1..6120e27b75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,7 +66,7 @@ if (ANDROID) set(GLES_OPTION ON) set(PLATFORM_QT_COMPONENTS AndroidExtras WebView) else () - set(PLATFORM_QT_COMPONENTS WebEngine WebEngineWidgets) + set(PLATFORM_QT_COMPONENTS WebEngine) endif () if (USE_GLES AND (NOT ANDROID)) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 58aad654db..700561325f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -52,6 +52,8 @@ #include #include +#include +#include #include #include @@ -971,9 +973,11 @@ OffscreenGLCanvas* _qmlShareContext { nullptr }; // and manually set THAT to be the shared context for the Chromium helper #if !defined(DISABLE_QML) OffscreenGLCanvas* _chromiumShareContext { nullptr }; -Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); #endif +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); +Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); + Setting::Handle sessionRunTime{ "sessionRunTime", 0 }; const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 60.0f; @@ -1370,7 +1374,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _glWidget->setMouseTracking(true); // Make sure the window is set to the correct size by processing the pending events QCoreApplication::processEvents(); - _glWidget->createContext(); // Create the main thread context, the GPU backend initializeGL(); @@ -2727,46 +2730,58 @@ void Application::initializeGL() { _isGLInitialized = true; } + _glWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat()); + + // When loading QtWebEngineWidgets, it creates a global share context on startup. + // We have to account for this possibility by checking here for an existing + // global share context + auto globalShareContext = qt_gl_global_share_context(); + _glWidget->createContext(globalShareContext); + if (!_glWidget->makeCurrent()) { qCWarning(interfaceapp, "Unable to make window context current"); } #if !defined(DISABLE_QML) - // Build a shared canvas / context for the Chromium processes - { - // Disable signed distance field font rendering on ATI/AMD GPUs, due to - // https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app - std::string vendor{ (const char*)glGetString(GL_VENDOR) }; - if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { - qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); - } + // Disable signed distance field font rendering on ATI/AMD GPUs, due to + // https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app + std::string vendor{ (const char*)glGetString(GL_VENDOR) }; + if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { + qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); + } + // Build a shared canvas / context for the Chromium processes + if (!globalShareContext) { // Chromium rendering uses some GL functions that prevent nSight from capturing // frames, so we only create the shared context if nsight is NOT active. if (!nsightActive()) { - _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext = new OffscreenGLCanvas(); _chromiumShareContext->setObjectName("ChromiumShareContext"); _chromiumShareContext->create(_glWidget->qglContext()); if (!_chromiumShareContext->makeCurrent()) { qCWarning(interfaceapp, "Unable to make chromium shared context current"); } - qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + globalShareContext = _chromiumShareContext->getContext(); + qt_gl_set_global_share_context(globalShareContext); _chromiumShareContext->doneCurrent(); // Restore the GL widget context if (!_glWidget->makeCurrent()) { qCWarning(interfaceapp, "Unable to make window context current"); } - } else { - qCWarning(interfaceapp) << "nSight detected, disabling chrome rendering"; } } #endif + if (!globalShareContext) { + globalShareContext = _glWidget->qglContext(); + qt_gl_set_global_share_context(globalShareContext); + } + // Build a shared canvas / context for the QML rendering { _qmlShareContext = new OffscreenGLCanvas(); _qmlShareContext->setObjectName("QmlShareContext"); - _qmlShareContext->create(_glWidget->qglContext()); + _qmlShareContext->create(globalShareContext); if (!_qmlShareContext->makeCurrent()) { qCWarning(interfaceapp, "Unable to make QML shared context current"); } diff --git a/interface/src/main.cpp b/interface/src/main.cpp index d9396ae4d1..12a02ffd32 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "AddressManager.h" #include "Application.h" @@ -40,6 +41,18 @@ extern "C" { #endif int main(int argc, const char* argv[]) { + auto format = getDefaultOpenGLSurfaceFormat(); +#ifdef Q_OS_MAC + // Deal with some weirdness in the chromium context sharing on Mac. + // The primary share context needs to be 3.2, so that the Chromium will + // succeed in it's creation of it's command stub contexts. + format.setVersion(3, 2); + // This appears to resolve the issues with corrupted fonts on OSX. No + // idea why. + qputenv("QT_ENABLE_GLYPH_CACHE_WORKAROUND", "true"); + // https://i.kym-cdn.com/entries/icons/original/000/008/342/ihave.jpg +#endif + QSurfaceFormat::setDefaultFormat(format); setupHifiApplication(BuildInfo::INTERFACE_NAME); QStringList arguments; diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 108f20b2dd..2fb40c8c30 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -115,6 +115,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { batch.resetViewTransform(); batch.setResourceTexture(0, _uiTexture); geometryCache->renderUnitQuad(batch, glm::vec4(1), _qmlGeometryId); + batch.setResourceTexture(0, nullptr); } void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) { diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index bc9ac84c91..bce5225a5c 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -261,6 +261,7 @@ void WebEntityRenderer::doRender(RenderArgs* args) { DependencyManager::get()->bindWebBrowserProgram(batch, fadeRatio < OPAQUE_ALPHA_THRESHOLD); DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, fadeRatio), _geometryId); batch.popProjectionJitter(); + batch.setResourceTexture(0, nullptr); } bool WebEntityRenderer::hasWebSurface() { diff --git a/libraries/gl/src/gl/Context.cpp b/libraries/gl/src/gl/Context.cpp index ad7e51fbd3..cd2b19beec 100644 --- a/libraries/gl/src/gl/Context.cpp +++ b/libraries/gl/src/gl/Context.cpp @@ -25,6 +25,7 @@ #include "GLLogging.h" #include "Config.h" #include "GLHelpers.h" +#include "QOpenGLContextWrapper.h" using namespace gl; @@ -68,8 +69,6 @@ void Context::updateSwapchainMemoryUsage(size_t prevSize, size_t newSize) { } -Context* Context::PRIMARY = nullptr; - Context::Context() {} Context::Context(QWindow* window) { @@ -97,9 +96,6 @@ void Context::release() { _context = nullptr; #endif _window = nullptr; - if (PRIMARY == this) { - PRIMARY = nullptr; - } updateSwapchainMemoryCounter(); } @@ -235,16 +231,10 @@ typedef HGLRC(APIENTRYP PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC hDC, HGLRC hShare GLAPI PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB; GLAPI PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB; +Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); -void Context::create() { - if (!PRIMARY) { - PRIMARY = static_cast(qApp->property(hifi::properties::gl::PRIMARY_CONTEXT).value()); - } - - if (PRIMARY) { - _version = PRIMARY->_version; - } +void Context::create(QOpenGLContext* shareContext) { assert(0 != _hwnd); assert(0 == _hdc); auto hwnd = _hwnd; @@ -338,7 +328,10 @@ void Context::create() { contextAttribs.push_back(0); } contextAttribs.push_back(0); - auto shareHglrc = PRIMARY ? PRIMARY->_hglrc : 0; + if (!shareContext) { + shareContext = qt_gl_global_share_context(); + } + HGLRC shareHglrc = (HGLRC)QOpenGLContextWrapper::nativeContext(shareContext); _hglrc = wglCreateContextAttribsARB(_hdc, shareHglrc, &contextAttribs[0]); } @@ -346,11 +339,6 @@ void Context::create() { throw std::runtime_error("Could not create GL context"); } - if (!PRIMARY) { - PRIMARY = this; - qApp->setProperty(hifi::properties::gl::PRIMARY_CONTEXT, QVariant::fromValue((void*)PRIMARY)); - } - if (!makeCurrent()) { throw std::runtime_error("Could not make context current"); } @@ -368,7 +356,7 @@ OffscreenContext::~OffscreenContext() { _window->deleteLater(); } -void OffscreenContext::create() { +void OffscreenContext::create(QOpenGLContext* shareContext) { if (!_window) { _window = new QWindow(); _window->setFlags(Qt::MSWindowsOwnDC); @@ -379,5 +367,5 @@ void OffscreenContext::create() { qCDebug(glLogging) << "New Offscreen GLContext, window size = " << windowSize.width() << " , " << windowSize.height(); QGuiApplication::processEvents(); } - Parent::create(); + Parent::create(shareContext); } diff --git a/libraries/gl/src/gl/Context.h b/libraries/gl/src/gl/Context.h index b6160cbd6c..05cb361725 100644 --- a/libraries/gl/src/gl/Context.h +++ b/libraries/gl/src/gl/Context.h @@ -21,6 +21,7 @@ class QSurface; class QWindow; class QOpenGLContext; class QThread; +class QOpenGLDebugMessage; #if defined(Q_OS_WIN) #define GL_CUSTOM_CONTEXT @@ -30,7 +31,6 @@ namespace gl { class Context { protected: QWindow* _window { nullptr }; - static Context* PRIMARY; static void destroyContext(QOpenGLContext* context); #if defined(GL_CUSTOM_CONTEXT) uint32_t _version { 0x0401 }; @@ -48,6 +48,9 @@ namespace gl { public: static bool enableDebugLogger(); + static void debugMessageHandler(const QOpenGLDebugMessage &debugMessage); + static void setupDebugLogging(QOpenGLContext* context); + Context(); Context(QWindow* window); void release(); @@ -59,14 +62,14 @@ namespace gl { static void makeCurrent(QOpenGLContext* context, QSurface* surface); void swapBuffers(); void doneCurrent(); - virtual void create(); + virtual void create(QOpenGLContext* shareContext = nullptr); QOpenGLContext* qglContext(); void moveToThread(QThread* thread); static size_t evalSurfaceMemoryUsage(uint32_t width, uint32_t height, uint32_t pixelSize); static size_t getSwapchainMemoryUsage(); static void updateSwapchainMemoryUsage(size_t prevSize, size_t newSize); - + private: static std::atomic _totalSwapchainMemoryUsage; @@ -81,7 +84,7 @@ namespace gl { QWindow* _window { nullptr }; public: virtual ~OffscreenContext(); - void create() override; + void create(QOpenGLContext* shareContext = nullptr) override; }; } diff --git a/libraries/gl/src/gl/ContextQt.cpp b/libraries/gl/src/gl/ContextQt.cpp index dd65c3076c..f554877b2a 100644 --- a/libraries/gl/src/gl/ContextQt.cpp +++ b/libraries/gl/src/gl/ContextQt.cpp @@ -17,6 +17,8 @@ #include #endif +#include + #include "GLHelpers.h" using namespace gl; @@ -47,6 +49,32 @@ void Context::moveToThread(QThread* thread) { qglContext()->moveToThread(thread); } +void Context::debugMessageHandler(const QOpenGLDebugMessage& debugMessage) { + auto severity = debugMessage.severity(); + switch (severity) { + case QOpenGLDebugMessage::NotificationSeverity: + case QOpenGLDebugMessage::LowSeverity: + return; + default: + break; + } + qDebug(glLogging) << debugMessage; + return; +} + +void Context::setupDebugLogging(QOpenGLContext *context) { + QOpenGLDebugLogger *logger = new QOpenGLDebugLogger(context); + QObject::connect(logger, &QOpenGLDebugLogger::messageLogged, nullptr, [](const QOpenGLDebugMessage& message){ + Context::debugMessageHandler(message); + }); + if (logger->initialize()) { + logger->enableMessages(); + logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); + } else { + qCWarning(glLogging) << "OpenGL context does not support debugging"; + } +} + #ifndef GL_CUSTOM_CONTEXT bool Context::makeCurrent() { updateSwapchainMemoryCounter(); @@ -65,21 +93,29 @@ void Context::doneCurrent() { } } +Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); const QSurfaceFormat& getDefaultOpenGLSurfaceFormat(); - -void Context::create() { +void Context::create(QOpenGLContext* shareContext) { _context = new QOpenGLContext(); - if (PRIMARY) { - _context->setShareContext(PRIMARY->qglContext()); - } else { - PRIMARY = this; + _context->setFormat(_window->format()); + if (!shareContext) { + shareContext = qt_gl_global_share_context(); } - _context->setFormat(getDefaultOpenGLSurfaceFormat()); - _context->create(); + _context->setShareContext(shareContext); + _context->create(); _swapchainPixelSize = evalGLFormatSwapchainPixelSize(_context->format()); updateSwapchainMemoryCounter(); + + if (!makeCurrent()) { + throw std::runtime_error("Could not make context current"); + } + if (enableDebugLogger()) { + setupDebugLogging(_context); + } + doneCurrent(); + } #endif diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index 7ebba4f8d8..c22bd0dde5 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -38,10 +38,15 @@ void gl::getTargetVersion(int& major, int& minor) { #if defined(USE_GLES) major = 3; minor = 2; +#else +#if defined(Q_OS_MAC) + major = 4; + minor = 1; #else major = 4; minor = disableGl45() ? 1 : 5; #endif +#endif } const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { @@ -57,6 +62,7 @@ const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { #else format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); #endif + format.setOption(QSurfaceFormat::DebugContext); // Qt Quick may need a depth and stencil buffer. Always make sure these are available. format.setDepthBufferSize(DEFAULT_GL_DEPTH_BUFFER_BITS); format.setStencilBufferSize(DEFAULT_GL_STENCIL_BUFFER_BITS); @@ -64,7 +70,6 @@ const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { ::gl::getTargetVersion(major, minor); format.setMajorVersion(major); format.setMinorVersion(minor); - QSurfaceFormat::setDefaultFormat(format); }); return format; } diff --git a/libraries/gl/src/gl/GLWidget.cpp b/libraries/gl/src/gl/GLWidget.cpp index 1c0ad1a85e..94702a9906 100644 --- a/libraries/gl/src/gl/GLWidget.cpp +++ b/libraries/gl/src/gl/GLWidget.cpp @@ -63,10 +63,10 @@ int GLWidget::getDeviceHeight() const { return height() * (windowHandle() ? (float)windowHandle()->devicePixelRatio() : 1.0f); } -void GLWidget::createContext() { +void GLWidget::createContext(QOpenGLContext* shareContext) { _context = new gl::Context(); _context->setWindow(windowHandle()); - _context->create(); + _context->create(shareContext); _context->makeCurrent(); _context->clear(); _context->doneCurrent(); diff --git a/libraries/gl/src/gl/GLWidget.h b/libraries/gl/src/gl/GLWidget.h index a0bf8ea0b0..777d43e8af 100644 --- a/libraries/gl/src/gl/GLWidget.h +++ b/libraries/gl/src/gl/GLWidget.h @@ -29,7 +29,7 @@ public: int getDeviceHeight() const; QSize getDeviceSize() const { return QSize(getDeviceWidth(), getDeviceHeight()); } QPaintEngine* paintEngine() const override; - void createContext(); + void createContext(QOpenGLContext* shareContext = nullptr); bool makeCurrent(); void doneCurrent(); void swapBuffers(); diff --git a/libraries/gl/src/gl/GLWindow.cpp b/libraries/gl/src/gl/GLWindow.cpp index e1e6279b1c..7930e050de 100644 --- a/libraries/gl/src/gl/GLWindow.cpp +++ b/libraries/gl/src/gl/GLWindow.cpp @@ -22,7 +22,7 @@ void GLWindow::createContext(QOpenGLContext* shareContext) { void GLWindow::createContext(const QSurfaceFormat& format, QOpenGLContext* shareContext) { _context = new gl::Context(); _context->setWindow(this); - _context->create(); + _context->create(shareContext); _context->makeCurrent(); _context->clear(); } diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index 6598a26c99..1256e316ff 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -75,32 +75,9 @@ bool OffscreenGLCanvas::create(QOpenGLContext* sharedContext) { } #endif - if (gl::Context::enableDebugLogger()) { - _context->makeCurrent(_offscreenSurface); - QOpenGLDebugLogger *logger = new QOpenGLDebugLogger(this); - connect(logger, &QOpenGLDebugLogger::messageLogged, this, &OffscreenGLCanvas::onMessageLogged); - logger->initialize(); - logger->enableMessages(); - logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); - _context->doneCurrent(); - } - return true; } -void OffscreenGLCanvas::onMessageLogged(const QOpenGLDebugMessage& debugMessage) { - auto severity = debugMessage.severity(); - switch (severity) { - case QOpenGLDebugMessage::NotificationSeverity: - case QOpenGLDebugMessage::LowSeverity: - return; - default: - break; - } - qDebug(glLogging) << debugMessage; - return; -} - bool OffscreenGLCanvas::makeCurrent() { bool result = _context->makeCurrent(_offscreenSurface); if (glGetString) { diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.h b/libraries/gl/src/gl/OffscreenGLCanvas.h index a4960ae234..374720a910 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.h +++ b/libraries/gl/src/gl/OffscreenGLCanvas.h @@ -35,9 +35,6 @@ public: void setThreadContext(); static bool restoreThreadContext(); -private slots: - void onMessageLogged(const QOpenGLDebugMessage &debugMessage); - protected: void clearThreadContext(); diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp index 0b153a5ae8..fbebb1128d 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp @@ -13,6 +13,10 @@ #include +#ifdef Q_OS_WIN +#include +#endif + uint32_t QOpenGLContextWrapper::currentContextVersion() { QOpenGLContext* context = QOpenGLContext::currentContext(); if (!context) { @@ -45,6 +49,19 @@ void QOpenGLContextWrapper::setFormat(const QSurfaceFormat& format) { _context->setFormat(format); } +#ifdef Q_OS_WIN +void* QOpenGLContextWrapper::nativeContext(QOpenGLContext* context) { + HGLRC result = 0; + if (context != nullptr) { + auto nativeHandle = context->nativeHandle(); + if (nativeHandle.canConvert()) { + result = nativeHandle.value().context(); + } + } + return result; +} +#endif + bool QOpenGLContextWrapper::create() { return _context->create(); } diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.h b/libraries/gl/src/gl/QOpenGLContextWrapper.h index b09ad1a4c3..32ba7f22e8 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.h +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.h @@ -13,6 +13,7 @@ #define hifi_QOpenGLContextWrapper_h #include +#include class QOpenGLContext; class QSurface; @@ -21,6 +22,10 @@ class QThread; class QOpenGLContextWrapper { public: +#ifdef Q_OS_WIN + static void* nativeContext(QOpenGLContext* context); +#endif + QOpenGLContextWrapper(); QOpenGLContextWrapper(QOpenGLContext* context); virtual ~QOpenGLContextWrapper(); diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp index 1203e65685..c1ce05c18b 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp @@ -708,37 +708,37 @@ void GLBackend::do_glColor4f(const Batch& batch, size_t paramOffset) { void GLBackend::releaseBuffer(GLuint id, Size size) const { Lock lock(_trashMutex); - _buffersTrash.push_back({ id, size }); + _currentFrameTrash.buffersTrash.push_back({ id, size }); } void GLBackend::releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const { Lock lock(_trashMutex); - _externalTexturesTrash.push_back({ id, recycler }); + _currentFrameTrash.externalTexturesTrash.push_back({ id, recycler }); } void GLBackend::releaseTexture(GLuint id, Size size) const { Lock lock(_trashMutex); - _texturesTrash.push_back({ id, size }); + _currentFrameTrash.texturesTrash.push_back({ id, size }); } void GLBackend::releaseFramebuffer(GLuint id) const { Lock lock(_trashMutex); - _framebuffersTrash.push_back(id); + _currentFrameTrash.framebuffersTrash.push_back(id); } void GLBackend::releaseShader(GLuint id) const { Lock lock(_trashMutex); - _shadersTrash.push_back(id); + _currentFrameTrash.shadersTrash.push_back(id); } void GLBackend::releaseProgram(GLuint id) const { Lock lock(_trashMutex); - _programsTrash.push_back(id); + _currentFrameTrash.programsTrash.push_back(id); } void GLBackend::releaseQuery(GLuint id) const { Lock lock(_trashMutex); - _queriesTrash.push_back(id); + _currentFrameTrash.queriesTrash.push_back(id); } void GLBackend::queueLambda(const std::function lambda) const { @@ -746,6 +746,81 @@ void GLBackend::queueLambda(const std::function lambda) const { _lambdaQueue.push_back(lambda); } +void GLBackend::FrameTrash::cleanup() { + glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(fence); + + { + std::vector ids; + ids.reserve(buffersTrash.size()); + for (auto pair : buffersTrash) { + ids.push_back(pair.first); + } + if (!ids.empty()) { + glDeleteBuffers((GLsizei)ids.size(), ids.data()); + } + } + + { + std::vector ids; + ids.reserve(framebuffersTrash.size()); + for (auto id : framebuffersTrash) { + ids.push_back(id); + } + if (!ids.empty()) { + glDeleteFramebuffers((GLsizei)ids.size(), ids.data()); + } + } + + { + std::vector ids; + ids.reserve(texturesTrash.size()); + for (auto pair : texturesTrash) { + ids.push_back(pair.first); + } + if (!ids.empty()) { + glDeleteTextures((GLsizei)ids.size(), ids.data()); + } + } + + { + if (!externalTexturesTrash.empty()) { + std::vector fences; + fences.resize(externalTexturesTrash.size()); + for (size_t i = 0; i < externalTexturesTrash.size(); ++i) { + fences[i] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + // External texture fences will be read in another thread/context, so we need a flush + glFlush(); + size_t index = 0; + for (auto pair : externalTexturesTrash) { + auto fence = fences[index++]; + pair.second(pair.first, fence); + } + } + } + + for (auto id : programsTrash) { + glDeleteProgram(id); + } + + for (auto id : shadersTrash) { + glDeleteShader(id); + } + + { + std::vector ids; + ids.reserve(queriesTrash.size()); + for (auto id : queriesTrash) { + ids.push_back(id); + } + if (!ids.empty()) { + glDeleteQueries((GLsizei)ids.size(), ids.data()); + } + } + +} + void GLBackend::recycle() const { PROFILE_RANGE(render_gpu_gl, __FUNCTION__) { @@ -759,112 +834,16 @@ void GLBackend::recycle() const { } } - { - std::vector ids; - std::list> buffersTrash; - { - Lock lock(_trashMutex); - std::swap(_buffersTrash, buffersTrash); - } - ids.reserve(buffersTrash.size()); - for (auto pair : buffersTrash) { - ids.push_back(pair.first); - } - if (!ids.empty()) { - glDeleteBuffers((GLsizei)ids.size(), ids.data()); - } + while (!_previousFrameTrashes.empty()) { + _previousFrameTrashes.front().cleanup(); + _previousFrameTrashes.pop_front(); } + _previousFrameTrashes.emplace_back(); { - std::vector ids; - std::list framebuffersTrash; - { - Lock lock(_trashMutex); - std::swap(_framebuffersTrash, framebuffersTrash); - } - ids.reserve(framebuffersTrash.size()); - for (auto id : framebuffersTrash) { - ids.push_back(id); - } - if (!ids.empty()) { - glDeleteFramebuffers((GLsizei)ids.size(), ids.data()); - } - } - - { - std::vector ids; - std::list> texturesTrash; - { - Lock lock(_trashMutex); - std::swap(_texturesTrash, texturesTrash); - } - ids.reserve(texturesTrash.size()); - for (auto pair : texturesTrash) { - ids.push_back(pair.first); - } - if (!ids.empty()) { - glDeleteTextures((GLsizei)ids.size(), ids.data()); - } - } - - { - std::list> externalTexturesTrash; - { - Lock lock(_trashMutex); - std::swap(_externalTexturesTrash, externalTexturesTrash); - } - if (!externalTexturesTrash.empty()) { - std::vector fences; - fences.resize(externalTexturesTrash.size()); - for (size_t i = 0; i < externalTexturesTrash.size(); ++i) { - fences[i] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - } - // External texture fences will be read in another thread/context, so we need a flush - glFlush(); - size_t index = 0; - for (auto pair : externalTexturesTrash) { - auto fence = fences[index++]; - pair.second(pair.first, fence); - } - } - } - - { - std::list programsTrash; - { - Lock lock(_trashMutex); - std::swap(_programsTrash, programsTrash); - } - for (auto id : programsTrash) { - glDeleteProgram(id); - } - } - - { - std::list shadersTrash; - { - Lock lock(_trashMutex); - std::swap(_shadersTrash, shadersTrash); - } - for (auto id : shadersTrash) { - glDeleteShader(id); - } - } - - { - std::vector ids; - std::list queriesTrash; - { - Lock lock(_trashMutex); - std::swap(_queriesTrash, queriesTrash); - } - ids.reserve(queriesTrash.size()); - for (auto id : queriesTrash) { - ids.push_back(id); - } - if (!ids.empty()) { - glDeleteQueries((GLsizei)ids.size(), ids.data()); - } + Lock lock(_trashMutex); + _previousFrameTrashes.back().swap(_currentFrameTrash); + _previousFrameTrashes.back().fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); } _textureManagement._transferEngine->manageMemory(); diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h index c309bcb864..37dde5b08e 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h @@ -419,16 +419,34 @@ protected: static const size_t INVALID_OFFSET = (size_t)-1; bool _inRenderTransferPass{ false }; int _currentDraw{ -1 }; - - std::list profileRanges; + + struct FrameTrash { + GLsync fence = nullptr; + std::list> buffersTrash; + std::list> texturesTrash; + std::list> externalTexturesTrash; + std::list framebuffersTrash; + std::list shadersTrash; + std::list programsTrash; + std::list queriesTrash; + + void swap(FrameTrash& other) { + buffersTrash.swap(other.buffersTrash); + texturesTrash.swap(other.texturesTrash); + externalTexturesTrash.swap(other.externalTexturesTrash); + framebuffersTrash.swap(other.framebuffersTrash); + shadersTrash.swap(other.shadersTrash); + programsTrash.swap(other.programsTrash); + queriesTrash.swap(other.queriesTrash); + } + + void cleanup(); + }; + mutable Mutex _trashMutex; - mutable std::list> _buffersTrash; - mutable std::list> _texturesTrash; - mutable std::list> _externalTexturesTrash; - mutable std::list _framebuffersTrash; - mutable std::list _shadersTrash; - mutable std::list _programsTrash; - mutable std::list _queriesTrash; + mutable FrameTrash _currentFrameTrash; + mutable std::list _previousFrameTrashes; + std::list profileRanges; mutable std::list> _lambdaQueue; void renderPassTransfer(const Batch& batch); diff --git a/libraries/qml/src/qml/impl/RenderEventHandler.cpp b/libraries/qml/src/qml/impl/RenderEventHandler.cpp index 39f3123d40..8a56929e6b 100644 --- a/libraries/qml/src/qml/impl/RenderEventHandler.cpp +++ b/libraries/qml/src/qml/impl/RenderEventHandler.cpp @@ -139,9 +139,9 @@ void RenderEventHandler::onRender() { glGenerateMipmap(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 0); auto fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + // Fence will be used in another thread / context, so a flush is required glFlush(); _shared->updateTextureAndFence({ texture, fence }); - // Fence will be used in another thread / context, so a flush is required _shared->_quickWindow->resetOpenGLState(); } } @@ -167,4 +167,5 @@ void RenderEventHandler::onQuit() { moveToThread(qApp->thread()); QThread::currentThread()->quit(); } -#endif \ No newline at end of file + +#endif diff --git a/libraries/qml/src/qml/impl/TextureCache.cpp b/libraries/qml/src/qml/impl/TextureCache.cpp index 7af8fa1ac9..7b4fb3adaf 100644 --- a/libraries/qml/src/qml/impl/TextureCache.cpp +++ b/libraries/qml/src/qml/impl/TextureCache.cpp @@ -51,8 +51,10 @@ uint32_t TextureCache::acquireTexture(const QSize& size) { if (!textureSet.returnedTextures.empty()) { auto textureAndFence = textureSet.returnedTextures.front(); textureSet.returnedTextures.pop_front(); - glWaitSync((GLsync)textureAndFence.second, 0, GL_TIMEOUT_IGNORED); - glDeleteSync((GLsync)textureAndFence.second); + if (textureAndFence.second) { + glWaitSync((GLsync)textureAndFence.second, 0, GL_TIMEOUT_IGNORED); + glDeleteSync((GLsync)textureAndFence.second); + } return textureAndFence.first; } return createTexture(size); @@ -101,9 +103,11 @@ void TextureCache::destroyTexture(uint32_t texture) { void TextureCache::destroy(const Value& textureAndFence) { const auto& fence = textureAndFence.second; - // FIXME prevents crash on shutdown, but we should migrate to a global functions object owned by the shared context. - glWaitSync((GLsync)fence, 0, GL_TIMEOUT_IGNORED); - glDeleteSync((GLsync)fence); + if (fence) { + // FIXME prevents crash on shutdown, but we should migrate to a global functions object owned by the shared context. + glWaitSync((GLsync)fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync((GLsync)fence); + } destroyTexture(textureAndFence.first); } diff --git a/tests-manual/qml/qml/MacQml.qml b/tests-manual/qml/qml/MacQml.qml new file mode 100644 index 0000000000..bb7e3a0dff --- /dev/null +++ b/tests-manual/qml/qml/MacQml.qml @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.1 +import QtQuick.Controls 1.2 +import QtWebEngine 1.5 + +Item { + width: 640 + height: 480 + + Rectangle { + width: 5 + height: 5 + color: "red" + ColorAnimation on color { loops: Animation.Infinite; from: "red"; to: "yellow"; duration: 1000 } + } + + + WebEngineView { + id: root + url: "https://google.com/" + x: 6; y: 6; + width: parent.width * 0.8 + height: parent.height * 0.8 + + } +} diff --git a/tests-manual/qml/src/MacQml.cpp b/tests-manual/qml/src/MacQml.cpp new file mode 100644 index 0000000000..7f7854ce87 --- /dev/null +++ b/tests-manual/qml/src/MacQml.cpp @@ -0,0 +1,78 @@ +#include "MacQml.h" + +#include + +#include + +#include + +using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence; +// +//void MacQml::destroySurface(QmlInfo& qmlInfo) { +// auto& surface = qmlInfo.surface; +// auto& currentTexture = qmlInfo.texture; +// if (currentTexture) { +// auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); +// glFlush(); +// _discardLamdba(currentTexture, readFence); +// } +// auto webView = surface->getRootItem(); +// if (webView) { +// // stop loading +// QMetaObject::invokeMethod(webView, "stop"); +// webView->setProperty(URL_PROPERTY, "about:blank"); +// } +// surface->pause(); +// surface.reset(); +//} + +void MacQml::update() { + auto rootItem =_surface->getRootItem(); + float now = sinf(secTimestampNow()); + rootItem->setProperty("level", abs(now)); + rootItem->setProperty("muted", now > 0.0f); + rootItem->setProperty("statsValue", rand()); + + // Fetch any new textures + TextureAndFence newTextureAndFence; + if (_surface->fetchTexture(newTextureAndFence)) { + if (_texture != 0) { + auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + _discardLamdba(_texture, readFence); + } + _texture = newTextureAndFence.first; + _glf.glWaitSync((GLsync)newTextureAndFence.second, 0, GL_TIMEOUT_IGNORED); + } +} + +void MacQml::init() { + Parent::init(); + _glf.glGenFramebuffers(1, &_fbo); + _surface.reset(new hifi::qml::OffscreenSurface()); + //QUrl url =getTestResource("qml/main.qml"); + QUrl url = getTestResource("qml/MacQml.qml"); + hifi::qml::QmlContextObjectCallback callback =[](QQmlContext* context, QQuickItem* item) { + }; + _surface->load(url, callback); + _surface->resize(_window->size()); + _surface->resume(); + +} + +void MacQml::draw() { + auto size = _window->geometry().size(); + if (_texture) { + _glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, _fbo); + _glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); + _glf.glBlitFramebuffer( + // src coordinates + 0, 0, size.width(), size.height(), + // dst coordinates + 0, 0, size.width(), size.height(), + // blit mask and filter + GL_COLOR_BUFFER_BIT, GL_NEAREST); + _glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + _glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + } +} diff --git a/tests-manual/qml/src/MacQml.h b/tests-manual/qml/src/MacQml.h new file mode 100644 index 0000000000..50f71cb72e --- /dev/null +++ b/tests-manual/qml/src/MacQml.h @@ -0,0 +1,16 @@ +#include "TestCase.h" + +#include + +class MacQml : public TestCase { + using Parent = TestCase; +public: + GLuint _texture{ 0 }; + QmlPtr _surface; + GLuint _fbo{ 0 }; + + MacQml(const QWindow* window) : Parent(window) {} + void update() override; + void init() override; + void draw() override; +}; diff --git a/tests-manual/qml/src/StressWeb.cpp b/tests-manual/qml/src/StressWeb.cpp new file mode 100644 index 0000000000..71293feb9a --- /dev/null +++ b/tests-manual/qml/src/StressWeb.cpp @@ -0,0 +1,131 @@ +#include "StressWeb.h" + +#include + +#include + +using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence; + +static const int DEFAULT_MAX_FPS = 10; +static const QString CONTROL_URL{ "/qml/controls/WebEntityView.qml" }; +static const char* URL_PROPERTY{ "url" }; + +QString StressWeb::getSourceUrl(bool video) { + static const std::vector SOURCE_URLS{ + "https://www.reddit.com/wiki/random", + "https://en.wikipedia.org/wiki/Wikipedia:Random", + "https://slashdot.org/", + }; + + static const std::vector VIDEO_SOURCE_URLS{ + "https://www.youtube.com/watch?v=gDXwhHm4GhM", + "https://www.youtube.com/watch?v=Ch_hoYPPeGc", + }; + + const auto& sourceUrls = video ? VIDEO_SOURCE_URLS : SOURCE_URLS; + auto index = rand() % sourceUrls.size(); + return sourceUrls[index]; +} + + + +void StressWeb::buildSurface(QmlInfo& qmlInfo, bool video) { + ++_surfaceCount; + auto lifetimeSecs = (uint32_t)(5.0f + (randFloat() * 10.0f)); + auto lifetimeUsecs = (USECS_PER_SECOND * lifetimeSecs); + qmlInfo.lifetime = lifetimeUsecs + usecTimestampNow(); + qmlInfo.texture = 0; + qmlInfo.surface.reset(new hifi::qml::OffscreenSurface()); + qmlInfo.surface->load(getTestResource(CONTROL_URL), [video](QQmlContext* context, QQuickItem* item) { + item->setProperty(URL_PROPERTY, getSourceUrl(video)); + }); + qmlInfo.surface->setMaxFps(DEFAULT_MAX_FPS); + qmlInfo.surface->resize(_qmlSize); + qmlInfo.surface->resume(); +} + +void StressWeb::destroySurface(QmlInfo& qmlInfo) { + auto& surface = qmlInfo.surface; + auto& currentTexture = qmlInfo.texture; + if (currentTexture) { + auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + _discardLamdba(currentTexture, readFence); + } + auto webView = surface->getRootItem(); + if (webView) { + // stop loading + QMetaObject::invokeMethod(webView, "stop"); + webView->setProperty(URL_PROPERTY, "about:blank"); + } + surface->pause(); + surface.reset(); +} + +void StressWeb::update() { + auto now = usecTimestampNow(); + // Fetch any new textures + for (size_t x = 0; x < DIVISIONS_X; ++x) { + for (size_t y = 0; y < DIVISIONS_Y; ++y) { + auto& qmlInfo = _surfaces[x][y]; + if (!qmlInfo.surface) { + if (now < _createStopTime && randFloat() > 0.99f) { + buildSurface(qmlInfo, x == 0 && y == 0); + } else { + continue; + } + } + + if (now > qmlInfo.lifetime) { + destroySurface(qmlInfo); + continue; + } + + auto& surface = qmlInfo.surface; + auto& currentTexture = qmlInfo.texture; + + TextureAndFence newTextureAndFence; + if (surface->fetchTexture(newTextureAndFence)) { + if (currentTexture != 0) { + auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + _discardLamdba(currentTexture, readFence); + } + currentTexture = newTextureAndFence.first; + _glf.glWaitSync((GLsync)newTextureAndFence.second, 0, GL_TIMEOUT_IGNORED); + } + } + } +} + +void StressWeb::init() { + Parent::init(); + _createStopTime = usecTimestampNow() + (3000u * USECS_PER_SECOND); + _glf.glGenFramebuffers(1, &_fbo); +} + +void StressWeb::draw() { + auto size = _window->geometry().size(); + auto incrementX = size.width() / DIVISIONS_X; + auto incrementY = size.height() / DIVISIONS_Y; + + for (uint32_t x = 0; x < DIVISIONS_X; ++x) { + for (uint32_t y = 0; y < DIVISIONS_Y; ++y) { + auto& qmlInfo = _surfaces[x][y]; + if (!qmlInfo.surface || !qmlInfo.texture) { + continue; + } + _glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, _fbo); + _glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, qmlInfo.texture, 0); + _glf.glBlitFramebuffer( + // src coordinates + 0, 0, _qmlSize.width() - 1, _qmlSize.height() - 1, + // dst coordinates + incrementX * x, incrementY * y, incrementX * (x + 1), incrementY * (y + 1), + // blit mask and filter + GL_COLOR_BUFFER_BIT, GL_NEAREST); + } + } + _glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + _glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); +} diff --git a/tests-manual/qml/src/StressWeb.h b/tests-manual/qml/src/StressWeb.h new file mode 100644 index 0000000000..a68e34d0c1 --- /dev/null +++ b/tests-manual/qml/src/StressWeb.h @@ -0,0 +1,34 @@ +#include "TestCase.h" + +#include + +#include + +#define DIVISIONS_X 5 +#define DIVISIONS_Y 5 + +class StressWeb : public TestCase { + using Parent = TestCase; +public: + using QmlPtr = QSharedPointer; + + struct QmlInfo { + QmlPtr surface; + GLuint texture{ 0 }; + uint64_t lifetime{ 0 }; + }; + + size_t _surfaceCount{ 0 }; + uint64_t _createStopTime{ 0 }; + const QSize _qmlSize{ 640, 480 }; + std::array, DIVISIONS_X> _surfaces; + GLuint _fbo{ 0 }; + + StressWeb(const QWindow* window) : Parent(window) {} + static QString getSourceUrl(bool video); + void buildSurface(QmlInfo& qmlInfo, bool video); + void destroySurface(QmlInfo& qmlInfo); + void update() override; + void init() override; + void draw() override; +}; diff --git a/tests-manual/qml/src/TestCase.cpp b/tests-manual/qml/src/TestCase.cpp new file mode 100644 index 0000000000..534de71e51 --- /dev/null +++ b/tests-manual/qml/src/TestCase.cpp @@ -0,0 +1,25 @@ +#include "TestCase.h" + +#include +#include + +void TestCase::destroy() { +} +void TestCase::update() { +} + +void TestCase::init() { + _glf.initializeOpenGLFunctions(); + _discardLamdba = hifi::qml::OffscreenSurface::getDiscardLambda(); +} + +QUrl TestCase::getTestResource(const QString& relativePath) { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; + qDebug() << "Resources Path: " << dir; + } + return QUrl::fromLocalFile(dir + relativePath); +} diff --git a/tests-manual/qml/src/TestCase.h b/tests-manual/qml/src/TestCase.h new file mode 100644 index 0000000000..191eecb408 --- /dev/null +++ b/tests-manual/qml/src/TestCase.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include + +class TestCase { +public: + using QmlPtr = QSharedPointer; + using Builder = std::function; + TestCase(const QWindow* window) : _window(window) {} + virtual void init(); + virtual void destroy(); + virtual void update(); + virtual void draw() = 0; + static QUrl getTestResource(const QString& relativePath); + +protected: + QOpenGLFunctions_4_1_Core _glf; + const QWindow* _window; + std::function _discardLamdba; +}; diff --git a/tests-manual/qml/src/main.cpp b/tests-manual/qml/src/main.cpp index d70bb52dde..1d98ebf8c8 100644 --- a/tests-manual/qml/src/main.cpp +++ b/tests-manual/qml/src/main.cpp @@ -43,6 +43,11 @@ #include #include #include +#include +#include + +#include "TestCase.h" +#include "MacQml.h" namespace gl { extern void initModuleGl(); @@ -67,53 +72,37 @@ QUrl getTestResource(const QString& relativePath) { return QUrl::fromLocalFile(dir + relativePath); } -#define DIVISIONS_X 5 -#define DIVISIONS_Y 5 - using QmlPtr = QSharedPointer; using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence; -struct QmlInfo { - QmlPtr surface; - GLuint texture{ 0 }; - uint64_t lifetime{ 0 }; -}; - class TestWindow : public QWindow { public: - TestWindow(); + TestWindow(const TestCase::Builder& caseBuilder); private: QOpenGLContext _glContext; OffscreenGLCanvas _sharedContext; - std::array, DIVISIONS_X> _surfaces; + TestCase* _testCase{ nullptr }; QOpenGLFunctions_4_1_Core _glf; - std::function _discardLamdba; QSize _size; - size_t _surfaceCount{ 0 }; - GLuint _fbo{ 0 }; - const QSize _qmlSize{ 640, 480 }; bool _aboutToQuit{ false }; - uint64_t _createStopTime; void initGl(); - void updateSurfaces(); - void buildSurface(QmlInfo& qmlInfo, bool allowVideo); - void destroySurface(QmlInfo& qmlInfo); void resizeWindow(const QSize& size); void draw(); void resizeEvent(QResizeEvent* ev) override; }; -TestWindow::TestWindow() { +TestWindow::TestWindow(const TestCase::Builder& builder) { Setting::init(); + + _testCase = builder(this); setSurfaceType(QSurface::OpenGLSurface); qmlRegisterType("Hifi", 1, 0, "TestItem"); show(); - _createStopTime = usecTimestampNow() + (3000u * USECS_PER_SECOND); resize(QSize(800, 600)); @@ -129,162 +118,84 @@ TestWindow::TestWindow() { }); } +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); +Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); +OffscreenGLCanvas* _chromiumShareContext{ nullptr}; void TestWindow::initGl() { _glContext.setFormat(format()); + + auto globalShareContext = qt_gl_global_share_context(); + if (globalShareContext) { + _glContext.setShareContext(globalShareContext); + globalShareContext->makeCurrent(this); + gl::Context::setupDebugLogging(globalShareContext); + globalShareContext->doneCurrent(); + } + if (!_glContext.create() || !_glContext.makeCurrent(this)) { qFatal("Unable to intialize Window GL context"); } + gl::Context::setupDebugLogging(&_glContext); gl::initModuleGl(); _glf.initializeOpenGLFunctions(); - _glf.glGenFramebuffers(1, &_fbo); if (!_sharedContext.create(&_glContext) || !_sharedContext.makeCurrent()) { qFatal("Unable to intialize Shared GL context"); } hifi::qml::OffscreenSurface::setSharedContext(_sharedContext.getContext()); - _discardLamdba = hifi::qml::OffscreenSurface::getDiscardLambda(); + + if (!globalShareContext) { + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->setObjectName("ChromiumShareContext"); + _chromiumShareContext->create(&_glContext); + if (!_chromiumShareContext->makeCurrent()) { + qFatal("Unable to make chromium shared context current"); + } + + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + _chromiumShareContext->doneCurrent(); + } + + // Restore the GL widget context + if (!_glContext.makeCurrent(this)) { + qFatal("Unable to make window context current"); + } + + _testCase->init(); } void TestWindow::resizeWindow(const QSize& size) { _size = size; } -static const int DEFAULT_MAX_FPS = 10; -static const QString CONTROL_URL{ "/qml/controls/WebEntityView.qml" }; -static const char* URL_PROPERTY{ "url" }; - -QString getSourceUrl(bool video) { - static const std::vector SOURCE_URLS{ - "https://www.reddit.com/wiki/random", - "https://en.wikipedia.org/wiki/Wikipedia:Random", - "https://slashdot.org/", - }; - - static const std::vector VIDEO_SOURCE_URLS{ - "https://www.youtube.com/watch?v=gDXwhHm4GhM", - "https://www.youtube.com/watch?v=Ch_hoYPPeGc", - }; - - const auto& sourceUrls = video ? VIDEO_SOURCE_URLS : SOURCE_URLS; - auto index = rand() % sourceUrls.size(); - return sourceUrls[index]; -} - -void TestWindow::buildSurface(QmlInfo& qmlInfo, bool video) { - ++_surfaceCount; - auto lifetimeSecs = (uint32_t)(5.0f + (randFloat() * 10.0f)); - auto lifetimeUsecs = (USECS_PER_SECOND * lifetimeSecs); - qmlInfo.lifetime = lifetimeUsecs + usecTimestampNow(); - qmlInfo.texture = 0; - qmlInfo.surface.reset(new hifi::qml::OffscreenSurface()); - qmlInfo.surface->load(getTestResource(CONTROL_URL), [video](QQmlContext* context, QQuickItem* item) { - item->setProperty(URL_PROPERTY, getSourceUrl(video)); - }); - qmlInfo.surface->setMaxFps(DEFAULT_MAX_FPS); - qmlInfo.surface->resize(_qmlSize); - qmlInfo.surface->resume(); -} - -void TestWindow::destroySurface(QmlInfo& qmlInfo) { - auto& surface = qmlInfo.surface; - auto& currentTexture = qmlInfo.texture; - if (currentTexture) { - auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - glFlush(); - _discardLamdba(currentTexture, readFence); - } - auto webView = surface->getRootItem(); - if (webView) { - // stop loading - QMetaObject::invokeMethod(webView, "stop"); - webView->setProperty(URL_PROPERTY, "about:blank"); - } - surface->pause(); - surface.reset(); -} - -void TestWindow::updateSurfaces() { - auto now = usecTimestampNow(); - // Fetch any new textures - for (size_t x = 0; x < DIVISIONS_X; ++x) { - for (size_t y = 0; y < DIVISIONS_Y; ++y) { - auto& qmlInfo = _surfaces[x][y]; - if (!qmlInfo.surface) { - if (now < _createStopTime && randFloat() > 0.99f) { - buildSurface(qmlInfo, x == 0 && y == 0); - } else { - continue; - } - } - - if (now > qmlInfo.lifetime) { - destroySurface(qmlInfo); - continue; - } - - auto& surface = qmlInfo.surface; - auto& currentTexture = qmlInfo.texture; - - TextureAndFence newTextureAndFence; - if (surface->fetchTexture(newTextureAndFence)) { - if (currentTexture != 0) { - auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - glFlush(); - _discardLamdba(currentTexture, readFence); - } - currentTexture = newTextureAndFence.first; - _glf.glWaitSync((GLsync)newTextureAndFence.second, 0, GL_TIMEOUT_IGNORED); - } - } - } -} - void TestWindow::draw() { if (_aboutToQuit) { return; } - + // Attempting to draw before we're visible and have a valid size will // produce GL errors. if (!isVisible() || _size.width() <= 0 || _size.height() <= 0) { return; } - + static std::once_flag once; std::call_once(once, [&] { initGl(); }); - + if (!_glContext.makeCurrent(this)) { return; } - - updateSurfaces(); - - auto size = this->geometry().size(); - auto incrementX = size.width() / DIVISIONS_X; - auto incrementY = size.height() / DIVISIONS_Y; + + _testCase->update(); + + auto size = geometry().size(); _glf.glViewport(0, 0, size.width(), size.height()); _glf.glClearColor(1, 0, 0, 1); _glf.glClear(GL_COLOR_BUFFER_BIT); - for (uint32_t x = 0; x < DIVISIONS_X; ++x) { - for (uint32_t y = 0; y < DIVISIONS_Y; ++y) { - auto& qmlInfo = _surfaces[x][y]; - if (!qmlInfo.surface || !qmlInfo.texture) { - continue; - } - _glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, _fbo); - _glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, qmlInfo.texture, 0); - _glf.glBlitFramebuffer( - // src coordinates - 0, 0, _qmlSize.width() - 1, _qmlSize.height() - 1, - // dst coordinates - incrementX * x, incrementY * y, incrementX * (x + 1), incrementY * (y + 1), - // blit mask and filter - GL_COLOR_BUFFER_BIT, GL_NEAREST); - } - } - _glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - _glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + + _testCase->draw(); + _glContext.swapBuffers(this); } @@ -292,19 +203,15 @@ void TestWindow::resizeEvent(QResizeEvent* ev) { resizeWindow(ev->size()); } -int main(int argc, char** argv) { - QSurfaceFormat format; - format.setDepthBufferSize(24); - format.setStencilBufferSize(8); +int main(int argc, char** argv) { + auto format = getDefaultOpenGLSurfaceFormat(); format.setVersion(4, 1); - format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); - format.setOption(QSurfaceFormat::DebugContext); QSurfaceFormat::setDefaultFormat(format); - // setFormat(format); QGuiApplication app(argc, argv); - TestWindow window; + TestCase::Builder builder = [](const QWindow* window)->TestCase*{ return new MacQml(window); }; + TestWindow window(builder); return app.exec(); } From 28b45ce1d9932fbb93041be2b7542ab4a6538564 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Mon, 1 Oct 2018 15:13:11 -0700 Subject: [PATCH 258/362] Resolve compositing glitches --- interface/src/Application.cpp | 52 +++++++++++-------- .../display-plugins/OpenGLDisplayPlugin.cpp | 2 + libraries/gl/src/gl/GLHelpers.cpp | 35 +++++++++++++ libraries/gl/src/gl/GLHelpers.h | 3 ++ libraries/gl/src/gl/OffscreenGLCanvas.cpp | 6 ++- libraries/gl/src/gl/OffscreenGLCanvas.h | 2 + .../qml/src/qml/impl/RenderEventHandler.cpp | 3 ++ 7 files changed, 80 insertions(+), 23 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 700561325f..6f65a124dd 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2736,7 +2736,36 @@ void Application::initializeGL() { // We have to account for this possibility by checking here for an existing // global share context auto globalShareContext = qt_gl_global_share_context(); - _glWidget->createContext(globalShareContext); + +#if !defined(DISABLE_QML) + // Build a shared canvas / context for the Chromium processes + if (!globalShareContext) { + // Chromium rendering uses some GL functions that prevent nSight from capturing + // frames, so we only create the shared context if nsight is NOT active. + if (!nsightActive()) { + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->setObjectName("ChromiumShareContext"); + auto format =QSurfaceFormat::defaultFormat(); +#ifdef Q_OS_MAC + // On mac, the primary shared OpenGL context must be a 3.2 core context, + // or chromium flips out and spews error spam (but renders fine) + format.setMajorVersion(3); + format.setMinorVersion(2); +#endif + _chromiumShareContext->setFormat(format); + _chromiumShareContext->create(); + if (!_chromiumShareContext->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make chromium shared context current"); + } + globalShareContext = _chromiumShareContext->getContext(); + qt_gl_set_global_share_context(globalShareContext); + _chromiumShareContext->doneCurrent(); + } + } +#endif + + + _glWidget->createContext(globalShareContext); if (!_glWidget->makeCurrent()) { qCWarning(interfaceapp, "Unable to make window context current"); @@ -2749,27 +2778,6 @@ void Application::initializeGL() { if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); } - - // Build a shared canvas / context for the Chromium processes - if (!globalShareContext) { - // Chromium rendering uses some GL functions that prevent nSight from capturing - // frames, so we only create the shared context if nsight is NOT active. - if (!nsightActive()) { - _chromiumShareContext = new OffscreenGLCanvas(); - _chromiumShareContext->setObjectName("ChromiumShareContext"); - _chromiumShareContext->create(_glWidget->qglContext()); - if (!_chromiumShareContext->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make chromium shared context current"); - } - globalShareContext = _chromiumShareContext->getContext(); - qt_gl_set_global_share_context(globalShareContext); - _chromiumShareContext->doneCurrent(); - // Restore the GL widget context - if (!_glWidget->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make window context current"); - } - } - } #endif if (!globalShareContext) { diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 6448c6d3a1..6525672aee 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -179,7 +179,9 @@ public: _context->makeCurrent(); { PROFILE_RANGE(render, "PluginPresent") + gl::globalLock(); currentPlugin->present(); + gl::globalRelease(false); CHECK_GL_ERROR(); } _context->doneCurrent(); diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index c22bd0dde5..ba27a13f58 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -34,6 +34,41 @@ bool gl::disableGl45() { #endif } +#ifdef Q_OS_MAC +#define SERIALIZE_GL_RENDERING +#endif + +#ifdef SERIALIZE_GL_RENDERING + +// This terrible terrible hack brought to you by the complete lack of reasonable +// OpenGL debugging tools on OSX. Without this serialization code, the UI textures +// frequently become 'glitchy' and get composited onto the main scene in what looks +// like a partially rendered state. +// This looks very much like either state bleeding across the contexts, or bad +// synchronization for the shared OpenGL textures. However, previous attempts to resolve +// it, even with gratuitous use of glFinish hasn't improved the situation + +static std::mutex _globalOpenGLLock; + +void gl::globalLock() { + _globalOpenGLLock.lock(); +} + +void gl::globalRelease(bool finish) { + if (finish) { + glFinish(); + } + _globalOpenGLLock.unlock(); +} + +#else + +void gl::globalLock() {} +void gl::globalRelease(bool finish) {} + +#endif + + void gl::getTargetVersion(int& major, int& minor) { #if defined(USE_GLES) major = 3; diff --git a/libraries/gl/src/gl/GLHelpers.h b/libraries/gl/src/gl/GLHelpers.h index 6252eba2f0..1865442ef6 100644 --- a/libraries/gl/src/gl/GLHelpers.h +++ b/libraries/gl/src/gl/GLHelpers.h @@ -35,6 +35,9 @@ int glVersionToInteger(QString glVersion); bool isRenderThread(); namespace gl { + void globalLock(); + void globalRelease(bool finish = true); + void withSavedContext(const std::function& f); bool checkGLError(const char* name); diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index 1256e316ff..37289745a4 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -33,6 +33,7 @@ OffscreenGLCanvas::OffscreenGLCanvas() : _context(new QOpenGLContext), _offscreenSurface(new QOffscreenSurface) { + setFormat(getDefaultOpenGLSurfaceFormat()); } OffscreenGLCanvas::~OffscreenGLCanvas() { @@ -49,12 +50,15 @@ OffscreenGLCanvas::~OffscreenGLCanvas() { } +void OffscreenGLCanvas::setFormat(const QSurfaceFormat& format) { + _context->setFormat(format); +} + bool OffscreenGLCanvas::create(QOpenGLContext* sharedContext) { if (nullptr != sharedContext) { sharedContext->doneCurrent(); _context->setShareContext(sharedContext); } - _context->setFormat(getDefaultOpenGLSurfaceFormat()); if (!_context->create()) { qFatal("Failed to create OffscreenGLCanvas context"); } diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.h b/libraries/gl/src/gl/OffscreenGLCanvas.h index 374720a910..3946f760cf 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.h +++ b/libraries/gl/src/gl/OffscreenGLCanvas.h @@ -18,11 +18,13 @@ class QOpenGLContext; class QOffscreenSurface; class QOpenGLDebugMessage; +class QSurfaceFormat; class OffscreenGLCanvas : public QObject { public: OffscreenGLCanvas(); ~OffscreenGLCanvas(); + void setFormat(const QSurfaceFormat& format); bool create(QOpenGLContext* sharedContext = nullptr); bool makeCurrent(); void doneCurrent(); diff --git a/libraries/qml/src/qml/impl/RenderEventHandler.cpp b/libraries/qml/src/qml/impl/RenderEventHandler.cpp index 8a56929e6b..46fc3490a7 100644 --- a/libraries/qml/src/qml/impl/RenderEventHandler.cpp +++ b/libraries/qml/src/qml/impl/RenderEventHandler.cpp @@ -12,6 +12,7 @@ #include #include +#include #include @@ -114,6 +115,7 @@ void RenderEventHandler::onRender() { PROFILE_RANGE(render_qml_gl, __FUNCTION__); + gl::globalLock(); if (!_shared->preRender()) { return; } @@ -144,6 +146,7 @@ void RenderEventHandler::onRender() { _shared->updateTextureAndFence({ texture, fence }); _shared->_quickWindow->resetOpenGLState(); } + gl::globalRelease(); } void RenderEventHandler::onQuit() { From 405ec228b8cae4aac0d340f6f132ddf825c6fb23 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 1 Oct 2018 16:07:02 -0700 Subject: [PATCH 259/362] Fix mac warning --- tests-manual/qml/src/MacQml.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests-manual/qml/src/MacQml.cpp b/tests-manual/qml/src/MacQml.cpp index 7f7854ce87..13d8642822 100644 --- a/tests-manual/qml/src/MacQml.cpp +++ b/tests-manual/qml/src/MacQml.cpp @@ -29,7 +29,7 @@ using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence; void MacQml::update() { auto rootItem =_surface->getRootItem(); float now = sinf(secTimestampNow()); - rootItem->setProperty("level", abs(now)); + rootItem->setProperty("level", fabs(now)); rootItem->setProperty("muted", now > 0.0f); rootItem->setProperty("statsValue", rand()); From 9a30073f27b0786186a8214e94cd20c27db8ea2b Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 2 Oct 2018 09:20:41 -0700 Subject: [PATCH 260/362] Only enable debug GL context on demand --- libraries/gl/src/gl/Context.cpp | 7 +++++-- libraries/gl/src/gl/GLHelpers.cpp | 6 +++++- tests-manual/qml/src/MacQml.cpp | 18 ------------------ 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/libraries/gl/src/gl/Context.cpp b/libraries/gl/src/gl/Context.cpp index cd2b19beec..a3e01018b5 100644 --- a/libraries/gl/src/gl/Context.cpp +++ b/libraries/gl/src/gl/Context.cpp @@ -31,6 +31,10 @@ using namespace gl; bool Context::enableDebugLogger() { +#if defined(Q_OS_MAC) + // OSX does not support GL_KHR_debug or GL_ARB_debug_output + return false; +#else #if defined(DEBUG) || defined(USE_GLES) static bool enableDebugLogger = true; #else @@ -45,6 +49,7 @@ bool Context::enableDebugLogger() { } }); return enableDebugLogger; +#endif } @@ -233,7 +238,6 @@ GLAPI PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB; Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); - void Context::create(QOpenGLContext* shareContext) { assert(0 != _hwnd); assert(0 == _hdc); @@ -351,7 +355,6 @@ void Context::create(QOpenGLContext* shareContext) { #endif - OffscreenContext::~OffscreenContext() { _window->deleteLater(); } diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index ba27a13f58..15a41c3dc1 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -13,6 +13,8 @@ #include #include +#include "Context.h" + size_t evalGLFormatSwapchainPixelSize(const QSurfaceFormat& format) { size_t pixelSize = format.redBufferSize() + format.greenBufferSize() + format.blueBufferSize() + format.alphaBufferSize(); // We don't apply the length of the swap chain into this pixelSize since it is not vsible for the Process (on windows). @@ -97,7 +99,9 @@ const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { #else format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); #endif - format.setOption(QSurfaceFormat::DebugContext); + if (gl::Context::enableDebugLogger()) { + format.setOption(QSurfaceFormat::DebugContext); + } // Qt Quick may need a depth and stencil buffer. Always make sure these are available. format.setDepthBufferSize(DEFAULT_GL_DEPTH_BUFFER_BITS); format.setStencilBufferSize(DEFAULT_GL_STENCIL_BUFFER_BITS); diff --git a/tests-manual/qml/src/MacQml.cpp b/tests-manual/qml/src/MacQml.cpp index 13d8642822..9c5f91041e 100644 --- a/tests-manual/qml/src/MacQml.cpp +++ b/tests-manual/qml/src/MacQml.cpp @@ -7,24 +7,6 @@ #include using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence; -// -//void MacQml::destroySurface(QmlInfo& qmlInfo) { -// auto& surface = qmlInfo.surface; -// auto& currentTexture = qmlInfo.texture; -// if (currentTexture) { -// auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); -// glFlush(); -// _discardLamdba(currentTexture, readFence); -// } -// auto webView = surface->getRootItem(); -// if (webView) { -// // stop loading -// QMetaObject::invokeMethod(webView, "stop"); -// webView->setProperty(URL_PROPERTY, "about:blank"); -// } -// surface->pause(); -// surface.reset(); -//} void MacQml::update() { auto rootItem =_surface->getRootItem(); From 7ca8b540399586683b075b4ab787bbf7a046e16f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 25 Oct 2018 14:34:23 -0700 Subject: [PATCH 261/362] Fix crash on startup on windows / android --- libraries/gl/src/gl/Context.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/libraries/gl/src/gl/Context.cpp b/libraries/gl/src/gl/Context.cpp index a3e01018b5..d4ecbaa5ba 100644 --- a/libraries/gl/src/gl/Context.cpp +++ b/libraries/gl/src/gl/Context.cpp @@ -41,13 +41,6 @@ bool Context::enableDebugLogger() { static const QString DEBUG_FLAG("HIFI_DEBUG_OPENGL"); static bool enableDebugLogger = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); #endif - static std::once_flag once; - std::call_once(once, [&] { - // If the previous run crashed, force GL debug logging on - if (qApp->property(hifi::properties::CRASHED).toBool()) { - enableDebugLogger = true; - } - }); return enableDebugLogger; #endif } From 36edb939c3dbe7f72563fe590f41046d7863e345 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 25 Oct 2018 16:28:30 -0700 Subject: [PATCH 262/362] Try to fix android crash --- interface/src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 12a02ffd32..5af0a9371d 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -41,8 +41,8 @@ extern "C" { #endif int main(int argc, const char* argv[]) { - auto format = getDefaultOpenGLSurfaceFormat(); #ifdef Q_OS_MAC + auto format = getDefaultOpenGLSurfaceFormat(); // Deal with some weirdness in the chromium context sharing on Mac. // The primary share context needs to be 3.2, so that the Chromium will // succeed in it's creation of it's command stub contexts. @@ -51,8 +51,8 @@ int main(int argc, const char* argv[]) { // idea why. qputenv("QT_ENABLE_GLYPH_CACHE_WORKAROUND", "true"); // https://i.kym-cdn.com/entries/icons/original/000/008/342/ihave.jpg -#endif QSurfaceFormat::setDefaultFormat(format); +#endif setupHifiApplication(BuildInfo::INTERFACE_NAME); QStringList arguments; From a3a952265a7e8d6cf42c43676a7be33717162740 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 26 Oct 2018 11:23:04 -0700 Subject: [PATCH 263/362] Fix android crash --- .../display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp | 1 + libraries/gl/src/gl/OffscreenGLCanvas.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 6525672aee..190d4d4104 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -88,6 +88,7 @@ public: // Move the OpenGL context to the present thread // Extra code because of the widget 'wrapper' context _context = context; + _context->doneCurrent(); _context->moveToThread(this); } diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index 37289745a4..f05acb50e9 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -73,6 +73,7 @@ bool OffscreenGLCanvas::create(QOpenGLContext* sharedContext) { if (!_context->makeCurrent(_offscreenSurface)) { qFatal("Unable to make offscreen surface current"); } + _context->doneCurrent(); #else if (!_offscreenSurface->isValid()) { qFatal("Offscreen surface is invalid"); From 1cca975b3713d8a1bca814f36b8600b2ccfa9064 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 27 Oct 2018 11:40:02 +1300 Subject: [PATCH 264/362] Fix Interface eating memory when minimized --- interface/src/Application.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 58aad654db..e54d2e0961 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6017,7 +6017,9 @@ void Application::update(float deltaTime) { { PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("overlays"); - _overlays.update(deltaTime); + if (qApp->shouldPaint()) { + _overlays.update(deltaTime); + } } // Update _viewFrustum with latest camera and view frustum data... @@ -6102,8 +6104,10 @@ void Application::update(float deltaTime) { PROFILE_RANGE_EX(app, "PostUpdateLambdas", 0xffff0000, (uint64_t)0); PerformanceTimer perfTimer("postUpdateLambdas"); std::unique_lock guard(_postUpdateLambdasLock); - for (auto& iter : _postUpdateLambdas) { - iter.second(); + if (qApp->shouldPaint()) { + for (auto& iter : _postUpdateLambdas) { + iter.second(); + } } _postUpdateLambdas.clear(); } From 62e4b3feadb32c1b98e77429d20dad7d502c5679 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 26 Oct 2018 16:02:17 -0700 Subject: [PATCH 265/362] Avatar mixer gets the updated position --- libraries/avatars/src/AvatarData.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index fcbe05ca52..aa3f83a33f 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1110,7 +1110,7 @@ public: QJsonObject toJson() const; void fromJson(const QJsonObject& json, bool useFrameSkeleton = true); - glm::vec3 getClientGlobalPosition() const { return _globalPosition; } + glm::vec3 getClientGlobalPosition() const { return _serverPosition; } AABox getGlobalBoundingBox() const { return AABox(_globalPosition + _globalBoundingBoxOffset - _globalBoundingBoxDimensions, _globalBoundingBoxDimensions); } /**jsdoc From 767f4e9d6dfb2537362a1cbb9f4da627de0a15d0 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 26 Oct 2018 17:16:43 -0700 Subject: [PATCH 266/362] Return server position --- libraries/avatars/src/AvatarData.cpp | 6 +++--- libraries/avatars/src/AvatarData.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index f3b8d29658..42c7afc43b 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -883,6 +883,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } _serverPosition = glm::vec3(data->globalPosition[0], data->globalPosition[1], data->globalPosition[2]) + offset; + if (_globalPosition != _serverPosition) { + _globalPositionChanged = now; + } auto oneStepDistance = glm::length(_globalPosition - _serverPosition); if (oneStepDistance <= AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE || oneStepDistance >= AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE) { _globalPosition = _serverPosition; @@ -891,9 +894,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { setLocalPosition(_serverPosition); } } - if (_globalPosition != _serverPosition) { - _globalPositionChanged = now; - } sourceBuffer += sizeof(AvatarDataPacket::AvatarGlobalPosition); int numBytesRead = sourceBuffer - startSection; _globalPositionRate.increment(numBytesRead); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index aa3f83a33f..32e75a64a9 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1111,7 +1111,7 @@ public: void fromJson(const QJsonObject& json, bool useFrameSkeleton = true); glm::vec3 getClientGlobalPosition() const { return _serverPosition; } - AABox getGlobalBoundingBox() const { return AABox(_globalPosition + _globalBoundingBoxOffset - _globalBoundingBoxDimensions, _globalBoundingBoxDimensions); } + AABox getGlobalBoundingBox() const { return AABox(_serverPosition + _globalBoundingBoxOffset - _globalBoundingBoxDimensions, _globalBoundingBoxDimensions); } /**jsdoc * @function MyAvatar.getAvatarEntityData From 056fe338e17262708f043e981184eb2f336eb720 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Sat, 27 Oct 2018 07:15:31 -0700 Subject: [PATCH 267/362] Apply transit logic if avatar instance is client side --- interface/src/avatar/AvatarManager.cpp | 1 + libraries/avatars/src/AvatarData.cpp | 24 +++++++++++++++++------- libraries/avatars/src/AvatarData.h | 6 ++++-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index dad4c44f4b..abc5185377 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -266,6 +266,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { if (avatar->getSkeletonModel()->isLoaded()) { // remove the orb if it is there avatar->removeOrb(); + avatar->setIsClientAvatar(true); if (avatar->needsPhysicsUpdate()) { _avatarsToChangeInPhysics.insert(avatar); } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 42c7afc43b..4f936900b0 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -883,13 +883,23 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } _serverPosition = glm::vec3(data->globalPosition[0], data->globalPosition[1], data->globalPosition[2]) + offset; - if (_globalPosition != _serverPosition) { - _globalPositionChanged = now; - } - auto oneStepDistance = glm::length(_globalPosition - _serverPosition); - if (oneStepDistance <= AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE || oneStepDistance >= AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE) { - _globalPosition = _serverPosition; - // if we don't have a parent, make sure to also set our local position + if (_isClientAvatar) { + auto oneStepDistance = glm::length(_globalPosition - _serverPosition); + if (oneStepDistance <= AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE || oneStepDistance >= AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE) { + _globalPosition = _serverPosition; + // if we don't have a parent, make sure to also set our local position + if (!hasParent()) { + setLocalPosition(_serverPosition); + } + } + if (_globalPosition != _serverPosition) { + _globalPositionChanged = now; + } + } else { + if (_globalPosition != _serverPosition) { + _globalPosition = _serverPosition; + _globalPositionChanged = now; + } if (!hasParent()) { setLocalPosition(_serverPosition); } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 32e75a64a9..e8858825d8 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1110,8 +1110,8 @@ public: QJsonObject toJson() const; void fromJson(const QJsonObject& json, bool useFrameSkeleton = true); - glm::vec3 getClientGlobalPosition() const { return _serverPosition; } - AABox getGlobalBoundingBox() const { return AABox(_serverPosition + _globalBoundingBoxOffset - _globalBoundingBoxDimensions, _globalBoundingBoxDimensions); } + glm::vec3 getClientGlobalPosition() const { return _globalPosition; } + AABox getGlobalBoundingBox() const { return AABox(_globalPosition + _globalBoundingBoxOffset - _globalBoundingBoxDimensions, _globalBoundingBoxDimensions); } /**jsdoc * @function MyAvatar.getAvatarEntityData @@ -1206,6 +1206,7 @@ public: void setIsNewAvatar(bool isNewAvatar) { _isNewAvatar = isNewAvatar; } bool getIsNewAvatar() { return _isNewAvatar; } + void setIsClientAvatar(bool isClientAvatar) { _isClientAvatar = isClientAvatar; } signals: @@ -1469,6 +1470,7 @@ protected: float _density; int _replicaIndex { 0 }; bool _isNewAvatar { true }; + bool _isClientAvatar { false }; // null unless MyAvatar or ScriptableAvatar sending traits data to mixer std::unique_ptr _clientTraitsHandler; From e49e9cdc5608568b7ffd1816cc9127a1fb8aaddc Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Mon, 29 Oct 2018 07:25:22 -0700 Subject: [PATCH 268/362] Set avatar client sooner --- interface/src/avatar/AvatarManager.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index abc5185377..aa57336a3c 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -261,12 +261,13 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { for (auto it = sortedAvatarVector.begin(); it != sortedAvatarVector.end(); ++it) { const SortableAvatar& sortData = *it; const auto avatar = std::static_pointer_cast(sortData.getAvatar()); - + if (!avatar->_isClientAvatar) { + avatar->setIsClientAvatar(true); + } // TODO: to help us scale to more avatars it would be nice to not have to poll this stuff every update if (avatar->getSkeletonModel()->isLoaded()) { // remove the orb if it is there avatar->removeOrb(); - avatar->setIsClientAvatar(true); if (avatar->needsPhysicsUpdate()) { _avatarsToChangeInPhysics.insert(avatar); } From df005eedbf7867fa886bdb8fc13c0e0fc09c049d Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 29 Oct 2018 17:43:16 -0700 Subject: [PATCH 269/362] fix shape not getting marked changed --- libraries/entities/src/EntityItemProperties.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index d901592759..c243f772e2 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -3207,6 +3207,8 @@ void EntityItemProperties::markAllChanged() { _queryAACubeChanged = true; + _shapeChanged = true; + _flyingAllowedChanged = true; _ghostingAllowedChanged = true; _filterURLChanged = true; From 015b5fa681e03bef2a676416212133c8daa8da5e Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 30 Oct 2018 08:01:22 -0700 Subject: [PATCH 270/362] Don't attempt purchase of sold out items --- scripts/system/html/js/marketplacesInject.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 24a96023da..c667d6dc28 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -312,11 +312,12 @@ var getString = "GET"; // Protection against the button getting stuck in the "BUY"/"GET" state. // That happens when the browser gets two MOUSEENTER events before getting a - // MOUSELEAVE event. - if ($this.text() === buyString || $this.text() === getString) { - return; - } - if ($this.text() === 'invalidated') { + // MOUSELEAVE event. Also, if not available for sale, just return. + if ($this.text() === buyString || + $this.text() === getString || + $this.text() === 'invalidated' || + $this.text() === 'sold out' || + $this.text() === 'not for sale' ) { return; } $this.data('initialHtml', $this.html()); @@ -337,7 +338,10 @@ $('.grid-item').find('#price-or-edit').find('a').on('click', function () { - if ($(this).closest('.grid-item').find('.price').text() === 'invalidated') { + var price = $(this).closest('.grid-item').find('.price').text(); + if (price === 'invalidated' || + price === 'sold out' || + price === 'not for sale') { return false; } buyButtonClicked($(this).closest('.grid-item').attr('data-item-id'), From 959c544fcdd9e4b6f2a09b3fb62a2e862ca79069 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 30 Oct 2018 12:05:41 -0700 Subject: [PATCH 271/362] Made path to VCPKG relative. --- tools/auto-tester/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/auto-tester/CMakeLists.txt b/tools/auto-tester/CMakeLists.txt index 06930ab0fe..1befa6332e 100644 --- a/tools/auto-tester/CMakeLists.txt +++ b/tools/auto-tester/CMakeLists.txt @@ -57,10 +57,10 @@ if (WIN32) ) # add a custom command to copy the SSL DLLs - add_custom_command( + add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_directory D:/GitHub/vcpkg/installed/x64-windows/bin "$" + COMMAND "${CMAKE_COMMAND}" -E copy_directory "$ENV{VCPKG_ROOT}/installed/x64-windows/bin" "$" ) endif () \ No newline at end of file From 1dc96fec79065ca4b2bed34bdd6b147cae158a82 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 30 Oct 2018 12:13:46 -0700 Subject: [PATCH 272/362] Implement CR comments for installer text logging --- cmake/installer/TextLog.nsh | 68 ----------------------- cmake/templates/CPackProperties.cmake.in | 1 - cmake/templates/NSIS.template.in | 71 +++++++++++++++++++++++- 3 files changed, 69 insertions(+), 71 deletions(-) delete mode 100644 cmake/installer/TextLog.nsh diff --git a/cmake/installer/TextLog.nsh b/cmake/installer/TextLog.nsh deleted file mode 100644 index a61ea1531e..0000000000 --- a/cmake/installer/TextLog.nsh +++ /dev/null @@ -1,68 +0,0 @@ -# TextLog.nsh v1.1 - 2005-12-26 -# Written by Mike Schinkel [http://www.mikeschinkel.com/blog/] - -Var /GLOBAL __TextLog_FileHandle -Var /GLOBAL __TextLog_FileName -Var /GLOBAL __TextLog_State - -!define LogMsg '!insertmacro LogMsgCall' -!macro LogMsgCall _text - Call LogSetOn - Push "${_text}" - Call LogText - Call LogSetOff -!macroend - - -!define LogText '!insertmacro LogTextCall' -!macro LogTextCall _text - Push "${_text}" - Call LogText -!macroend - -Function LogText - Exch $0 ; pABC -> 0ABC - FileWrite $__TextLog_FileHandle "$0$\r$\n" - Pop $0 ; 0ABC -> ABC -FunctionEnd - -!define LogSetFileName '!insertmacro LogSetFileNameCall' -!macro LogSetFileNameCall _filename - Push "${_filename}" - Call LogSetFileName -!macroend - -Function LogSetFileName - Exch $0 ; pABC -> 0ABC - StrCpy $__TextLog_FileName "$0" - StrCmp $__TextLog_State "open" +1 +3 - Call LogSetOff - Call LogSetOn - Pop $0 ; 0ABC -> ABC -FunctionEnd - -!define LogSetOn '!insertmacro LogSetOnCall' -!macro LogSetOnCall - Call LogSetOn -!macroend - -Function LogSetOn - StrCmp $__TextLog_FileName "" +1 AlreadySet - StrCpy $__TextLog_FileName "$INSTDIR\install.log" -AlreadySet: - StrCmp $__TextLog_State "open" +2 - FileOpen $__TextLog_FileHandle "$__TextLog_FileName" a - FileSeek $__TextLog_FileHandle 0 END - StrCpy $__TextLog_State "open" -FunctionEnd - -!define LogSetOff '!insertmacro LogSetOffCall' -!macro LogSetOffCall - Call LogSetOff -!macroend - -Function LogSetOff - StrCmp $__TextLog_State "open" +1 +2 - FileClose $__TextLog_FileHandle - StrCpy $__TextLog_State "" -FunctionEnd \ No newline at end of file diff --git a/cmake/templates/CPackProperties.cmake.in b/cmake/templates/CPackProperties.cmake.in index 73f52d72bf..cb6474b010 100644 --- a/cmake/templates/CPackProperties.cmake.in +++ b/cmake/templates/CPackProperties.cmake.in @@ -54,4 +54,3 @@ set(CLIENT_COMPONENT_CONDITIONAL "@CLIENT_COMPONENT_CONDITIONAL@") set(INSTALLER_TYPE "@INSTALLER_TYPE@") set(APP_USER_MODEL_ID "@APP_USER_MODEL_ID@") set(BYPASS_SIGNING "@BYPASS_SIGNING@") -set(HF_CMAKE_DIR "@HF_CMAKE_DIR@") diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 4a9b67d843..19f661b42b 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -30,8 +30,75 @@ ;-------------------------------- ;Include Installer Logging - !addincludedir "@HF_CMAKE_DIR@\installer" - !include "TextLog.nsh" +; taken from http://nsis.sourceforge.net/Logging:_Simple_Text_File_Logging_Functions_and_Macros +; TextLog.nsh v1.1 - 2005-12-26 +; Written by Mike Schinkel [http://www.mikeschinkel.com/blog/] + + Var /GLOBAL __TextLog_FileHandle + Var /GLOBAL __TextLog_FileName + Var /GLOBAL __TextLog_State + + !define LogMsg '!insertmacro LogMsgCall' + !macro LogMsgCall _text + Call LogSetOn + Push "${_text}" + Call LogText + Call LogSetOff + !macroend + + + !define LogText '!insertmacro LogTextCall' + !macro LogTextCall _text + Push "${_text}" + Call LogText + !macroend + + Function LogText + Exch $0 ; pABC -> 0ABC + FileWrite $__TextLog_FileHandle "$0$\r$\n" + Pop $0 ; 0ABC -> ABC + FunctionEnd + + !define LogSetFileName '!insertmacro LogSetFileNameCall' + !macro LogSetFileNameCall _filename + Push "${_filename}" + Call LogSetFileName + !macroend + + Function LogSetFileName + Exch $0 ; pABC -> 0ABC + StrCpy $__TextLog_FileName "$0" + StrCmp $__TextLog_State "open" +1 +3 + Call LogSetOff + Call LogSetOn + Pop $0 ; 0ABC -> ABC + FunctionEnd + + !define LogSetOn '!insertmacro LogSetOnCall' + !macro LogSetOnCall + Call LogSetOn + !macroend + + Function LogSetOn + StrCmp $__TextLog_FileName "" +1 AlreadySet + StrCpy $__TextLog_FileName "$INSTDIR\install.log" + AlreadySet: + StrCmp $__TextLog_State "open" +2 + FileOpen $__TextLog_FileHandle "$__TextLog_FileName" a + FileSeek $__TextLog_FileHandle 0 END + StrCpy $__TextLog_State "open" + FunctionEnd + + !define LogSetOff '!insertmacro LogSetOffCall' + !macro LogSetOffCall + Call LogSetOff + !macroend + + Function LogSetOff + StrCmp $__TextLog_State "open" +1 +2 + FileClose $__TextLog_FileHandle + StrCpy $__TextLog_State "" + FunctionEnd ;-------------------------------- ; Utilities and Functions From e64a4d0536be1a0f59b8b9c2d9739b6e4b7ec5b8 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 30 Oct 2018 12:47:44 -0700 Subject: [PATCH 273/362] try to fix unnecessary queryAACube updates --- libraries/shared/src/SpatiallyNestable.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index fd2ff6e790..97e20f5627 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -74,10 +74,12 @@ void SpatiallyNestable::setParentID(const QUuid& parentID) { } }); - bool success = false; - auto parent = getParentPointer(success); - if (success && parent) { - parent->updateQueryAACube(); + if (!_parentKnowsMe) { + bool success = false; + auto parent = getParentPointer(success); + if (success && parent) { + parent->updateQueryAACube(); + } } } From b6344acc1d047dc630b266f594b08fb11f7e089a Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 30 Oct 2018 15:03:10 -0700 Subject: [PATCH 274/362] Big checkpoint. be aware - things may be broken --- .../qml/hifi/avatarapp/MessageBoxes.qml | 2 +- .../qml/hifi/commerce/checkout/Checkout.qml | 15 +- .../common/EmulatedMarketplaceHeader.qml | 54 -- .../commerce/common/sendAsset/SendAsset.qml | 12 +- .../hifi/commerce/purchases/PurchasedItem.qml | 1 + .../qml/hifi/commerce/purchases/Purchases.qml | 26 +- .../qml/hifi/commerce/wallet/Wallet.qml | 93 ++- interface/src/Application.cpp | 8 +- interface/src/commerce/Ledger.cpp | 1 - .../src/scripting/WalletScriptingInterface.h | 28 +- interface/src/ui/overlays/Web3DOverlay.cpp | 3 +- scripts/system/avatarapp.js | 8 +- scripts/system/commerce/wallet.js | 161 +++-- scripts/system/edit.js | 2 +- scripts/system/html/js/marketplacesInject.js | 47 +- scripts/system/marketplaces/marketplace.js | 2 +- scripts/system/marketplaces/marketplaces.js | 568 +----------------- scripts/system/notifications.js | 2 +- 18 files changed, 279 insertions(+), 754 deletions(-) diff --git a/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml b/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml index 2c02272b46..89a8eff025 100644 --- a/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml +++ b/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml @@ -89,7 +89,7 @@ MessageBox { function showDeleteFavorite(favoriteName, callback) { popup.titleText = 'Delete Favorite: {AvatarName}'.replace('{AvatarName}', favoriteName) - popup.bodyText = 'This will delete your favorite. You will retain access to the wearables and avatar that made up the favorite from My Purchases.' + popup.bodyText = 'This will delete your favorite. You will retain access to the wearables and avatar that made up the favorite from Inventory.' popup.imageSource = null; popup.button1text = 'CANCEL' popup.button2text = 'DELETE' diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index f88a70ff05..95b040eceb 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -559,7 +559,7 @@ Rectangle { } } - // "View in My Purchases" button + // "View in Inventory" button HifiControlsUit.Button { id: viewInMyPurchasesButton; visible: false; @@ -833,7 +833,7 @@ Rectangle { } lightboxPopup.button2text = "OPEN GOTO"; lightboxPopup.button2method = function() { - sendToScript({method: 'purchases_openGoTo'}); + sendToScript({method: 'checkout_openGoTo'}); lightboxPopup.visible = false; }; lightboxPopup.visible = true; @@ -886,7 +886,8 @@ Rectangle { RalewaySemiBold { id: walletLink; - text: 'View receipt in Wallet'; + visible: !WalletScriptingInterface.limitedCommerce; + text: 'View receipt in Recent Activity'; // Text size size: 18; // Anchors @@ -902,18 +903,18 @@ Rectangle { horizontalAlignment: Text.AlignLeft; verticalAlignment: Text.AlignVCenter; onLinkActivated: { - sendToScript({method: 'purchases_openWallet'}); + sendToScript({method: 'checkout_openWallet'}); } } RalewayRegular { id: pendingText; - text: 'Your item is marked "pending" while it is being confirmed. ' + + text: 'Your item is marked "pending" while the transfer is being confirmed. ' + 'Learn More'; // Text size size: 18; // Anchors - anchors.top: walletLink.bottom; + anchors.top: walletLink.visible ? walletLink.bottom : myPurchasesLink.bottom; anchors.topMargin: 32; height: paintedHeight; anchors.left: parent.left; @@ -926,7 +927,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter; onLinkActivated: { lightboxPopup.titleText = "Confirmations"; - lightboxPopup.bodyText = 'Your item is marked "pending" while it is being confirmed.

' + + lightboxPopup.bodyText = 'Your item is marked "pending" while the transfer is being confirmed.

' + 'Confirmations usually take about 90 seconds.'; lightboxPopup.button1text = "CLOSE"; lightboxPopup.button1method = function() { diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index de9c62c197..4b0166425a 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -27,7 +27,6 @@ Item { property string referrerURL: (Account.metaverseServerURL + "/marketplace?"); readonly property int additionalDropdownHeight: usernameDropdown.height - myUsernameButton.anchors.bottomMargin; property alias usernameDropdownVisible: usernameDropdown.visible; - property bool messagesWaiting: false; height: mainContainer.height + additionalDropdownHeight; @@ -38,7 +37,6 @@ Item { if (walletStatus === 0) { sendToParent({method: "needsLogIn"}); } else if (walletStatus === 5) { - Commerce.getAvailableUpdates(); Commerce.getSecurityImage(); } else if (walletStatus > 5) { console.log("ERROR in EmulatedMarketplaceHeader.qml: Unknown wallet status: " + walletStatus); @@ -59,14 +57,6 @@ Item { securityImage.source = "image://security/securityImage"; } } - - onAvailableUpdatesResult: { - if (result.status !== 'success') { - console.log("Failed to get Available Updates", result.data.message); - } else { - root.messagesWaiting = result.data.updates.length > 0; - } - } } Component.onCompleted: { @@ -118,50 +108,6 @@ Item { anchors.right: securityImage.left; anchors.rightMargin: 6; - Rectangle { - id: myPurchasesLink; - anchors.right: myUsernameButton.left; - anchors.rightMargin: 8; - anchors.verticalCenter: parent.verticalCenter; - height: 40; - width: myPurchasesText.paintedWidth + 10; - - RalewaySemiBold { - id: myPurchasesText; - text: "Inventory"; - // Text size - size: 18; - // Style - color: hifi.colors.blueAccent; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - // Anchors - anchors.centerIn: parent; - } - - MouseArea { - anchors.fill: parent; - hoverEnabled: enabled; - onClicked: { - sendToParent({ method: 'header_goToPurchases', hasUpdates: root.messagesWaiting }); - } - onEntered: myPurchasesText.color = hifi.colors.blueHighlight; - onExited: myPurchasesText.color = hifi.colors.blueAccent; - } - } - - Rectangle { - id: messagesWaitingLight; - visible: root.messagesWaiting; - anchors.right: myPurchasesLink.left; - anchors.rightMargin: -2; - anchors.verticalCenter: parent.verticalCenter; - height: 10; - width: height; - radius: height/2; - color: "red"; - } - TextMetrics { id: textMetrics; font.family: "Raleway" diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 0a5c3e8053..bb4bb624bc 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -73,6 +73,10 @@ Item { } onTransferAssetToNodeResult: { + if (!root.visible) { + return; + } + root.isCurrentlySendingAsset = false; if (result.status === 'success') { @@ -92,6 +96,10 @@ Item { } onTransferAssetToUsernameResult: { + if (!root.visible) { + return; + } + root.isCurrentlySendingAsset = false; if (result.status === 'success') { @@ -1309,13 +1317,13 @@ Item { Rectangle { anchors.top: parent.top; - anchors.topMargin: root.assetName === "" ? 15 : 150; + anchors.topMargin: root.assetName === "" ? 15 : 125; anchors.left: parent.left; anchors.leftMargin: root.assetName === "" ? 15 : 50; anchors.right: parent.right; anchors.rightMargin: root.assetName === "" ? 15 : 50; anchors.bottom: parent.bottom; - anchors.bottomMargin: root.assetName === "" ? 15 : 240; + anchors.bottomMargin: root.assetName === "" ? 15 : 125; color: "#FFFFFF"; RalewaySemiBold { diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index eeb9ac3c54..d13473af22 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -345,6 +345,7 @@ Item { Rectangle { id: permissionExplanationCard; + visible: false; anchors.left: parent.left; anchors.leftMargin: 30; anchors.top: parent.top; diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 655af79e68..eeb98eeb8c 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -19,7 +19,6 @@ import "../../../controls" as HifiControls import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. import "../wallet" as HifiWallet import "../common" as HifiCommerceCommon -import "../inspectionCertificate" as HifiInspectionCertificate import "../common/sendAsset" as HifiSendAsset import "../.." as HifiCommon @@ -118,19 +117,6 @@ Rectangle { } } - HifiInspectionCertificate.InspectionCertificate { - id: inspectionCertificate; - z: 998; - visible: false; - anchors.fill: parent; - - Connections { - onSendToScript: { - sendToScript(message); - } - } - } - HifiCommerceCommon.CommerceLightbox { id: lightboxPopup; z: 999; @@ -200,11 +186,6 @@ Rectangle { lightboxPopup.button1method = function() { lightboxPopup.visible = false; } - lightboxPopup.button2text = "GO TO WALLET"; - lightboxPopup.button2method = function() { - sendToScript({method: 'purchases_openWallet'}); - lightboxPopup.visible = false; - }; lightboxPopup.visible = true; } else { sendToScript(msg); @@ -627,8 +608,6 @@ Rectangle { sendToScript({ method: 'purchases_updateWearables' }); } } else if (msg.method === 'purchases_itemCertificateClicked') { - inspectionCertificate.visible = true; - inspectionCertificate.isLightbox = true; sendToScript(msg); } else if (msg.method === "showInvalidatedLightbox") { lightboxPopup.titleText = "Item Invalidated"; @@ -641,7 +620,7 @@ Rectangle { lightboxPopup.visible = true; } else if (msg.method === "showPendingLightbox") { lightboxPopup.titleText = "Item Pending"; - lightboxPopup.bodyText = 'Your item is marked "pending" while your purchase is being confirmed. ' + + lightboxPopup.bodyText = 'Your item is marked "pending" while the transfer is being confirmed. ' + "Usually, purchases take about 90 seconds to confirm."; lightboxPopup.button1text = "CLOSE"; lightboxPopup.button1method = function() { @@ -1066,9 +1045,6 @@ Rectangle { titleBarContainer.referrerURL = message.referrerURL || ""; filterBar.text = message.filterText ? message.filterText : ""; break; - case 'inspectionCertificate_setCertificateId': - inspectionCertificate.fromScript(message); - break; case 'purchases_showMyItems': root.isShowingMyItems = true; break; diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index ee86844aaa..a958e62aad 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -21,6 +21,7 @@ import "../common" as HifiCommerceCommon import "../common/sendAsset" import "../.." as HifiCommon import "../purchases" as HifiPurchases +import "../inspectionCertificate" as HifiInspectionCertificate Rectangle { HifiConstants { id: hifi; } @@ -30,7 +31,6 @@ Rectangle { property string activeView: "initialize"; property bool keyboardRaised: false; property bool isPassword: false; - property bool limitedCommerce: true; anchors.fill: (typeof parent === undefined) ? undefined : parent; @@ -67,6 +67,7 @@ Rectangle { } else if (walletStatus === 5) { if (root.activeView !== "walletSetup") { root.activeView = "walletInventory"; + Commerce.getAvailableUpdates(); Commerce.getSecurityImage(); } } else { @@ -88,6 +89,14 @@ Rectangle { titleBarSecurityImage.source = "image://security/securityImage"; } } + + onAvailableUpdatesResult: { + if (result.status !== 'success') { + console.log("Failed to get Available Updates", result.data.message); + } else { + exchangeMoneyButtonContainer.messagesWaiting = result.data.updates.length > 0; + } + } } HifiCommerceCommon.CommerceLightbox { @@ -339,6 +348,10 @@ Rectangle { if (msg.method === 'transactionHistory_usernameLinkClicked') { userInfoViewer.url = msg.usernameLink; userInfoViewer.visible = true; + } else if (msg.method === 'goToPurchases_fromWalletHome') { + root.activeView = "walletInventory"; + walletInventory.isShowingMyItems = false; + tabButtonsContainer.resetTabButtonColors(); } else { sendToScript(msg); } @@ -346,7 +359,20 @@ Rectangle { } } - HifiPurchases.Purchases { + HifiInspectionCertificate.InspectionCertificate { + id: inspectionCertificate; + z: 998; + visible: false; + anchors.fill: parent; + + Connections { + onSendToScript: { + sendToScript(message); + } + } + } + + HifiPurchases.Purchases { id: walletInventory; visible: root.activeView === "walletInventory"; anchors.top: titleBarContainer.bottom; @@ -355,10 +381,15 @@ Rectangle { anchors.right: parent.right; Connections { onSendToScript: { - sendToScript(message); + if (message.method === 'purchases_itemCertificateClicked') { + inspectionCertificate.visible = true; + inspectionCertificate.isLightbox = true; + sendToScript(message); + } else { + sendToScript(message); + } } } - } HifiCommon.RootHttpRequest { @@ -480,7 +511,7 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: -2; // Style - color: root.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); + color: WalletScriptingInterface.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); } RalewaySemiBold { @@ -495,7 +526,7 @@ Rectangle { anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: root.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); + color: WalletScriptingInterface.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; @@ -504,7 +535,7 @@ Rectangle { MouseArea { id: walletHomeTabMouseArea; anchors.fill: parent; - enabled: !root.limitedCommerce; + enabled: !WalletScriptingInterface.limitedCommerce; hoverEnabled: enabled; onClicked: { root.activeView = "walletHome"; @@ -518,6 +549,8 @@ Rectangle { // "EXCHANGE MONEY" tab button Rectangle { id: exchangeMoneyButtonContainer; + property bool messagesWaiting: false; + visible: !walletSetup.visible; color: root.activeView === "walletInventory" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; @@ -535,7 +568,20 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: -2; // Style - color: root.activeView === "walletInventory" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + color: root.activeView === "walletInventory" || inventoryTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + } + + Rectangle { + id: messagesWaitingLight; + visible: parent.messagesWaiting; + anchors.right: exchangeMoneyTabIcon.left; + anchors.rightMargin: -4; + anchors.top: exchangeMoneyTabIcon.top; + anchors.topMargin: 16; + height: 10; + width: height; + radius: height/2; + color: "red"; } RalewaySemiBold { @@ -550,7 +596,7 @@ Rectangle { anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: root.activeView === "walletInventory" || walletHomeTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; + color: root.activeView === "walletInventory" || inventoryTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; @@ -592,7 +638,7 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: -2; // Style - color: root.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); + color: WalletScriptingInterface.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); } RalewaySemiBold { @@ -607,7 +653,7 @@ Rectangle { anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: root.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); + color: WalletScriptingInterface.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; @@ -617,7 +663,7 @@ Rectangle { MouseArea { id: sendMoneyTabMouseArea; anchors.fill: parent; - enabled: !root.limitedCommerce; + enabled: !WalletScriptingInterface.limitedCommerce; hoverEnabled: enabled; onClicked: { root.activeView = "sendMoney"; @@ -825,17 +871,26 @@ Rectangle { break; case 'http.response': http.handleHttpResponse(message); + // Duplicate handler is required because we don't track referrer for `http` + walletInventory.fromScript(message); break; case 'palIsStale': case 'avatarDisconnected': // Because we don't have "channels" for sending messages to a specific QML object, the messages are broadcast to all QML Items. If an Item of yours happens to be visible when some script sends a message with a method you don't expect, you'll get "Unrecognized message..." logs. break; - case 'setLimitedCommerce': - root.limitedCommerce = message.limitedCommerce; - break; - default: - // HRS FIXME console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); + case 'inspectionCertificate_setCertificateId': + inspectionCertificate.fromScript(message); + break; + case 'updatePurchases': + case 'purchases_showMyItems': + case 'updateConnections': + case 'selectRecipient': + case 'updateSelectedRecipientUsername': + case 'updateWearables': walletInventory.fromScript(message); + break; + default: + console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); } } signal sendToScript(var message); @@ -881,7 +936,9 @@ Rectangle { root.activeView = "initialize"; Commerce.getWalletStatus(); } else if (msg.referrer === 'purchases') { - sendToScript({method: 'goToPurchases'}); + root.activeView = "walletInventory"; + walletInventory.isShowingMyItems = false; + tabButtonsContainer.resetTabButtonColors(); } else if (msg.referrer === 'marketplace cta' || msg.referrer === 'mainPage') { sendToScript({method: 'goToMarketplaceMainPage', itemId: msg.referrer}); } else { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3f354f64b5..70c25ce823 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3154,7 +3154,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); surfaceContext->setContextProperty("Selection", DependencyManager::get().data()); surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); - surfaceContext->setContextProperty("Wallet", DependencyManager::get().data()); + surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); @@ -6876,7 +6876,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); scriptEngine->registerGlobalObject("Selection", DependencyManager::get().data()); scriptEngine->registerGlobalObject("ContextOverlay", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Wallet", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("WalletScriptingInterface", DependencyManager::get().data()); scriptEngine->registerGlobalObject("AddressManager", DependencyManager::get().data()); scriptEngine->registerGlobalObject("HifiAbout", AboutUtil::getInstance()); scriptEngine->registerGlobalObject("ResourceRequestObserver", DependencyManager::get().data()); @@ -7881,6 +7881,10 @@ void Application::loadAvatarBrowser() const { auto tablet = dynamic_cast(DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); // construct the url to the marketplace item QString url = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace?category=avatars"; + if (DependencyManager::get()->getLimitedCommerce()) { + url += "&isFree=1"; + } + QString MARKETPLACES_INJECT_SCRIPT_PATH = "file:///" + qApp->applicationDirPath() + "/scripts/system/html/js/marketplacesInject.js"; tablet->gotoWebScreen(url, MARKETPLACES_INJECT_SCRIPT_PATH); DependencyManager::get()->openTablet(); diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 67303f2a9b..f4bd7a84b3 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -240,7 +240,6 @@ QString transactionString(const QJsonObject& valueObject) { return result; } -static const QString MARKETPLACE_ITEMS_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace/items/"; void Ledger::historySuccess(QNetworkReply* reply) { // here we send a historyResult with some extra stuff in it // Namely, the styled text we'd like to show. The issue is the diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h index c4ab0da851..8aebb68a47 100644 --- a/interface/src/scripting/WalletScriptingInterface.h +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -36,29 +36,29 @@ public: * @hifi-client-entity * * @property {number} walletStatus + * @property {bool} limitedCommerce */ class WalletScriptingInterface : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY - Q_PROPERTY(uint walletStatus READ getWalletStatus WRITE setWalletStatus NOTIFY walletStatusChanged) - Q_PROPERTY(bool limitedCommerce READ getLimitedCommerce WRITE setLimitedCommerce) + Q_PROPERTY(bool limitedCommerce READ getLimitedCommerce WRITE setLimitedCommerce NOTIFY limitedCommerceChanged) public: /**jsdoc - * @function Wallet.refreshWalletStatus + * @function WalletScriptingInterface.refreshWalletStatus */ Q_INVOKABLE void refreshWalletStatus(); /**jsdoc - * @function Wallet.getWalletStatus + * @function WalletScriptingInterface.getWalletStatus * @returns {number} */ Q_INVOKABLE uint getWalletStatus() { return _walletStatus; } /**jsdoc - * @function Wallet.proveAvatarEntityOwnershipVerification + * @function WalletScriptingInterface.proveAvatarEntityOwnershipVerification * @param {Uuid} entityID */ Q_INVOKABLE void proveAvatarEntityOwnershipVerification(const QUuid& entityID); @@ -67,32 +67,38 @@ public: // scripts could cause the Wallet to incorrectly report its status. void setWalletStatus(const uint& status); - bool getLimitedCommerce() { return _limitedCommerce; } - void setLimitedCommerce(bool isLimited) { _limitedCommerce = isLimited; } + bool getLimitedCommerce() { return _limitedCommerce; } + void setLimitedCommerce(bool isLimited) { _limitedCommerce = isLimited; } signals: /**jsdoc - * @function Wallet.walletStatusChanged + * @function WalletScriptingInterface.walletStatusChanged * @returns {Signal} */ void walletStatusChanged(); /**jsdoc - * @function Wallet.walletNotSetup + * @function WalletScriptingInterface.limitedCommerceChanged + * @returns {Signal} + */ + void limitedCommerceChanged(); + + /**jsdoc + * @function WalletScriptingInterface.walletNotSetup * @returns {Signal} */ void walletNotSetup(); /**jsdoc - * @function Wallet.ownershipVerificationSuccess + * @function WalletScriptingInterface.ownershipVerificationSuccess * @param {Uuid} entityID * @returns {Signal} */ void ownershipVerificationSuccess(const QUuid& entityID); /**jsdoc - * @function Wallet.ownershipVerificationFailed + * @function WalletScriptingInterface.ownershipVerificationFailed * @param {Uuid} entityID * @returns {Signal} */ diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 1a1bd00655..f13d25f22c 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -96,7 +96,6 @@ Web3DOverlay::Web3DOverlay() { _webSurface->getSurfaceContext()->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED _webSurface->getSurfaceContext()->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); _webSurface->getSurfaceContext()->setContextProperty("AddressManager", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Wallet", DependencyManager::get().data()); } Web3DOverlay::Web3DOverlay(const Web3DOverlay* Web3DOverlay) : @@ -271,8 +270,8 @@ void Web3DOverlay::setupQmlSurface(bool isTablet) { _webSurface->getSurfaceContext()->setContextProperty("Web3DOverlay", this); _webSurface->getSurfaceContext()->setContextProperty("Window", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); - _webSurface->getSurfaceContext()->setContextProperty("Wallet", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("HiFiAbout", AboutUtil::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); // Override min fps for tablet UI, for silky smooth scrolling diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index ece35acce7..2f05b1b337 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -159,8 +159,8 @@ var selectedAvatarEntityGrabbable = false; var selectedAvatarEntityID = null; var grabbedAvatarEntityChangeNotifier = null; -var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; -var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; +var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace" + (WalletScriptingInterface.limitedCommerce ? "?isFree=1" : ""); var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("html/js/marketplacesInject.js"); function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. @@ -285,9 +285,9 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'navigate': var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system") if(message.url.indexOf('app://') === 0) { - if(message.url === 'app://marketplace') { + if (message.url === 'app://marketplace') { tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); - } else if(message.url === 'app://purchases') { + } else if (message.url === 'app://purchases') { tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); } diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 58f39ca54c..353145035e 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -11,15 +11,15 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global getConnectionData */ +/* global getConnectionData getControllerWorldLocation openLoginWindow WalletScriptingInterface */ (function () { // BEGIN LOCAL_SCOPE Script.include("/~/system/libraries/accountUtils.js"); Script.include("/~/system/libraries/connectionUtils.js"); var AppUi = Script.require('appUi'); -var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; - +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace" + + (WalletScriptingInterface.limitedCommerce ? "?isFree=1" : ""); // BEGIN AVATAR SELECTOR LOGIC var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 }; @@ -48,7 +48,6 @@ ExtendedOverlay.prototype.editOverlay = function (properties) { // change displa function color(selected, hovering) { var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR; function scale(component) { - var delta = 0xFF - component; return component; } return { red: scale(base.red), green: scale(base.green), blue: scale(base.blue) }; @@ -105,7 +104,8 @@ ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId // hit(overlay) on the one overlay intersected by pickRay, if any. // noHit() if no ExtendedOverlay was intersected (helps with hover) ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) { - var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones. + // Depends on nearer coverOverlays to extend closer to us than farther ones. + var pickedOverlay = Overlays.findRayIntersection(pickRay); if (!pickedOverlay.intersects) { if (noHit) { return noHit(); @@ -131,6 +131,7 @@ function addAvatarNode(id) { } var pingPong = true; +var OVERLAY_SCALE = 0.032; function updateOverlays() { var eye = Camera.position; AvatarList.getAvatarIdentifiers().forEach(function (id) { @@ -148,7 +149,8 @@ function updateOverlays() { var target = avatar.position; var distance = Vec3.distance(target, eye); var offset = 0.2; - var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) + // get diff between target and eye (a vector pointing to the eye from avatar position) + var diff = Vec3.subtract(target, eye); var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can if (headIndex > 0) { offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; @@ -164,7 +166,7 @@ function updateOverlays() { overlay.editOverlay({ color: color(ExtendedOverlay.isSelected(id), overlay.hovering), position: target, - dimensions: 0.032 * distance + dimensions: OVERLAY_SCALE * distance }); }); pingPong = !pingPong; @@ -380,6 +382,23 @@ function onUsernameChanged() { Settings.setValue("wallet/autoLogout", false); Settings.setValue("wallet/savedUsername", ""); } +} + +var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); +var METAVERSE_SERVER_URL = Account.metaverseServerURL; +var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. +function openMarketplace(optionalItemOrUrl) { + // This is a bit of a kluge, but so is the whole file. + // If given a whole path, use it with no cta. + // If given an id, build the appropriate url and use the id as the cta. + // Otherwise, use home and 'marketplace cta'. + // AND... if call onMarketplaceOpen to setupWallet if we need to. + var url = optionalItemOrUrl || MARKETPLACE_URL_INITIAL; + // If optionalItemOrUrl contains the metaverse base, then it's a url, not an item id. + if (optionalItemOrUrl && optionalItemOrUrl.indexOf(METAVERSE_SERVER_URL) === -1) { + url = MARKETPLACE_URL + '/items/' + optionalItemOrUrl; + } + ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL); } // Function Name: fromQml() @@ -387,8 +406,6 @@ function onUsernameChanged() { // Description: // -Called when a message is received from SpectatorCamera.qml. The "message" argument is what is sent from the QML // in the format "{method, params}", like json-rpc. See also sendToQml(). -var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; -var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); function fromQml(message) { switch (message.method) { case 'passphrasePopup_cancelClicked': @@ -422,10 +439,6 @@ function fromQml(message) { case 'transactionHistory_linkClicked': ui.open(message.marketplaceLink, MARKETPLACES_INJECT_SCRIPT_URL); break; - case 'goToPurchases_fromWalletHome': - case 'goToPurchases': - ui.open(MARKETPLACE_PURCHASES_QML_PATH); - break; case 'goToMarketplaceMainPage': ui.open(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); break; @@ -450,22 +463,20 @@ function fromQml(message) { removeOverlays(); break; case 'sendAsset_sendPublicly': - if (message.assetName === "") { - deleteSendMoneyParticleEffect(); - sendMoneyRecipient = message.recipient; - var amount = message.amount; - var props = SEND_MONEY_PARTICLE_PROPERTIES; - props.parentID = MyAvatar.sessionUUID; - props.position = MyAvatar.position; - props.position.y += 0.2; - if (message.effectImage) { - props.textures = message.effectImage; - } - sendMoneyParticleEffect = Entities.addEntity(props, true); - particleEffectTimestamp = Date.now(); - updateSendMoneyParticleEffect(); - sendMoneyParticleEffectUpdateTimer = Script.setInterval(updateSendMoneyParticleEffect, SEND_MONEY_PARTICLE_TIMER_UPDATE); + deleteSendMoneyParticleEffect(); + sendMoneyRecipient = message.recipient; + var props = SEND_MONEY_PARTICLE_PROPERTIES; + props.parentID = MyAvatar.sessionUUID; + props.position = MyAvatar.position; + props.position.y += 0.2; + if (message.effectImage) { + props.textures = message.effectImage; } + sendMoneyParticleEffect = Entities.addEntity(props, true); + particleEffectTimestamp = Date.now(); + updateSendMoneyParticleEffect(); + sendMoneyParticleEffectUpdateTimer = + Script.setInterval(updateSendMoneyParticleEffect, SEND_MONEY_PARTICLE_TIMER_UPDATE); break; case 'transactionHistory_goToBank': if (Account.metaverseServerURL.indexOf("staging") >= 0) { @@ -474,6 +485,46 @@ function fromQml(message) { Window.location = "hifi://BankOfHighFidelity"; } break; + case 'purchases_updateWearables': + var currentlyWornWearables = []; + var ATTACHMENT_SEARCH_RADIUS = 100; // meters (just in case) + + var nearbyEntities = Entities.findEntitiesByType('Model', MyAvatar.position, ATTACHMENT_SEARCH_RADIUS); + + for (var i = 0; i < nearbyEntities.length; i++) { + var currentProperties = Entities.getEntityProperties( + nearbyEntities[i], ['certificateID', 'editionNumber', 'parentID'] + ); + if (currentProperties.parentID === MyAvatar.sessionUUID) { + currentlyWornWearables.push({ + entityID: nearbyEntities[i], + entityCertID: currentProperties.certificateID, + entityEdition: currentProperties.editionNumber + }); + } + } + + ui.tablet.sendToQml({ method: 'updateWearables', wornWearables: currentlyWornWearables }); + break; + case 'purchases_availableUpdatesReceived': + shouldShowDot = message.numUpdates > 0; + ui.messagesWaiting(shouldShowDot && !ui.isOpen); + break; + case 'purchases_walletNotSetUp': + ui.tablet.sendToQml({ + method: 'updateWalletReferrer', + referrer: "purchases" + }); + break; + case 'purchases_openGoTo': + ui.open("hifi/tablet/TabletAddressDialog.qml"); + break; + case 'purchases_itemInfoClicked': + var itemId = message.itemId; + if (itemId && itemId !== "") { + openMarketplace(itemId); + } + break; case 'http.request': // Handled elsewhere, don't log. break; @@ -482,23 +533,28 @@ function fromQml(message) { } } +var isWired = false; function walletOpened() { Users.usernameFromIDReply.connect(usernameFromIDReply); Controller.mousePressEvent.connect(handleMouseEvent); Controller.mouseMoveEvent.connect(handleMouseMoveEvent); triggerMapping.enable(); triggerPressMapping.enable(); + isWired = true; shouldShowDot = false; ui.messagesWaiting(shouldShowDot); - ui.sendMessage({method: 'setLimitedCommerce', limitedCommerce: Wallet.limitedCommerce}); // HRS FIXME Wallet should be accessible in qml. Why isn't it? } function walletClosed() { off(); } -function notificationDataProcessPage(data) { - return data.data.updates; // HRS FIXME .history; +function notificationDataProcessPageUpdates(data) { + return data.data.updates; +} + +function notificationDataProcessPageHistory(data) { + return data.data.history; } var shouldShowDot = false; @@ -511,7 +567,7 @@ function notificationPollCallbackUpdates(updatesArray) { if (!ui.notificationInitialCallbackMade) { message = updatesArray.length + " of your purchased items " + (updatesArray.length === 1 ? "has an update " : "have updates ") + - "available. Open MARKET to update."; + "available. Open WALLET to update."; ui.notificationDisplayBanner(message); ui.notificationPollCaresAboutSince = true; @@ -519,13 +575,13 @@ function notificationPollCallbackUpdates(updatesArray) { for (var i = 0; i < updatesArray.length; i++) { message = "Update available for \"" + updatesArray[i].base_item_title + "\"." + - "Open MARKET to update."; + "Open WALLET to update."; ui.notificationDisplayBanner(message); } } } } -function notificationPollCallback(historyArray) { +function notificationPollCallbackHistory(historyArray) { if (!ui.isOpen) { var notificationCount = historyArray.length; shouldShowDot = shouldShowDot || notificationCount > 0; @@ -548,7 +604,12 @@ function notificationPollCallback(historyArray) { } } -function isReturnedDataEmpty(data) { +function isReturnedDataEmptyUpdates(data) { + var historyArray = data.data.history; + return historyArray.length === 0; +} + +function isReturnedDataEmptyHistory(data) { var historyArray = data.data.history; return historyArray.length === 0; } @@ -585,6 +646,7 @@ function uninstallMarketplaceItemTester() { var BUTTON_NAME = "ASSETS"; var WALLET_QML_SOURCE = "hifi/commerce/wallet/Wallet.qml"; +var NOTIFICATION_POLL_TIMEOUT = 300000; var ui; function startup() { ui = new AppUi({ @@ -595,13 +657,20 @@ function startup() { onOpened: walletOpened, onClosed: walletClosed, onMessage: fromQml, - // How are we going to handle two polls when --limitedCommerce is false? - notificationPollEndpoint: "/api/v1/commerce/available_updates?per_page=10", // HRS FIXME "/api/v1/commerce/history?per_page=10", +/* Gotta re-add all this stuff once I get it working + notificationPollEndpoint: ["/api/v1/commerce/available_updates?per_page=10", "/api/v1/commerce/history?per_page=10"], + notificationPollTimeoutMs: [NOTIFICATION_POLL_TIMEOUT, NOTIFICATION_POLL_TIMEOUT], + notificationDataProcessPage: [notificationDataProcessPageUpdates, notificationDataProcessPageHistory], + notificationPollCallback: [notificationPollCallbackUpdates, notificationPollCallbackHistory], + notificationPollStopPaginatingConditionMet: [isReturnedDataEmptyUpdates, isReturnedDataEmptyHistory], + notificationPollCaresAboutSince: [false, true] +*/ + notificationPollEndpoint: "/api/v1/commerce/available_updates?per_page=10", notificationPollTimeoutMs: 300000, - notificationDataProcessPage: notificationDataProcessPage, + notificationDataProcessPage: notificationDataProcessPageUpdates, notificationPollCallback: notificationPollCallbackUpdates, - notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, - notificationPollCaresAboutSince: false // HRS FIXME true + notificationPollStopPaginatingConditionMet: isReturnedDataEmptyUpdates, + notificationPollCaresAboutSince: false }); GlobalServices.myUsernameChanged.connect(onUsernameChanged); installMarketplaceItemTester(); @@ -609,11 +678,13 @@ function startup() { var isUpdateOverlaysWired = false; function off() { - Users.usernameFromIDReply.disconnect(usernameFromIDReply); - Controller.mousePressEvent.disconnect(handleMouseEvent); - Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); - triggerMapping.disable(); - triggerPressMapping.disable(); + if (isWired) { + Users.usernameFromIDReply.disconnect(usernameFromIDReply); + Controller.mousePressEvent.disconnect(handleMouseEvent); + Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); + triggerMapping.disable(); + triggerPressMapping.disable(); + } if (isUpdateOverlaysWired) { Script.update.disconnect(updateOverlays); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 6425806771..ee1f5ecbec 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -193,7 +193,7 @@ var importingSVOTextOverlay = Overlays.addOverlay("text", { visible: false }); -var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace" + (WalletScriptingInterface.limitedCommerce ? "?isFree=1" : ""); var marketplaceWindow = new OverlayWebWindow({ title: 'Marketplace', source: "about:blank", diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 43fd9ab935..fd228e2596 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -60,7 +60,7 @@ ); // Footer. - var isInitialHiFiPage = location.href === marketplaceBaseURL + "/marketplace?"; + var isInitialHiFiPage = location.href === (marketplaceBaseURL + "/marketplace" + (limitedCommerce ? "?isFree=1" : "?")); $("body").append( '
' + (!isInitialHiFiPage ? '' : '') + @@ -72,7 +72,7 @@ // Footer actions. $("#back-button").on("click", function () { - (document.referrer !== "") ? window.history.back() : window.location = (marketplaceBaseURL + "/marketplace?"); + (document.referrer !== "") ? window.history.back() : window.location = (marketplaceBaseURL + "/marketplace?") + (limitedCommerce ? "isFree=1" : ""); }); $("#all-markets").on("click", function () { EventBridge.emitWebEvent(JSON.stringify({ @@ -93,7 +93,7 @@ window.location = "https://clara.io/library?gameCheck=true&public=true"; }); $('#exploreHifiMarketplace').on('click', function () { - window.location = marketplaceBaseURL + "/marketplace"; + window.location = marketplaceBaseURL + "/marketplace" + (limitedCommerce ? "?isFree=1" : "?"); }); } @@ -196,43 +196,6 @@ } } - function maybeAddPurchasesButton() { - if (userIsLoggedIn) { - // Why isn't this an id?! This really shouldn't be a class on the website, but it is. - var navbarBrandElement = document.getElementsByClassName('navbar-brand')[0]; - var purchasesElement = document.createElement('a'); - var dropDownElement = document.getElementById('user-dropdown'); - - $('#user-dropdown').find('.username')[0].style = "max-width:80px;white-space:nowrap;overflow:hidden;" + - "text-overflow:ellipsis;display:inline-block;position:relative;top:4px;"; - $('#user-dropdown').find('.caret')[0].style = "position:relative;top:-3px;"; - - purchasesElement.id = "purchasesButton"; - purchasesElement.setAttribute('href', "#"); - purchasesElement.innerHTML = ""; - if (messagesWaiting) { - purchasesElement.innerHTML += " "; - } - purchasesElement.innerHTML += "My Purchases"; - // FRONTEND WEBDEV RANT: The username dropdown should REALLY not be programmed to be on the same - // line as the search bar, overlaid on top of the search bar, floated right, and then relatively bumped up using "top:-50px". - $('.navbar-brand').css('margin-right', '10px'); - purchasesElement.style = "height:100%;margin-top:18px;font-weight:bold;float:right;margin-right:" + (dropDownElement.offsetWidth + 30) + - "px;position:relative;z-index:999;"; - navbarBrandElement.parentNode.insertAdjacentElement('beforeend', purchasesElement); - if (limitedCommerce) { - $('#purchasesButton').css('display', 'none'); - } - $('#purchasesButton').on('click', function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "PURCHASES", - referrerURL: window.location.href, - hasUpdates: messagesWaiting - })); - }); - } - } - function changeDropdownMenu() { var logInOrOutButton = document.createElement('a'); logInOrOutButton.id = "logInOrOutButton"; @@ -409,7 +372,6 @@ // Try this here in case it works (it will if the user just pressed the "back" button, // since that doesn't trigger another AJAX request. injectBuyButtonOnMainPage(); - maybeAddPurchasesButton(); } } @@ -458,7 +420,7 @@ if (isUpdating) { purchaseButton.html('UPDATE FOR FREE'); } else if (availability !== 'available') { - purchaseButton.html('UNAVAILABLE' + (availability ? ('(' + availability + ')') : '')); + purchaseButton.html('UNAVAILABLE ' + (availability ? ('(' + availability + ')') : '')); } else if (parseInt(cost) > 0 && $('#side-info').find('#buyItemButton').size() === 0) { purchaseButton.html('PURCHASE ' + cost); @@ -476,7 +438,6 @@ type); } }); - maybeAddPurchasesButton(); } } diff --git a/scripts/system/marketplaces/marketplace.js b/scripts/system/marketplaces/marketplace.js index d90695c767..2f749656d3 100644 --- a/scripts/system/marketplaces/marketplace.js +++ b/scripts/system/marketplaces/marketplace.js @@ -15,7 +15,7 @@ Script.include("../libraries/WebTablet.js"); var toolIconUrl = Script.resolvePath("../assets/images/tools/"); -var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace" + (WalletScriptingInterface.limitedCommerce ? "?isFree=1" : ""); var marketplaceWindow = new OverlayWebWindow({ title: "Marketplace", source: "about:blank", diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 5b1d5c8897..9f55bade6a 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -10,7 +10,7 @@ /* global Tablet, Script, HMD, UserActivityLogger, Entities, Account, Wallet, ContextOverlay, Settings, Camera, Vec3, Quat, MyAvatar, Clipboard, Menu, Grid, Uuid, GlobalServices, openLoginWindow, getConnectionData, Overlays, SoundCache, - DesktopPreviewProvider */ + DesktopPreviewProvider, ResourceRequestObserver */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ var selectionDisplay = null; // for gridTool.js to ignore @@ -115,10 +115,10 @@ function setTabletVisibleInSecondaryCamera(visibleInSecondaryCam) { tabletShouldBeVisibleInSecondaryCamera = Overlays.getProperty(HMD.tabletID, "isVisibleInSecondaryCamera"); } - Overlays.editOverlay(HMD.tabletID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); - Overlays.editOverlay(HMD.homeButtonID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); - Overlays.editOverlay(HMD.homeButtonHighlightID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); - Overlays.editOverlay(HMD.tabletScreenID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); + Overlays.editOverlay(HMD.tabletID, { isVisibleInSecondaryCamera: visibleInSecondaryCam }); + Overlays.editOverlay(HMD.homeButtonID, { isVisibleInSecondaryCamera: visibleInSecondaryCam }); + Overlays.editOverlay(HMD.homeButtonHighlightID, { isVisibleInSecondaryCamera: visibleInSecondaryCam }); + Overlays.editOverlay(HMD.tabletScreenID, { isVisibleInSecondaryCamera: visibleInSecondaryCam }); } function openWallet() { @@ -137,7 +137,7 @@ function setupWallet(referrer) { } function onMarketplaceOpen(referrer) { - var cta = referrer, match; + var match; if (Account.loggedIn && walletNeedsSetup()) { if (referrer === MARKETPLACE_URL_INITIAL) { setupWallet('marketplace cta'); @@ -165,6 +165,9 @@ function openMarketplace(optionalItemOrUrl) { if (optionalItemOrUrl && optionalItemOrUrl.indexOf(METAVERSE_SERVER_URL) === -1) { url = MARKETPLACE_URL + '/items/' + optionalItemOrUrl; } + if (WalletScriptingInterface.limitedCommerce) { + url += "?isFree=1"; + } ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL); } @@ -218,7 +221,7 @@ function onUsernameChanged() { } function walletNeedsSetup() { - return Wallet.walletStatus === 1; + return WalletScriptingInterface.walletStatus === 1; } function sendCommerceSettings() { @@ -230,290 +233,11 @@ function sendCommerceSettings() { userIsLoggedIn: Account.loggedIn, walletNeedsSetup: walletNeedsSetup(), metaverseServerURL: Account.metaverseServerURL, - messagesWaiting: shouldShowDot, - limitedCommerce: Wallet.limitedCommerce + limitedCommerce: WalletScriptingInterface.limitedCommerce } }); } -// BEGIN AVATAR SELECTOR LOGIC -var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 }; -var SELECTED_COLOR = { red: 0xF3, green: 0x91, blue: 0x29 }; -var HOVER_COLOR = { red: 0xD0, green: 0xD0, blue: 0xD0 }; - -var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier. - -function ExtendedOverlay(key, type, properties) { // A wrapper around overlays to store the key it is associated with. - overlays[key] = this; - this.key = key; - this.selected = false; - this.hovering = false; - this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected... -} -// Instance methods: -ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and data of this overlay - Overlays.deleteOverlay(this.activeOverlay); - delete overlays[this.key]; -}; - -ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay - Overlays.editOverlay(this.activeOverlay, properties); -}; - -function color(selected, hovering) { - var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR; - function scale(component) { - return component; - } - return { red: scale(base.red), green: scale(base.green), blue: scale(base.blue) }; -} -// so we don't have to traverse the overlays to get the last one -var lastHoveringId = 0; -ExtendedOverlay.prototype.hover = function (hovering) { - this.hovering = hovering; - if (this.key === lastHoveringId) { - if (hovering) { - return; - } - lastHoveringId = 0; - } - this.editOverlay({ color: color(this.selected, hovering) }); - if (hovering) { - // un-hover the last hovering overlay - if (lastHoveringId && lastHoveringId !== this.key) { - ExtendedOverlay.get(lastHoveringId).hover(false); - } - lastHoveringId = this.key; - } -}; -ExtendedOverlay.prototype.select = function (selected) { - if (this.selected === selected) { - return; - } - - this.editOverlay({ color: color(selected, this.hovering) }); - this.selected = selected; -}; -// Class methods: -var selectedId = false; -ExtendedOverlay.isSelected = function (id) { - return selectedId === id; -}; -ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier - return overlays[key]; -}; -ExtendedOverlay.some = function (iterator) { // Bails early as soon as iterator returns truthy. - var key; - for (key in overlays) { - if (iterator(ExtendedOverlay.get(key))) { - return; - } - } -}; -ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId (if any) - if (lastHoveringId) { - ExtendedOverlay.get(lastHoveringId).hover(false); - } -}; - -// hit(overlay) on the one overlay intersected by pickRay, if any. -// noHit() if no ExtendedOverlay was intersected (helps with hover) -ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) { - var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones. - if (!pickedOverlay.intersects) { - if (noHit) { - return noHit(); - } - return; - } - ExtendedOverlay.some(function (overlay) { // See if pickedOverlay is one of ours. - if ((overlay.activeOverlay) === pickedOverlay.overlayID) { - hit(overlay); - return true; - } - }); -}; - -function addAvatarNode(id) { - return new ExtendedOverlay(id, "sphere", { - drawInFront: true, - solid: true, - alpha: 0.8, - color: color(false, false), - ignoreRayIntersection: false - }); -} - -var pingPong = true; -function updateOverlays() { - var eye = Camera.position; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - if (!id) { - return; // don't update ourself, or avatars we're not interested in - } - var avatar = AvatarList.getAvatar(id); - if (!avatar) { - return; // will be deleted below if there had been an overlay. - } - var overlay = ExtendedOverlay.get(id); - if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. - overlay = addAvatarNode(id); - } - var target = avatar.position; - var distance = Vec3.distance(target, eye); - var offset = 0.2; - var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) - var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can - if (headIndex > 0) { - offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; - } - - // move a bit in front, towards the camera - target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); - - // now bump it up a bit - target.y = target.y + offset; - - overlay.ping = pingPong; - overlay.editOverlay({ - color: color(ExtendedOverlay.isSelected(id), overlay.hovering), - position: target, - dimensions: 0.032 * distance - }); - }); - pingPong = !pingPong; - ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.) - if (overlay.ping === pingPong) { - overlay.deleteOverlay(); - } - }); -} -function removeOverlays() { - selectedId = false; - lastHoveringId = 0; - ExtendedOverlay.some(function (overlay) { - overlay.deleteOverlay(); - }); -} - -// -// Clicks. -// -function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { - if (selectedId === id) { - var message = { - method: 'updateSelectedRecipientUsername', - userName: username === "" ? "unknown username" : username - }; - ui.tablet.sendToQml(message); - } -} -function handleClick(pickRay) { - ExtendedOverlay.applyPickRay(pickRay, function (overlay) { - var nextSelectedStatus = !overlay.selected; - var avatarId = overlay.key; - selectedId = nextSelectedStatus ? avatarId : false; - if (nextSelectedStatus) { - Users.requestUsernameFromID(avatarId); - } - var message = { - method: 'selectRecipient', - id: avatarId, - isSelected: nextSelectedStatus, - displayName: '"' + AvatarList.getAvatar(avatarId).sessionDisplayName + '"', - userName: '' - }; - ui.tablet.sendToQml(message); - - ExtendedOverlay.some(function (overlay) { - var id = overlay.key; - var selected = ExtendedOverlay.isSelected(id); - overlay.select(selected); - }); - - return true; - }); -} -function handleMouseEvent(mousePressEvent) { // handleClick if we get one. - if (!mousePressEvent.isLeftButton) { - return; - } - handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y)); -} -function handleMouseMove(pickRay) { // given the pickRay, just do the hover logic - ExtendedOverlay.applyPickRay(pickRay, function (overlay) { - overlay.hover(true); - }, function () { - ExtendedOverlay.unHover(); - }); -} - -// handy global to keep track of which hand is the mouse (if any) -var currentHandPressed = 0; -var TRIGGER_CLICK_THRESHOLD = 0.85; -var TRIGGER_PRESS_THRESHOLD = 0.05; - -function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position - var pickRay; - if (HMD.active) { - if (currentHandPressed !== 0) { - pickRay = controllerComputePickRay(currentHandPressed); - } else { - // nothing should hover, so - ExtendedOverlay.unHover(); - return; - } - } else { - pickRay = Camera.computePickRay(event.x, event.y); - } - handleMouseMove(pickRay); -} -function handleTriggerPressed(hand, value) { - // The idea is if you press one trigger, it is the one - // we will consider the mouse. Even if the other is pressed, - // we ignore it until this one is no longer pressed. - var isPressed = value > TRIGGER_PRESS_THRESHOLD; - if (currentHandPressed === 0) { - currentHandPressed = isPressed ? hand : 0; - return; - } - if (currentHandPressed === hand) { - currentHandPressed = isPressed ? hand : 0; - return; - } - // otherwise, the other hand is still triggered - // so do nothing. -} - -// We get mouseMoveEvents from the handControllers, via handControllerPointer. -// But we don't get mousePressEvents. -var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); -var triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press'); -function controllerComputePickRay(hand) { - var controllerPose = getControllerWorldLocation(hand, true); - if (controllerPose.valid) { - return { origin: controllerPose.position, direction: Quat.getUp(controllerPose.orientation) }; - } -} -function makeClickHandler(hand) { - return function (clicked) { - if (clicked > TRIGGER_CLICK_THRESHOLD) { - var pickRay = controllerComputePickRay(hand); - handleClick(pickRay); - } - }; -} -function makePressHandler(hand) { - return function (value) { - handleTriggerPressed(hand, value); - }; -} -triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); -triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); -triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); -triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); -// END AVATAR SELECTOR LOGIC - var grid = new Grid(); function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { // Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original @@ -563,6 +287,7 @@ function defaultFor(arg, val) { return typeof arg !== 'undefined' ? arg : val; } +var CERT_ID_URLPARAM_LENGTH = 15; // length of "certificate_id=" function rezEntity(itemHref, itemType, marketplaceItemTesterId) { var isWearable = itemType === "wearable"; var success = Clipboard.importEntities(itemHref, true, marketplaceItemTesterId); @@ -585,7 +310,7 @@ function rezEntity(itemHref, itemType, marketplaceItemTesterId) { } var certPos = itemHref.search("certificate_id="); // TODO how do I parse a URL from here? if (certPos >= 0) { - certPos += 15; // length of "certificate_id=" + certPos += CERT_ID_URLPARAM_LENGTH; var certURLEncoded = itemHref.substring(certPos); var certB64Encoded = decodeURIComponent(certURLEncoded); for (var key in wearableTransforms) { @@ -594,7 +319,7 @@ function rezEntity(itemHref, itemType, marketplaceItemTesterId) { if (certificateTransforms) { for (var certID in certificateTransforms) { if (certificateTransforms.hasOwnProperty(certID) && - certID == certB64Encoded) { + certID === certB64Encoded) { var certificateTransform = certificateTransforms[certID]; wearableLocalPosition = certificateTransform.localPosition; wearableLocalRotation = certificateTransform.localRotation; @@ -637,8 +362,10 @@ function rezEntity(itemHref, itemType, marketplaceItemTesterId) { targetDirection = Vec3.multiplyQbyV(targetDirection, Vec3.UNIT_Z); var targetPosition = getPositionToCreateEntity(); - var deltaParallel = HALF_TREE_SCALE; // Distance to move entities parallel to targetDirection. - var deltaPerpendicular = Vec3.ZERO; // Distance to move entities perpendicular to targetDirection. + // Distance to move entities parallel to targetDirection. + var deltaParallel = HALF_TREE_SCALE; + // Distance to move entities perpendicular to targetDirection. + var deltaPerpendicular = Vec3.ZERO; for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { var curLoopEntityProps = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", "registrationPoint", "rotation", "parentID"]); @@ -666,7 +393,8 @@ function rezEntity(itemHref, itemType, marketplaceItemTesterId) { } if (!Vec3.equal(deltaPosition, Vec3.ZERO)) { - for (var editEntityIndex = 0, numEntities = pastedEntityIDs.length; editEntityIndex < numEntities; editEntityIndex++) { + for (var editEntityIndex = 0, + numEntities = pastedEntityIDs.length; editEntityIndex < numEntities; editEntityIndex++) { if (Uuid.isNull(entityParentIDs[editEntityIndex])) { Entities.editEntity(pastedEntityIDs[editEntityIndex], { position: Vec3.sum(deltaPosition, entityPositions[editEntityIndex]) @@ -769,79 +497,6 @@ function onWebEventReceived(message) { }); } } -var sendAssetRecipient; -var sendAssetParticleEffectUpdateTimer; -var particleEffectTimestamp; -var sendAssetParticleEffect; -var SEND_ASSET_PARTICLE_TIMER_UPDATE = 250; -var SEND_ASSET_PARTICLE_EMITTING_DURATION = 3000; -var SEND_ASSET_PARTICLE_LIFETIME_SECONDS = 8; -var SEND_ASSET_PARTICLE_PROPERTIES = { - accelerationSpread: { x: 0, y: 0, z: 0 }, - alpha: 1, - alphaFinish: 1, - alphaSpread: 0, - alphaStart: 1, - azimuthFinish: 0, - azimuthStart: -6, - color: { red: 255, green: 222, blue: 255 }, - colorFinish: { red: 255, green: 229, blue: 225 }, - colorSpread: { red: 0, green: 0, blue: 0 }, - colorStart: { red: 243, green: 255, blue: 255 }, - emitAcceleration: { x: 0, y: 0, z: 0 }, // Immediately gets updated to be accurate - emitDimensions: { x: 0, y: 0, z: 0 }, - emitOrientation: { x: 0, y: 0, z: 0 }, - emitRate: 4, - emitSpeed: 2.1, - emitterShouldTrail: true, - isEmitting: 1, - lifespan: SEND_ASSET_PARTICLE_LIFETIME_SECONDS + 1, // Immediately gets updated to be accurate - lifetime: SEND_ASSET_PARTICLE_LIFETIME_SECONDS + 1, - maxParticles: 20, - name: 'asset-particles', - particleRadius: 0.2, - polarFinish: 0, - polarStart: 0, - radiusFinish: 0.05, - radiusSpread: 0, - radiusStart: 0.2, - speedSpread: 0, - textures: "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle-HFC.png", - type: 'ParticleEffect' -}; - -function updateSendAssetParticleEffect() { - var timestampNow = Date.now(); - if ((timestampNow - particleEffectTimestamp) > (SEND_ASSET_PARTICLE_LIFETIME_SECONDS * 1000)) { - deleteSendAssetParticleEffect(); - return; - } else if ((timestampNow - particleEffectTimestamp) > SEND_ASSET_PARTICLE_EMITTING_DURATION) { - Entities.editEntity(sendAssetParticleEffect, { - isEmitting: 0 - }); - } else if (sendAssetParticleEffect) { - var recipientPosition = AvatarList.getAvatar(sendAssetRecipient).position; - var distance = Vec3.distance(recipientPosition, MyAvatar.position); - var accel = Vec3.subtract(recipientPosition, MyAvatar.position); - accel.y -= 3.0; - var life = Math.sqrt(2 * distance / Vec3.length(accel)); - Entities.editEntity(sendAssetParticleEffect, { - emitAcceleration: accel, - lifespan: life - }); - } -} - -function deleteSendAssetParticleEffect() { - if (sendAssetParticleEffectUpdateTimer) { - Script.clearInterval(sendAssetParticleEffectUpdateTimer); - sendAssetParticleEffectUpdateTimer = null; - } - if (sendAssetParticleEffect) { - sendAssetParticleEffect = Entities.deleteEntity(sendAssetParticleEffect); - } - sendAssetRecipient = null; -} var savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview"); var UI_FADE_TIMEOUT_MS = 150; @@ -868,26 +523,17 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { } switch (message.method) { case 'gotoBank': - ui.close(); + ui.close(); if (Account.metaverseServerURL.indexOf("staging") >= 0) { Window.location = "hifi://hifiqa-master-metaverse-staging"; // So that we can test in staging. } else { Window.location = "hifi://BankOfHighFidelity"; } - break; - case 'purchases_openWallet': + break; case 'checkout_openWallet': case 'checkout_setUpClicked': openWallet(); break; - case 'purchases_walletNotSetUp': - wireQmlEventBridge(true); - ui.tablet.sendToQml({ - method: 'updateWalletReferrer', - referrer: "purchases" - }); - openWallet(); - break; case 'checkout_walletNotSetUp': wireQmlEventBridge(true); ui.tablet.sendToQml({ @@ -911,12 +557,6 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'checkout_continueShopping': openMarketplace(); break; - case 'purchases_itemInfoClicked': - var itemId = message.itemId; - if (itemId && itemId !== "") { - openMarketplace(itemId); - } - break; case 'checkout_rezClicked': case 'purchases_rezClicked': case 'tester_rezClicked': @@ -945,7 +585,6 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { } break; case 'header_marketplaceImageClicked': - case 'purchases_backClicked': openMarketplace(message.referrerURL); break; case 'purchases_goToMarketplaceClicked': @@ -975,13 +614,9 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'maybeEnableHmdPreview': maybeEnableHMDPreview(); break; - case 'purchases_openGoTo': + case 'checkout_openGoTo': ui.open("hifi/tablet/TabletAddressDialog.qml"); break; - case 'purchases_itemCertificateClicked': - contextOverlayEntity = ""; - setCertificateInfo(contextOverlayEntity, message.itemCertificateId); - break; case 'inspectionCertificate_closeClicked': ui.close(); break; @@ -1000,85 +635,11 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { method: 'purchases_showMyItems' }); break; - case 'refreshConnections': - // Guard to prevent this code from being executed while sending money -- - // we only want to execute this while sending non-HFC gifts - if (!onWalletScreen) { - print('Refreshing Connections...'); - getConnectionData(false); - } - break; - case 'enable_ChooseRecipientNearbyMode': - // Guard to prevent this code from being executed while sending money -- - // we only want to execute this while sending non-HFC gifts - if (!onWalletScreen) { - if (!isUpdateOverlaysWired) { - Script.update.connect(updateOverlays); - isUpdateOverlaysWired = true; - } - } - break; - case 'disable_ChooseRecipientNearbyMode': - // Guard to prevent this code from being executed while sending money -- - // we only want to execute this while sending non-HFC gifts - if (!onWalletScreen) { - if (isUpdateOverlaysWired) { - Script.update.disconnect(updateOverlays); - isUpdateOverlaysWired = false; - } - removeOverlays(); - } - break; - case 'purchases_availableUpdatesReceived': - shouldShowDot = message.numUpdates > 0; - ui.messagesWaiting(shouldShowDot && !ui.isOpen); - break; - case 'purchases_updateWearables': - var currentlyWornWearables = []; - var ATTACHMENT_SEARCH_RADIUS = 100; // meters (just in case) - - var nearbyEntities = Entities.findEntitiesByType('Model', MyAvatar.position, ATTACHMENT_SEARCH_RADIUS); - - for (var i = 0; i < nearbyEntities.length; i++) { - var currentProperties = Entities.getEntityProperties( - nearbyEntities[i], ['certificateID', 'editionNumber', 'parentID'] - ); - if (currentProperties.parentID === MyAvatar.sessionUUID) { - currentlyWornWearables.push({ - entityID: nearbyEntities[i], - entityCertID: currentProperties.certificateID, - entityEdition: currentProperties.editionNumber - }); - } - } - - ui.tablet.sendToQml({ method: 'updateWearables', wornWearables: currentlyWornWearables }); - break; - case 'sendAsset_sendPublicly': - if (message.assetName !== "") { - deleteSendAssetParticleEffect(); - sendAssetRecipient = message.recipient; - var props = SEND_ASSET_PARTICLE_PROPERTIES; - props.parentID = MyAvatar.sessionUUID; - props.position = MyAvatar.position; - props.position.y += 0.2; - if (message.effectImage) { - props.textures = message.effectImage; - } - sendAssetParticleEffect = Entities.addEntity(props, true); - particleEffectTimestamp = Date.now(); - updateSendAssetParticleEffect(); - sendAssetParticleEffectUpdateTimer = Script.setInterval(updateSendAssetParticleEffect, - SEND_ASSET_PARTICLE_TIMER_UPDATE); - } - break; case 'http.request': // Handled elsewhere, don't log. break; - case 'goToPurchases_fromWalletHome': // HRS FIXME What's this about? - break; default: - print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message)); + print('Unrecognized message from Checkout.qml: ' + JSON.stringify(message)); } }; @@ -1163,15 +724,7 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { } if (onCommerceScreen) { - if (!isWired) { - Users.usernameFromIDReply.connect(usernameFromIDReply); - Controller.mousePressEvent.connect(handleMouseEvent); - Controller.mouseMoveEvent.connect(handleMouseMoveEvent); - triggerMapping.enable(); - triggerPressMapping.enable(); - } - isWired = true; - Wallet.refreshWalletStatus(); + WalletScriptingInterface.refreshWalletStatus(); } else { if (onMarketplaceScreen) { onMarketplaceOpen('marketplace cta'); @@ -1193,44 +746,11 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { "\nNew screen URL: " + url + "\nCurrent app open status: " + ui.isOpen + "\n"); }; -function notificationDataProcessPage(data) { - return data.data.updates; -} - -var shouldShowDot = false; -function notificationPollCallback(updatesArray) { - shouldShowDot = shouldShowDot || updatesArray.length > 0; - ui.messagesWaiting(shouldShowDot && !ui.isOpen); - - if (updatesArray.length > 0) { - var message; - if (!ui.notificationInitialCallbackMade) { - message = updatesArray.length + " of your purchased items " + - (updatesArray.length === 1 ? "has an update " : "have updates ") + - "available. Open MARKET to update."; - ui.notificationDisplayBanner(message); - - ui.notificationPollCaresAboutSince = true; - } else { - for (var i = 0; i < updatesArray.length; i++) { - message = "Update available for \"" + - updatesArray[i].base_item_title + "\"." + - "Open MARKET to update."; - ui.notificationDisplayBanner(message); - } - } - } -} - -function isReturnedDataEmpty(data) { - var historyArray = data.data.updates; - return historyArray.length === 0; -} - var BUTTON_NAME = "MARKET"; -var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; -var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. +var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace" + (WalletScriptingInterface.limitedCommerce ? "?isFree=1" : ""); +// Append "?" if necessary to signal injected script that it's the initial page. +var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + (MARKETPLACE_URL.indexOf("?") > -1 ? "" : "?"); var ui; function startup() { ui = new AppUi({ @@ -1239,50 +759,26 @@ function startup() { inject: MARKETPLACES_INJECT_SCRIPT_URL, home: MARKETPLACE_URL_INITIAL, onScreenChanged: onTabletScreenChanged, - onMessage: onQmlMessageReceived, - // notificationPollEndpoint: "/api/v1/commerce/available_updates?per_page=10", - // notificationPollTimeoutMs: 300000, - // notificationDataProcessPage: notificationDataProcessPage, - // notificationPollCallback: notificationPollCallback, - // notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, - // notificationPollCaresAboutSince: false // Changes to true after first poll + onMessage: onQmlMessageReceived }); ContextOverlay.contextOverlayClicked.connect(openInspectionCertificateQML); Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); GlobalServices.myUsernameChanged.connect(onUsernameChanged); ui.tablet.webEventReceived.connect(onWebEventReceived); - Wallet.walletStatusChanged.connect(sendCommerceSettings); + WalletScriptingInterface.walletStatusChanged.connect(sendCommerceSettings); Window.messageBoxClosed.connect(onMessageBoxClosed); ResourceRequestObserver.resourceRequestEvent.connect(onResourceRequestEvent); - Wallet.refreshWalletStatus(); + WalletScriptingInterface.refreshWalletStatus(); } -var isWired = false; -var isUpdateOverlaysWired = false; function off() { - if (isWired) { - Users.usernameFromIDReply.disconnect(usernameFromIDReply); - Controller.mousePressEvent.disconnect(handleMouseEvent); - Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); - triggerMapping.disable(); - triggerPressMapping.disable(); - - isWired = false; - } - - if (isUpdateOverlaysWired) { - Script.update.disconnect(updateOverlays); - isUpdateOverlaysWired = false; - } - removeOverlays(); } function shutdown() { maybeEnableHMDPreview(); - deleteSendAssetParticleEffect(); Window.messageBoxClosed.disconnect(onMessageBoxClosed); - Wallet.walletStatusChanged.disconnect(sendCommerceSettings); + WalletScriptingInterface.walletStatusChanged.disconnect(sendCommerceSettings); ui.tablet.webEventReceived.disconnect(onWebEventReceived); GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 36fe264274..9558b99310 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -634,7 +634,7 @@ Window.notifyEditError = onEditError; Window.notify = onNotify; Tablet.tabletNotification.connect(tabletNotification); - Wallet.walletNotSetup.connect(walletNotSetup); + WalletScriptingInterface.walletNotSetup.connect(walletNotSetup); Messages.subscribe(NOTIFICATIONS_MESSAGE_CHANNEL); Messages.messageReceived.connect(onMessageReceived); From 90e944f130cdba4dc99252683f65183dfea62ec3 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 30 Oct 2018 16:20:33 -0700 Subject: [PATCH 275/362] always zero velocity on near-grab release of non-dynamic --- .../controllerModules/nearParentGrabEntity.js | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index bbdcbaaa64..f354067a77 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -149,20 +149,12 @@ Script.include("/~/system/libraries/controllers.js"); this.hapticTargetID = null; var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; if (this.thisHandIsParent(props) && !this.robbed) { - if (this.previousParentID[this.targetEntityID] === Uuid.NULL || this.previousParentID === undefined) { - Entities.editEntity(this.targetEntityID, { - parentID: this.previousParentID[this.targetEntityID], - parentJointIndex: this.previousParentJointIndex[this.targetEntityID] - }); - } else { - // we're putting this back as a child of some other parent, so zero its velocity - Entities.editEntity(this.targetEntityID, { - parentID: this.previousParentID[this.targetEntityID], - parentJointIndex: this.previousParentJointIndex[this.targetEntityID], - localVelocity: {x: 0, y: 0, z: 0}, - localAngularVelocity: {x: 0, y: 0, z: 0} - }); - } + Entities.editEntity(this.targetEntityID, { + parentID: this.previousParentID[this.targetEntityID], + parentJointIndex: this.previousParentJointIndex[this.targetEntityID], + localVelocity: {x: 0, y: 0, z: 0}, + localAngularVelocity: {x: 0, y: 0, z: 0} + }); } var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; From 91b199a456e61886464d07deb3144f1da2a18394 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 30 Oct 2018 16:29:20 -0700 Subject: [PATCH 276/362] Checkpoint Proofs Filter in Inventory --- .../resources/qml/controls-uit/FilterBar.qml | 70 +++++++++++-------- .../qml/hifi/commerce/purchases/Purchases.qml | 5 ++ interface/src/commerce/Ledger.cpp | 10 ++- interface/src/commerce/Ledger.h | 2 +- interface/src/commerce/QmlCommerce.cpp | 5 +- interface/src/commerce/QmlCommerce.h | 2 +- 6 files changed, 57 insertions(+), 37 deletions(-) diff --git a/interface/resources/qml/controls-uit/FilterBar.qml b/interface/resources/qml/controls-uit/FilterBar.qml index ecae790b22..71aa1f64ab 100644 --- a/interface/resources/qml/controls-uit/FilterBar.qml +++ b/interface/resources/qml/controls-uit/FilterBar.qml @@ -260,38 +260,50 @@ Item { interactive: false; anchors.fill: parent; model: filterBarModel; - delegate: Rectangle { - id: dropDownButton; - color: hifi.colors.white; - width: parent.width; + delegate: Item { + width: parent.width; height: 50; + Rectangle { + id: dropDownButton; + color: hifi.colors.white; + width: parent.width; + height: 50; + visible: true; - RalewaySemiBold { - id: dropDownButtonText; - text: model.displayName; - anchors.fill: parent; - anchors.leftMargin: 12; - color: hifi.colors.baseGray; - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - size: 18; + RalewaySemiBold { + id: dropDownButtonText; + text: model.displayName; + anchors.fill: parent; + anchors.topMargin: 2; + anchors.leftMargin: 12; + color: hifi.colors.baseGray; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + size: 18; + } + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + propagateComposedEvents: false; + onEntered: { + dropDownButton.color = hifi.colors.blueHighlight; + } + onExited: { + dropDownButton.color = hifi.colors.white; + } + onClicked: { + textField.forceActiveFocus(); + root.primaryFilter_index = index; + dropdownContainer.visible = false; + } + } } - - MouseArea { - anchors.fill: parent; - hoverEnabled: true; - propagateComposedEvents: false; - onEntered: { - dropDownButton.color = hifi.colors.blueHighlight; - } - onExited: { - dropDownButton.color = hifi.colors.white; - } - onClicked: { - textField.forceActiveFocus(); - root.primaryFilter_index = index; - dropdownContainer.visible = false; - } + Rectangle { + height: 2; + width: parent.width; + color: hifi.colors.lightGray; + visible: model.separator } } } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 655af79e68..9cbabb769f 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -518,8 +518,13 @@ Rectangle { "filterName": "wearable" }, { + "separator" : true, "displayName": "Updatable", "filterName": "updated" + }, + { + "displayName": "Proofs", + "filterName": "proofs" } ] filterBar.primaryFilterChoices.clear(); diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 67303f2a9b..d206d773de 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -152,10 +152,14 @@ void Ledger::balance(const QStringList& keys) { keysQuery("balance", "balanceSuccess", "balanceFailure"); } -void Ledger::inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage) { +void Ledger::inventory(const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage) { QJsonObject params; - params["edition_filter"] = editionFilter; - params["type_filter"] = typeFilter; + if (typeFilter == "proofs") { + params["edition_filter"] = "proofs"; + } else { + params["type_filter"] = typeFilter; + } + params["title_filter"] = titleFilter; params["page"] = page; params["per_page"] = perPage; diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 427395ee11..7bff9abe9b 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -29,7 +29,7 @@ public: bool receiveAt(const QString& hfc_key, const QString& signing_key, const QByteArray& locker); bool receiveAt(); void balance(const QStringList& keys); - void inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage); + void inventory(const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage); void history(const QStringList& keys, const int& pageNumber, const int& itemsPerPage); void account(); void updateLocation(const QString& asset_id, const QString& location, const bool& alsoUpdateSiblings = false, const bool controlledFailure = false); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index ffe89ffc5b..369b03d610 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -151,8 +151,7 @@ void QmlCommerce::balance() { } } -void QmlCommerce::inventory(const QString& editionFilter, - const QString& typeFilter, +void QmlCommerce::inventory(const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage) { @@ -160,7 +159,7 @@ void QmlCommerce::inventory(const QString& editionFilter, auto wallet = DependencyManager::get(); QStringList cachedPublicKeys = wallet->listPublicKeys(); if (!cachedPublicKeys.isEmpty()) { - ledger->inventory(editionFilter, typeFilter, titleFilter, page, perPage); + ledger->inventory(typeFilter, titleFilter, page, perPage); } } diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 2e3c0ec24d..a8eb45261d 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -74,7 +74,7 @@ protected: Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); - Q_INVOKABLE void inventory(const QString& editionFilter = QString(), const QString& typeFilter = QString(), const QString& titleFilter = QString(), const int& page = 1, const int& perPage = 20); + Q_INVOKABLE void inventory(const QString& typeFilter = QString(), const QString& titleFilter = QString(), const int& page = 1, const int& perPage = 20); Q_INVOKABLE void history(const int& pageNumber, const int& itemsPerPage = 100); Q_INVOKABLE void generateKeyPair(); Q_INVOKABLE void account(); From becee7f010571d3ffd61deeb062d16a73a06e2a1 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 30 Oct 2018 17:28:42 -0700 Subject: [PATCH 277/362] Re-name FBXGeometry to HFMGeometry and do the same for related classes --- .../src/avatars/ScriptableAvatar.cpp | 12 +- interface/src/ModelPackager.cpp | 4 +- interface/src/ModelPackager.h | 6 +- interface/src/ModelPropertiesDialog.cpp | 4 +- interface/src/ModelPropertiesDialog.h | 4 +- interface/src/avatar/MyAvatar.cpp | 14 +- interface/src/avatar/MySkeletonModel.cpp | 2 +- interface/src/raypick/CollisionPick.cpp | 30 ++-- interface/src/ui/overlays/ModelOverlay.cpp | 22 +-- libraries/animation/src/AnimClip.cpp | 18 +-- libraries/animation/src/AnimSkeleton.cpp | 16 +- libraries/animation/src/AnimSkeleton.h | 8 +- libraries/animation/src/AnimationCache.cpp | 18 +-- libraries/animation/src/AnimationCache.h | 12 +- libraries/animation/src/AnimationObject.cpp | 8 +- libraries/animation/src/AnimationObject.h | 4 +- libraries/animation/src/Rig.cpp | 18 +-- libraries/animation/src/Rig.h | 22 +-- .../src/avatars-renderer/Avatar.cpp | 6 +- .../src/avatars-renderer/SkeletonModel.cpp | 16 +- .../src/avatars-renderer/SkeletonModel.h | 4 +- libraries/baking/src/FBXBaker.cpp | 6 +- libraries/baking/src/FBXBaker.h | 2 +- libraries/baking/src/ModelBaker.cpp | 2 +- libraries/baking/src/ModelBaker.h | 2 +- libraries/baking/src/OBJBaker.cpp | 8 +- libraries/baking/src/OBJBaker.h | 4 +- .../src/RenderableModelEntityItem.cpp | 66 ++++---- libraries/fbx/src/FBX.h | 90 +++++------ libraries/fbx/src/FBXReader.cpp | 150 +++++++++--------- libraries/fbx/src/FBXReader.h | 16 +- libraries/fbx/src/FBXReader_Material.cpp | 40 ++--- libraries/fbx/src/FBXReader_Mesh.cpp | 76 ++++----- libraries/fbx/src/GLTFReader.cpp | 62 ++++---- libraries/fbx/src/GLTFReader.h | 10 +- libraries/fbx/src/OBJReader.cpp | 84 +++++----- libraries/fbx/src/OBJReader.h | 12 +- .../src/model-networking/ModelCache.cpp | 54 +++---- .../src/model-networking/ModelCache.h | 14 +- .../render-utils/src/CauterizedModel.cpp | 18 +-- .../render-utils/src/MeshPartPayload.cpp | 4 +- libraries/render-utils/src/Model.cpp | 76 ++++----- libraries/render-utils/src/Model.h | 6 +- .../render-utils/src/SoftAttachmentModel.cpp | 6 +- tests-manual/gpu/src/TestFbx.cpp | 2 +- tests-manual/gpu/src/TestFbx.h | 2 +- .../src/AnimInverseKinematicsTests.cpp | 8 +- tools/skeleton-dump/src/SkeletonDumpApp.cpp | 4 +- tools/vhacd-util/src/VHACDUtil.cpp | 44 ++--- tools/vhacd-util/src/VHACDUtil.h | 12 +- tools/vhacd-util/src/VHACDUtilApp.cpp | 26 +-- tools/vhacd-util/src/VHACDUtilApp.h | 2 +- 52 files changed, 578 insertions(+), 578 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index 7d2b267a05..385f94d9f3 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -69,10 +69,10 @@ void ScriptableAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { AvatarData::setSkeletonModelURL(skeletonModelURL); } -static AnimPose composeAnimPose(const FBXJoint& fbxJoint, const glm::quat rotation, const glm::vec3 translation) { +static AnimPose composeAnimPose(const HFMJoint& joint, const glm::quat rotation, const glm::vec3 translation) { glm::mat4 translationMat = glm::translate(translation); - glm::mat4 rotationMat = glm::mat4_cast(fbxJoint.preRotation * rotation * fbxJoint.postRotation); - glm::mat4 finalMat = translationMat * fbxJoint.preTransform * rotationMat * fbxJoint.postTransform; + glm::mat4 rotationMat = glm::mat4_cast(joint.preRotation * rotation * joint.postRotation); + glm::mat4 finalMat = translationMat * joint.preTransform * rotationMat * joint.postTransform; return AnimPose(finalMat); } @@ -93,7 +93,7 @@ void ScriptableAvatar::update(float deltatime) { } _animationDetails.currentFrame = currentFrame; - const QVector& modelJoints = _bind->getGeometry().joints; + const QVector& modelJoints = _bind->getGeometry().joints; QStringList animationJointNames = _animation->getJointNames(); const int nJoints = modelJoints.size(); @@ -102,8 +102,8 @@ void ScriptableAvatar::update(float deltatime) { } const int frameCount = _animation->getFrames().size(); - const FBXAnimationFrame& floorFrame = _animation->getFrames().at((int)glm::floor(currentFrame) % frameCount); - const FBXAnimationFrame& ceilFrame = _animation->getFrames().at((int)glm::ceil(currentFrame) % frameCount); + const HFMAnimationFrame& floorFrame = _animation->getFrames().at((int)glm::floor(currentFrame) % frameCount); + const HFMAnimationFrame& ceilFrame = _animation->getFrames().at((int)glm::ceil(currentFrame) % frameCount); const float frameFraction = glm::fract(currentFrame); std::vector poses = _animSkeleton->getRelativeDefaultPoses(); diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index 3a5d92eb8c..0b2846006b 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -235,7 +235,7 @@ bool ModelPackager::zipModel() { return true; } -void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const FBXGeometry& geometry) { +void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const HFMGeometry& geometry) { bool isBodyType = _modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL; @@ -370,7 +370,7 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename void ModelPackager::listTextures() { _textures.clear(); - foreach (const FBXMaterial mat, _geometry->materials) { + foreach (const HFMMaterial mat, _geometry->materials) { if (!mat.albedoTexture.filename.isEmpty() && mat.albedoTexture.content.isEmpty() && !_textures.contains(mat.albedoTexture.filename)) { _textures << mat.albedoTexture.filename; diff --git a/interface/src/ModelPackager.h b/interface/src/ModelPackager.h index 76295e5a85..b68d9e746d 100644 --- a/interface/src/ModelPackager.h +++ b/interface/src/ModelPackager.h @@ -19,7 +19,7 @@ #include "ui/ModelsBrowser.h" -class FBXGeometry; +class HFMGeometry; class ModelPackager : public QObject { public: @@ -32,7 +32,7 @@ private: bool editProperties(); bool zipModel(); - void populateBasicMapping(QVariantHash& mapping, QString filename, const FBXGeometry& geometry); + void populateBasicMapping(QVariantHash& mapping, QString filename, const HFMGeometry& geometry); void listTextures(); bool copyTextures(const QString& oldDir, const QDir& newDir); @@ -44,7 +44,7 @@ private: QString _scriptDir; QVariantHash _mapping; - std::unique_ptr _geometry; + std::unique_ptr _geometry; QStringList _textures; QStringList _scripts; }; diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp index 8984f89d07..dcda85d117 100644 --- a/interface/src/ModelPropertiesDialog.cpp +++ b/interface/src/ModelPropertiesDialog.cpp @@ -27,7 +27,7 @@ ModelPropertiesDialog::ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, - const QString& basePath, const FBXGeometry& geometry) : + const QString& basePath, const HFMGeometry& geometry) : _modelType(modelType), _originalMapping(originalMapping), _basePath(basePath), @@ -249,7 +249,7 @@ QComboBox* ModelPropertiesDialog::createJointBox(bool withNone) const { if (withNone) { box->addItem("(none)"); } - foreach (const FBXJoint& joint, _geometry.joints) { + foreach (const HFMJoint& joint, _geometry.joints) { if (joint.isSkeletonJoint || !_geometry.hasSkeletonJoints) { box->addItem(joint.name); } diff --git a/interface/src/ModelPropertiesDialog.h b/interface/src/ModelPropertiesDialog.h index e3c2d8ed6a..d1a020bec3 100644 --- a/interface/src/ModelPropertiesDialog.h +++ b/interface/src/ModelPropertiesDialog.h @@ -30,7 +30,7 @@ class ModelPropertiesDialog : public QDialog { public: ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, - const QString& basePath, const FBXGeometry& geometry); + const QString& basePath, const HFMGeometry& geometry); QVariantHash getMapping() const; @@ -50,7 +50,7 @@ private: FSTReader::ModelType _modelType; QVariantHash _originalMapping; QString _basePath; - FBXGeometry _geometry; + HFMGeometry _geometry; QLineEdit* _name = nullptr; QPushButton* _textureDirectory = nullptr; QPushButton* _scriptDirectory = nullptr; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 3299bd10e7..b3a66f70b8 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -155,7 +155,7 @@ MyAvatar::MyAvatar(QThread* thread) : }); connect(_skeletonModel.get(), &Model::rigReady, this, [this]() { if (_shouldLoadScripts) { - auto geometry = getSkeletonModel()->getFBXGeometry(); + auto geometry = getSkeletonModel()->getHFMGeometry(); qApp->loadAvatarScripts(geometry.scripts); _shouldLoadScripts = false; } @@ -2429,10 +2429,10 @@ void MyAvatar::attachmentDataToEntityProperties(const AttachmentData& data, Enti void MyAvatar::initHeadBones() { int neckJointIndex = -1; if (_skeletonModel->isLoaded()) { - neckJointIndex = _skeletonModel->getFBXGeometry().neckJointIndex; + neckJointIndex = _skeletonModel->getHFMGeometry().neckJointIndex; } if (neckJointIndex == -1) { - neckJointIndex = (_skeletonModel->getFBXGeometry().headJointIndex - 1); + neckJointIndex = (_skeletonModel->getHFMGeometry().headJointIndex - 1); if (neckJointIndex < 0) { // return if the head is not even there. can't cauterize!! return; @@ -2443,7 +2443,7 @@ void MyAvatar::initHeadBones() { q.push(neckJointIndex); _headBoneSet.insert(neckJointIndex); - // fbxJoints only hold links to parents not children, so we have to do a bit of extra work here. + // hfmJoints only hold links to parents not children, so we have to do a bit of extra work here. while (q.size() > 0) { int jointIndex = q.front(); for (int i = 0; i < _skeletonModel->getJointStateCount(); i++) { @@ -2592,11 +2592,11 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { if (_skeletonModel && _skeletonModel->isLoaded()) { const Rig& rig = _skeletonModel->getRig(); - const FBXGeometry& geometry = _skeletonModel->getFBXGeometry(); + const HFMGeometry& geometry = _skeletonModel->getHFMGeometry(); for (int i = 0; i < rig.getJointStateCount(); i++) { AnimPose jointPose; rig.getAbsoluteJointPoseInRigFrame(i, jointPose); - const FBXJointShapeInfo& shapeInfo = geometry.joints[i].shapeInfo; + const HFMJointShapeInfo& shapeInfo = geometry.joints[i].shapeInfo; const AnimPose pose = rigToWorldPose * jointPose; for (size_t j = 0; j < shapeInfo.debugLines.size() / 2; j++) { glm::vec3 pointA = pose.xformPoint(shapeInfo.debugLines[2 * j]); @@ -4012,7 +4012,7 @@ float MyAvatar::getSitStandStateChange() const { } QVector MyAvatar::getScriptUrls() { - QVector scripts = _skeletonModel->isLoaded() ? _skeletonModel->getFBXGeometry().scripts : QVector(); + QVector scripts = _skeletonModel->isLoaded() ? _skeletonModel->getHFMGeometry().scripts : QVector(); return scripts; } diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index c6aae6124a..3ec40d372b 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -90,7 +90,7 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { // Called within Model::simulate call, below. void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); Head* head = _owningAvatar->getHead(); diff --git a/interface/src/raypick/CollisionPick.cpp b/interface/src/raypick/CollisionPick.cpp index 25927c5b68..e8a53aa9b6 100644 --- a/interface/src/raypick/CollisionPick.cpp +++ b/interface/src/raypick/CollisionPick.cpp @@ -131,7 +131,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha // should never fall in here when collision model not fully loaded // TODO: assert that all geometries exist and are loaded //assert(_model && _model->isLoaded() && _compoundShapeResource && _compoundShapeResource->isLoaded()); - const FBXGeometry& collisionGeometry = resource->getFBXGeometry(); + const HFMGeometry& collisionGeometry = resource->getHFMGeometry(); ShapeInfo::PointCollection& pointCollection = shapeInfo.getPointCollection(); pointCollection.clear(); @@ -139,15 +139,15 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha // the way OBJ files get read, each section under a "g" line is its own meshPart. We only expect // to find one actual "mesh" (with one or more meshParts in it), but we loop over the meshes, just in case. - foreach (const FBXMesh& mesh, collisionGeometry.meshes) { + foreach (const HFMMesh& mesh, collisionGeometry.meshes) { // each meshPart is a convex hull - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { pointCollection.push_back(QVector()); ShapeInfo::PointList& pointsInPart = pointCollection[i]; // run through all the triangles and (uniquely) add each point to the hull uint32_t numIndices = (uint32_t)meshPart.triangleIndices.size(); - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices % TRIANGLE_STRIDE == 0); numIndices -= numIndices % TRIANGLE_STRIDE; // WORKAROUND lack of sanity checking in FBXReader @@ -168,7 +168,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha // run through all the quads and (uniquely) add each point to the hull numIndices = (uint32_t)meshPart.quadIndices.size(); - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices % QUAD_STRIDE == 0); numIndices -= numIndices % QUAD_STRIDE; // WORKAROUND lack of sanity checking in FBXReader @@ -206,7 +206,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha // to the visual model and apply them to the collision model (without regard for the // collision model's extents). - glm::vec3 scaleToFit = dimensions / resource->getFBXGeometry().getUnscaledMeshExtents().size(); + glm::vec3 scaleToFit = dimensions / resource->getHFMGeometry().getUnscaledMeshExtents().size(); // multiply each point by scale for (int32_t i = 0; i < pointCollection.size(); i++) { for (int32_t j = 0; j < pointCollection[i].size(); j++) { @@ -216,11 +216,11 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha } shapeInfo.setParams(type, dimensions, resource->getURL().toString()); } else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) { - const FBXGeometry& fbxGeometry = resource->getFBXGeometry(); - int numFbxMeshes = fbxGeometry.meshes.size(); + const HFMGeometry& hfmGeometry = resource->getHFMGeometry(); + int numHFMMeshes = hfmGeometry.meshes.size(); int totalNumVertices = 0; - for (int i = 0; i < numFbxMeshes; i++) { - const FBXMesh& mesh = fbxGeometry.meshes.at(i); + for (int i = 0; i < numHFMMeshes; i++) { + const HFMMesh& mesh = hfmGeometry.meshes.at(i); totalNumVertices += mesh.vertices.size(); } const int32_t MAX_VERTICES_PER_STATIC_MESH = 1e6; @@ -230,7 +230,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha return; } - auto& meshes = resource->getFBXGeometry().meshes; + auto& meshes = resource->getHFMGeometry().meshes; int32_t numMeshes = (int32_t)(meshes.size()); const int MAX_ALLOWED_MESH_COUNT = 1000; @@ -285,12 +285,12 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha if (type == SHAPE_TYPE_STATIC_MESH) { // copy into triangleIndices size_t triangleIndicesCount = 0; - for (const FBXMeshPart& meshPart : mesh.parts) { + for (const HFMMeshPart& meshPart : mesh.parts) { triangleIndicesCount += meshPart.triangleIndices.count(); } triangleIndices.reserve((int)triangleIndicesCount); - for (const FBXMeshPart& meshPart : mesh.parts) { + for (const HFMMeshPart& meshPart : mesh.parts) { const int* indexItr = meshPart.triangleIndices.cbegin(); while (indexItr != meshPart.triangleIndices.cend()) { triangleIndices.push_back(*indexItr); @@ -299,11 +299,11 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha } } else if (type == SHAPE_TYPE_SIMPLE_COMPOUND) { // for each mesh copy unique part indices, separated by special bogus (flag) index values - for (const FBXMeshPart& meshPart : mesh.parts) { + for (const HFMMeshPart& meshPart : mesh.parts) { // collect unique list of indices for this part std::set uniqueIndices; auto numIndices = meshPart.triangleIndices.count(); - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices% TRIANGLE_STRIDE == 0); numIndices -= numIndices % TRIANGLE_STRIDE; // WORKAROUND lack of sanity checking in FBXReader diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index eee8222051..1b66ff08ad 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -446,7 +446,7 @@ QVariant ModelOverlay::getProperty(const QString& property) { if (property == "jointNames") { if (_model && _model->isActive()) { - // note: going through Rig because Model::getJointNames() (which proxies to FBXGeometry) was always empty + // note: going through Rig because Model::getJointNames() (which proxies to HFMGeometry) was always empty const Rig* rig = &(_model->getRig()); return mapJoints([rig](int jointIndex) -> QString { return rig->nameOfJoint(jointIndex); @@ -574,7 +574,7 @@ void ModelOverlay::animate() { QVector jointsData; - const QVector& frames = _animation->getFramesReference(); // NOTE: getFrames() is too heavy + const QVector& frames = _animation->getFramesReference(); // NOTE: getFrames() is too heavy int frameCount = frames.size(); if (frameCount <= 0) { return; @@ -606,10 +606,10 @@ void ModelOverlay::animate() { } QStringList animationJointNames = _animation->getGeometry().getJointNames(); - auto& fbxJoints = _animation->getGeometry().joints; + auto& hfmJoints = _animation->getGeometry().joints; - auto& originalFbxJoints = _model->getFBXGeometry().joints; - auto& originalFbxIndices = _model->getFBXGeometry().jointIndices; + auto& originalHFMJoints = _model->getHFMGeometry().joints; + auto& originalFbxIndices = _model->getHFMGeometry().jointIndices; const QVector& rotations = frames[_lastKnownCurrentFrame].rotations; const QVector& translations = frames[_lastKnownCurrentFrame].translations; @@ -626,23 +626,23 @@ void ModelOverlay::animate() { translationMat = glm::translate(translations[index]); } } else if (index < animationJointNames.size()) { - QString jointName = fbxJoints[index].name; + QString jointName = hfmJoints[index].name; if (originalFbxIndices.contains(jointName)) { // Making sure the joint names exist in the original model the animation is trying to apply onto. If they do, then remap and get its translation. int remappedIndex = originalFbxIndices[jointName] - 1; // JointIndeces seem to always start from 1 and the found index is always 1 higher than actual. - translationMat = glm::translate(originalFbxJoints[remappedIndex].translation); + translationMat = glm::translate(originalHFMJoints[remappedIndex].translation); } } glm::mat4 rotationMat; if (index < rotations.size()) { - rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * rotations[index] * fbxJoints[index].postRotation); + rotationMat = glm::mat4_cast(hfmJoints[index].preRotation * rotations[index] * hfmJoints[index].postRotation); } else { - rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * fbxJoints[index].postRotation); + rotationMat = glm::mat4_cast(hfmJoints[index].preRotation * hfmJoints[index].postRotation); } - glm::mat4 finalMat = (translationMat * fbxJoints[index].preTransform * - rotationMat * fbxJoints[index].postTransform); + glm::mat4 finalMat = (translationMat * hfmJoints[index].preTransform * + rotationMat * hfmJoints[index].postTransform); auto& jointData = jointsData[j]; jointData.translation = extractTranslation(finalMat); jointData.translationIsDefaultPose = false; diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index f9195a608b..d630218165 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -101,7 +101,7 @@ void AnimClip::copyFromNetworkAnim() { // build a mapping from animation joint indices to skeleton joint indices. // by matching joints with the same name. - const FBXGeometry& geom = _networkAnim->getGeometry(); + const HFMGeometry& geom = _networkAnim->getGeometry(); AnimSkeleton animSkeleton(geom); const auto animJointCount = animSkeleton.getNumJoints(); const auto skeletonJointCount = _skeleton->getNumJoints(); @@ -120,7 +120,7 @@ void AnimClip::copyFromNetworkAnim() { for (int frame = 0; frame < frameCount; frame++) { - const FBXAnimationFrame& fbxAnimFrame = geom.animationFrames[frame]; + const HFMAnimationFrame& hfmAnimFrame = geom.animationFrames[frame]; // init all joints in animation to default pose // this will give us a resonable result for bones in the model skeleton but not in the animation. @@ -132,8 +132,8 @@ void AnimClip::copyFromNetworkAnim() { for (int animJoint = 0; animJoint < animJointCount; animJoint++) { int skeletonJoint = jointMap[animJoint]; - const glm::vec3& fbxAnimTrans = fbxAnimFrame.translations[animJoint]; - const glm::quat& fbxAnimRot = fbxAnimFrame.rotations[animJoint]; + const glm::vec3& hfmAnimTrans = hfmAnimFrame.translations[animJoint]; + const glm::quat& hfmAnimRot = hfmAnimFrame.rotations[animJoint]; // skip joints that are in the animation but not in the skeleton. if (skeletonJoint >= 0 && skeletonJoint < skeletonJointCount) { @@ -146,19 +146,19 @@ void AnimClip::copyFromNetworkAnim() { preRot.scale() = glm::vec3(1.0f); postRot.scale() = glm::vec3(1.0f); - AnimPose rot(glm::vec3(1.0f), fbxAnimRot, glm::vec3()); + AnimPose rot(glm::vec3(1.0f), hfmAnimRot, glm::vec3()); // adjust translation offsets, so large translation animatons on the reference skeleton // will be adjusted when played on a skeleton with short limbs. - const glm::vec3& fbxZeroTrans = geom.animationFrames[0].translations[animJoint]; + const glm::vec3& hfmZeroTrans = geom.animationFrames[0].translations[animJoint]; const AnimPose& relDefaultPose = _skeleton->getRelativeDefaultPose(skeletonJoint); float boneLengthScale = 1.0f; const float EPSILON = 0.0001f; - if (fabsf(glm::length(fbxZeroTrans)) > EPSILON) { - boneLengthScale = glm::length(relDefaultPose.trans()) / glm::length(fbxZeroTrans); + if (fabsf(glm::length(hfmZeroTrans)) > EPSILON) { + boneLengthScale = glm::length(relDefaultPose.trans()) / glm::length(hfmZeroTrans); } - AnimPose trans = AnimPose(glm::vec3(1.0f), glm::quat(), relDefaultPose.trans() + boneLengthScale * (fbxAnimTrans - fbxZeroTrans)); + AnimPose trans = AnimPose(glm::vec3(1.0f), glm::quat(), relDefaultPose.trans() + boneLengthScale * (hfmAnimTrans - hfmZeroTrans)); _anim[frame][skeletonJoint] = trans * preRot * rot * postRot; } diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index bed9c590be..fc4114ac7b 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -16,17 +16,17 @@ #include "AnimationLogging.h" -AnimSkeleton::AnimSkeleton(const FBXGeometry& fbxGeometry) { +AnimSkeleton::AnimSkeleton(const HFMGeometry& geometry) { // convert to std::vector of joints - std::vector joints; - joints.reserve(fbxGeometry.joints.size()); - for (auto& joint : fbxGeometry.joints) { + std::vector joints; + joints.reserve(geometry.joints.size()); + for (auto& joint : geometry.joints) { joints.push_back(joint); } buildSkeletonFromJoints(joints); } -AnimSkeleton::AnimSkeleton(const std::vector& joints) { +AnimSkeleton::AnimSkeleton(const std::vector& joints) { buildSkeletonFromJoints(joints); } @@ -166,7 +166,7 @@ void AnimSkeleton::mirrorAbsolutePoses(AnimPoseVec& poses) const { } } -void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) { +void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) { _joints = joints; _jointsSize = (int)joints.size(); // build a cache of bind poses @@ -177,7 +177,7 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) _relativePreRotationPoses.reserve(_jointsSize); _relativePostRotationPoses.reserve(_jointsSize); - // iterate over FBXJoints and extract the bind pose information. + // iterate over HFMJoints and extract the bind pose information. for (int i = 0; i < _jointsSize; i++) { // build pre and post transforms @@ -240,7 +240,7 @@ void AnimSkeleton::dump(bool verbose) const { qCDebug(animation) << " absDefaultPose =" << getAbsoluteDefaultPose(i); qCDebug(animation) << " relDefaultPose =" << getRelativeDefaultPose(i); if (verbose) { - qCDebug(animation) << " fbxJoint ="; + qCDebug(animation) << " hfmJoint ="; qCDebug(animation) << " isFree =" << _joints[i].isFree; qCDebug(animation) << " freeLineage =" << _joints[i].freeLineage; qCDebug(animation) << " parentIndex =" << _joints[i].parentIndex; diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 2ebf3f4f5d..1717d75985 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -23,8 +23,8 @@ public: using Pointer = std::shared_ptr; using ConstPointer = std::shared_ptr; - explicit AnimSkeleton(const FBXGeometry& fbxGeometry); - explicit AnimSkeleton(const std::vector& joints); + explicit AnimSkeleton(const HFMGeometry& geometry); + explicit AnimSkeleton(const std::vector& joints); int nameToJointIndex(const QString& jointName) const; const QString& getJointName(int jointIndex) const; int getNumJoints() const; @@ -64,9 +64,9 @@ public: std::vector lookUpJointIndices(const std::vector& jointNames) const; protected: - void buildSkeletonFromJoints(const std::vector& joints); + void buildSkeletonFromJoints(const std::vector& joints); - std::vector _joints; + std::vector _joints; int _jointsSize { 0 }; AnimPoseVec _relativeDefaultPoses; AnimPoseVec _absoluteDefaultPoses; diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index 04b7952ddb..b5b27c3a24 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -69,7 +69,7 @@ void AnimationReader::run() { if (urlValid) { // Parse the FBX directly from the QNetworkReply - FBXGeometry::Pointer fbxgeo; + HFMGeometry::Pointer fbxgeo; if (_url.path().toLower().endsWith(".fbx")) { fbxgeo.reset(readFBX(_data, QVariantHash(), _url.path())); } else { @@ -100,40 +100,40 @@ QStringList Animation::getJointNames() const { } QStringList names; if (_geometry) { - foreach (const FBXJoint& joint, _geometry->joints) { + foreach (const HFMJoint& joint, _geometry->joints) { names.append(joint.name); } } return names; } -QVector Animation::getFrames() const { +QVector Animation::getFrames() const { if (QThread::currentThread() != thread()) { - QVector result; + QVector result; BLOCKING_INVOKE_METHOD(const_cast(this), "getFrames", - Q_RETURN_ARG(QVector, result)); + Q_RETURN_ARG(QVector, result)); return result; } if (_geometry) { return _geometry->animationFrames; } else { - return QVector(); + return QVector(); } } -const QVector& Animation::getFramesReference() const { +const QVector& Animation::getFramesReference() const { return _geometry->animationFrames; } void Animation::downloadFinished(const QByteArray& data) { // parse the animation/fbx file on a background thread. AnimationReader* animationReader = new AnimationReader(_url, data); - connect(animationReader, SIGNAL(onSuccess(FBXGeometry::Pointer)), SLOT(animationParseSuccess(FBXGeometry::Pointer))); + connect(animationReader, SIGNAL(onSuccess(HFMGeometry::Pointer)), SLOT(animationParseSuccess(HFMGeometry::Pointer))); connect(animationReader, SIGNAL(onError(int, QString)), SLOT(animationParseError(int, QString))); QThreadPool::globalInstance()->start(animationReader); } -void Animation::animationParseSuccess(FBXGeometry::Pointer geometry) { +void Animation::animationParseSuccess(HFMGeometry::Pointer geometry) { qCDebug(animation) << "Animation parse success" << _url.toDisplayString(); diff --git a/libraries/animation/src/AnimationCache.h b/libraries/animation/src/AnimationCache.h index 483350e2b5..302f23a4e7 100644 --- a/libraries/animation/src/AnimationCache.h +++ b/libraries/animation/src/AnimationCache.h @@ -66,7 +66,7 @@ public: QString getType() const override { return "Animation"; } - const FBXGeometry& getGeometry() const { return *_geometry; } + const HFMGeometry& getGeometry() const { return *_geometry; } virtual bool isLoaded() const override; @@ -80,20 +80,20 @@ public: * @function AnimationObject.getFrames * @returns {FBXAnimationFrame[]} */ - Q_INVOKABLE QVector getFrames() const; + Q_INVOKABLE QVector getFrames() const; - const QVector& getFramesReference() const; + const QVector& getFramesReference() const; protected: virtual void downloadFinished(const QByteArray& data) override; protected slots: - void animationParseSuccess(FBXGeometry::Pointer geometry); + void animationParseSuccess(HFMGeometry::Pointer geometry); void animationParseError(int error, QString str); private: - FBXGeometry::Pointer _geometry; + HFMGeometry::Pointer _geometry; }; /// Reads geometry in a worker thread. @@ -105,7 +105,7 @@ public: virtual void run() override; signals: - void onSuccess(FBXGeometry::Pointer geometry); + void onSuccess(HFMGeometry::Pointer geometry); void onError(int error, QString str); private: diff --git a/libraries/animation/src/AnimationObject.cpp b/libraries/animation/src/AnimationObject.cpp index 7f0f35b104..bcbf497199 100644 --- a/libraries/animation/src/AnimationObject.cpp +++ b/libraries/animation/src/AnimationObject.cpp @@ -19,17 +19,17 @@ QStringList AnimationObject::getJointNames() const { return qscriptvalue_cast(thisObject())->getJointNames(); } -QVector AnimationObject::getFrames() const { +QVector AnimationObject::getFrames() const { return qscriptvalue_cast(thisObject())->getFrames(); } QVector AnimationFrameObject::getRotations() const { - return qscriptvalue_cast(thisObject()).rotations; + return qscriptvalue_cast(thisObject()).rotations; } void registerAnimationTypes(QScriptEngine* engine) { - qScriptRegisterSequenceMetaType >(engine); - engine->setDefaultPrototype(qMetaTypeId(), engine->newQObject( + qScriptRegisterSequenceMetaType >(engine); + engine->setDefaultPrototype(qMetaTypeId(), engine->newQObject( new AnimationFrameObject(), QScriptEngine::ScriptOwnership)); engine->setDefaultPrototype(qMetaTypeId(), engine->newQObject( new AnimationObject(), QScriptEngine::ScriptOwnership)); diff --git a/libraries/animation/src/AnimationObject.h b/libraries/animation/src/AnimationObject.h index aa69e78ceb..83880ed2ab 100644 --- a/libraries/animation/src/AnimationObject.h +++ b/libraries/animation/src/AnimationObject.h @@ -23,13 +23,13 @@ class QScriptEngine; class AnimationObject : public QObject, protected QScriptable { Q_OBJECT Q_PROPERTY(QStringList jointNames READ getJointNames) - Q_PROPERTY(QVector frames READ getFrames) + Q_PROPERTY(QVector frames READ getFrames) public: Q_INVOKABLE QStringList getJointNames() const; - Q_INVOKABLE QVector getFrames() const; + Q_INVOKABLE QVector getFrames() const; }; /// Scriptable wrapper for animation frames. diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 33b9569758..2641be92da 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -260,7 +260,7 @@ void Rig::destroyAnimGraph() { _rightEyeJointChildren.clear(); } -void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOffset) { +void Rig::initJointStates(const HFMGeometry& geometry, const glm::mat4& modelOffset) { _geometryOffset = AnimPose(geometry.offset); _invGeometryOffset = _geometryOffset.inverse(); _geometryToRigTransform = modelOffset * geometry.offset; @@ -307,7 +307,7 @@ void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOff _rightEyeJointChildren = _animSkeleton->getChildrenOfJoint(geometry.rightEyeJointIndex); } -void Rig::reset(const FBXGeometry& geometry) { +void Rig::reset(const HFMGeometry& geometry) { _geometryOffset = AnimPose(geometry.offset); _invGeometryOffset = _geometryOffset.inverse(); _animSkeleton = std::make_shared(geometry); @@ -1253,7 +1253,7 @@ const glm::vec3 DOP14_NORMALS[DOP14_COUNT] = { // returns true if the given point lies inside of the k-dop, specified by shapeInfo & shapePose. // if the given point does lie within the k-dop, it also returns the amount of displacement necessary to push that point outward // such that it lies on the surface of the kdop. -static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& shapePose, const FBXJointShapeInfo& shapeInfo, glm::vec3& displacementOut) { +static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& shapePose, const HFMJointShapeInfo& shapeInfo, glm::vec3& displacementOut) { // transform point into local space of jointShape. glm::vec3 localPoint = shapePose.inverse().xformPoint(point); @@ -1299,8 +1299,8 @@ static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& sh } } -glm::vec3 Rig::deflectHandFromTorso(const glm::vec3& handPosition, const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, - const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo) const { +glm::vec3 Rig::deflectHandFromTorso(const glm::vec3& handPosition, const HFMJointShapeInfo& hipsShapeInfo, const HFMJointShapeInfo& spineShapeInfo, + const HFMJointShapeInfo& spine1ShapeInfo, const HFMJointShapeInfo& spine2ShapeInfo) const { glm::vec3 position = handPosition; glm::vec3 displacement; int hipsJoint = indexOfJoint("Hips"); @@ -1349,8 +1349,8 @@ glm::vec3 Rig::deflectHandFromTorso(const glm::vec3& handPosition, const FBXJoin void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnabled, bool hipsEstimated, bool leftArmEnabled, bool rightArmEnabled, bool headEnabled, float dt, const AnimPose& leftHandPose, const AnimPose& rightHandPose, - const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, - const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo, + const HFMJointShapeInfo& hipsShapeInfo, const HFMJointShapeInfo& spineShapeInfo, + const HFMJointShapeInfo& spine1ShapeInfo, const HFMJointShapeInfo& spine2ShapeInfo, const glm::mat4& rigToSensorMatrix, const glm::mat4& sensorToRigMatrix) { const bool ENABLE_POLE_VECTORS = true; @@ -2008,7 +2008,7 @@ void Rig::computeExternalPoses(const glm::mat4& modelOffsetMat) { } void Rig::computeAvatarBoundingCapsule( - const FBXGeometry& geometry, + const HFMGeometry& geometry, float& radiusOut, float& heightOut, glm::vec3& localOffsetOut) const { @@ -2041,7 +2041,7 @@ void Rig::computeAvatarBoundingCapsule( // from the head to the hips when computing the rest of the bounding capsule. int index = indexOfJoint("Head"); while (index != -1) { - const FBXJointShapeInfo& shapeInfo = geometry.joints.at(index).shapeInfo; + const HFMJointShapeInfo& shapeInfo = geometry.joints.at(index).shapeInfo; AnimPose pose = _animSkeleton->getAbsoluteDefaultPose(index); if (shapeInfo.points.size() > 0) { for (auto& point : shapeInfo.points) { diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 7a090bd7bd..61e8672972 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -86,10 +86,10 @@ public: AnimPose secondaryControllerPoses[NumSecondaryControllerTypes]; // rig space uint8_t secondaryControllerFlags[NumSecondaryControllerTypes]; bool isTalking; - FBXJointShapeInfo hipsShapeInfo; - FBXJointShapeInfo spineShapeInfo; - FBXJointShapeInfo spine1ShapeInfo; - FBXJointShapeInfo spine2ShapeInfo; + HFMJointShapeInfo hipsShapeInfo; + HFMJointShapeInfo spineShapeInfo; + HFMJointShapeInfo spine1ShapeInfo; + HFMJointShapeInfo spine2ShapeInfo; }; struct EyeParameters { @@ -122,8 +122,8 @@ public: void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); void restoreRoleAnimation(const QString& role); - void initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOffset); - void reset(const FBXGeometry& geometry); + void initJointStates(const HFMGeometry& geometry, const glm::mat4& modelOffset); + void reset(const HFMGeometry& geometry); bool jointStatesEmpty(); int getJointStateCount() const; int indexOfJoint(const QString& jointName) const; @@ -210,7 +210,7 @@ public: void copyJointsFromJointData(const QVector& jointDataVec); void computeExternalPoses(const glm::mat4& modelOffsetMat); - void computeAvatarBoundingCapsule(const FBXGeometry& geometry, float& radiusOut, float& heightOut, glm::vec3& offsetOut) const; + void computeAvatarBoundingCapsule(const HFMGeometry& geometry, float& radiusOut, float& heightOut, glm::vec3& offsetOut) const; void setEnableInverseKinematics(bool enable); void setEnableAnimations(bool enable); @@ -245,8 +245,8 @@ protected: void updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnabled, bool hipsEstimated, bool leftArmEnabled, bool rightArmEnabled, bool headEnabled, float dt, const AnimPose& leftHandPose, const AnimPose& rightHandPose, - const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, - const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo, + const HFMJointShapeInfo& hipsShapeInfo, const HFMJointShapeInfo& spineShapeInfo, + const HFMJointShapeInfo& spine1ShapeInfo, const HFMJointShapeInfo& spine2ShapeInfo, const glm::mat4& rigToSensorMatrix, const glm::mat4& sensorToRigMatrix); void updateFeet(bool leftFootEnabled, bool rightFootEnabled, bool headEnabled, const AnimPose& leftFootPose, const AnimPose& rightFootPose, @@ -257,8 +257,8 @@ protected: bool calculateElbowPoleVector(int handIndex, int elbowIndex, int armIndex, int oppositeArmIndex, glm::vec3& poleVector) const; glm::vec3 calculateKneePoleVector(int footJointIndex, int kneeJoint, int upLegIndex, int hipsIndex, const AnimPose& targetFootPose) const; - glm::vec3 deflectHandFromTorso(const glm::vec3& handPosition, const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, - const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo) const; + glm::vec3 deflectHandFromTorso(const glm::vec3& handPosition, const HFMJointShapeInfo& hipsShapeInfo, const HFMJointShapeInfo& spineShapeInfo, + const HFMJointShapeInfo& spine1ShapeInfo, const HFMJointShapeInfo& spine2ShapeInfo) const; AnimPose _modelOffset; // model to rig space diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 86f5bd69a5..b8448e389d 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1311,7 +1311,7 @@ glm::quat Avatar::getAbsoluteJointRotationInObjectFrame(int index) const { case CAMERA_MATRIX_INDEX: { glm::quat rotation; if (_skeletonModel && _skeletonModel->isActive()) { - int headJointIndex = _skeletonModel->getFBXGeometry().headJointIndex; + int headJointIndex = _skeletonModel->getHFMGeometry().headJointIndex; if (headJointIndex >= 0) { _skeletonModel->getAbsoluteJointRotationInRigFrame(headJointIndex, rotation); } @@ -1360,7 +1360,7 @@ glm::vec3 Avatar::getAbsoluteJointTranslationInObjectFrame(int index) const { case CAMERA_MATRIX_INDEX: { glm::vec3 translation; if (_skeletonModel && _skeletonModel->isActive()) { - int headJointIndex = _skeletonModel->getFBXGeometry().headJointIndex; + int headJointIndex = _skeletonModel->getHFMGeometry().headJointIndex; if (headJointIndex >= 0) { _skeletonModel->getAbsoluteJointTranslationInRigFrame(headJointIndex, translation); } @@ -1416,7 +1416,7 @@ void Avatar::withValidJointIndicesCache(std::function const& worker) con if (!_modelJointsCached) { _modelJointIndicesCache.clear(); if (_skeletonModel && _skeletonModel->isActive()) { - _modelJointIndicesCache = _skeletonModel->getFBXGeometry().jointIndices; + _modelJointIndicesCache = _skeletonModel->getHFMGeometry().jointIndices; _modelJointsCached = true; } } diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index 1ec58fd704..a41cff528b 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -54,7 +54,7 @@ void SkeletonModel::setTextures(const QVariantMap& textures) { } void SkeletonModel::initJointStates() { - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); glm::mat4 modelOffset = glm::scale(_scale) * glm::translate(_offset); _rig.initJointStates(geometry, modelOffset); @@ -96,7 +96,7 @@ void SkeletonModel::initJointStates() { // Called within Model::simulate call, below. void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { assert(!_owningAvatar->isMyAvatar()); - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); Head* head = _owningAvatar->getHead(); @@ -259,22 +259,22 @@ bool SkeletonModel::getRightShoulderPosition(glm::vec3& position) const { } bool SkeletonModel::getHeadPosition(glm::vec3& headPosition) const { - return isActive() && getJointPositionInWorldFrame(getFBXGeometry().headJointIndex, headPosition); + return isActive() && getJointPositionInWorldFrame(getHFMGeometry().headJointIndex, headPosition); } bool SkeletonModel::getNeckPosition(glm::vec3& neckPosition) const { - return isActive() && getJointPositionInWorldFrame(getFBXGeometry().neckJointIndex, neckPosition); + return isActive() && getJointPositionInWorldFrame(getHFMGeometry().neckJointIndex, neckPosition); } bool SkeletonModel::getLocalNeckPosition(glm::vec3& neckPosition) const { - return isActive() && getJointPosition(getFBXGeometry().neckJointIndex, neckPosition); + return isActive() && getJointPosition(getHFMGeometry().neckJointIndex, neckPosition); } bool SkeletonModel::getEyeModelPositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const { if (!isActive()) { return false; } - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); if (getJointPosition(geometry.leftEyeJointIndex, firstEyePosition) && getJointPosition(geometry.rightEyeJointIndex, secondEyePosition)) { @@ -330,7 +330,7 @@ void SkeletonModel::computeBoundingShape() { return; } - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); if (geometry.joints.isEmpty() || geometry.rootJointIndex == -1) { // rootJointIndex == -1 if the avatar model has no skeleton return; @@ -369,7 +369,7 @@ void SkeletonModel::renderBoundingCollisionShapes(RenderArgs* args, gpu::Batch& } bool SkeletonModel::hasSkeleton() { - return isActive() ? getFBXGeometry().rootJointIndex != -1 : false; + return isActive() ? getHFMGeometry().rootJointIndex != -1 : false; } void SkeletonModel::onInvalidate() { diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h index d82fce7412..6c533a5941 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h @@ -41,10 +41,10 @@ public: void updateAttitude(const glm::quat& orientation); /// Returns the index of the left hand joint, or -1 if not found. - int getLeftHandJointIndex() const { return isActive() ? getFBXGeometry().leftHandJointIndex : -1; } + int getLeftHandJointIndex() const { return isActive() ? getHFMGeometry().leftHandJointIndex : -1; } /// Returns the index of the right hand joint, or -1 if not found. - int getRightHandJointIndex() const { return isActive() ? getFBXGeometry().rightHandJointIndex : -1; } + int getRightHandJointIndex() const { return isActive() ? getHFMGeometry().rightHandJointIndex : -1; } bool getLeftGrabPosition(glm::vec3& position) const; bool getRightGrabPosition(glm::vec3& position) const; diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index b90082d969..0b76c275d4 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -206,7 +206,7 @@ void FBXBaker::importScene() { } #endif - _geometry = reader.extractFBXGeometry({}, _modelURL.toString()); + _geometry = reader.extractHFMGeometry({}, _modelURL.toString()); _textureContentMap = reader._textureContent; } @@ -329,7 +329,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { for (FBXNode& textureChild : object->children) { if (textureChild.name == "RelativeFilename") { - QString fbxTextureFileName { textureChild.properties.at(0).toString() }; + QString hfmTextureFileName { textureChild.properties.at(0).toString() }; // grab the ID for this texture so we can figure out the // texture type from the loaded materials @@ -337,7 +337,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { auto textureType = textureTypes[textureID]; // Compress the texture information and return the new filename to be added into the FBX scene - auto bakedTextureFile = compressTexture(fbxTextureFileName, textureType); + auto bakedTextureFile = compressTexture(hfmTextureFileName, textureType); // If no errors or warnings have occurred during texture compression add the filename to the FBX scene if (!bakedTextureFile.isNull()) { diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 9d41209d4c..8edaf91c79 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -53,7 +53,7 @@ private: void rewriteAndBakeSceneModels(); void rewriteAndBakeSceneTextures(); - FBXGeometry* _geometry; + HFMGeometry* _geometry; QHash _textureNameMatchCount; QHash _remappedTexturePaths; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 75e10c54ab..ca352cebae 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -75,7 +75,7 @@ void ModelBaker::abort() { } } -bool ModelBaker::compressMesh(FBXMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback) { +bool ModelBaker::compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback) { if (mesh.wasCompressed) { handleError("Cannot re-bake a file that contains compressed mesh"); return false; diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 1fd77ab761..cda4478b1d 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -39,7 +39,7 @@ public: const QString& bakedOutputDirectory, const QString& originalOutputDirectory = ""); virtual ~ModelBaker(); - bool compressMesh(FBXMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr); + bool compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr); QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE); virtual void setWasAborted(bool wasAborted) override; diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index cf62bc4fa8..e9130e3fbd 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -153,7 +153,7 @@ void OBJBaker::bakeOBJ() { checkIfTexturesFinished(); } -void OBJBaker::createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry) { +void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMGeometry& geometry) { // Generating FBX Header Node FBXNode headerNode; headerNode.name = FBX_HEADER_EXTENSION; @@ -235,7 +235,7 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry) { auto size = meshParts.size(); for (int i = 0; i < size; i++) { QString material = meshParts[i].materialID; - FBXMaterial currentMaterial = geometry.materials[material]; + HFMMaterial currentMaterial = geometry.materials[material]; if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) { auto textureID = nextNodeID(); _mapTextureMaterial.emplace_back(textureID, i); @@ -325,12 +325,12 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry) { } // Set properties for material nodes -void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, FBXGeometry& geometry) { +void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMGeometry& geometry) { auto materialID = nextNodeID(); _materialIDs.push_back(materialID); materialNode.properties = { materialID, material, MESH }; - FBXMaterial currentMaterial = geometry.materials[material]; + HFMMaterial currentMaterial = geometry.materials[material]; // Setting the hierarchy: Material -> Properties70 -> P -> Properties FBXNode properties70Node; diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index 8e49692d35..875a500129 100644 --- a/libraries/baking/src/OBJBaker.h +++ b/libraries/baking/src/OBJBaker.h @@ -39,8 +39,8 @@ private slots: private: void loadOBJ(); - void createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry); - void setMaterialNodeProperties(FBXNode& materialNode, QString material, FBXGeometry& geometry); + void createFBXNodeTree(FBXNode& rootNode, HFMGeometry& geometry); + void setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMGeometry& geometry); NodeID nextNodeID() { return _nodeID++; } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index c6337dc872..c36f60600f 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -268,7 +268,7 @@ EntityItemProperties RenderableModelEntityItem::getProperties(const EntityProper if (model->isLoaded()) { // TODO: improve naturalDimensions in the future, // for now we've added this hack for setting natural dimensions of models - Extents meshExtents = model->getFBXGeometry().getUnscaledMeshExtents(); + Extents meshExtents = model->getHFMGeometry().getUnscaledMeshExtents(); properties.setNaturalDimensions(meshExtents.maximum - meshExtents.minimum); properties.calculateNaturalPosition(meshExtents.minimum, meshExtents.maximum); } @@ -403,7 +403,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // should never fall in here when collision model not fully loaded // TODO: assert that all geometries exist and are loaded //assert(_model && _model->isLoaded() && _compoundShapeResource && _compoundShapeResource->isLoaded()); - const FBXGeometry& collisionGeometry = _compoundShapeResource->getFBXGeometry(); + const HFMGeometry& collisionGeometry = _compoundShapeResource->getHFMGeometry(); ShapeInfo::PointCollection& pointCollection = shapeInfo.getPointCollection(); pointCollection.clear(); @@ -411,15 +411,15 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // the way OBJ files get read, each section under a "g" line is its own meshPart. We only expect // to find one actual "mesh" (with one or more meshParts in it), but we loop over the meshes, just in case. - foreach (const FBXMesh& mesh, collisionGeometry.meshes) { + foreach (const HFMMesh& mesh, collisionGeometry.meshes) { // each meshPart is a convex hull - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { pointCollection.push_back(QVector()); ShapeInfo::PointList& pointsInPart = pointCollection[i]; // run through all the triangles and (uniquely) add each point to the hull uint32_t numIndices = (uint32_t)meshPart.triangleIndices.size(); - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices % TRIANGLE_STRIDE == 0); numIndices -= numIndices % TRIANGLE_STRIDE; // WORKAROUND lack of sanity checking in FBXReader @@ -440,7 +440,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // run through all the quads and (uniquely) add each point to the hull numIndices = (uint32_t)meshPart.quadIndices.size(); - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices % QUAD_STRIDE == 0); numIndices -= numIndices % QUAD_STRIDE; // WORKAROUND lack of sanity checking in FBXReader @@ -478,7 +478,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // to the visual model and apply them to the collision model (without regard for the // collision model's extents). - glm::vec3 scaleToFit = dimensions / model->getFBXGeometry().getUnscaledMeshExtents().size(); + glm::vec3 scaleToFit = dimensions / model->getHFMGeometry().getUnscaledMeshExtents().size(); // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. glm::vec3 registrationOffset = dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint()); @@ -498,14 +498,14 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // compute meshPart local transforms QVector localTransforms; - const FBXGeometry& fbxGeometry = model->getFBXGeometry(); - int numFbxMeshes = fbxGeometry.meshes.size(); + const HFMGeometry& hfmGeometry = model->getHFMGeometry(); + int numHFMMeshes = hfmGeometry.meshes.size(); int totalNumVertices = 0; glm::mat4 invRegistraionOffset = glm::translate(dimensions * (getRegistrationPoint() - ENTITY_ITEM_DEFAULT_REGISTRATION_POINT)); - for (int i = 0; i < numFbxMeshes; i++) { - const FBXMesh& mesh = fbxGeometry.meshes.at(i); + for (int i = 0; i < numHFMMeshes; i++) { + const HFMMesh& mesh = hfmGeometry.meshes.at(i); if (mesh.clusters.size() > 0) { - const FBXCluster& cluster = mesh.clusters.at(0); + const HFMCluster& cluster = mesh.clusters.at(0); auto jointMatrix = model->getRig().getJointTransform(cluster.jointIndex); // we backtranslate by the registration offset so we can apply that offset to the shapeInfo later localTransforms.push_back(invRegistraionOffset * jointMatrix * cluster.inverseBindMatrix); @@ -524,10 +524,10 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { std::vector> meshes; if (type == SHAPE_TYPE_SIMPLE_COMPOUND) { - auto& fbxMeshes = _compoundShapeResource->getFBXGeometry().meshes; - meshes.reserve(fbxMeshes.size()); - for (auto& fbxMesh : fbxMeshes) { - meshes.push_back(fbxMesh._mesh); + auto& hfmMeshes = _compoundShapeResource->getHFMGeometry().meshes; + meshes.reserve(hfmMeshes.size()); + for (auto& hfmMesh : hfmMeshes) { + meshes.push_back(hfmMesh._mesh); } } else { meshes = model->getGeometry()->getMeshes(); @@ -594,7 +594,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { while (partItr != parts.cend()) { auto numIndices = partItr->_numIndices; if (partItr->_topology == graphics::Mesh::TRIANGLES) { - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices % TRIANGLE_STRIDE == 0); numIndices -= numIndices % TRIANGLE_STRIDE; // WORKAROUND lack of sanity checking in FBXReader @@ -605,7 +605,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { ++indexItr; } } else if (partItr->_topology == graphics::Mesh::TRIANGLE_STRIP) { - // TODO: resurrect assert after we start sanitizing FBXMesh higher up + // TODO: resurrect assert after we start sanitizing HFMMesh higher up //assert(numIndices > 2); uint32_t approxNumIndices = TRIANGLE_STRIDE * numIndices; @@ -651,7 +651,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { std::set uniqueIndices; auto numIndices = partItr->_numIndices; if (partItr->_topology == graphics::Mesh::TRIANGLES) { - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices% TRIANGLE_STRIDE == 0); numIndices -= numIndices % TRIANGLE_STRIDE; // WORKAROUND lack of sanity checking in FBXReader @@ -662,7 +662,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { ++indexItr; } } else if (partItr->_topology == graphics::Mesh::TRIANGLE_STRIP) { - // TODO: resurrect assert after we start sanitizing FBXMesh higher up + // TODO: resurrect assert after we start sanitizing HFMMesh higher up //assert(numIndices > TRIANGLE_STRIDE - 1); auto indexItr = indices.cbegin() + partItr->_startIndex; @@ -755,7 +755,7 @@ int RenderableModelEntityItem::avatarJointIndex(int modelJointIndex) { bool RenderableModelEntityItem::contains(const glm::vec3& point) const { auto model = getModel(); if (EntityItem::contains(point) && model && _compoundShapeResource && _compoundShapeResource->isLoaded()) { - return _compoundShapeResource->getFBXGeometry().convexHullContains(worldToEntity(point)); + return _compoundShapeResource->getHFMGeometry().convexHullContains(worldToEntity(point)); } return false; @@ -1135,7 +1135,7 @@ void ModelEntityRenderer::animate(const TypedEntityPointer& entity) { QVector jointsData; - const QVector& frames = _animation->getFramesReference(); // NOTE: getFrames() is too heavy + const QVector& frames = _animation->getFramesReference(); // NOTE: getFrames() is too heavy int frameCount = frames.size(); if (frameCount <= 0) { return; @@ -1160,10 +1160,10 @@ void ModelEntityRenderer::animate(const TypedEntityPointer& entity) { } QStringList animationJointNames = _animation->getGeometry().getJointNames(); - auto& fbxJoints = _animation->getGeometry().joints; + auto& hfmJoints = _animation->getGeometry().joints; - auto& originalFbxJoints = _model->getFBXGeometry().joints; - auto& originalFbxIndices = _model->getFBXGeometry().jointIndices; + auto& originalHFMJoints = _model->getHFMGeometry().joints; + auto& originalHFMIndices = _model->getHFMGeometry().jointIndices; bool allowTranslation = entity->getAnimationAllowTranslation(); @@ -1182,22 +1182,22 @@ void ModelEntityRenderer::animate(const TypedEntityPointer& entity) { translationMat = glm::translate(translations[index]); } } else if (index < animationJointNames.size()) { - QString jointName = fbxJoints[index].name; // Pushing this here so its not done on every entity, with the exceptions of those allowing for translation - if (originalFbxIndices.contains(jointName)) { + QString jointName = hfmJoints[index].name; // Pushing this here so its not done on every entity, with the exceptions of those allowing for translation + if (originalHFMIndices.contains(jointName)) { // Making sure the joint names exist in the original model the animation is trying to apply onto. If they do, then remap and get it's translation. - int remappedIndex = originalFbxIndices[jointName] - 1; // JointIndeces seem to always start from 1 and the found index is always 1 higher than actual. - translationMat = glm::translate(originalFbxJoints[remappedIndex].translation); + int remappedIndex = originalHFMIndices[jointName] - 1; // JointIndeces seem to always start from 1 and the found index is always 1 higher than actual. + translationMat = glm::translate(originalHFMJoints[remappedIndex].translation); } } glm::mat4 rotationMat; if (index < rotations.size()) { - rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * rotations[index] * fbxJoints[index].postRotation); + rotationMat = glm::mat4_cast(hfmJoints[index].preRotation * rotations[index] * hfmJoints[index].postRotation); } else { - rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * fbxJoints[index].postRotation); + rotationMat = glm::mat4_cast(hfmJoints[index].preRotation * hfmJoints[index].postRotation); } - glm::mat4 finalMat = (translationMat * fbxJoints[index].preTransform * - rotationMat * fbxJoints[index].postTransform); + glm::mat4 finalMat = (translationMat * hfmJoints[index].preTransform * + rotationMat * hfmJoints[index].postTransform); auto& jointData = jointsData[j]; jointData.translation = extractTranslation(finalMat); jointData.translationSet = true; diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index fdebb16bc8..8051dbafea 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -70,8 +70,8 @@ public: }; -/// A single blendshape extracted from an FBX document. -class FBXBlendshape { +/// A single blendshape. +class HFMBlendshape { public: QVector indices; QVector vertices; @@ -79,19 +79,19 @@ public: QVector tangents; }; -struct FBXJointShapeInfo { - // same units and frame as FBXJoint.translation +struct HFMJointShapeInfo { + // same units and frame as HFMJoint.translation glm::vec3 avgPoint; std::vector dots; std::vector points; std::vector debugLines; }; -/// A single joint (transformation node) extracted from an FBX document. -class FBXJoint { +/// A single joint (transformation node). +class HFMJoint { public: - FBXJointShapeInfo shapeInfo; + HFMJointShapeInfo shapeInfo; QVector freeLineage; bool isFree; int parentIndex; @@ -126,8 +126,8 @@ public: }; -/// A single binding to a joint in an FBX document. -class FBXCluster { +/// A single binding to a joint. +class HFMCluster { public: int jointIndex; @@ -137,8 +137,8 @@ public: const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; -/// A texture map in an FBX document. -class FBXTexture { +/// A texture map. +class HFMTexture { public: QString id; QString name; @@ -156,7 +156,7 @@ public: }; /// A single part of a mesh (with the same material). -class FBXMeshPart { +class HFMMeshPart { public: QVector quadIndices; // original indices from the FBX mesh @@ -166,10 +166,10 @@ public: QString materialID; }; -class FBXMaterial { +class HFMMaterial { public: - FBXMaterial() {}; - FBXMaterial(const glm::vec3& diffuseColor, const glm::vec3& specularColor, const glm::vec3& emissiveColor, + HFMMaterial() {}; + HFMMaterial(const glm::vec3& diffuseColor, const glm::vec3& specularColor, const glm::vec3& emissiveColor, float shininess, float opacity) : diffuseColor(diffuseColor), specularColor(specularColor), @@ -203,17 +203,17 @@ public: QString shadingModel; graphics::MaterialPointer _material; - FBXTexture normalTexture; - FBXTexture albedoTexture; - FBXTexture opacityTexture; - FBXTexture glossTexture; - FBXTexture roughnessTexture; - FBXTexture specularTexture; - FBXTexture metallicTexture; - FBXTexture emissiveTexture; - FBXTexture occlusionTexture; - FBXTexture scatteringTexture; - FBXTexture lightmapTexture; + HFMTexture normalTexture; + HFMTexture albedoTexture; + HFMTexture opacityTexture; + HFMTexture glossTexture; + HFMTexture roughnessTexture; + HFMTexture specularTexture; + HFMTexture metallicTexture; + HFMTexture emissiveTexture; + HFMTexture occlusionTexture; + HFMTexture scatteringTexture; + HFMTexture lightmapTexture; glm::vec2 lightmapParams{ 0.0f, 1.0f }; @@ -232,10 +232,10 @@ public: }; /// A single mesh (with optional blendshapes) extracted from an FBX document. -class FBXMesh { +class HFMMesh { public: - QVector parts; + QVector parts; QVector vertices; QVector normals; @@ -247,12 +247,12 @@ public: QVector clusterWeights; QVector originalIndices; - QVector clusters; + QVector clusters; Extents meshExtents; glm::mat4 modelTransform; - QVector blendshapes; + QVector blendshapes; unsigned int meshIndex; // the order the meshes appeared in the object file @@ -265,7 +265,7 @@ public: class ExtractedMesh { public: - FBXMesh mesh; + HFMMesh mesh; QMultiHash newIndices; QVector > blendshapeIndexMaps; QVector > partMaterialTextures; @@ -278,14 +278,14 @@ public: * @property {Vec3[]} translations */ /// A single animation frame extracted from an FBX document. -class FBXAnimationFrame { +class HFMAnimationFrame { public: QVector rotations; QVector translations; }; -/// A light in an FBX document. -class FBXLight { +/// A light. +class HFMLight { public: QString name; Transform transform; @@ -293,7 +293,7 @@ public: float fogValue; glm::vec3 color; - FBXLight() : + HFMLight() : name(), transform(), intensity(1.0f), @@ -302,26 +302,26 @@ public: {} }; -Q_DECLARE_METATYPE(FBXAnimationFrame) -Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(HFMAnimationFrame) +Q_DECLARE_METATYPE(QVector) /// A set of meshes extracted from an FBX document. -class FBXGeometry { +class HFMGeometry { public: - using Pointer = std::shared_ptr; + using Pointer = std::shared_ptr; QString originalURL; QString author; QString applicationName; ///< the name of the application that generated the model - QVector joints; + QVector joints; QHash jointIndices; ///< 1-based, so as to more easily detect missing indices bool hasSkeletonJoints; - QVector meshes; + QVector meshes; QVector scripts; - QHash materials; + QHash materials; glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file @@ -348,7 +348,7 @@ public: Extents bindExtents; Extents meshExtents; - QVector animationFrames; + QVector animationFrames; int getJointIndex(const QString& name) const { return jointIndices.value(name) - 1; } QStringList getJointNames() const; @@ -368,7 +368,7 @@ public: QList blendshapeChannelNames; }; -Q_DECLARE_METATYPE(FBXGeometry) -Q_DECLARE_METATYPE(FBXGeometry::Pointer) +Q_DECLARE_METATYPE(HFMGeometry) +Q_DECLARE_METATYPE(HFMGeometry::Pointer) #endif // hifi_FBX_h_ diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index dd766f002c..df6abbfdf2 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -40,19 +40,19 @@ using namespace std; -int FBXGeometryPointerMetaTypeId = qRegisterMetaType(); +int HFMGeometryPointerMetaTypeId = qRegisterMetaType(); -QStringList FBXGeometry::getJointNames() const { +QStringList HFMGeometry::getJointNames() const { QStringList names; - foreach (const FBXJoint& joint, joints) { + foreach (const HFMJoint& joint, joints) { names.append(joint.name); } return names; } -bool FBXGeometry::hasBlendedMeshes() const { +bool HFMGeometry::hasBlendedMeshes() const { if (!meshes.isEmpty()) { - foreach (const FBXMesh& mesh, meshes) { + foreach (const HFMMesh& mesh, meshes) { if (!mesh.blendshapes.isEmpty()) { return true; } @@ -61,7 +61,7 @@ bool FBXGeometry::hasBlendedMeshes() const { return false; } -Extents FBXGeometry::getUnscaledMeshExtents() const { +Extents HFMGeometry::getUnscaledMeshExtents() const { const Extents& extents = meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which @@ -74,12 +74,12 @@ Extents FBXGeometry::getUnscaledMeshExtents() const { } // TODO: Move to graphics::Mesh when Sam's ready -bool FBXGeometry::convexHullContains(const glm::vec3& point) const { +bool HFMGeometry::convexHullContains(const glm::vec3& point) const { if (!getUnscaledMeshExtents().containsPoint(point)) { return false; } - auto checkEachPrimitive = [=](FBXMesh& mesh, QVector indices, int primitiveSize) -> bool { + auto checkEachPrimitive = [=](HFMMesh& mesh, QVector indices, int primitiveSize) -> bool { // Check whether the point is "behind" all the primitives. int verticesSize = mesh.vertices.size(); for (int j = 0; @@ -124,16 +124,16 @@ bool FBXGeometry::convexHullContains(const glm::vec3& point) const { return false; } -QString FBXGeometry::getModelNameOfMesh(int meshIndex) const { +QString HFMGeometry::getModelNameOfMesh(int meshIndex) const { if (meshIndicesToModelNames.contains(meshIndex)) { return meshIndicesToModelNames.value(meshIndex); } return QString(); } -int fbxGeometryMetaTypeId = qRegisterMetaType(); -int fbxAnimationFrameMetaTypeId = qRegisterMetaType(); -int fbxAnimationFrameVectorMetaTypeId = qRegisterMetaType >(); +int hfmGeometryMetaTypeId = qRegisterMetaType(); +int hfmAnimationFrameMetaTypeId = qRegisterMetaType(); +int hfmAnimationFrameVectorMetaTypeId = qRegisterMetaType >(); glm::vec3 parseVec3(const QString& string) { @@ -303,7 +303,7 @@ glm::mat4 getGlobalTransform(const QMultiMap& _connectionParen class ExtractedBlendshape { public: QString id; - FBXBlendshape blendshape; + HFMBlendshape blendshape; }; void printNode(const FBXNode& node, int indentLevel) { @@ -346,8 +346,8 @@ void appendModelIDs(const QString& parentID, const QMultiMap& } } -FBXBlendshape extractBlendshape(const FBXNode& object) { - FBXBlendshape blendshape; +HFMBlendshape extractBlendshape(const FBXNode& object) { + HFMBlendshape blendshape; foreach (const FBXNode& data, object.children) { if (data.name == "Indexes") { blendshape.indices = FBXReader::getIntVector(data); @@ -362,9 +362,9 @@ FBXBlendshape extractBlendshape(const FBXNode& object) { return blendshape; } -using IndexAccessor = std::function; +using IndexAccessor = std::function; -static void setTangents(const FBXMesh& mesh, const IndexAccessor& vertexAccessor, int firstIndex, int secondIndex, +static void setTangents(const HFMMesh& mesh, const IndexAccessor& vertexAccessor, int firstIndex, int secondIndex, const QVector& vertices, const QVector& normals, QVector& tangents) { glm::vec3 vertex[2]; glm::vec3 normal; @@ -381,14 +381,14 @@ static void setTangents(const FBXMesh& mesh, const IndexAccessor& vertexAccessor } } -static void createTangents(const FBXMesh& mesh, bool generateFromTexCoords, +static void createTangents(const HFMMesh& mesh, bool generateFromTexCoords, const QVector& vertices, const QVector& normals, QVector& tangents, IndexAccessor accessor) { // if we have a normal map (and texture coordinates), we must compute tangents if (generateFromTexCoords && !mesh.texCoords.isEmpty()) { tangents.resize(vertices.size()); - foreach(const FBXMeshPart& part, mesh.parts) { + foreach(const HFMMeshPart& part, mesh.parts) { for (int i = 0; i < part.quadIndices.size(); i += 4) { setTangents(mesh, accessor, part.quadIndices.at(i), part.quadIndices.at(i + 1), vertices, normals, tangents); setTangents(mesh, accessor, part.quadIndices.at(i + 1), part.quadIndices.at(i + 2), vertices, normals, tangents); @@ -403,27 +403,27 @@ static void createTangents(const FBXMesh& mesh, bool generateFromTexCoords, setTangents(mesh, accessor, part.triangleIndices.at(i + 2), part.triangleIndices.at(i), vertices, normals, tangents); } if ((part.triangleIndices.size() % 3) != 0) { - qCDebug(modelformat) << "Error in extractFBXGeometry part.triangleIndices.size() is not divisible by three "; + qCDebug(modelformat) << "Error in extractHFMGeometry part.triangleIndices.size() is not divisible by three "; } } } } -static void _createBlendShapeTangents(FBXMesh& mesh, bool generateFromTexCoords, FBXBlendshape& blendShape); +static void _createBlendShapeTangents(HFMMesh& mesh, bool generateFromTexCoords, HFMBlendshape& blendShape); -void FBXMesh::createBlendShapeTangents(bool generateTangents) { +void HFMMesh::createBlendShapeTangents(bool generateTangents) { for (auto& blendShape : blendshapes) { _createBlendShapeTangents(*this, generateTangents, blendShape); } } -void FBXMesh::createMeshTangents(bool generateFromTexCoords) { - FBXMesh& mesh = *this; +void HFMMesh::createMeshTangents(bool generateFromTexCoords) { + HFMMesh& mesh = *this; // This is the only workaround I've found to trick the compiler into understanding that mesh.tangents isn't // const in the lambda function. auto& tangents = mesh.tangents; createTangents(mesh, generateFromTexCoords, mesh.vertices, mesh.normals, mesh.tangents, - [&](const FBXMesh& mesh, int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec3& outNormal) { + [&](const HFMMesh& mesh, int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec3& outNormal) { outVertices[0] = mesh.vertices[firstIndex]; outVertices[1] = mesh.vertices[secondIndex]; outNormal = mesh.normals[firstIndex]; @@ -431,7 +431,7 @@ void FBXMesh::createMeshTangents(bool generateFromTexCoords) { }); } -static void _createBlendShapeTangents(FBXMesh& mesh, bool generateFromTexCoords, FBXBlendshape& blendShape) { +static void _createBlendShapeTangents(HFMMesh& mesh, bool generateFromTexCoords, HFMBlendshape& blendShape) { // Create lookup to get index in blend shape from vertex index in mesh std::vector reverseIndices; reverseIndices.resize(mesh.vertices.size()); @@ -443,7 +443,7 @@ static void _createBlendShapeTangents(FBXMesh& mesh, bool generateFromTexCoords, } createTangents(mesh, generateFromTexCoords, blendShape.vertices, blendShape.normals, blendShape.tangents, - [&](const FBXMesh& mesh, int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec3& outNormal) { + [&](const HFMMesh& mesh, int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec3& outNormal) { const auto index1 = reverseIndices[firstIndex]; const auto index2 = reverseIndices[secondIndex]; @@ -481,7 +481,7 @@ void addBlendshapes(const ExtractedBlendshape& extracted, const QList& blendshapeIndexMap = extractedMesh.blendshapeIndexMaps[index.first]; for (int i = 0; i < extracted.blendshape.indices.size(); i++) { int oldIndex = extracted.blendshape.indices.at(i); @@ -539,7 +539,7 @@ public: QVector values; }; -bool checkMaterialsHaveTextures(const QHash& materials, +bool checkMaterialsHaveTextures(const QHash& materials, const QHash& textureFilenames, const QMultiMap& _connectionChildMap) { foreach (const QString& materialID, materials.keys()) { foreach (const QString& childID, _connectionChildMap.values(materialID)) { @@ -569,8 +569,8 @@ int matchTextureUVSetToAttributeChannel(const QString& texUVSetName, const QHash } -FBXLight extractLight(const FBXNode& object) { - FBXLight light; +HFMLight extractLight(const FBXNode& object) { + HFMLight light; foreach (const FBXNode& subobject, object.children) { QString childname = QString(subobject.name); if (subobject.name == "Properties70") { @@ -615,7 +615,7 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { return filepath.mid(filepath.lastIndexOf('/') + 1); } -FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) { +HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; QMap meshes; QHash modelIDsToNames; @@ -636,7 +636,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS QHash yComponents; QHash zComponents; - std::map lights; + std::map lights; QVariantHash joints = mapping.value("joint").toHash(); QString jointEyeLeftName = processID(getString(joints.value("jointEyeLeft", "jointEyeLeft"))); @@ -689,8 +689,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS #if defined(DEBUG_FBXREADER) int unknown = 0; #endif - FBXGeometry* geometryPtr = new FBXGeometry; - FBXGeometry& geometry = *geometryPtr; + HFMGeometry* geometryPtr = new HFMGeometry; + HFMGeometry& geometry = *geometryPtr; geometry.originalURL = url; @@ -944,7 +944,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS lightprop = vprop.toString(); } - FBXLight light = extractLight(object); + HFMLight light = extractLight(object); } } } else { @@ -1102,7 +1102,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS _textureContent.insert(filepath, content); } } else if (object.name == "Material") { - FBXMaterial material; + HFMMaterial material; material.name = (object.properties.at(1).toString()); foreach (const FBXNode& subobject, object.children) { bool properties = false; @@ -1255,7 +1255,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS #endif } material.materialID = getID(object.properties); - _fbxMaterials.insert(material.materialID, material); + _hfmMaterials.insert(material.materialID, material); } else if (object.name == "NodeAttribute") { @@ -1276,7 +1276,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS if (!attributetype.isEmpty()) { if (attributetype == "Light") { - FBXLight light = extractLight(object); + HFMLight light = extractLight(object); lights[attribID] = light; } } @@ -1345,7 +1345,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS QString parentID = getID(connection.properties, 2); ooChildToParent.insert(childID, parentID); if (!hifiGlobalNodeID.isEmpty() && (parentID == hifiGlobalNodeID)) { - std::map< QString, FBXLight >::iterator lightIt = lights.find(childID); + std::map< QString, HFMLight >::iterator lightIt = lights.find(childID); if (lightIt != lights.end()) { _lightmapLevel = (*lightIt).second.intensity; if (_lightmapLevel <= 0.0f) { @@ -1504,7 +1504,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS frameCount = qMax(frameCount, curve.values.size()); } for (int i = 0; i < frameCount; i++) { - FBXAnimationFrame frame; + HFMAnimationFrame frame; frame.rotations.resize(modelIDs.size()); frame.translations.resize(modelIDs.size()); geometry.animationFrames.append(frame); @@ -1515,7 +1515,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS geometry.hasSkeletonJoints = false; foreach (const QString& modelID, modelIDs) { const FBXModel& model = models[modelID]; - FBXJoint joint; + HFMJoint joint; joint.isFree = freeJoints.contains(model.name); joint.parentIndex = model.parentIndex; @@ -1553,7 +1553,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS joint.distanceToParent = 0.0f; } else { - const FBXJoint& parentJoint = geometry.joints.at(joint.parentIndex); + const HFMJoint& parentJoint = geometry.joints.at(joint.parentIndex); joint.transform = parentJoint.transform * glm::translate(joint.translation) * joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform; joint.inverseDefaultRotation = glm::inverse(combinedRotation) * parentJoint.inverseDefaultRotation; @@ -1631,7 +1631,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS geometry.meshExtents.reset(); // Create the Material Library - consolidateFBXMaterials(mapping); + consolidateHFMMaterials(mapping); // We can't allow the scaling of a given image to different sizes, because the hash used for the KTX cache is based on the original image // Allowing scaling of the same image to different sizes would cause different KTX files to target the same cache key @@ -1643,7 +1643,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // 33 - 128 textures --> 512 // etc... QSet uniqueTextures; - for (auto& material : _fbxMaterials) { + for (auto& material : _hfmMaterials) { material.getTextureNames(uniqueTextures); } int numTextures = uniqueTextures.size(); @@ -1659,15 +1659,15 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } while (numTextureThreshold < numTextures && maxWidth > MIN_MIP_TEXTURE_WIDTH); qCDebug(modelformat) << "Capped square texture width =" << maxWidth << "for model" << url << "with" << numTextures << "textures"; - for (auto& material : _fbxMaterials) { + for (auto& material : _hfmMaterials) { material.setMaxNumPixelsPerTexture(maxWidth * maxWidth); } } #endif - geometry.materials = _fbxMaterials; + geometry.materials = _hfmMaterials; // see if any materials have texture children - bool materialsHaveTextures = checkMaterialsHaveTextures(_fbxMaterials, _textureFilenames, _connectionChildMap); + bool materialsHaveTextures = checkMaterialsHaveTextures(_hfmMaterials, _textureFilenames, _connectionChildMap); for (QMap::iterator it = meshes.begin(); it != meshes.end(); it++) { ExtractedMesh& extracted = it.value(); @@ -1698,13 +1698,13 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS for (int i = children.size() - 1; i >= 0; i--) { const QString& childID = children.at(i); - if (_fbxMaterials.contains(childID)) { + if (_hfmMaterials.contains(childID)) { // the pure material associated with this part - FBXMaterial material = _fbxMaterials.value(childID); + HFMMaterial material = _hfmMaterials.value(childID); for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { if (extracted.partMaterialTextures.at(j).first == materialIndex) { - FBXMeshPart& part = extracted.mesh.parts[j]; + HFMMeshPart& part = extracted.mesh.parts[j]; part.materialID = material.materialID; generateTangents |= material.needTangentSpace(); } @@ -1713,7 +1713,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS materialIndex++; } else if (_textureFilenames.contains(childID)) { - FBXTexture texture = getTexture(childID); + HFMTexture texture = getTexture(childID); for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { int partTexture = extracted.partMaterialTextures.at(j).second; if (partTexture == textureIndex && !(partTexture == 0 && materialsHaveTextures)) { @@ -1736,34 +1736,34 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS if (!clusters.contains(clusterID)) { continue; } - FBXCluster fbxCluster; + HFMCluster hfmCluster; const Cluster& cluster = clusters[clusterID]; clusterIDs.append(clusterID); // see http://stackoverflow.com/questions/13566608/loading-skinning-information-from-fbx for a discussion // of skinning information in FBX QString jointID = _connectionChildMap.value(clusterID); - fbxCluster.jointIndex = modelIDs.indexOf(jointID); - if (fbxCluster.jointIndex == -1) { + hfmCluster.jointIndex = modelIDs.indexOf(jointID); + if (hfmCluster.jointIndex == -1) { qCDebug(modelformat) << "Joint not in model list: " << jointID; - fbxCluster.jointIndex = 0; + hfmCluster.jointIndex = 0; } - fbxCluster.inverseBindMatrix = glm::inverse(cluster.transformLink) * modelTransform; + hfmCluster.inverseBindMatrix = glm::inverse(cluster.transformLink) * modelTransform; // slam bottom row to (0, 0, 0, 1), we KNOW this is not a perspective matrix and // sometimes floating point fuzz can be introduced after the inverse. - fbxCluster.inverseBindMatrix[0][3] = 0.0f; - fbxCluster.inverseBindMatrix[1][3] = 0.0f; - fbxCluster.inverseBindMatrix[2][3] = 0.0f; - fbxCluster.inverseBindMatrix[3][3] = 1.0f; + hfmCluster.inverseBindMatrix[0][3] = 0.0f; + hfmCluster.inverseBindMatrix[1][3] = 0.0f; + hfmCluster.inverseBindMatrix[2][3] = 0.0f; + hfmCluster.inverseBindMatrix[3][3] = 1.0f; - fbxCluster.inverseBindTransform = Transform(fbxCluster.inverseBindMatrix); + hfmCluster.inverseBindTransform = Transform(hfmCluster.inverseBindMatrix); - extracted.mesh.clusters.append(fbxCluster); + extracted.mesh.clusters.append(hfmCluster); // override the bind rotation with the transform link - FBXJoint& joint = geometry.joints[fbxCluster.jointIndex]; + HFMJoint& joint = geometry.joints[hfmCluster.jointIndex]; joint.inverseBindRotation = glm::inverse(extractRotation(cluster.transformLink)); joint.bindTransform = cluster.transformLink; joint.bindTransformFoundInCluster = true; @@ -1776,7 +1776,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // if we don't have a skinned joint, parent to the model itself if (extracted.mesh.clusters.isEmpty()) { - FBXCluster cluster; + HFMCluster cluster; cluster.jointIndex = modelIDs.indexOf(modelID); if (cluster.jointIndex == -1) { qCDebug(modelformat) << "Model not in model list: " << modelID; @@ -1786,7 +1786,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } // whether we're skinned depends on how many clusters are attached - const FBXCluster& firstFBXCluster = extracted.mesh.clusters.at(0); + const HFMCluster& firstHFMCluster = extracted.mesh.clusters.at(0); glm::mat4 inverseModelTransform = glm::inverse(modelTransform); if (clusterIDs.size() > 1) { // this is a multi-mesh joint @@ -1799,9 +1799,9 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS for (int i = 0; i < clusterIDs.size(); i++) { QString clusterID = clusterIDs.at(i); const Cluster& cluster = clusters[clusterID]; - const FBXCluster& fbxCluster = extracted.mesh.clusters.at(i); - int jointIndex = fbxCluster.jointIndex; - FBXJoint& joint = geometry.joints[jointIndex]; + const HFMCluster& hfmCluster = extracted.mesh.clusters.at(i); + int jointIndex = hfmCluster.jointIndex; + HFMJoint& joint = geometry.joints[jointIndex]; glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform; glm::vec3 boneEnd = extractTranslation(transformJointToMesh); glm::vec3 boneBegin = boneEnd; @@ -1881,8 +1881,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } } else { // this is a single-mesh joint - int jointIndex = firstFBXCluster.jointIndex; - FBXJoint& joint = geometry.joints[jointIndex]; + int jointIndex = firstHFMCluster.jointIndex; + HFMJoint& joint = geometry.joints[jointIndex]; // transform cluster vertices to joint-frame and save for later glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; @@ -1924,7 +1924,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // now that all joints have been scanned compute a k-Dop bounding volume of mesh for (int i = 0; i < geometry.joints.size(); ++i) { - FBXJoint& joint = geometry.joints[i]; + HFMJoint& joint = geometry.joints[i]; // NOTE: points are in joint-frame ShapeVertices& points = shapeVertices.at(i); @@ -1994,13 +1994,13 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS return geometryPtr; } -FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { +HFMGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { QBuffer buffer(const_cast(&model)); buffer.open(QIODevice::ReadOnly); return readFBX(&buffer, mapping, url, loadLightmaps, lightmapLevel); } -FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { +HFMGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { FBXReader reader; reader._rootNode = FBXReader::parseFBX(device); reader._loadLightmaps = loadLightmaps; @@ -2008,5 +2008,5 @@ FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QStri qCDebug(modelformat) << "Reading FBX: " << url; - return reader.extractFBXGeometry(mapping, url); + return reader.extractHFMGeometry(mapping, url); } diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index c391ea6647..f95ba7fe73 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -36,11 +36,11 @@ class FBXNode; /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); +HFMGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); +HFMGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); class TextureParam { public: @@ -103,20 +103,20 @@ class ExtractedMesh; class FBXReader { public: - FBXGeometry* _fbxGeometry; + HFMGeometry* _hfmGeometry; FBXNode _rootNode; static FBXNode parseFBX(QIODevice* device); - FBXGeometry* extractFBXGeometry(const QVariantHash& mapping, const QString& url); + HFMGeometry* extractHFMGeometry(const QVariantHash& mapping, const QString& url); static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true); QHash meshes; - static void buildModelMesh(FBXMesh& extractedMesh, const QString& url); + static void buildModelMesh(HFMMesh& extractedMesh, const QString& url); static glm::vec3 normalizeDirForPacking(const glm::vec3& dir); - FBXTexture getTexture(const QString& textureID); + HFMTexture getTexture(const QString& textureID); QHash _textureNames; // Hashes the original RelativeFilename of textures @@ -142,9 +142,9 @@ public: QHash ambientFactorTextures; QHash occlusionTextures; - QHash _fbxMaterials; + QHash _hfmMaterials; - void consolidateFBXMaterials(const QVariantHash& mapping); + void consolidateHFMMaterials(const QVariantHash& mapping); bool _loadLightmaps = true; float _lightmapOffset = 0.0f; diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index d5902962e5..ff1de30b97 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -27,7 +27,7 @@ #include "ModelFormatLogging.h" -void FBXMaterial::getTextureNames(QSet& textureList) const { +void HFMMaterial::getTextureNames(QSet& textureList) const { if (!normalTexture.isNull()) { textureList.insert(normalTexture.name); } @@ -63,7 +63,7 @@ void FBXMaterial::getTextureNames(QSet& textureList) const { } } -void FBXMaterial::setMaxNumPixelsPerTexture(int maxNumPixels) { +void HFMMaterial::setMaxNumPixelsPerTexture(int maxNumPixels) { normalTexture.maxNumPixels = maxNumPixels; albedoTexture.maxNumPixels = maxNumPixels; opacityTexture.maxNumPixels = maxNumPixels; @@ -77,12 +77,12 @@ void FBXMaterial::setMaxNumPixelsPerTexture(int maxNumPixels) { lightmapTexture.maxNumPixels = maxNumPixels; } -bool FBXMaterial::needTangentSpace() const { +bool HFMMaterial::needTangentSpace() const { return !normalTexture.isNull(); } -FBXTexture FBXReader::getTexture(const QString& textureID) { - FBXTexture texture; +HFMTexture FBXReader::getTexture(const QString& textureID) { + HFMTexture texture; const QByteArray& filepath = _textureFilepaths.value(textureID); texture.content = _textureContent.value(filepath); @@ -123,7 +123,7 @@ FBXTexture FBXReader::getTexture(const QString& textureID) { return texture; } -void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { +void FBXReader::consolidateHFMMaterials(const QVariantHash& mapping) { QString materialMapString = mapping.value("materialMap").toString(); QJsonDocument materialMapDocument = QJsonDocument::fromJson(materialMapString.toUtf8()); @@ -133,16 +133,16 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { qCDebug(modelformat) << "fbx Material Map found but did not produce valid JSON:" << materialMapString; } } - for (QHash::iterator it = _fbxMaterials.begin(); it != _fbxMaterials.end(); it++) { - FBXMaterial& material = (*it); + for (QHash::iterator it = _hfmMaterials.begin(); it != _hfmMaterials.end(); it++) { + HFMMaterial& material = (*it); // Maya is the exporting the shading model and we are trying to use it bool isMaterialLambert = (material.shadingModel.toLower() == "lambert"); // the pure material associated with this part bool detectDifferentUVs = false; - FBXTexture diffuseTexture; - FBXTexture diffuseFactorTexture; + HFMTexture diffuseTexture; + HFMTexture diffuseFactorTexture; QString diffuseTextureID = diffuseTextures.value(material.materialID); QString diffuseFactorTextureID = diffuseFactorTextures.value(material.materialID); @@ -169,7 +169,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { detectDifferentUVs = (diffuseTexture.texcoordSet != 0) || (!diffuseTexture.transform.isIdentity()); } - FBXTexture transparentTexture; + HFMTexture transparentTexture; QString transparentTextureID = transparentTextures.value(material.materialID); // If PBS Material, systematically bind the albedo texture as transparency texture and check for the alpha channel if (material.isPBSMaterial) { @@ -181,7 +181,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { detectDifferentUVs |= (transparentTexture.texcoordSet != 0) || (!transparentTexture.transform.isIdentity()); } - FBXTexture normalTexture; + HFMTexture normalTexture; QString bumpTextureID = bumpTextures.value(material.materialID); QString normalTextureID = normalTextures.value(material.materialID); if (!normalTextureID.isNull()) { @@ -198,7 +198,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { detectDifferentUVs |= (normalTexture.texcoordSet != 0) || (!normalTexture.transform.isIdentity()); } - FBXTexture specularTexture; + HFMTexture specularTexture; QString specularTextureID = specularTextures.value(material.materialID); if (!specularTextureID.isNull()) { specularTexture = getTexture(specularTextureID); @@ -206,7 +206,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { material.specularTexture = specularTexture; } - FBXTexture metallicTexture; + HFMTexture metallicTexture; QString metallicTextureID = metallicTextures.value(material.materialID); if (!metallicTextureID.isNull()) { metallicTexture = getTexture(metallicTextureID); @@ -214,7 +214,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { material.metallicTexture = metallicTexture; } - FBXTexture roughnessTexture; + HFMTexture roughnessTexture; QString roughnessTextureID = roughnessTextures.value(material.materialID); if (!roughnessTextureID.isNull()) { roughnessTexture = getTexture(roughnessTextureID); @@ -222,7 +222,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { detectDifferentUVs |= (roughnessTexture.texcoordSet != 0) || (!roughnessTexture.transform.isIdentity()); } - FBXTexture shininessTexture; + HFMTexture shininessTexture; QString shininessTextureID = shininessTextures.value(material.materialID); if (!shininessTextureID.isNull()) { shininessTexture = getTexture(shininessTextureID); @@ -230,7 +230,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { detectDifferentUVs |= (shininessTexture.texcoordSet != 0) || (!shininessTexture.transform.isIdentity()); } - FBXTexture emissiveTexture; + HFMTexture emissiveTexture; QString emissiveTextureID = emissiveTextures.value(material.materialID); if (!emissiveTextureID.isNull()) { emissiveTexture = getTexture(emissiveTextureID); @@ -245,7 +245,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { } } - FBXTexture occlusionTexture; + HFMTexture occlusionTexture; QString occlusionTextureID = occlusionTextures.value(material.materialID); if (occlusionTextureID.isNull()) { // 2nd chance @@ -265,7 +265,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { lightmapParams.x = _lightmapOffset; lightmapParams.y = _lightmapLevel; - FBXTexture ambientTexture; + HFMTexture ambientTexture; QString ambientTextureID = ambientTextures.value(material.materialID); if (ambientTextureID.isNull()) { // 2nd chance @@ -326,7 +326,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { if (materialOptions.contains("scatteringMap")) { QByteArray scatteringMap = materialOptions.value("scatteringMap").toVariant().toByteArray(); - material.scatteringTexture = FBXTexture(); + material.scatteringTexture = HFMTexture(); material.scatteringTexture.name = material.name + ".scatteringMap"; material.scatteringTexture.filename = scatteringMap; } diff --git a/libraries/fbx/src/FBXReader_Mesh.cpp b/libraries/fbx/src/FBXReader_Mesh.cpp index c9b004c3a8..e098aff99a 100644 --- a/libraries/fbx/src/FBXReader_Mesh.cpp +++ b/libraries/fbx/src/FBXReader_Mesh.cpp @@ -42,9 +42,9 @@ using vec2h = glm::tvec2; -#define FBX_PACK_COLORS 1 +#define HFM_PACK_COLORS 1 -#if FBX_PACK_COLORS +#if HFM_PACK_COLORS using ColorType = glm::uint32; #define FBX_COLOR_ELEMENT gpu::Element::COLOR_RGBA_32 #else @@ -469,7 +469,7 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn QPair materialTexture(materialID, 0); - // grab or setup the FBXMeshPart for the part this face belongs to + // grab or setup the HFMMeshPart for the part this face belongs to int& partIndexPlusOne = materialTextureParts[materialTexture]; if (partIndexPlusOne == 0) { data.extracted.partMaterialTextures.append(materialTexture); @@ -478,7 +478,7 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn } // give the mesh part this index - FBXMeshPart& part = data.extracted.mesh.parts[partIndexPlusOne - 1]; + HFMMeshPart& part = data.extracted.mesh.parts[partIndexPlusOne - 1]; part.triangleIndices.append(firstCorner.value()); part.triangleIndices.append(dracoFace[1].value()); part.triangleIndices.append(dracoFace[2].value()); @@ -511,7 +511,7 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1); partIndex = data.extracted.mesh.parts.size(); } - FBXMeshPart& part = data.extracted.mesh.parts[partIndex - 1]; + HFMMeshPart& part = data.extracted.mesh.parts[partIndex - 1]; if (endIndex - beginIndex == 4) { appendIndex(data, part.quadIndices, beginIndex++, deduplicate); @@ -565,9 +565,9 @@ glm::vec3 FBXReader::normalizeDirForPacking(const glm::vec3& dir) { return dir; } -void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { +void FBXReader::buildModelMesh(HFMMesh& extractedMesh, const QString& url) { unsigned int totalSourceIndices = 0; - foreach(const FBXMeshPart& part, extractedMesh.parts) { + foreach(const HFMMeshPart& part, extractedMesh.parts) { totalSourceIndices += (part.quadTrianglesIndices.size() + part.triangleIndices.size()); } @@ -583,17 +583,17 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { return; } - FBXMesh& fbxMesh = extractedMesh; + HFMMesh& hfmMesh = extractedMesh; graphics::MeshPointer mesh(new graphics::Mesh()); int numVerts = extractedMesh.vertices.size(); - if (!fbxMesh.normals.empty() && fbxMesh.tangents.empty()) { + if (!hfmMesh.normals.empty() && hfmMesh.tangents.empty()) { // Fill with a dummy value to force tangents to be present if there are normals - fbxMesh.tangents.reserve(fbxMesh.normals.size()); - std::fill_n(std::back_inserter(fbxMesh.tangents), fbxMesh.normals.size(), Vectors::UNIT_X); + hfmMesh.tangents.reserve(hfmMesh.normals.size()); + std::fill_n(std::back_inserter(hfmMesh.tangents), hfmMesh.normals.size(), Vectors::UNIT_X); } // Same thing with blend shapes - for (auto& blendShape : fbxMesh.blendshapes) { + for (auto& blendShape : hfmMesh.blendshapes) { if (!blendShape.normals.empty() && blendShape.tangents.empty()) { // Fill with a dummy value to force tangents to be present if there are normals blendShape.tangents.reserve(blendShape.normals.size()); @@ -609,8 +609,8 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { // Normal and tangent are always there together packed in normalized xyz32bits word (times 2) const auto normalElement = FBX_NORMAL_ELEMENT; - const int normalsSize = fbxMesh.normals.size() * normalElement.getSize(); - const int tangentsSize = fbxMesh.tangents.size() * normalElement.getSize(); + const int normalsSize = hfmMesh.normals.size() * normalElement.getSize(); + const int tangentsSize = hfmMesh.tangents.size() * normalElement.getSize(); // If there are normals then there should be tangents assert(normalsSize <= tangentsSize); if (tangentsSize > normalsSize) { @@ -620,22 +620,22 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { // Color attrib const auto colorElement = FBX_COLOR_ELEMENT; - const int colorsSize = fbxMesh.colors.size() * colorElement.getSize(); + const int colorsSize = hfmMesh.colors.size() * colorElement.getSize(); // Texture coordinates are stored in 2 half floats const auto texCoordsElement = gpu::Element(gpu::VEC2, gpu::HALF, gpu::UV); - const int texCoordsSize = fbxMesh.texCoords.size() * texCoordsElement.getSize(); - const int texCoords1Size = fbxMesh.texCoords1.size() * texCoordsElement.getSize(); + const int texCoordsSize = hfmMesh.texCoords.size() * texCoordsElement.getSize(); + const int texCoords1Size = hfmMesh.texCoords1.size() * texCoordsElement.getSize(); // Support for 4 skinning clusters: // 4 Indices are uint8 ideally, uint16 if more than 256. - const auto clusterIndiceElement = (fbxMesh.clusters.size() < UINT8_MAX ? gpu::Element(gpu::VEC4, gpu::UINT8, gpu::XYZW) : gpu::Element(gpu::VEC4, gpu::UINT16, gpu::XYZW)); + const auto clusterIndiceElement = (hfmMesh.clusters.size() < UINT8_MAX ? gpu::Element(gpu::VEC4, gpu::UINT8, gpu::XYZW) : gpu::Element(gpu::VEC4, gpu::UINT16, gpu::XYZW)); // 4 Weights are normalized 16bits const auto clusterWeightElement = gpu::Element(gpu::VEC4, gpu::NUINT16, gpu::XYZW); // Cluster indices and weights must be the same sizes const int NUM_CLUSTERS_PER_VERT = 4; - const int numVertClusters = (fbxMesh.clusterIndices.size() == fbxMesh.clusterWeights.size() ? fbxMesh.clusterIndices.size() / NUM_CLUSTERS_PER_VERT : 0); + const int numVertClusters = (hfmMesh.clusterIndices.size() == hfmMesh.clusterWeights.size() ? hfmMesh.clusterIndices.size() / NUM_CLUSTERS_PER_VERT : 0); const int clusterIndicesSize = numVertClusters * clusterIndiceElement.getSize(); const int clusterWeightsSize = numVertClusters * clusterWeightElement.getSize(); @@ -660,9 +660,9 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { if (normalsSize > 0) { std::vector normalsAndTangents; - normalsAndTangents.reserve(fbxMesh.normals.size() + fbxMesh.tangents.size()); - for (auto normalIt = fbxMesh.normals.constBegin(), tangentIt = fbxMesh.tangents.constBegin(); - normalIt != fbxMesh.normals.constEnd(); + normalsAndTangents.reserve(hfmMesh.normals.size() + hfmMesh.tangents.size()); + for (auto normalIt = hfmMesh.normals.constBegin(), tangentIt = hfmMesh.tangents.constBegin(); + normalIt != hfmMesh.normals.constEnd(); ++normalIt, ++tangentIt) { #if FBX_PACK_NORMALS const auto normal = normalizeDirForPacking(*normalIt); @@ -681,24 +681,24 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { // Pack colors if (colorsSize > 0) { -#if FBX_PACK_COLORS +#if HFM_PACK_COLORS std::vector colors; - colors.reserve(fbxMesh.colors.size()); - for (const auto& color : fbxMesh.colors) { + colors.reserve(hfmMesh.colors.size()); + for (const auto& color : hfmMesh.colors) { colors.push_back(glm::packUnorm4x8(glm::vec4(color, 1.0f))); } vertBuffer->setSubData(colorsOffset, colorsSize, (const gpu::Byte*) colors.data()); #else - vertBuffer->setSubData(colorsOffset, colorsSize, (const gpu::Byte*) fbxMesh.colors.constData()); + vertBuffer->setSubData(colorsOffset, colorsSize, (const gpu::Byte*) hfmMesh.colors.constData()); #endif } // Pack Texcoords 0 and 1 (if exists) if (texCoordsSize > 0) { QVector texCoordData; - texCoordData.reserve(fbxMesh.texCoords.size()); - for (auto& texCoordVec2f : fbxMesh.texCoords) { + texCoordData.reserve(hfmMesh.texCoords.size()); + for (auto& texCoordVec2f : hfmMesh.texCoords) { vec2h texCoordVec2h; texCoordVec2h.x = glm::detail::toFloat16(texCoordVec2f.x); @@ -709,8 +709,8 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { } if (texCoords1Size > 0) { QVector texCoordData; - texCoordData.reserve(fbxMesh.texCoords1.size()); - for (auto& texCoordVec2f : fbxMesh.texCoords1) { + texCoordData.reserve(hfmMesh.texCoords1.size()); + for (auto& texCoordVec2f : hfmMesh.texCoords1) { vec2h texCoordVec2h; texCoordVec2h.x = glm::detail::toFloat16(texCoordVec2f.x); @@ -722,22 +722,22 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { // Clusters data if (clusterIndicesSize > 0) { - if (fbxMesh.clusters.size() < UINT8_MAX) { + if (hfmMesh.clusters.size() < UINT8_MAX) { // yay! we can fit the clusterIndices within 8-bits - int32_t numIndices = fbxMesh.clusterIndices.size(); + int32_t numIndices = hfmMesh.clusterIndices.size(); QVector clusterIndices; clusterIndices.resize(numIndices); for (int32_t i = 0; i < numIndices; ++i) { - assert(fbxMesh.clusterIndices[i] <= UINT8_MAX); - clusterIndices[i] = (uint8_t)(fbxMesh.clusterIndices[i]); + assert(hfmMesh.clusterIndices[i] <= UINT8_MAX); + clusterIndices[i] = (uint8_t)(hfmMesh.clusterIndices[i]); } vertBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (const gpu::Byte*) clusterIndices.constData()); } else { - vertBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (const gpu::Byte*) fbxMesh.clusterIndices.constData()); + vertBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (const gpu::Byte*) hfmMesh.clusterIndices.constData()); } } if (clusterWeightsSize > 0) { - vertBuffer->setSubData(clusterWeightsOffset, clusterWeightsSize, (const gpu::Byte*) fbxMesh.clusterWeights.constData()); + vertBuffer->setSubData(clusterWeightsOffset, clusterWeightsSize, (const gpu::Byte*) hfmMesh.clusterWeights.constData()); } @@ -856,7 +856,7 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { // Index and Part Buffers unsigned int totalIndices = 0; - foreach(const FBXMeshPart& part, extractedMesh.parts) { + foreach(const HFMMeshPart& part, extractedMesh.parts) { totalIndices += (part.quadTrianglesIndices.size() + part.triangleIndices.size()); } @@ -875,7 +875,7 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { if (extractedMesh.parts.size() > 1) { indexNum = 0; } - foreach(const FBXMeshPart& part, extractedMesh.parts) { + foreach(const HFMMeshPart& part, extractedMesh.parts) { graphics::Mesh::Part modelPart(indexNum, 0, 0, graphics::Mesh::TRIANGLES); if (part.quadTrianglesIndices.size()) { diff --git a/libraries/fbx/src/GLTFReader.cpp b/libraries/fbx/src/GLTFReader.cpp index b93dc3541b..05534b5264 100644 --- a/libraries/fbx/src/GLTFReader.cpp +++ b/libraries/fbx/src/GLTFReader.cpp @@ -697,7 +697,7 @@ glm::mat4 GLTFReader::getModelTransform(const GLTFNode& node) { return tmat; } -bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { +bool GLTFReader::buildGeometry(HFMGeometry& geometry, const QUrl& url) { //Build dependencies QVector> nodeDependencies(_file.nodes.size()); @@ -750,10 +750,10 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { for (int i = 0; i < materialIDs.size(); i++) { QString& matid = materialIDs[i]; - geometry.materials[matid] = FBXMaterial(); - FBXMaterial& fbxMaterial = geometry.materials[matid]; - fbxMaterial._material = std::make_shared(); - setFBXMaterial(fbxMaterial, _file.materials[i]); + geometry.materials[matid] = HFMMaterial(); + HFMMaterial& hfmMaterial = geometry.materials[matid]; + hfmMaterial._material = std::make_shared(); + setHFMMaterial(hfmMaterial, _file.materials[i]); } @@ -765,9 +765,9 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { if (node.defined["mesh"]) { qCDebug(modelformat) << "node_transforms" << node.transforms; foreach(auto &primitive, _file.meshes[node.mesh].primitives) { - geometry.meshes.append(FBXMesh()); - FBXMesh& mesh = geometry.meshes[geometry.meshes.size() - 1]; - FBXCluster cluster; + geometry.meshes.append(HFMMesh()); + HFMMesh& mesh = geometry.meshes[geometry.meshes.size() - 1]; + HFMCluster cluster; cluster.jointIndex = 0; cluster.inverseBindMatrix = glm::mat4(1, 0, 0, 0, 0, 1, 0, 0, @@ -775,7 +775,7 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { 0, 0, 0, 1); mesh.clusters.append(cluster); - FBXMeshPart part = FBXMeshPart(); + HFMMeshPart part = HFMMeshPart(); int indicesAccessorIdx = primitive.indices; @@ -910,7 +910,7 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { return true; } -FBXGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping, +HFMGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping, const QUrl& url, bool loadLightmaps, float lightmapLevel) { _url = url; @@ -924,8 +924,8 @@ FBXGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping parseGLTF(model); //_file.dump(); - FBXGeometry* geometryPtr = new FBXGeometry(); - FBXGeometry& geometry = *geometryPtr; + HFMGeometry* geometryPtr = new HFMGeometry(); + HFMGeometry& geometry = *geometryPtr; buildGeometry(geometry, url); @@ -997,8 +997,8 @@ QNetworkReply* GLTFReader::request(QUrl& url, bool isTest) { return netReply; // trying to sync later on. } -FBXTexture GLTFReader::getFBXTexture(const GLTFTexture& texture) { - FBXTexture fbxtex = FBXTexture(); +HFMTexture GLTFReader::getHFMTexture(const GLTFTexture& texture) { + HFMTexture fbxtex = HFMTexture(); fbxtex.texcoordSet = 0; if (texture.defined["source"]) { @@ -1014,7 +1014,7 @@ FBXTexture GLTFReader::getFBXTexture(const GLTFTexture& texture) { return fbxtex; } -void GLTFReader::setFBXMaterial(FBXMaterial& fbxmat, const GLTFMaterial& material) { +void GLTFReader::setHFMMaterial(HFMMaterial& fbxmat, const GLTFMaterial& material) { if (material.defined["name"]) { @@ -1029,17 +1029,17 @@ void GLTFReader::setFBXMaterial(FBXMaterial& fbxmat, const GLTFMaterial& materia } if (material.defined["emissiveTexture"]) { - fbxmat.emissiveTexture = getFBXTexture(_file.textures[material.emissiveTexture]); + fbxmat.emissiveTexture = getHFMTexture(_file.textures[material.emissiveTexture]); fbxmat.useEmissiveMap = true; } if (material.defined["normalTexture"]) { - fbxmat.normalTexture = getFBXTexture(_file.textures[material.normalTexture]); + fbxmat.normalTexture = getHFMTexture(_file.textures[material.normalTexture]); fbxmat.useNormalMap = true; } if (material.defined["occlusionTexture"]) { - fbxmat.occlusionTexture = getFBXTexture(_file.textures[material.occlusionTexture]); + fbxmat.occlusionTexture = getHFMTexture(_file.textures[material.occlusionTexture]); fbxmat.useOcclusionMap = true; } @@ -1050,14 +1050,14 @@ void GLTFReader::setFBXMaterial(FBXMaterial& fbxmat, const GLTFMaterial& materia fbxmat.metallic = material.pbrMetallicRoughness.metallicFactor; } if (material.pbrMetallicRoughness.defined["baseColorTexture"]) { - fbxmat.opacityTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]); - fbxmat.albedoTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]); + fbxmat.opacityTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]); + fbxmat.albedoTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]); fbxmat.useAlbedoMap = true; } if (material.pbrMetallicRoughness.defined["metallicRoughnessTexture"]) { - fbxmat.roughnessTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]); + fbxmat.roughnessTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]); fbxmat.useRoughnessMap = true; - fbxmat.metallicTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]); + fbxmat.metallicTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]); fbxmat.useMetallicMap = true; } if (material.pbrMetallicRoughness.defined["roughnessFactor"]) { @@ -1181,8 +1181,8 @@ void GLTFReader::retriangulate(const QVector& inIndices, const QVector materialMeshIdMap; - QVector fbxMeshParts; + QVector hfmMeshParts; for (int i = 0, meshPartCount = 0; i < mesh.parts.count(); i++, meshPartCount++) { - FBXMeshPart& meshPart = mesh.parts[i]; + HFMMeshPart& meshPart = mesh.parts[i]; FaceGroup faceGroup = faceGroups[meshPartCount]; bool specifiesUV = false; foreach(OBJFace face, faceGroup) { // Go through all of the OBJ faces and determine the number of different materials necessary (each different material will be a unique mesh). // NOTE (trent/mittens 3/30/17): this seems hardcore wasteful and is slowed down a bit by iterating through the face group twice, but it's the best way I've thought of to hack multi-material support in an OBJ into this pipeline. if (!materialMeshIdMap.contains(face.materialName)) { - // Create a new FBXMesh for this material mapping. + // Create a new HFMMesh for this material mapping. materialMeshIdMap.insert(face.materialName, materialMeshIdMap.count()); - fbxMeshParts.append(FBXMeshPart()); - FBXMeshPart& meshPartNew = fbxMeshParts.last(); + hfmMeshParts.append(HFMMeshPart()); + HFMMeshPart& meshPartNew = hfmMeshParts.last(); meshPartNew.quadIndices = QVector(meshPart.quadIndices); // Copy over quad indices [NOTE (trent/mittens, 4/3/17): Likely unnecessary since they go unused anyway]. meshPartNew.quadTrianglesIndices = QVector(meshPart.quadTrianglesIndices); // Copy over quad triangulated indices [NOTE (trent/mittens, 4/3/17): Likely unnecessary since they go unused anyway]. meshPartNew.triangleIndices = QVector(meshPart.triangleIndices); // Copy over triangle indices. @@ -745,14 +745,14 @@ FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m // clean up old mesh parts. int unmodifiedMeshPartCount = mesh.parts.count(); mesh.parts.clear(); - mesh.parts = QVector(fbxMeshParts); + mesh.parts = QVector(hfmMeshParts); for (int i = 0, meshPartCount = 0; i < unmodifiedMeshPartCount; i++, meshPartCount++) { FaceGroup faceGroup = faceGroups[meshPartCount]; // Now that each mesh has been created with its own unique material mappings, fill them with data (vertex data is duplicated, face data is not). foreach(OBJFace face, faceGroup) { - FBXMeshPart& meshPart = mesh.parts[materialMeshIdMap[face.materialName]]; + HFMMeshPart& meshPart = mesh.parts[materialMeshIdMap[face.materialName]]; glm::vec3 v0 = checked_at(vertices, face.vertexIndices[0]); glm::vec3 v1 = checked_at(vertices, face.vertexIndices[1]); @@ -885,38 +885,38 @@ FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m if (!objMaterial.used) { continue; } - geometry.materials[materialID] = FBXMaterial(objMaterial.diffuseColor, + geometry.materials[materialID] = HFMMaterial(objMaterial.diffuseColor, objMaterial.specularColor, objMaterial.emissiveColor, objMaterial.shininess, objMaterial.opacity); - FBXMaterial& fbxMaterial = geometry.materials[materialID]; - fbxMaterial.materialID = materialID; - fbxMaterial._material = std::make_shared(); - graphics::MaterialPointer modelMaterial = fbxMaterial._material; + HFMMaterial& hfmMaterial = geometry.materials[materialID]; + hfmMaterial.materialID = materialID; + hfmMaterial._material = std::make_shared(); + graphics::MaterialPointer modelMaterial = hfmMaterial._material; if (!objMaterial.diffuseTextureFilename.isEmpty()) { - fbxMaterial.albedoTexture.filename = objMaterial.diffuseTextureFilename; + hfmMaterial.albedoTexture.filename = objMaterial.diffuseTextureFilename; } if (!objMaterial.specularTextureFilename.isEmpty()) { - fbxMaterial.specularTexture.filename = objMaterial.specularTextureFilename; + hfmMaterial.specularTexture.filename = objMaterial.specularTextureFilename; } if (!objMaterial.emissiveTextureFilename.isEmpty()) { - fbxMaterial.emissiveTexture.filename = objMaterial.emissiveTextureFilename; + hfmMaterial.emissiveTexture.filename = objMaterial.emissiveTextureFilename; } if (!objMaterial.bumpTextureFilename.isEmpty()) { - fbxMaterial.normalTexture.filename = objMaterial.bumpTextureFilename; - fbxMaterial.normalTexture.isBumpmap = true; - fbxMaterial.bumpMultiplier = objMaterial.bumpTextureOptions.bumpMultiplier; + hfmMaterial.normalTexture.filename = objMaterial.bumpTextureFilename; + hfmMaterial.normalTexture.isBumpmap = true; + hfmMaterial.bumpMultiplier = objMaterial.bumpTextureOptions.bumpMultiplier; } if (!objMaterial.opacityTextureFilename.isEmpty()) { - fbxMaterial.opacityTexture.filename = objMaterial.opacityTextureFilename; + hfmMaterial.opacityTexture.filename = objMaterial.opacityTextureFilename; } - modelMaterial->setEmissive(fbxMaterial.emissiveColor); - modelMaterial->setAlbedo(fbxMaterial.diffuseColor); - modelMaterial->setMetallic(glm::length(fbxMaterial.specularColor)); - modelMaterial->setRoughness(graphics::Material::shininessToRoughness(fbxMaterial.shininess)); + modelMaterial->setEmissive(hfmMaterial.emissiveColor); + modelMaterial->setAlbedo(hfmMaterial.diffuseColor); + modelMaterial->setMetallic(glm::length(hfmMaterial.specularColor)); + modelMaterial->setRoughness(graphics::Material::shininessToRoughness(hfmMaterial.shininess)); bool applyTransparency = false; bool applyShininess = false; @@ -971,7 +971,7 @@ FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m } if (applyTransparency) { - fbxMaterial.opacity = std::max(fbxMaterial.opacity, ILLUMINATION_MODEL_MIN_OPACITY); + hfmMaterial.opacity = std::max(hfmMaterial.opacity, ILLUMINATION_MODEL_MIN_OPACITY); } if (applyShininess) { modelMaterial->setRoughness(ILLUMINATION_MODEL_APPLY_SHININESS); @@ -985,18 +985,18 @@ FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m modelMaterial->setFresnel(glm::vec3(1.0f)); } - modelMaterial->setOpacity(fbxMaterial.opacity); + modelMaterial->setOpacity(hfmMaterial.opacity); } return geometryPtr; } -void fbxDebugDump(const FBXGeometry& fbxgeo) { - qCDebug(modelformat) << "---------------- fbxGeometry ----------------"; +void fbxDebugDump(const HFMGeometry& fbxgeo) { + qCDebug(modelformat) << "---------------- hfmGeometry ----------------"; qCDebug(modelformat) << " hasSkeletonJoints =" << fbxgeo.hasSkeletonJoints; qCDebug(modelformat) << " offset =" << fbxgeo.offset; qCDebug(modelformat) << " meshes.count() =" << fbxgeo.meshes.count(); - foreach (FBXMesh mesh, fbxgeo.meshes) { + foreach (HFMMesh mesh, fbxgeo.meshes) { qCDebug(modelformat) << " vertices.count() =" << mesh.vertices.count(); qCDebug(modelformat) << " colors.count() =" << mesh.colors.count(); qCDebug(modelformat) << " normals.count() =" << mesh.normals.count(); @@ -1014,7 +1014,7 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) { qCDebug(modelformat) << " meshExtents =" << mesh.meshExtents; qCDebug(modelformat) << " modelTransform =" << mesh.modelTransform; qCDebug(modelformat) << " parts.count() =" << mesh.parts.count(); - foreach (FBXMeshPart meshPart, mesh.parts) { + foreach (HFMMeshPart meshPart, mesh.parts) { qCDebug(modelformat) << " quadIndices.count() =" << meshPart.quadIndices.count(); qCDebug(modelformat) << " triangleIndices.count() =" << meshPart.triangleIndices.count(); /* @@ -1031,7 +1031,7 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) { */ } qCDebug(modelformat) << " clusters.count() =" << mesh.clusters.count(); - foreach (FBXCluster cluster, mesh.clusters) { + foreach (HFMCluster cluster, mesh.clusters) { qCDebug(modelformat) << " jointIndex =" << cluster.jointIndex; qCDebug(modelformat) << " inverseBindMatrix =" << cluster.inverseBindMatrix; } @@ -1040,7 +1040,7 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) { qCDebug(modelformat) << " jointIndices =" << fbxgeo.jointIndices; qCDebug(modelformat) << " joints.count() =" << fbxgeo.joints.count(); - foreach (FBXJoint joint, fbxgeo.joints) { + foreach (HFMJoint joint, fbxgeo.joints) { qCDebug(modelformat) << " isFree =" << joint.isFree; qCDebug(modelformat) << " freeLineage" << joint.freeLineage; qCDebug(modelformat) << " parentIndex" << joint.parentIndex; diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index e432a3ea51..edfe6f23e8 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -42,7 +42,7 @@ public: bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors); // Return a set of one or more OBJFaces from this one, in which each is just a triangle. - // Even though FBXMeshPart can handle quads, it would be messy to try to keep track of mixed-size faces, so we treat everything as triangles. + // Even though HFMMeshPart can handle quads, it would be messy to try to keep track of mixed-size faces, so we treat everything as triangles. QVector triangulate(); private: void addFrom(const OBJFace* face, int index); @@ -54,7 +54,7 @@ public: } ; // Materials and references to material names can come in any order, and different mesh parts can refer to the same material. -// Therefore it would get pretty hacky to try to use FBXMeshPart to store these as we traverse the files. +// Therefore it would get pretty hacky to try to use HFMMeshPart to store these as we traverse the files. class OBJMaterial { public: float shininess; @@ -87,13 +87,13 @@ public: QString currentMaterialName; QHash materials; - FBXGeometry::Pointer readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url = QUrl()); + HFMGeometry::Pointer readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url = QUrl()); private: QUrl _url; QHash librariesSeen; - bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, + bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMGeometry& geometry, float& scaleGuess, bool combineParts); void parseMaterialLibrary(QIODevice* device); void parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions); @@ -103,5 +103,5 @@ private: }; // What are these utilities doing here? One is used by fbx loading code in VHACD Utils, and the other a general debugging utility. -void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID); -void fbxDebugDump(const FBXGeometry& fbxgeo); +void setMeshPartDefaults(HFMMeshPart& meshPart, QString materialID); +void fbxDebugDump(const HFMGeometry& fbxgeo); diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index e96815d391..a950e1df3c 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -128,7 +128,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { void GeometryMappingResource::onGeometryMappingLoaded(bool success) { if (success && _geometryResource) { - _fbxGeometry = _geometryResource->_fbxGeometry; + _hfmGeometry = _geometryResource->_hfmGeometry; _meshParts = _geometryResource->_meshParts; _meshes = _geometryResource->_meshes; _materials = _geometryResource->_materials; @@ -193,38 +193,38 @@ void GeometryReader::run() { _url.path().toLower().endsWith(".obj.gz") || _url.path().toLower().endsWith(".gltf"))) { - FBXGeometry::Pointer fbxGeometry; + HFMGeometry::Pointer hfmGeometry; if (_url.path().toLower().endsWith(".fbx")) { - fbxGeometry.reset(readFBX(_data, _mapping, _url.path())); - if (fbxGeometry->meshes.size() == 0 && fbxGeometry->joints.size() == 0) { + hfmGeometry.reset(readFBX(_data, _mapping, _url.path())); + if (hfmGeometry->meshes.size() == 0 && hfmGeometry->joints.size() == 0) { throw QString("empty geometry, possibly due to an unsupported FBX version"); } } else if (_url.path().toLower().endsWith(".obj")) { - fbxGeometry = OBJReader().readOBJ(_data, _mapping, _combineParts, _url); + hfmGeometry = OBJReader().readOBJ(_data, _mapping, _combineParts, _url); } else if (_url.path().toLower().endsWith(".obj.gz")) { QByteArray uncompressedData; if (gunzip(_data, uncompressedData)){ - fbxGeometry = OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url); + hfmGeometry = OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url); } else { throw QString("failed to decompress .obj.gz"); } } else if (_url.path().toLower().endsWith(".gltf")) { std::shared_ptr glreader = std::make_shared(); - fbxGeometry.reset(glreader->readGLTF(_data, _mapping, _url)); - if (fbxGeometry->meshes.size() == 0 && fbxGeometry->joints.size() == 0) { + hfmGeometry.reset(glreader->readGLTF(_data, _mapping, _url)); + if (hfmGeometry->meshes.size() == 0 && hfmGeometry->joints.size() == 0) { throw QString("empty geometry, possibly due to an unsupported GLTF version"); } } else { throw QString("unsupported format"); } - // Add scripts to fbxgeometry + // Add scripts to hfmGeometry if (!_mapping.value(SCRIPT_FIELD).isNull()) { QVariantList scripts = _mapping.values(SCRIPT_FIELD); for (auto &script : scripts) { - fbxGeometry->scripts.push_back(script.toString()); + hfmGeometry->scripts.push_back(script.toString()); } } @@ -234,7 +234,7 @@ void GeometryReader::run() { qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; } else { QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition", - Q_ARG(FBXGeometry::Pointer, fbxGeometry)); + Q_ARG(HFMGeometry::Pointer, hfmGeometry)); } } else { throw QString("url is invalid"); @@ -262,7 +262,7 @@ public: virtual void downloadFinished(const QByteArray& data) override; protected: - Q_INVOKABLE void setGeometryDefinition(FBXGeometry::Pointer fbxGeometry); + Q_INVOKABLE void setGeometryDefinition(HFMGeometry::Pointer hfmGeometry); private: QVariantHash _mapping; @@ -277,13 +277,13 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { QThreadPool::globalInstance()->start(new GeometryReader(_self, _effectiveBaseURL, _mapping, data, _combineParts)); } -void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxGeometry) { +void GeometryDefinitionResource::setGeometryDefinition(HFMGeometry::Pointer hfmGeometry) { // Assume ownership of the geometry pointer - _fbxGeometry = fbxGeometry; + _hfmGeometry = hfmGeometry; // Copy materials QHash materialIDAtlas; - for (const FBXMaterial& material : _fbxGeometry->materials) { + for (const HFMMaterial& material : _hfmGeometry->materials) { materialIDAtlas[material.materialID] = _materials.size(); _materials.push_back(std::make_shared(material, _textureBaseUrl)); } @@ -291,11 +291,11 @@ void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxG std::shared_ptr meshes = std::make_shared(); std::shared_ptr parts = std::make_shared(); int meshID = 0; - for (const FBXMesh& mesh : _fbxGeometry->meshes) { + for (const HFMMesh& mesh : _hfmGeometry->meshes) { // Copy mesh pointers meshes->emplace_back(mesh._mesh); int partID = 0; - for (const FBXMeshPart& part : mesh.parts) { + for (const HFMMeshPart& part : mesh.parts) { // Construct local parts parts->push_back(std::make_shared(meshID, partID, (int)materialIDAtlas[part.materialID])); partID++; @@ -371,7 +371,7 @@ const QVariantMap Geometry::getTextures() const { // FIXME: The materials should only be copied when modified, but the Model currently caches the original Geometry::Geometry(const Geometry& geometry) { - _fbxGeometry = geometry._fbxGeometry; + _hfmGeometry = geometry._hfmGeometry; _meshes = geometry._meshes; _meshParts = geometry._meshParts; @@ -444,8 +444,8 @@ void GeometryResource::deleter() { } void GeometryResource::setTextures() { - if (_fbxGeometry) { - for (const FBXMaterial& material : _fbxGeometry->materials) { + if (_hfmGeometry) { + for (const HFMMaterial& material : _hfmGeometry->materials) { _materials.push_back(std::make_shared(material, _textureBaseUrl)); } } @@ -512,7 +512,7 @@ const QString& NetworkMaterial::getTextureName(MapChannel channel) { return NO_TEXTURE; } -QUrl NetworkMaterial::getTextureUrl(const QUrl& baseUrl, const FBXTexture& texture) { +QUrl NetworkMaterial::getTextureUrl(const QUrl& baseUrl, const HFMTexture& texture) { if (texture.content.isEmpty()) { // External file: search relative to the baseUrl, in case filename is relative return baseUrl.resolved(QUrl(texture.filename)); @@ -529,22 +529,22 @@ QUrl NetworkMaterial::getTextureUrl(const QUrl& baseUrl, const FBXTexture& textu } } -graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, const FBXTexture& fbxTexture, +graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture, image::TextureUsage::Type type, MapChannel channel) { if (baseUrl.isEmpty()) { return nullptr; } - const auto url = getTextureUrl(baseUrl, fbxTexture); - const auto texture = DependencyManager::get()->getTexture(url, type, fbxTexture.content, fbxTexture.maxNumPixels); - _textures[channel] = Texture { fbxTexture.name, texture }; + const auto url = getTextureUrl(baseUrl, hfmTexture); + const auto texture = DependencyManager::get()->getTexture(url, type, hfmTexture.content, hfmTexture.maxNumPixels); + _textures[channel] = Texture { hfmTexture.name, texture }; auto map = std::make_shared(); if (texture) { map->setTextureSource(texture->_textureSource); } - map->setTextureTransform(fbxTexture.transform); + map->setTextureTransform(hfmTexture.transform); return map; } @@ -624,7 +624,7 @@ void NetworkMaterial::setLightmapMap(const QUrl& url) { } } -NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl) : +NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl) : graphics::Material(*material._material), _textures(MapChannel::NUM_MAP_CHANNELS) { diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 5cbe96ea03..2283c355d8 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -45,9 +45,9 @@ public: // Mutable, but must retain structure of vector using NetworkMaterials = std::vector>; - bool isGeometryLoaded() const { return (bool)_fbxGeometry; } + bool isGeometryLoaded() const { return (bool)_hfmGeometry; } - const FBXGeometry& getFBXGeometry() const { return *_fbxGeometry; } + const HFMGeometry& getHFMGeometry() const { return *_hfmGeometry; } const GeometryMeshes& getMeshes() const { return *_meshes; } const std::shared_ptr getShapeMaterial(int shapeID) const; @@ -62,7 +62,7 @@ protected: friend class GeometryMappingResource; // Shared across all geometries, constant throughout lifetime - std::shared_ptr _fbxGeometry; + std::shared_ptr _hfmGeometry; std::shared_ptr _meshes; std::shared_ptr _meshParts; @@ -94,7 +94,7 @@ protected: // Geometries may not hold onto textures while cached - that is for the texture cache // Instead, these methods clear and reset textures from the geometry when caching/loading - bool shouldSetTextures() const { return _fbxGeometry && _materials.empty(); } + bool shouldSetTextures() const { return _hfmGeometry && _materials.empty(); } void setTextures(); void resetTextures(); @@ -165,7 +165,7 @@ public: using MapChannel = graphics::Material::MapChannel; NetworkMaterial() : _textures(MapChannel::NUM_MAP_CHANNELS) {} - NetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl); + NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl); NetworkMaterial(const NetworkMaterial& material); void setAlbedoMap(const QUrl& url, bool useAlphaChannel); @@ -201,8 +201,8 @@ protected: private: // Helpers for the ctors - QUrl getTextureUrl(const QUrl& baseUrl, const FBXTexture& fbxTexture); - graphics::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const FBXTexture& fbxTexture, + QUrl getTextureUrl(const QUrl& baseUrl, const HFMTexture& hfmTexture); + graphics::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture, image::TextureUsage::Type type, MapChannel channel); graphics::TextureMapPointer fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel); diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 81a017a46d..31d6cef060 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -32,8 +32,8 @@ bool CauterizedModel::updateGeometry() { bool needsFullUpdate = Model::updateGeometry(); if (_isCauterized && needsFullUpdate) { assert(_cauterizeMeshStates.empty()); - const FBXGeometry& fbxGeometry = getFBXGeometry(); - foreach (const FBXMesh& mesh, fbxGeometry.meshes) { + const HFMGeometry& hfmGeometry = getHFMGeometry(); + foreach (const HFMMesh& mesh, hfmGeometry.meshes) { Model::MeshState state; if (_useDualQuaternionSkinning) { state.clusterDualQuaternions.resize(mesh.clusters.size()); @@ -76,7 +76,7 @@ void CauterizedModel::createRenderItemSet() { // Run through all of the meshes, and place them into their segregated, but unsorted buckets int shapeID = 0; uint32_t numMeshes = (uint32_t)meshes.size(); - const FBXGeometry& fbxGeometry = getFBXGeometry(); + const HFMGeometry& hfmGeometry = getHFMGeometry(); for (uint32_t i = 0; i < numMeshes; i++) { const auto& mesh = meshes.at(i); if (!mesh) { @@ -86,7 +86,7 @@ void CauterizedModel::createRenderItemSet() { // Create the render payloads int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { - initializeBlendshapes(fbxGeometry.meshes[i], i); + initializeBlendshapes(hfmGeometry.meshes[i], i); auto ptr = std::make_shared(shared_from_this(), i, partIndex, shapeID, transform, offset); _modelMeshRenderItems << std::static_pointer_cast(ptr); @@ -109,13 +109,13 @@ void CauterizedModel::updateClusterMatrices() { return; } _needsUpdateClusterMatrices = false; - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); for (int i = 0; i < (int)_meshStates.size(); i++) { Model::MeshState& state = _meshStates[i]; - const FBXMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { - const FBXCluster& cluster = mesh.clusters.at(j); + const HFMCluster& cluster = mesh.clusters.at(j); if (_useDualQuaternionSkinning) { auto jointPose = _rig.getJointPose(cluster.jointIndex); Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); @@ -145,10 +145,10 @@ void CauterizedModel::updateClusterMatrices() { for (int i = 0; i < _cauterizeMeshStates.size(); i++) { Model::MeshState& state = _cauterizeMeshStates[i]; - const FBXMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { - const FBXCluster& cluster = mesh.clusters.at(j); + const HFMCluster& cluster = mesh.clusters.at(j); if (_useDualQuaternionSkinning) { if (_cauterizeBoneSet.find(cluster.jointIndex) == _cauterizeBoneSet.end()) { diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 4ebd92bb05..8e2541fdda 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -260,8 +260,8 @@ void ModelMeshPartPayload::initCache(const ModelPointer& model) { _hasColorAttrib = vertexFormat->hasAttribute(gpu::Stream::COLOR); _isSkinned = vertexFormat->hasAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT) && vertexFormat->hasAttribute(gpu::Stream::SKIN_CLUSTER_INDEX); - const FBXGeometry& geometry = model->getFBXGeometry(); - const FBXMesh& mesh = geometry.meshes.at(_meshIndex); + const HFMGeometry& geometry = model->getHFMGeometry(); + const HFMMesh& mesh = geometry.meshes.at(_meshIndex); _isBlendShaped = !mesh.blendshapes.isEmpty(); _hasTangents = !mesh.tangents.isEmpty(); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 53009e8bfa..65b3fef7c0 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -183,7 +183,7 @@ bool Model::shouldInvalidatePayloadShapeKey(int meshIndex) { return true; } - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); const auto& networkMeshes = getGeometry()->getMeshes(); // if our index is ever out of range for either meshes or networkMeshes, then skip it, and set our _meshGroupsKnown // to false to rebuild out mesh groups. @@ -278,7 +278,7 @@ void Model::setRenderItemsNeedUpdate() { void Model::reset() { if (isLoaded()) { - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); _rig.reset(geometry); emit rigReset(); emit rigReady(); @@ -295,13 +295,13 @@ bool Model::updateGeometry() { _needsReload = false; // TODO: should all Models have a valid _rig? - if (_rig.jointStatesEmpty() && getFBXGeometry().joints.size() > 0) { + if (_rig.jointStatesEmpty() && getHFMGeometry().joints.size() > 0) { initJointStates(); assert(_meshStates.empty()); - const FBXGeometry& fbxGeometry = getFBXGeometry(); + const HFMGeometry& hfmGeometry = getHFMGeometry(); int i = 0; - foreach (const FBXMesh& mesh, fbxGeometry.meshes) { + foreach (const HFMMesh& mesh, hfmGeometry.meshes) { MeshState state; state.clusterDualQuaternions.resize(mesh.clusters.size()); state.clusterMatrices.resize(mesh.clusters.size()); @@ -319,7 +319,7 @@ bool Model::updateGeometry() { // virtual void Model::initJointStates() { - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); glm::mat4 modelOffset = glm::scale(_scale) * glm::translate(_offset); _rig.initJointStates(geometry, modelOffset); @@ -363,7 +363,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g int bestShapeID = 0; int bestSubMeshIndex = 0; - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); if (!_triangleSetsValid) { calculateTriangleSets(geometry); } @@ -506,7 +506,7 @@ bool Model::findParabolaIntersectionAgainstSubMeshes(const glm::vec3& origin, co int bestShapeID = 0; int bestSubMeshIndex = 0; - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); if (!_triangleSetsValid) { calculateTriangleSets(geometry); } @@ -641,7 +641,7 @@ bool Model::convexHullContains(glm::vec3 point) { QMutexLocker locker(&_mutex); if (!_triangleSetsValid) { - calculateTriangleSets(getFBXGeometry()); + calculateTriangleSets(getHFMGeometry()); } // If we are inside the models box, then consider the submeshes... @@ -753,14 +753,14 @@ bool Model::replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointe } // update triangles for picking { - FBXGeometry geometry; + HFMGeometry geometry; for (const auto& newMesh : meshes) { - FBXMesh mesh; + HFMMesh mesh; mesh._mesh = newMesh.getMeshPointer(); mesh.vertices = buffer_helpers::mesh::attributeToVector(mesh._mesh, gpu::Stream::POSITION); int numParts = (int)newMesh.getMeshPointer()->getNumParts(); for (int partID = 0; partID < numParts; partID++) { - FBXMeshPart part; + HFMMeshPart part; part.triangleIndices = buffer_helpers::bufferToVector(mesh._mesh->getIndexBuffer(), "part.triangleIndices"); mesh.parts << part; } @@ -789,12 +789,12 @@ scriptable::ScriptableModelBase Model::getScriptableModel() { return result; } - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); int numberOfMeshes = geometry.meshes.size(); int shapeID = 0; for (int i = 0; i < numberOfMeshes; i++) { - const FBXMesh& fbxMesh = geometry.meshes.at(i); - if (auto mesh = fbxMesh._mesh) { + const HFMMesh& hfmMesh = geometry.meshes.at(i); + if (auto mesh = hfmMesh._mesh) { result.append(mesh); int numParts = (int)mesh->getNumParts(); @@ -808,7 +808,7 @@ scriptable::ScriptableModelBase Model::getScriptableModel() { return result; } -void Model::calculateTriangleSets(const FBXGeometry& geometry) { +void Model::calculateTriangleSets(const HFMGeometry& geometry) { PROFILE_RANGE(render, __FUNCTION__); int numberOfMeshes = geometry.meshes.size(); @@ -818,14 +818,14 @@ void Model::calculateTriangleSets(const FBXGeometry& geometry) { _modelSpaceMeshTriangleSets.resize(numberOfMeshes); for (int i = 0; i < numberOfMeshes; i++) { - const FBXMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = geometry.meshes.at(i); const int numberOfParts = mesh.parts.size(); auto& meshTriangleSets = _modelSpaceMeshTriangleSets[i]; meshTriangleSets.resize(numberOfParts); for (int j = 0; j < numberOfParts; j++) { - const FBXMeshPart& part = mesh.parts.at(j); + const HFMMeshPart& part = mesh.parts.at(j); auto& partTriangleSet = meshTriangleSets[j]; @@ -1114,7 +1114,7 @@ Extents Model::getBindExtents() const { if (!isActive()) { return Extents(); } - const Extents& bindExtents = getFBXGeometry().bindExtents; + const Extents& bindExtents = getHFMGeometry().bindExtents; Extents scaledExtents = { bindExtents.minimum * _scale, bindExtents.maximum * _scale }; return scaledExtents; } @@ -1128,12 +1128,12 @@ Extents Model::getMeshExtents() const { if (!isActive()) { return Extents(); } - const Extents& extents = getFBXGeometry().meshExtents; + const Extents& extents = getHFMGeometry().meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which // is captured in the offset matrix - glm::vec3 minimum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f)); - glm::vec3 maximum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f)); + glm::vec3 minimum = glm::vec3(getHFMGeometry().offset * glm::vec4(extents.minimum, 1.0f)); + glm::vec3 maximum = glm::vec3(getHFMGeometry().offset * glm::vec4(extents.maximum, 1.0f)); Extents scaledExtents = { minimum * _scale, maximum * _scale }; return scaledExtents; } @@ -1143,12 +1143,12 @@ Extents Model::getUnscaledMeshExtents() const { return Extents(); } - const Extents& extents = getFBXGeometry().meshExtents; + const Extents& extents = getHFMGeometry().meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which // is captured in the offset matrix - glm::vec3 minimum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f)); - glm::vec3 maximum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f)); + glm::vec3 minimum = glm::vec3(getHFMGeometry().offset * glm::vec4(extents.minimum, 1.0f)); + glm::vec3 maximum = glm::vec3(getHFMGeometry().offset * glm::vec4(extents.maximum, 1.0f)); Extents scaledExtents = { minimum, maximum }; return scaledExtents; @@ -1171,11 +1171,11 @@ void Model::setJointTranslation(int index, bool valid, const glm::vec3& translat } int Model::getParentJointIndex(int jointIndex) const { - return (isActive() && jointIndex != -1) ? getFBXGeometry().joints.at(jointIndex).parentIndex : -1; + return (isActive() && jointIndex != -1) ? getHFMGeometry().joints.at(jointIndex).parentIndex : -1; } int Model::getLastFreeJointIndex(int jointIndex) const { - return (isActive() && jointIndex != -1) ? getFBXGeometry().joints.at(jointIndex).freeLineage.last() : -1; + return (isActive() && jointIndex != -1) ? getHFMGeometry().joints.at(jointIndex).freeLineage.last() : -1; } void Model::setTextures(const QVariantMap& textures) { @@ -1275,7 +1275,7 @@ QStringList Model::getJointNames() const { Q_RETURN_ARG(QStringList, result)); return result; } - return isActive() ? getFBXGeometry().getJointNames() : QStringList(); + return isActive() ? getHFMGeometry().getJointNames() : QStringList(); } void Model::setScaleToFit(bool scaleToFit, const glm::vec3& dimensions, bool forceRescale) { @@ -1415,12 +1415,12 @@ void Model::updateClusterMatrices() { } _needsUpdateClusterMatrices = false; - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); for (int i = 0; i < (int) _meshStates.size(); i++) { MeshState& state = _meshStates[i]; - const FBXMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { - const FBXCluster& cluster = mesh.clusters.at(j); + const HFMCluster& cluster = mesh.clusters.at(j); if (_useDualQuaternionSkinning) { auto jointPose = _rig.getJointPose(cluster.jointIndex); Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); @@ -1505,7 +1505,7 @@ void Model::createRenderItemSet() { // Run through all of the meshes, and place them into their segregated, but unsorted buckets int shapeID = 0; uint32_t numMeshes = (uint32_t)meshes.size(); - auto& fbxGeometry = getFBXGeometry(); + auto& hfmGeometry = getHFMGeometry(); for (uint32_t i = 0; i < numMeshes; i++) { const auto& mesh = meshes.at(i); if (!mesh) { @@ -1515,7 +1515,7 @@ void Model::createRenderItemSet() { // Create the render payloads int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { - initializeBlendshapes(fbxGeometry.meshes[i], i); + initializeBlendshapes(hfmGeometry.meshes[i], i); _modelMeshRenderItems << std::make_shared(shared_from_this(), i, partIndex, shapeID, transform, offset); auto material = getGeometry()->getShapeMaterial(shapeID); _modelMeshMaterialNames.push_back(material ? material->getName() : ""); @@ -1600,7 +1600,7 @@ void Model::removeMaterial(graphics::MaterialPointer material, const std::string class CollisionRenderGeometry : public Geometry { public: CollisionRenderGeometry(graphics::MeshPointer mesh) { - _fbxGeometry = std::make_shared(); + _hfmGeometry = std::make_shared(); std::shared_ptr meshes = std::make_shared(); meshes->push_back(mesh); _meshes = meshes; @@ -1656,9 +1656,9 @@ void Blender::run() { if (_model && _model->isLoaded()) { DETAILED_PROFILE_RANGE_EX(simulation_animation, __FUNCTION__, 0xFFFF0000, 0, { { "url", _model->getURL().toString() } }); int offset = 0; - auto meshes = _model->getFBXGeometry().meshes; + auto meshes = _model->getHFMGeometry().meshes; int meshIndex = 0; - foreach(const FBXMesh& mesh, meshes) { + foreach(const HFMMesh& mesh, meshes) { auto modelMeshBlendshapeOffsets = _model->_blendshapeOffsets.find(meshIndex++); if (mesh.blendshapes.isEmpty() || modelMeshBlendshapeOffsets == _model->_blendshapeOffsets.end()) { // Not blendshaped or not initialized @@ -1688,7 +1688,7 @@ void Blender::run() { } float normalCoefficient = vertexCoefficient * NORMAL_COEFFICIENT_SCALE; - const FBXBlendshape& blendshape = mesh.blendshapes.at(i); + const HFMBlendshape& blendshape = mesh.blendshapes.at(i); tbb::parallel_for(tbb::blocked_range(0, blendshape.indices.size()), [&](const tbb::blocked_range& range) { for (auto j = range.begin(); j < range.end(); j++) { @@ -1731,7 +1731,7 @@ bool Model::maybeStartBlender() { return false; } -void Model::initializeBlendshapes(const FBXMesh& mesh, int index) { +void Model::initializeBlendshapes(const HFMMesh& mesh, int index) { if (mesh.blendshapes.empty()) { // mesh doesn't have blendshape, did we allocate one though ? if (_blendshapeOffsets.find(index) != _blendshapeOffsets.end()) { diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 71809821eb..db5625b3d8 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -185,7 +185,7 @@ public: /// Provided as a convenience, will crash if !isLoaded() // And so that getGeometry() isn't chained everywhere - const FBXGeometry& getFBXGeometry() const { assert(isLoaded()); return _renderGeometry->getFBXGeometry(); } + const HFMGeometry& getHFMGeometry() const { assert(isLoaded()); return _renderGeometry->getHFMGeometry(); } bool isActive() const { return isLoaded(); } @@ -450,7 +450,7 @@ protected: bool _overrideModelTransform { false }; bool _triangleSetsValid { false }; - void calculateTriangleSets(const FBXGeometry& geometry); + void calculateTriangleSets(const HFMGeometry& geometry); std::vector> _modelSpaceMeshTriangleSets; // model space triangles for all sub meshes virtual void createRenderItemSet(); @@ -506,7 +506,7 @@ protected: bool shouldInvalidatePayloadShapeKey(int meshIndex); - void initializeBlendshapes(const FBXMesh& mesh, int index); + void initializeBlendshapes(const HFMMesh& mesh, int index); private: float _loadingPriority { 0.0f }; diff --git a/libraries/render-utils/src/SoftAttachmentModel.cpp b/libraries/render-utils/src/SoftAttachmentModel.cpp index 90015768d0..77b09caa1d 100644 --- a/libraries/render-utils/src/SoftAttachmentModel.cpp +++ b/libraries/render-utils/src/SoftAttachmentModel.cpp @@ -41,14 +41,14 @@ void SoftAttachmentModel::updateClusterMatrices() { _needsUpdateClusterMatrices = false; - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); for (int i = 0; i < (int) _meshStates.size(); i++) { MeshState& state = _meshStates[i]; - const FBXMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { - const FBXCluster& cluster = mesh.clusters.at(j); + const HFMCluster& cluster = mesh.clusters.at(j); // TODO: cache these look-ups as an optimization int jointIndexOverride = getJointIndexOverride(cluster.jointIndex); diff --git a/tests-manual/gpu/src/TestFbx.cpp b/tests-manual/gpu/src/TestFbx.cpp index 538bb0a973..ba94fb810c 100644 --- a/tests-manual/gpu/src/TestFbx.cpp +++ b/tests-manual/gpu/src/TestFbx.cpp @@ -100,7 +100,7 @@ bool TestFbx::isReady() const { void TestFbx::parseFbx(const QByteArray& fbxData) { QVariantHash mapping; - FBXGeometry* fbx = readFBX(fbxData, mapping); + HFMGeometry* fbx = readFBX(fbxData, mapping); size_t totalVertexCount = 0; size_t totalIndexCount = 0; size_t totalPartCount = 0; diff --git a/tests-manual/gpu/src/TestFbx.h b/tests-manual/gpu/src/TestFbx.h index 391fff1091..4e22928460 100644 --- a/tests-manual/gpu/src/TestFbx.h +++ b/tests-manual/gpu/src/TestFbx.h @@ -11,7 +11,7 @@ #include -class FBXGeometry; +class HFMGeometry; class TestFbx : public GpuTestBase { size_t _partCount { 0 }; diff --git a/tests/animation/src/AnimInverseKinematicsTests.cpp b/tests/animation/src/AnimInverseKinematicsTests.cpp index f5d3597f56..f51fe12ecb 100644 --- a/tests/animation/src/AnimInverseKinematicsTests.cpp +++ b/tests/animation/src/AnimInverseKinematicsTests.cpp @@ -28,8 +28,8 @@ const glm::quat identity = glm::quat(); const glm::quat quaterTurnAroundZ = glm::angleAxis(0.5f * PI, zAxis); -void makeTestFBXJoints(FBXGeometry& geometry) { - FBXJoint joint; +void makeTestFBXJoints(HFMGeometry& geometry) { + HFMJoint joint; joint.isFree = false; joint.freeLineage.clear(); joint.parentIndex = -1; @@ -79,7 +79,7 @@ void makeTestFBXJoints(FBXGeometry& geometry) { // compute each joint's transform for (int i = 1; i < (int)geometry.joints.size(); ++i) { - FBXJoint& j = geometry.joints[i]; + HFMJoint& j = geometry.joints[i]; int parentIndex = j.parentIndex; // World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1) j.transform = geometry.joints[parentIndex].transform * @@ -96,7 +96,7 @@ void AnimInverseKinematicsTests::testSingleChain() { AnimContext context(false, false, false, glm::mat4(), glm::mat4()); - FBXGeometry geometry; + HFMGeometry geometry; makeTestFBXJoints(geometry); // create a skeleton and doll diff --git a/tools/skeleton-dump/src/SkeletonDumpApp.cpp b/tools/skeleton-dump/src/SkeletonDumpApp.cpp index e9d8243e38..5107931da1 100644 --- a/tools/skeleton-dump/src/SkeletonDumpApp.cpp +++ b/tools/skeleton-dump/src/SkeletonDumpApp.cpp @@ -54,8 +54,8 @@ SkeletonDumpApp::SkeletonDumpApp(int argc, char* argv[]) : QCoreApplication(argc return; } QByteArray blob = file.readAll(); - std::unique_ptr fbxGeometry(readFBX(blob, QVariantHash())); - std::unique_ptr skeleton(new AnimSkeleton(*fbxGeometry)); + std::unique_ptr geometry(readFBX(blob, QVariantHash())); + std::unique_ptr skeleton(new AnimSkeleton(*geometry)); skeleton->dump(verbose); } diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index a52e948f01..bb2958e11d 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -19,16 +19,16 @@ // FBXReader jumbles the order of the meshes by reading them back out of a hashtable. This will put // them back in the order in which they appeared in the file. -bool FBXGeometryLessThan(const FBXMesh& e1, const FBXMesh& e2) { +bool HFMGeometryLessThan(const HFMMesh& e1, const HFMMesh& e2) { return e1.meshIndex < e2.meshIndex; } -void reSortFBXGeometryMeshes(FBXGeometry& geometry) { - qSort(geometry.meshes.begin(), geometry.meshes.end(), FBXGeometryLessThan); +void reSortHFMGeometryMeshes(HFMGeometry& geometry) { + qSort(geometry.meshes.begin(), geometry.meshes.end(), HFMGeometryLessThan); } // Read all the meshes from provided FBX file -bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { +bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMGeometry& result) { if (_verbose) { qDebug() << "reading FBX file =" << filename << "..."; } @@ -41,7 +41,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } try { QByteArray fbxContents = fbx.readAll(); - FBXGeometry::Pointer geom; + HFMGeometry::Pointer geom; if (filename.toLower().endsWith(".obj")) { bool combineParts = false; geom = OBJReader().readOBJ(fbxContents, QVariantHash(), combineParts); @@ -53,7 +53,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } result = *geom; - reSortFBXGeometryMeshes(result); + reSortHFMGeometryMeshes(result); } catch (const QString& error) { qWarning() << "error reading" << filename << ":" << error; return false; @@ -63,7 +63,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } -void getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector& triangleIndices) { +void getTrianglesInMeshPart(const HFMMeshPart &meshPart, std::vector& triangleIndices) { // append triangle indices triangleIndices.reserve(triangleIndices.size() + (size_t)meshPart.triangleIndices.size()); for (auto index : meshPart.triangleIndices) { @@ -88,12 +88,12 @@ void getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector& trian } } -void vhacd::VHACDUtil::fattenMesh(const FBXMesh& mesh, const glm::mat4& geometryOffset, FBXMesh& result) const { +void vhacd::VHACDUtil::fattenMesh(const HFMMesh& mesh, const glm::mat4& geometryOffset, HFMMesh& result) const { // this is used to make meshes generated from a highfield collidable. each triangle // is converted into a tetrahedron and made into its own mesh-part. std::vector triangleIndices; - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { getTrianglesInMeshPart(meshPart, triangleIndices); } @@ -145,7 +145,7 @@ void vhacd::VHACDUtil::fattenMesh(const FBXMesh& mesh, const glm::mat4& geometry int index3 = result.vertices.size(); result.vertices << p3; // add the new point to the result mesh - FBXMeshPart newMeshPart; + HFMMeshPart newMeshPart; setMeshPartDefaults(newMeshPart, "unknown"); newMeshPart.triangleIndices << index0 << index1 << index2; newMeshPart.triangleIndices << index0 << index3 << index1; @@ -155,7 +155,7 @@ void vhacd::VHACDUtil::fattenMesh(const FBXMesh& mesh, const glm::mat4& geometry } } -AABox getAABoxForMeshPart(const FBXMesh& mesh, const FBXMeshPart &meshPart) { +AABox getAABoxForMeshPart(const HFMMesh& mesh, const HFMMeshPart &meshPart) { AABox aaBox; const int TRIANGLE_STRIDE = 3; for (int i = 0; i < meshPart.triangleIndices.size(); i += TRIANGLE_STRIDE) { @@ -242,7 +242,7 @@ bool isClosedManifold(const std::vector& triangleIndices) { return true; } -void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& resultMesh) const { +void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, HFMMesh& resultMesh) const { // Number of hulls for this input meshPart uint32_t numHulls = convexifier->GetNConvexHulls(); if (_verbose) { @@ -256,8 +256,8 @@ void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& res VHACD::IVHACD::ConvexHull hull; convexifier->GetConvexHull(j, hull); - resultMesh.parts.append(FBXMeshPart()); - FBXMeshPart& resultMeshPart = resultMesh.parts.last(); + resultMesh.parts.append(HFMMeshPart()); + HFMMeshPart& resultMeshPart = resultMesh.parts.last(); int hullIndexStart = resultMesh.vertices.size(); resultMesh.vertices.reserve(hullIndexStart + hull.m_nPoints); @@ -288,9 +288,9 @@ float computeDt(uint64_t start) { return (float)(usecTimestampNow() - start) / (float)USECS_PER_SECOND; } -bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, +bool vhacd::VHACDUtil::computeVHACD(HFMGeometry& geometry, VHACD::IVHACD::Parameters params, - FBXGeometry& result, + HFMGeometry& result, float minimumMeshSize, float maximumMeshSize) { if (_verbose) { qDebug() << "meshes =" << geometry.meshes.size(); @@ -298,7 +298,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, // count the mesh-parts int numParts = 0; - foreach (const FBXMesh& mesh, geometry.meshes) { + foreach (const HFMMesh& mesh, geometry.meshes) { numParts += mesh.parts.size(); } if (_verbose) { @@ -308,15 +308,15 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, VHACD::IVHACD * convexifier = VHACD::CreateVHACD(); result.meshExtents.reset(); - result.meshes.append(FBXMesh()); - FBXMesh &resultMesh = result.meshes.last(); + result.meshes.append(HFMMesh()); + HFMMesh &resultMesh = result.meshes.last(); const uint32_t POINT_STRIDE = 3; const uint32_t TRIANGLE_STRIDE = 3; int meshIndex = 0; int validPartsFound = 0; - foreach (const FBXMesh& mesh, geometry.meshes) { + foreach (const HFMMesh& mesh, geometry.meshes) { // find duplicate points int numDupes = 0; @@ -354,7 +354,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, int partIndex = 0; std::vector triangleIndices; - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { triangleIndices.clear(); getTrianglesInMeshPart(meshPart, triangleIndices); @@ -421,7 +421,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, triangleIndices.clear(); for (auto index : openParts) { - const FBXMeshPart &meshPart = mesh.parts[index]; + const HFMMeshPart &meshPart = mesh.parts[index]; getTrianglesInMeshPart(meshPart, triangleIndices); } diff --git a/tools/vhacd-util/src/VHACDUtil.h b/tools/vhacd-util/src/VHACDUtil.h index 35ec3ef56b..64e86ed7df 100644 --- a/tools/vhacd-util/src/VHACDUtil.h +++ b/tools/vhacd-util/src/VHACDUtil.h @@ -27,16 +27,16 @@ namespace vhacd { public: void setVerbose(bool verbose) { _verbose = verbose; } - bool loadFBX(const QString filename, FBXGeometry& result); + bool loadFBX(const QString filename, HFMGeometry& result); - void fattenMesh(const FBXMesh& mesh, const glm::mat4& gometryOffset, FBXMesh& result) const; + void fattenMesh(const HFMMesh& mesh, const glm::mat4& gometryOffset, HFMMesh& result) const; - bool computeVHACD(FBXGeometry& geometry, + bool computeVHACD(HFMGeometry& geometry, VHACD::IVHACD::Parameters params, - FBXGeometry& result, + HFMGeometry& result, float minimumMeshSize, float maximumMeshSize); - void getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& resultMesh) const; + void getConvexResults(VHACD::IVHACD* convexifier, HFMMesh& resultMesh) const; ~VHACDUtil(); @@ -55,6 +55,6 @@ namespace vhacd { }; } -AABox getAABoxForMeshPart(const FBXMeshPart &meshPart); +AABox getAABoxForMeshPart(const HFMMeshPart &meshPart); #endif //hifi_VHACDUtil_h diff --git a/tools/vhacd-util/src/VHACDUtilApp.cpp b/tools/vhacd-util/src/VHACDUtilApp.cpp index c263dce609..0941198234 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.cpp +++ b/tools/vhacd-util/src/VHACDUtilApp.cpp @@ -36,7 +36,7 @@ QString formatFloat(double n) { } -bool VHACDUtilApp::writeOBJ(QString outFileName, FBXGeometry& geometry, bool outputCentimeters, int whichMeshPart) { +bool VHACDUtilApp::writeOBJ(QString outFileName, HFMGeometry& geometry, bool outputCentimeters, int whichMeshPart) { QFile file(outFileName); if (!file.open(QIODevice::WriteOnly)) { qWarning() << "unable to write to" << outFileName; @@ -56,9 +56,9 @@ bool VHACDUtilApp::writeOBJ(QString outFileName, FBXGeometry& geometry, bool out int vertexIndexOffset = 0; - foreach (const FBXMesh& mesh, geometry.meshes) { + foreach (const HFMMesh& mesh, geometry.meshes) { bool verticesHaveBeenOutput = false; - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { if (whichMeshPart >= 0 && nth != (unsigned int) whichMeshPart) { nth++; continue; @@ -297,7 +297,7 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } // load the mesh - FBXGeometry fbx; + HFMGeometry fbx; auto begin = std::chrono::high_resolution_clock::now(); if (!vUtil.loadFBX(inputFilename, fbx)){ _returnCode = VHACD_RETURN_CODE_FAILURE_TO_READ; @@ -315,8 +315,8 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : QVector infileExtensions = {"fbx", "obj"}; QString baseFileName = fileNameWithoutExtension(outputFilename, infileExtensions); int count = 0; - foreach (const FBXMesh& mesh, fbx.meshes) { - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMesh& mesh, fbx.meshes) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { QString outputFileName = baseFileName + "-" + QString::number(count) + ".obj"; writeOBJ(outputFileName, fbx, outputCentimeters, count); count++; @@ -358,7 +358,7 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } begin = std::chrono::high_resolution_clock::now(); - FBXGeometry result; + HFMGeometry result; bool success = vUtil.computeVHACD(fbx, params, result, minimumMeshSize, maximumMeshSize); end = std::chrono::high_resolution_clock::now(); @@ -377,9 +377,9 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : int totalVertices = 0; int totalTriangles = 0; - foreach (const FBXMesh& mesh, result.meshes) { + foreach (const HFMMesh& mesh, result.meshes) { totalVertices += mesh.vertices.size(); - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { totalTriangles += meshPart.triangleIndices.size() / 3; // each quad was made into two triangles totalTriangles += 2 * meshPart.quadIndices.size() / 4; @@ -398,17 +398,17 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } if (fattenFaces) { - FBXGeometry newFbx; - FBXMesh result; + HFMGeometry newFbx; + HFMMesh result; // count the mesh-parts unsigned int meshCount = 0; - foreach (const FBXMesh& mesh, fbx.meshes) { + foreach (const HFMMesh& mesh, fbx.meshes) { meshCount += mesh.parts.size(); } result.modelTransform = glm::mat4(); // Identity matrix - foreach (const FBXMesh& mesh, fbx.meshes) { + foreach (const HFMMesh& mesh, fbx.meshes) { vUtil.fattenMesh(mesh, fbx.offset, result); } diff --git a/tools/vhacd-util/src/VHACDUtilApp.h b/tools/vhacd-util/src/VHACDUtilApp.h index 0d75275802..3db49456a0 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.h +++ b/tools/vhacd-util/src/VHACDUtilApp.h @@ -28,7 +28,7 @@ public: VHACDUtilApp(int argc, char* argv[]); ~VHACDUtilApp(); - bool writeOBJ(QString outFileName, FBXGeometry& geometry, bool outputCentimeters, int whichMeshPart = -1); + bool writeOBJ(QString outFileName, HFMGeometry& geometry, bool outputCentimeters, int whichMeshPart = -1); int getReturnCode() const { return _returnCode; } From e5f06690b509480d8b16c6b2180cab63a84b0e35 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 30 Oct 2018 17:31:31 -0700 Subject: [PATCH 278/362] Strip out references to 'my purchases' vs 'my items' code --- .../qml/hifi/commerce/purchases/Purchases.qml | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 7297e046e2..09cea01ccf 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -33,7 +33,6 @@ Rectangle { property bool securityImageResultReceived: false; property bool purchasesReceived: false; property bool punctuationMode: false; - property bool isShowingMyItems: false; property bool isDebuggingFirstUseTutorial: false; property string installedApps; property bool keyboardRaised: false; @@ -105,10 +104,6 @@ Rectangle { } } - onIsShowingMyItemsChanged: { - getPurchases(); - } - Timer { id: notSetUpTimer; interval: 200; @@ -457,7 +452,7 @@ Rectangle { anchors.left: parent.left; anchors.leftMargin: 16; width: paintedWidth; - text: isShowingMyItems ? "My Items" : "Inventory"; + text: "Inventory"; color: hifi.colors.black; size: 22; } @@ -520,6 +515,7 @@ Rectangle { onTextChanged: { purchasesModel.searchFilter = filterBar.text; filterBar.previousText = filterBar.text; + } } } @@ -543,9 +539,8 @@ Rectangle { listModelName: 'purchases'; listView: purchasesContentsList; getPage: function () { - console.debug('getPage', purchasesModel.listModelName, root.isShowingMyItems, filterBar.primaryFilter_filterName, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage); + console.debug('getPage', purchasesModel.listModelName, filterBar.primaryFilter_filterName, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage); Commerce.inventory( - root.isShowingMyItems ? "proofs" : "purchased", filterBar.primaryFilter_filterName, filterBar.text, purchasesModel.currentPageToRetrieve, @@ -596,7 +591,6 @@ Rectangle { upgradeUrl: model.upgrade_url; upgradeTitle: model.upgrade_title; itemType: model.item_type; - isShowingMyItems: root.isShowingMyItems; valid: model.valid; anchors.topMargin: 10; anchors.bottomMargin: 10; @@ -807,7 +801,8 @@ Rectangle { Rectangle { id: updatesAvailableBanner; - visible: root.numUpdatesAvailable > 0 && !root.isShowingMyItems; + visible: root.numUpdatesAvailable > 0 && + filterBar.primaryFilter_filterName !== "proofs"; anchors.bottom: parent.bottom; anchors.left: parent.left; anchors.right: parent.right; @@ -868,9 +863,8 @@ Rectangle { id: noItemsAlertContainer; visible: !purchasesContentsList.visible && root.purchasesReceived && - root.isShowingMyItems && filterBar.text === "" && - filterBar.primaryFilter_displayName === ""; + filterBar.primaryFilter_filterName === "proofs"; anchors.top: filterBarContainer.bottom; anchors.topMargin: 12; anchors.left: parent.left; @@ -918,7 +912,6 @@ Rectangle { id: noPurchasesAlertContainer; visible: !purchasesContentsList.visible && root.purchasesReceived && - !root.isShowingMyItems && filterBar.text === "" && filterBar.primaryFilter_displayName === ""; anchors.top: filterBarContainer.bottom; @@ -1051,7 +1044,9 @@ Rectangle { filterBar.text = message.filterText ? message.filterText : ""; break; case 'purchases_showMyItems': - root.isShowingMyItems = true; + filterBar.primaryFilter_filterName = "proofs"; + filterBar.primaryFilter_displayName = "Proofs"; + filterBar.primaryFilter_index = 6; break; case 'updateConnections': sendAsset.updateConnections(message.connections); From 93cff8dfdabe1691e5212867deb1386fbbeb6e08 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Wed, 31 Oct 2018 08:48:42 -0700 Subject: [PATCH 279/362] Corrected indentation. --- tools/auto-tester/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/auto-tester/CMakeLists.txt b/tools/auto-tester/CMakeLists.txt index 1befa6332e..c8c0a336d2 100644 --- a/tools/auto-tester/CMakeLists.txt +++ b/tools/auto-tester/CMakeLists.txt @@ -57,7 +57,7 @@ if (WIN32) ) # add a custom command to copy the SSL DLLs - add_custom_command( + add_custom_command( TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory "$ENV{VCPKG_ROOT}/installed/x64-windows/bin" "$" From 628ec8ec2be2346865a68b595a28749484e735e1 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Wed, 31 Oct 2018 10:09:01 -0700 Subject: [PATCH 280/362] Removed QmlCommerce.cpp, Ledger.cpp changes as requested --- .../qml/hifi/commerce/purchases/Purchases.qml | 11 ++++++++++- interface/src/commerce/Ledger.cpp | 10 +++------- interface/src/commerce/Ledger.h | 2 +- interface/src/commerce/QmlCommerce.cpp | 5 +++-- interface/src/commerce/QmlCommerce.h | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 09cea01ccf..7000b632e2 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -540,8 +540,17 @@ Rectangle { listView: purchasesContentsList; getPage: function () { console.debug('getPage', purchasesModel.listModelName, filterBar.primaryFilter_filterName, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage); + var editionFilter = ""; + var primaryFilter = ""; + + if (filterBar.primaryFilter_filterName === "proofs") { + editionFilter = "proofs"; + } else { + primaryFilter = filterBar.primaryFilter_filterName; + } Commerce.inventory( - filterBar.primaryFilter_filterName, + editionFilter, + primaryFilter, filterBar.text, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 9703f7377f..f4bd7a84b3 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -152,14 +152,10 @@ void Ledger::balance(const QStringList& keys) { keysQuery("balance", "balanceSuccess", "balanceFailure"); } -void Ledger::inventory(const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage) { +void Ledger::inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage) { QJsonObject params; - if (typeFilter == "proofs") { - params["edition_filter"] = "proofs"; - } else { - params["type_filter"] = typeFilter; - } - + params["edition_filter"] = editionFilter; + params["type_filter"] = typeFilter; params["title_filter"] = titleFilter; params["page"] = page; params["per_page"] = perPage; diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 7bff9abe9b..427395ee11 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -29,7 +29,7 @@ public: bool receiveAt(const QString& hfc_key, const QString& signing_key, const QByteArray& locker); bool receiveAt(); void balance(const QStringList& keys); - void inventory(const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage); + void inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage); void history(const QStringList& keys, const int& pageNumber, const int& itemsPerPage); void account(); void updateLocation(const QString& asset_id, const QString& location, const bool& alsoUpdateSiblings = false, const bool controlledFailure = false); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 369b03d610..ffe89ffc5b 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -151,7 +151,8 @@ void QmlCommerce::balance() { } } -void QmlCommerce::inventory(const QString& typeFilter, +void QmlCommerce::inventory(const QString& editionFilter, + const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage) { @@ -159,7 +160,7 @@ void QmlCommerce::inventory(const QString& typeFilter, auto wallet = DependencyManager::get(); QStringList cachedPublicKeys = wallet->listPublicKeys(); if (!cachedPublicKeys.isEmpty()) { - ledger->inventory(typeFilter, titleFilter, page, perPage); + ledger->inventory(editionFilter, typeFilter, titleFilter, page, perPage); } } diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index a8eb45261d..2e3c0ec24d 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -74,7 +74,7 @@ protected: Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); - Q_INVOKABLE void inventory(const QString& typeFilter = QString(), const QString& titleFilter = QString(), const int& page = 1, const int& perPage = 20); + Q_INVOKABLE void inventory(const QString& editionFilter = QString(), const QString& typeFilter = QString(), const QString& titleFilter = QString(), const int& page = 1, const int& perPage = 20); Q_INVOKABLE void history(const int& pageNumber, const int& itemsPerPage = 100); Q_INVOKABLE void generateKeyPair(); Q_INVOKABLE void account(); From 0279eef3a4958896b403a562913fc9aa7841dedb Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 31 Oct 2018 10:13:32 -0700 Subject: [PATCH 281/362] improving interstitial loading bar --- scripts/system/interstitialPage.js | 83 ++++++++++++++++-------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/scripts/system/interstitialPage.js b/scripts/system/interstitialPage.js index 670d21c7a7..34eee605ae 100644 --- a/scripts/system/interstitialPage.js +++ b/scripts/system/interstitialPage.js @@ -17,7 +17,8 @@ Script.include("/~/system/libraries/globals.js"); var DEBUG = false; var MIN_LOADING_PROGRESS = 3.6; - var TOTAL_LOADING_PROGRESS = 3.8; + var TOTAL_LOADING_PROGRESS = 3.7; + var FINAL_Y_DIMENSIONS = 2.8; var EPSILON = 0.05; var TEXTURE_EPSILON = 0.01; var isVisible = false; @@ -27,6 +28,7 @@ var MAX_LEFT_MARGIN = 1.9; var INNER_CIRCLE_WIDTH = 4.7; var DEFAULT_Z_OFFSET = 5.45; + var LOADING_IMAGE_WIDTH_PIXELS = 1024; var previousCameraMode = Camera.mode; var renderViewTask = Render.getConfig("RenderMainView"); @@ -182,12 +184,13 @@ parentID: anchorOverlay }); - var loadingBarPlacard = Overlays.addOverlay("image3d", { - name: "Loading-Bar-Placard", - localPosition: { x: 0.0, y: -0.99, z: 0.3 }, - url: Script.resourcesPath() + "images/loadingBar_placard.png", + + var loadingBarProgress = Overlays.addOverlay("image3d", { + name: "Loading-Bar-Progress", + localPosition: { x: 0.0, y: -0.86, z: 0.0 }, + url: Script.resourcesPath() + "images/loadingBar_progress.png", alpha: 1, - dimensions: { x: 4, y: 2.8 }, + dimensions: { x: 3.8, y: 2.8 }, visible: isVisible, emissive: true, ignoreRayIntersection: false, @@ -197,12 +200,12 @@ parentID: anchorOverlay }); - var loadingBarProgress = Overlays.addOverlay("image3d", { - name: "Loading-Bar-Progress", - localPosition: { x: 0.0, y: -0.90, z: 0.0 }, - url: Script.resourcesPath() + "images/loadingBar_progress.png", + var loadingBarPlacard = Overlays.addOverlay("image3d", { + name: "Loading-Bar-Placard", + localPosition: { x: 0.0, y: -0.99, z: 0.4 }, + url: Script.resourcesPath() + "images/loadingBar_placard.png", alpha: 1, - dimensions: { x: 3.8, y: 2.8 }, + dimensions: { x: 4, y: 2.8 }, visible: isVisible, emissive: true, ignoreRayIntersection: false, @@ -245,15 +248,7 @@ } function resetValues() { - var properties = { - localPosition: { x: 1.85, y: -0.935, z: 0.0 }, - dimensions: { - x: 0.1, - y: 2.8 - } - }; - - Overlays.editOverlay(loadingBarProgress, properties); + updateProgressBar(0.0); } function startInterstitialPage() { @@ -382,8 +377,8 @@ function updateOverlays(physicsEnabled) { if (isInterstitialOverlaysVisible !== !physicsEnabled && !physicsEnabled === true) { - // visible changed to true. - isInterstitialOverlaysVisible = !physicsEnabled; + // visible changed to true. + isInterstitialOverlaysVisible = !physicsEnabled; } var properties = { @@ -400,7 +395,7 @@ }; var loadingBarProperties = { - dimensions: { x: 0.0, y: 2.8 }, + dimensions: { x: 2.0, y: 2.8 }, visible: !physicsEnabled }; @@ -434,8 +429,8 @@ } if (isInterstitialOverlaysVisible !== !physicsEnabled && !physicsEnabled === false) { - // visible changed to false. - isInterstitialOverlaysVisible = !physicsEnabled; + // visible changed to false. + isInterstitialOverlaysVisible = !physicsEnabled; } } @@ -459,6 +454,27 @@ } } + function updateProgressBar(progress) { + var progressPercentage = progress / TOTAL_LOADING_PROGRESS; + var subImageWidth = progressPercentage * LOADING_IMAGE_WIDTH_PIXELS; + + var properties = { + localPosition: { x: (TOTAL_LOADING_PROGRESS / 2) - (currentProgress / 2), y: -0.86, z: 0.0 }, + dimensions: { + x: currentProgress, + y: FINAL_Y_DIMENSIONS * (subImageWidth / LOADING_IMAGE_WIDTH_PIXELS) + }, + subImage: { + x: 0.0, + y: 0.0, + width: subImageWidth, + height: 90 + } + }; + + Overlays.editOverlay(loadingBarProgress, properties); + } + var MAX_TEXTURE_STABILITY_COUNT = 30; var INTERVAL_PROGRESS = 0.04; function update() { @@ -503,15 +519,8 @@ } currentProgress = lerp(currentProgress, target, INTERVAL_PROGRESS); - var properties = { - localPosition: { x: (1.85 - (currentProgress / 2) - (-0.029 * (currentProgress / TOTAL_LOADING_PROGRESS))), y: -0.935, z: 0.0 }, - dimensions: { - x: currentProgress, - y: 2.8 - } - }; - Overlays.editOverlay(loadingBarProgress, properties); + updateProgressBar(currentProgress); if (errorConnectingToDomain) { updateOverlays(errorConnectingToDomain); @@ -542,17 +551,11 @@ } var whiteColor = { red: 255, green: 255, blue: 255 }; var greyColor = { red: 125, green: 125, blue: 125 }; + Overlays.mouseReleaseOnOverlay.connect(clickedOnOverlay); Overlays.hoverEnterOverlay.connect(onEnterOverlay); - Overlays.hoverLeaveOverlay.connect(onLeaveOverlay); - location.hostChanged.connect(domainChanged); - location.lookupResultsFinished.connect(function() { - Script.setTimeout(function() { - connectionToDomainFailed = !location.isConnected; - }, 1200); - }); Window.redirectErrorStateChanged.connect(toggleInterstitialPage); MyAvatar.sensorToWorldScaleChanged.connect(scaleInterstitialPage); From b5a48d31b6e92d1500a77b88b048d9a01991cfac Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Wed, 31 Oct 2018 10:18:04 -0700 Subject: [PATCH 282/362] Fix code-review spacing issue --- interface/resources/qml/controls-uit/FilterBar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/controls-uit/FilterBar.qml b/interface/resources/qml/controls-uit/FilterBar.qml index 71aa1f64ab..3d4e18ed48 100644 --- a/interface/resources/qml/controls-uit/FilterBar.qml +++ b/interface/resources/qml/controls-uit/FilterBar.qml @@ -261,7 +261,7 @@ Item { anchors.fill: parent; model: filterBarModel; delegate: Item { - width: parent.width; + width: parent.width; height: 50; Rectangle { id: dropDownButton; From 225e8e055613526598bfd7181f3d45185711b73b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 31 Oct 2018 10:20:21 -0700 Subject: [PATCH 283/362] always zero velocity on far-grab release of non-dynamic --- .../controllers/controllerModules/farParentGrabEntity.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/farParentGrabEntity.js b/scripts/system/controllers/controllerModules/farParentGrabEntity.js index ac6c41d4d6..f85869aa7f 100644 --- a/scripts/system/controllers/controllerModules/farParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farParentGrabEntity.js @@ -262,7 +262,9 @@ Script.include("/~/system/libraries/controllers.js"); if (this.thisFarGrabJointIsParent(endProps)) { Entities.editEntity(this.targetEntityID, { parentID: this.previousParentID[this.targetEntityID], - parentJointIndex: this.previousParentJointIndex[this.targetEntityID] + parentJointIndex: this.previousParentJointIndex[this.targetEntityID], + localVelocity: {x: 0, y: 0, z: 0}, + localAngularVelocity: {x: 0, y: 0, z: 0} }); } From 92888e9d3e1ff7533b470ca038ea9126027129be Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 31 Oct 2018 18:26:23 +0100 Subject: [PATCH 284/362] tooltip CR changes --- .../system/assets/data/createAppTooltips.json | 24 ------------------- scripts/system/edit.js | 7 ++++++ scripts/system/html/css/edit-style.css | 7 +++--- scripts/system/html/js/createAppTooltip.js | 10 ++++---- scripts/system/html/js/entityProperties.js | 6 ++++- 5 files changed, 20 insertions(+), 34 deletions(-) diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json index 83ddcaa34b..5572779d46 100644 --- a/scripts/system/assets/data/createAppTooltips.json +++ b/scripts/system/assets/data/createAppTooltips.json @@ -196,12 +196,6 @@ "particleRadius": { "tooltip": "The size of each particle." }, - "radiusStart": { - "tooltip": "" - }, - "radiusFinish": { - "tooltip": "" - }, "radiusSpread": { "tooltip": "The spread in size that each particle is given, resulting in a variety of sizes." }, @@ -215,12 +209,6 @@ "alpha": { "tooltip": "The alpha of each particle." }, - "alphaStart": { - "tooltip": "" - }, - "alphaFinish": { - "tooltip": "" - }, "alphaSpread": { "tooltip": "The spread in alpha that each particle is given, resulting in a variety of alphas." }, @@ -233,12 +221,6 @@ "particleSpin": { "tooltip": "The spin of each particle in the system." }, - "spinStart": { - "tooltip": "" - }, - "spinFinish": { - "tooltip": "" - }, "spinSpread": { "tooltip": "The spread in spin that each particle is given, resulting in a variety of spins." }, @@ -248,15 +230,9 @@ "polarStart": { "tooltip": "The angle in deg at which particles are emitted. Starts in the entity's -z direction, and rotates around its y axis." }, - "polarFinish": { - "tooltip": "" - }, "azimuthStart": { "tooltip": "The angle in deg at which particles are emitted. Starts in the entity's -z direction, and rotates around its y axis." }, - "azimuthFinish": { - "tooltip": "" - }, "lightColor": { "tooltip": "The color of the light emitted.", "jsPropertyName": "color" diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 6425806771..17fe9e2d40 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2490,6 +2490,13 @@ var PropertiesTool = function (opts) { } }; + HMD.displayModeChanged.connect(function() { + emitScriptEvent({ + type: 'hmdActiveChanged', + hmdActive: HMD.active, + }); + }); + createToolsWindow.webEventReceived.addListener(this, onWebEventReceived); webView.webEventReceived.connect(onWebEventReceived); diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index cf7124d9eb..7d2350e1c8 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1598,7 +1598,7 @@ input.rename-entity { padding-left: 2px; } -.createAppTooltip { +.create-app-tooltip { position: absolute; background: #6a6a6a; border: 1px solid black; @@ -1607,17 +1607,16 @@ input.rename-entity { padding: 5px; } -.createAppTooltip .createAppTooltipDescription { +.create-app-tooltip .create-app-tooltip-description { font-size: 12px; font-style: italic; color: #ffffff; } -.createAppTooltip .createAppTooltipJSAttribute { +.create-app-tooltip .create-app-tooltip-js-attribute { font-family: Raleway-SemiBold; font-size: 11px; color: #000000; bottom: 0; margin-top: 5px; } - diff --git a/scripts/system/html/js/createAppTooltip.js b/scripts/system/html/js/createAppTooltip.js index a42e5efe05..a5c961a7e2 100644 --- a/scripts/system/html/js/createAppTooltip.js +++ b/scripts/system/html/js/createAppTooltip.js @@ -58,15 +58,15 @@ CreateAppTooltip.prototype = { if (!TOOLTIP_DEBUG) { return; } - tooltipData = {tooltip: 'PLEASE SET THIS TOOLTIP'}; + tooltipData = { tooltip: 'PLEASE SET THIS TOOLTIP' }; } let elementRect = element.getBoundingClientRect(); let elTip = document.createElement("div"); - elTip.className = "createAppTooltip"; + elTip.className = "create-app-tooltip"; let elTipDescription = document.createElement("div"); - elTipDescription.className = "createAppTooltipDescription"; + elTipDescription.className = "create-app-tooltip-description"; elTipDescription.innerText = tooltipData.tooltip; elTip.appendChild(elTipDescription); @@ -77,7 +77,7 @@ CreateAppTooltip.prototype = { if (!tooltipData.skipJSProperty) { let elTipJSAttribute = document.createElement("div"); - elTipJSAttribute.className = "createAppTooltipJSAttribute"; + elTipJSAttribute.className = "create-app-tooltip-js-attribute"; elTipJSAttribute.innerText = `JS Attribute: ${jsAttribute}`; elTip.appendChild(elTipJSAttribute); } @@ -93,7 +93,7 @@ CreateAppTooltip.prototype = { // show above when otherwise out of bounds elTip.style.top = elementTop - CREATE_APP_TOOLTIP_OFFSET - elTip.clientHeight; } else { - // show tooltip on below by default + // show tooltip below by default elTip.style.top = desiredTooltipTop; } if ((window.innerWidth + window.pageXOffset) < (desiredTooltipLeft + elTip.clientWidth)) { diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 78de0d075a..c7c75a243b 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -3165,8 +3165,13 @@ function loaded() { } else if (data.type === 'tooltipsReply') { createAppTooltip.setIsEnabled(!data.hmdActive); createAppTooltip.setTooltipData(data.tooltips); + } else if (data.type === 'hmdActiveChanged') { + createAppTooltip.setIsEnabled(!data.hmdActive); } }); + + // Request tooltips as soon as we can process a reply: + EventBridge.emitWebEvent(JSON.stringify({ type: 'tooltipsRequest' })); } // Server Script Status @@ -3397,6 +3402,5 @@ function loaded() { setTimeout(function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'propertiesPageReady' })); - EventBridge.emitWebEvent(JSON.stringify({ type: 'tooltipsRequest' })); }, 1000); } From 2482bb9731f038be80ed6a4630a8d3301d0bd397 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Wed, 31 Oct 2018 11:07:26 -0700 Subject: [PATCH 285/362] Add logging for install options --- cmake/templates/NSIS.template.in | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 19f661b42b..9ce11ca032 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -968,30 +968,43 @@ Function ReadPostInstallOptions ; check if the user asked for a desktop shortcut to console ${NSD_GetState} $DesktopConsoleCheckbox $DesktopConsoleState - + ${LogText} "Option: Start Desktop Console: $DesktopConsoleState" + ; check if the user asked to have console launched every startup ${NSD_GetState} $ConsoleStartupCheckbox $ConsoleStartupState + ${LogText} "Option: Start Desktop Console On Startup: $ConsoleStartupState" + ${If} @SERVER_COMPONENT_CONDITIONAL@ + ${LogText} "Option: Install Server" + ${EndIf} + ${If} @CLIENT_COMPONENT_CONDITIONAL@ + ${LogText} "Option: Install Client" ; check if the user asked for a desktop shortcut to High Fidelity ${NSD_GetState} $DesktopClientCheckbox $DesktopClientState + ${LogText} "Option: Create Client Desktop Shortcut: $DesktopClientState" ${EndIf} ${If} @PR_BUILD@ == 1 + ${LogText} "Option: PR Build" ; check if we need to copy settings/content from production for this PR build ${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState + ${LogText} "Option: Copy Settings From Production: $CopyFromProductionState" ${EndIf} ; check if we need to launch the console post-install ${NSD_GetState} $LaunchConsoleNowCheckbox $LaunchConsoleNowState + ${LogText} "Option: Launch Console Now: $LaunchConsoleNowState" ${If} @CLIENT_COMPONENT_CONDITIONAL@ ; check if we need to launch the client post-install ${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState + ${LogText} "Option: Launch Client Now: $LaunchClientNowState" ${EndIf} ; check if the user asked for a clean install ${NSD_GetState} $CleanInstallCheckbox $CleanInstallState + ${LogText} "Option: Clean Install: $CleanInstallState" FunctionEnd Function HandlePostInstallOptions From b3539b101b6a5be02d1ef435c0ed930ffb19ce53 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 31 Oct 2018 11:04:30 -0700 Subject: [PATCH 286/362] faster loading bar interpolation once physics is enabled --- scripts/system/interstitialPage.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/system/interstitialPage.js b/scripts/system/interstitialPage.js index 34eee605ae..e6e8d3d6d6 100644 --- a/scripts/system/interstitialPage.js +++ b/scripts/system/interstitialPage.js @@ -19,6 +19,7 @@ var MIN_LOADING_PROGRESS = 3.6; var TOTAL_LOADING_PROGRESS = 3.7; var FINAL_Y_DIMENSIONS = 2.8; + var BEGIN_Y_DIMENSIONS = 0.03; var EPSILON = 0.05; var TEXTURE_EPSILON = 0.01; var isVisible = false; @@ -457,12 +458,13 @@ function updateProgressBar(progress) { var progressPercentage = progress / TOTAL_LOADING_PROGRESS; var subImageWidth = progressPercentage * LOADING_IMAGE_WIDTH_PIXELS; + var subImageWidthPercentage = subImageWidth / LOADING_IMAGE_WIDTH_PIXELS; var properties = { localPosition: { x: (TOTAL_LOADING_PROGRESS / 2) - (currentProgress / 2), y: -0.86, z: 0.0 }, dimensions: { x: currentProgress, - y: FINAL_Y_DIMENSIONS * (subImageWidth / LOADING_IMAGE_WIDTH_PIXELS) + y: (subImageWidthPercentage * (FINAL_Y_DIMENSIONS - BEGIN_Y_DIMENSIONS)) + BEGIN_Y_DIMENSIONS }, subImage: { x: 0.0, @@ -477,6 +479,7 @@ var MAX_TEXTURE_STABILITY_COUNT = 30; var INTERVAL_PROGRESS = 0.04; + var INTERVAL_PROGRESS_PHYSICS_ENABLED = 0.2; function update() { var renderStats = Render.getConfig("Stats"); var physicsEnabled = Window.isPhysicsEnabled(); @@ -518,7 +521,7 @@ target = TOTAL_LOADING_PROGRESS; } - currentProgress = lerp(currentProgress, target, INTERVAL_PROGRESS); + currentProgress = lerp(currentProgress, target, (physicsEnabled ? INTERVAL_PROGRESS_PHYSICS_ENABLED : INTERVAL_PROGRESS)); updateProgressBar(currentProgress); From 8633dc0f663129728423997908347430b7bd4910 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 31 Oct 2018 12:34:20 -0700 Subject: [PATCH 287/362] disable interstitial on android --- libraries/networking/src/DomainHandler.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index ddd23339df..c0c5a4d059 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -231,7 +231,11 @@ private: QString _pendingPath; QTimer _settingsTimer; mutable ReadWriteLockable _interstitialModeSettingLock; - Setting::Handle _enableInterstitialMode{ "enableInterstitialMode", true }; +#ifdef Q_OS_ANDROID + Setting::Handle _enableInterstitialMode{ "enableInterstitialMode", false }; +#else + Setting::Handle _enableInterstitialMode { "enableInterstitialMode", true }; +#endif QSet _domainConnectionRefusals; bool _hasCheckedForAccessToken { false }; From 448be8847ff82b62232b2521a3ce035949d8f32b Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 31 Oct 2018 13:23:58 -0700 Subject: [PATCH 288/362] Notifications are working! --- .../qml/hifi/commerce/purchases/Purchases.qml | 1 - .../qml/hifi/commerce/wallet/Wallet.qml | 26 +++- scripts/modules/appUi.js | 144 +++++++++++------- scripts/modules/request.js | 5 +- scripts/system/commerce/wallet.js | 46 +++--- scripts/system/pal.js | 16 +- scripts/system/tablet-goto.js | 16 +- 7 files changed, 155 insertions(+), 99 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index eeb98eeb8c..3f77a17ac0 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -91,7 +91,6 @@ Rectangle { if (result.status !== 'success') { console.log("Failed to get Available Updates", result.data.message); } else { - sendToScript({method: 'purchases_availableUpdatesReceived', numUpdates: result.data.updates.length }); root.numUpdatesAvailable = result.total_entries; } } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index a958e62aad..e1e58bf7c7 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -99,6 +99,13 @@ Rectangle { } } + onActiveViewChanged: { + if (activeView === "walletHome") { + walletHomeButtonContainer.messagesWaiting = false; + sendToScript({method: 'clearShouldShowDotHistory'}); + } + } + HifiCommerceCommon.CommerceLightbox { id: lightboxPopup; visible: false; @@ -494,6 +501,7 @@ Rectangle { // "WALLET HOME" tab button Rectangle { id: walletHomeButtonContainer; + property bool messagesWaiting: false; visible: !walletSetup.visible; color: root.activeView === "walletHome" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; @@ -514,6 +522,19 @@ Rectangle { color: WalletScriptingInterface.limitedCommerce ? hifi.colors.lightGray50 : ((root.activeView === "walletHome" || walletHomeTabMouseArea.containsMouse) ? hifi.colors.white : hifi.colors.blueHighlight); } + Rectangle { + id: recentActivityMessagesWaitingLight; + visible: parent.messagesWaiting; + anchors.right: homeTabIcon.left; + anchors.rightMargin: -4; + anchors.top: homeTabIcon.top; + anchors.topMargin: 16; + height: 10; + width: height; + radius: height/2; + color: "red"; + } + RalewaySemiBold { text: "RECENT ACTIVITY"; // Text size @@ -572,7 +593,7 @@ Rectangle { } Rectangle { - id: messagesWaitingLight; + id: exchangeMoneyMessagesWaitingLight; visible: parent.messagesWaiting; anchors.right: exchangeMoneyTabIcon.left; anchors.rightMargin: -4; @@ -889,6 +910,9 @@ Rectangle { case 'updateWearables': walletInventory.fromScript(message); break; + case 'updateRecentActivityMessageLight': + walletHomeButtonContainer.messagesWaiting = message.messagesWaiting; + break; default: console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); } diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js index efb842a9bb..c340dfecd2 100644 --- a/scripts/modules/appUi.js +++ b/scripts/modules/appUi.js @@ -95,16 +95,16 @@ function AppUi(properties) { activeIcon: isWaiting ? that.activeMessagesButton : that.activeButton }); }; - that.notificationPollTimeout = false; - that.notificationPollTimeoutMs = 60000; - that.notificationPollEndpoint = false; - that.notificationPollStopPaginatingConditionMet = false; + 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.notificationPollCallback = [that.ignore]; + that.notificationPollCaresAboutSince = [false]; + that.notificationInitialCallbackMade = [false]; that.notificationDisplayBanner = function (message) { if (!that.isOpen) { Window.displayAnnouncement(message); @@ -149,73 +149,105 @@ function AppUi(properties) { // // START Notification Handling // + + var currentDataPageToRetrieve = []; + var concatenatedServerResponse = []; + for (var i = 0; i < that.notificationPollEndpoint.length; i++) { + currentDataPageToRetrieve[i] = 1; + concatenatedServerResponse[i] = new Array(); + } + + function requestCallback(error, response, optionalParams) { + var indexOfRequest = optionalParams.indexOfRequest; + var urlOfRequest = optionalParams.urlOfRequest; + + if (error || (response.status !== 'success')) { + print("Error: unable to get", urlOfRequest, error || response.status); + that.notificationPollTimeout[indexOfRequest] = Script.setTimeout( + that.notificationPoll(indexOfRequest), that.notificationPollTimeoutMs[indexOfRequest]); + return; + } + + if (!that.notificationPollStopPaginatingConditionMet[indexOfRequest] || + that.notificationPollStopPaginatingConditionMet[indexOfRequest](response)) { + that.notificationPollTimeout[indexOfRequest] = Script.setTimeout(function () { + that.notificationPoll(indexOfRequest); + }, that.notificationPollTimeoutMs[indexOfRequest]); + + var notificationData; + if (concatenatedServerResponse[indexOfRequest].length) { + notificationData = concatenatedServerResponse[indexOfRequest]; + } else { + notificationData = that.notificationDataProcessPage[indexOfRequest](response); + } + console.debug(that.buttonName, that.notificationPollEndpoint[indexOfRequest], + 'notification data for processing:', JSON.stringify(notificationData)); + 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 currentDataPageToRetrieve = 1; - var concatenatedServerResponse = new Array(); - that.notificationPoll = function () { - if (!that.notificationPollEndpoint) { + that.notificationPoll = function (i) { + if (!that.notificationPollEndpoint[i]) { return; } // User is "appearing offline" or is offline if (GlobalServices.findableBy === "none" || Account.username === "") { - that.notificationPollTimeout = Script.setTimeout(that.notificationPoll, that.notificationPollTimeoutMs); + that.notificationPollTimeout[i] = Script.setTimeout( + that.notificationPoll(i), that.notificationPollTimeoutMs[i]); return; } - var url = METAVERSE_BASE + that.notificationPollEndpoint; + var url = METAVERSE_BASE + that.notificationPollEndpoint[i]; - var settingsKey = "notifications/" + that.buttonName + "/lastPoll"; + var settingsKey = "notifications/" + that.notificationPollEndpoint[i] + "/lastPoll"; var currentTimestamp = new Date().getTime(); var lastPollTimestamp = Settings.getValue(settingsKey, currentTimestamp); - if (that.notificationPollCaresAboutSince) { - url = url + "&since=" + lastPollTimestamp/1000; + if (that.notificationPollCaresAboutSince[i]) { + url = url + "&since=" + lastPollTimestamp / 1000; } Settings.setValue(settingsKey, currentTimestamp); console.debug(that.buttonName, 'polling for notifications at endpoint', url); - function requestCallback(error, response) { - if (error || (response.status !== 'success')) { - print("Error: unable to get", url, error || response.status); - that.notificationPollTimeout = Script.setTimeout(that.notificationPoll, that.notificationPollTimeoutMs); - return; - } - - if (!that.notificationPollStopPaginatingConditionMet || that.notificationPollStopPaginatingConditionMet(response)) { - that.notificationPollTimeout = Script.setTimeout(that.notificationPoll, that.notificationPollTimeoutMs); - - var notificationData; - if (concatenatedServerResponse.length) { - notificationData = concatenatedServerResponse; - } else { - notificationData = that.notificationDataProcessPage(response); - } - console.debug(that.buttonName, 'notification data for processing:', JSON.stringify(notificationData)); - that.notificationPollCallback(notificationData); - that.notificationInitialCallbackMade = true; - currentDataPageToRetrieve = 1; - concatenatedServerResponse = new Array(); - } else { - concatenatedServerResponse = concatenatedServerResponse.concat(that.notificationDataProcessPage(response)); - currentDataPageToRetrieve++; - request({ json: true, uri: (url + "&page=" + currentDataPageToRetrieve) }, requestCallback); - } - } - - request({ json: true, uri: url }, requestCallback); + request({ + json: true, + uri: url + }, + requestCallback, + { + indexOfRequest: i, + urlOfRequest: url + }); }; // This won't do anything if there isn't a notification endpoint set - that.notificationPoll(); + for (i = 0; i < that.notificationPollEndpoint.length; i++) { + that.notificationPoll(i); + } function restartNotificationPoll() { - that.notificationInitialCallbackMade = false; - if (that.notificationPollTimeout) { - Script.clearTimeout(that.notificationPollTimeout); - that.notificationPollTimeout = false; + 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); } - that.notificationPoll(); } // // END Notification Handling @@ -322,9 +354,11 @@ function AppUi(properties) { } that.tablet.removeButton(that.button); } - if (that.notificationPollTimeout) { - Script.clearInterval(that.notificationPollTimeout); - that.notificationPollTimeout = false; + 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. @@ -333,7 +367,7 @@ function AppUi(properties) { Script.scriptEnding.connect(that.onScriptEnding); GlobalServices.findableByChanged.connect(restartNotificationPoll); GlobalServices.myUsernameChanged.connect(restartNotificationPoll); - if (that.buttonName == Settings.getValue("startUpApp")) { + if (that.buttonName === Settings.getValue("startUpApp")) { Settings.setValue("startUpApp", ""); Script.setTimeout(function () { that.open(); diff --git a/scripts/modules/request.js b/scripts/modules/request.js index d0037f9b43..37f3ac0d7b 100644 --- a/scripts/modules/request.js +++ b/scripts/modules/request.js @@ -18,7 +18,8 @@ module.exports = { // ------------------------------------------------------------------ - request: function (options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. + // 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 () { @@ -38,7 +39,7 @@ module.exports = { if (error) { response = { statusCode: httpRequest.status }; } - callback(error, response); + callback(error, response, optionalCallbackParameter); } }; if (typeof options === 'string') { diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 353145035e..5396014866 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -506,10 +506,6 @@ function fromQml(message) { ui.tablet.sendToQml({ method: 'updateWearables', wornWearables: currentlyWornWearables }); break; - case 'purchases_availableUpdatesReceived': - shouldShowDot = message.numUpdates > 0; - ui.messagesWaiting(shouldShowDot && !ui.isOpen); - break; case 'purchases_walletNotSetUp': ui.tablet.sendToQml({ method: 'updateWalletReferrer', @@ -525,6 +521,10 @@ function fromQml(message) { openMarketplace(itemId); } break; + case 'clearShouldShowDotHistory': + shouldShowDotHistory = false; + ui.messagesWaiting(shouldShowDotUpdates || shouldShowDotHistory); + break; case 'http.request': // Handled elsewhere, don't log. break; @@ -541,8 +541,13 @@ function walletOpened() { triggerMapping.enable(); triggerPressMapping.enable(); isWired = true; - shouldShowDot = false; - ui.messagesWaiting(shouldShowDot); + + if (shouldShowDotHistory) { + ui.sendMessage({ + method: 'updateRecentActivityMessageLight', + messagesWaiting: shouldShowDotHistory + }); + } } function walletClosed() { @@ -557,20 +562,20 @@ function notificationDataProcessPageHistory(data) { return data.data.history; } -var shouldShowDot = false; +var shouldShowDotUpdates = false; function notificationPollCallbackUpdates(updatesArray) { - shouldShowDot = shouldShowDot || updatesArray.length > 0; - ui.messagesWaiting(shouldShowDot && !ui.isOpen); + shouldShowDotUpdates = shouldShowDotUpdates || updatesArray.length > 0; + ui.messagesWaiting(shouldShowDotUpdates || shouldShowDotHistory); if (updatesArray.length > 0) { var message; - if (!ui.notificationInitialCallbackMade) { + if (!ui.notificationInitialCallbackMade[0]) { message = updatesArray.length + " of your purchased items " + (updatesArray.length === 1 ? "has an update " : "have updates ") + "available. Open WALLET to update."; ui.notificationDisplayBanner(message); - ui.notificationPollCaresAboutSince = true; + ui.notificationPollCaresAboutSince[0] = true; } else { for (var i = 0; i < updatesArray.length; i++) { message = "Update available for \"" + @@ -581,15 +586,16 @@ function notificationPollCallbackUpdates(updatesArray) { } } } +var shouldShowDotHistory = false; function notificationPollCallbackHistory(historyArray) { if (!ui.isOpen) { var notificationCount = historyArray.length; - shouldShowDot = shouldShowDot || notificationCount > 0; - ui.messagesWaiting(shouldShowDot); + shouldShowDotHistory = shouldShowDotHistory || notificationCount > 0; + ui.messagesWaiting(shouldShowDotUpdates || shouldShowDotHistory); if (notificationCount > 0) { var message; - if (!ui.notificationInitialCallbackMade) { + if (!ui.notificationInitialCallbackMade[1]) { message = "You have " + notificationCount + " unread wallet " + "transaction" + (notificationCount === 1 ? "" : "s") + ". Open WALLET to see all activity."; ui.notificationDisplayBanner(message); @@ -605,8 +611,8 @@ function notificationPollCallbackHistory(historyArray) { } function isReturnedDataEmptyUpdates(data) { - var historyArray = data.data.history; - return historyArray.length === 0; + var updatesArray = data.data.updates; + return updatesArray.length === 0; } function isReturnedDataEmptyHistory(data) { @@ -657,20 +663,12 @@ function startup() { onOpened: walletOpened, onClosed: walletClosed, onMessage: fromQml, -/* Gotta re-add all this stuff once I get it working notificationPollEndpoint: ["/api/v1/commerce/available_updates?per_page=10", "/api/v1/commerce/history?per_page=10"], notificationPollTimeoutMs: [NOTIFICATION_POLL_TIMEOUT, NOTIFICATION_POLL_TIMEOUT], notificationDataProcessPage: [notificationDataProcessPageUpdates, notificationDataProcessPageHistory], notificationPollCallback: [notificationPollCallbackUpdates, notificationPollCallbackHistory], notificationPollStopPaginatingConditionMet: [isReturnedDataEmptyUpdates, isReturnedDataEmptyHistory], notificationPollCaresAboutSince: [false, true] -*/ - notificationPollEndpoint: "/api/v1/commerce/available_updates?per_page=10", - notificationPollTimeoutMs: 300000, - notificationDataProcessPage: notificationDataProcessPageUpdates, - notificationPollCallback: notificationPollCallbackUpdates, - notificationPollStopPaginatingConditionMet: isReturnedDataEmptyUpdates, - notificationPollCaresAboutSince: false }); GlobalServices.myUsernameChanged.connect(onUsernameChanged); installMarketplaceItemTester(); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index a2ebae1a33..341ce9ebc8 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -844,7 +844,7 @@ function notificationPollCallback(connectionsArray) { newOnlineUsers++; storedOnlineUsers[user.username] = user; - if (!ui.isOpen && ui.notificationInitialCallbackMade) { + if (!ui.isOpen && ui.notificationInitialCallbackMade[0]) { message = user.username + " is available in " + user.location.root.name + ". Open PEOPLE to join them."; ui.notificationDisplayBanner(message); @@ -868,7 +868,7 @@ function notificationPollCallback(connectionsArray) { shouldShowDot: shouldShowDot }); - if (newOnlineUsers > 0 && !ui.notificationInitialCallbackMade) { + if (newOnlineUsers > 0 && !ui.notificationInitialCallbackMade[0]) { message = newOnlineUsers + " of your connections " + (newOnlineUsers === 1 ? "is" : "are") + " available online. Open PEOPLE to join them."; ui.notificationDisplayBanner(message); @@ -889,12 +889,12 @@ function startup() { onOpened: palOpened, onClosed: off, onMessage: fromQml, - notificationPollEndpoint: "/api/v1/users?filter=connections&status=online&per_page=10", - notificationPollTimeoutMs: 60000, - notificationDataProcessPage: notificationDataProcessPage, - notificationPollCallback: notificationPollCallback, - notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, - notificationPollCaresAboutSince: false + notificationPollEndpoint: ["/api/v1/users?filter=connections&status=online&per_page=10"], + notificationPollTimeoutMs: [60000], + notificationDataProcessPage: [notificationDataProcessPage], + notificationPollCallback: [notificationPollCallback], + notificationPollStopPaginatingConditionMet: [isReturnedDataEmpty], + notificationPollCaresAboutSince: [false] }); Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 6d8ba3a927..94117fd9ea 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -37,7 +37,7 @@ function notificationPollCallback(userStoriesArray) { // pingPong = !pingPong; var totalNewStories = 0; - var shouldNotifyIndividually = !ui.isOpen && ui.notificationInitialCallbackMade; + var shouldNotifyIndividually = !ui.isOpen && ui.notificationInitialCallbackMade[0]; userStoriesArray.forEach(function (story) { if (story.audience !== "for_connections" && story.audience !== "for_feed") { @@ -91,7 +91,7 @@ function notificationPollCallback(userStoriesArray) { shouldShowDot = totalNewStories > 0 || (totalStories > 0 && shouldShowDot); ui.messagesWaiting(shouldShowDot && !ui.isOpen); - if (totalStories > 0 && !ui.isOpen && !ui.notificationInitialCallbackMade) { + if (totalStories > 0 && !ui.isOpen && !ui.notificationInitialCallbackMade[0]) { message = "There " + (totalStories === 1 ? "is " : "are ") + totalStories + " event" + (totalStories === 1 ? "" : "s") + " to know about. " + "Open GOTO to see " + (totalStories === 1 ? "it" : "them") + "."; @@ -122,12 +122,12 @@ function startup() { sortOrder: 8, onOpened: gotoOpened, home: GOTO_QML_SOURCE, - notificationPollEndpoint: endpoint, - notificationPollTimeoutMs: 60000, - notificationDataProcessPage: notificationDataProcessPage, - notificationPollCallback: notificationPollCallback, - notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, - notificationPollCaresAboutSince: false + notificationPollEndpoint: [endpoint], + notificationPollTimeoutMs: [60000], + notificationDataProcessPage: [notificationDataProcessPage], + notificationPollCallback: [notificationPollCallback], + notificationPollStopPaginatingConditionMet: [isReturnedDataEmpty], + notificationPollCaresAboutSince: [false] }); } From c4c4d2c98deebd8f5f5165e1a972997476f8a9de Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 31 Oct 2018 13:07:48 -0700 Subject: [PATCH 289/362] possibly fix mac issues --- libraries/gpu/src/gpu/MipGeneration.slh | 2 +- libraries/render-utils/src/debug_deferred_buffer.slf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/gpu/src/gpu/MipGeneration.slh b/libraries/gpu/src/gpu/MipGeneration.slh index bc8dd39042..b5d4ab3303 100644 --- a/libraries/gpu/src/gpu/MipGeneration.slh +++ b/libraries/gpu/src/gpu/MipGeneration.slh @@ -13,7 +13,7 @@ <@include gpu/ShaderConstants.h@> -layout(binding=GPU_TEXTURE_MIP_CREATION_INPUT) uniform sampler2D texMap; +LAYOUT(binding=GPU_TEXTURE_MIP_CREATION_INPUT) uniform sampler2D texMap; in vec2 varTexCoord0; diff --git a/libraries/render-utils/src/debug_deferred_buffer.slf b/libraries/render-utils/src/debug_deferred_buffer.slf index c6e3c49e54..ccbe5c491f 100644 --- a/libraries/render-utils/src/debug_deferred_buffer.slf +++ b/libraries/render-utils/src/debug_deferred_buffer.slf @@ -25,7 +25,7 @@ LAYOUT(binding=RENDER_UTILS_TEXTURE_SHADOW) uniform sampler2DArrayShadow shadowM <@include debug_deferred_buffer_shared.slh@> -layout(std140, binding=RENDER_UTILS_BUFFER_DEBUG_DEFERRED_PARAMS) uniform parametersBuffer { +LAYOUT_STD140(binding=RENDER_UTILS_BUFFER_DEBUG_DEFERRED_PARAMS) uniform parametersBuffer { DebugParameters parameters; }; From a1276ba8995dc91ada1f25e491c11207958e5702 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 31 Oct 2018 13:31:29 -0700 Subject: [PATCH 290/362] Don't poll for history notifs if limitedCommerce is true --- scripts/system/commerce/wallet.js | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 5396014866..2ae30b5b35 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -655,6 +655,22 @@ var WALLET_QML_SOURCE = "hifi/commerce/wallet/Wallet.qml"; var NOTIFICATION_POLL_TIMEOUT = 300000; var ui; function startup() { + var notificationPollEndpointArray = ["/api/v1/commerce/available_updates?per_page=10"]; + var notificationPollTimeoutMsArray = [NOTIFICATION_POLL_TIMEOUT]; + var notificationDataProcessPageArray = [notificationDataProcessPageUpdates]; + var notificationPollCallbackArray = [notificationPollCallbackUpdates]; + var notificationPollStopPaginatingConditionMetArray = [isReturnedDataEmptyUpdates]; + var notificationPollCaresAboutSinceArray = [false]; + + if (!WalletScriptingInterface.limitedCommerce) { + notificationPollEndpointArray[1] = "/api/v1/commerce/history?per_page=10"; + notificationPollTimeoutMsArray[1] = NOTIFICATION_POLL_TIMEOUT; + notificationDataProcessPageArray[1] = notificationDataProcessPageHistory; + notificationPollCallbackArray[1] = notificationPollCallbackHistory; + notificationPollStopPaginatingConditionMetArray[1] = isReturnedDataEmptyHistory; + notificationPollCaresAboutSinceArray[1] = true; + } + ui = new AppUi({ buttonName: BUTTON_NAME, buttonPrefix: "wallet-", @@ -663,12 +679,12 @@ function startup() { onOpened: walletOpened, onClosed: walletClosed, onMessage: fromQml, - notificationPollEndpoint: ["/api/v1/commerce/available_updates?per_page=10", "/api/v1/commerce/history?per_page=10"], - notificationPollTimeoutMs: [NOTIFICATION_POLL_TIMEOUT, NOTIFICATION_POLL_TIMEOUT], - notificationDataProcessPage: [notificationDataProcessPageUpdates, notificationDataProcessPageHistory], - notificationPollCallback: [notificationPollCallbackUpdates, notificationPollCallbackHistory], - notificationPollStopPaginatingConditionMet: [isReturnedDataEmptyUpdates, isReturnedDataEmptyHistory], - notificationPollCaresAboutSince: [false, true] + notificationPollEndpoint: notificationPollEndpointArray, + notificationPollTimeoutMs: notificationPollTimeoutMsArray, + notificationDataProcessPage: notificationDataProcessPageArray, + notificationPollCallback: notificationPollCallbackArray, + notificationPollStopPaginatingConditionMet: notificationPollStopPaginatingConditionMetArray, + notificationPollCaresAboutSince: notificationPollCaresAboutSinceArray }); GlobalServices.myUsernameChanged.connect(onUsernameChanged); installMarketplaceItemTester(); From 41ec026f8d521d10f10f9f27718ffb65f1462cbd Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 29 Oct 2018 16:16:16 -0700 Subject: [PATCH 291/362] cleanup packet times, avoid ambiguous RTT calcs, allow fast re-transmit --- .../networking/src/udt/CongestionControl.h | 2 + libraries/networking/src/udt/Connection.cpp | 22 +- libraries/networking/src/udt/SendQueue.cpp | 1 + libraries/networking/src/udt/TCPVegasCC.cpp | 246 +++++++++++------- libraries/networking/src/udt/TCPVegasCC.h | 19 +- 5 files changed, 177 insertions(+), 113 deletions(-) diff --git a/libraries/networking/src/udt/CongestionControl.h b/libraries/networking/src/udt/CongestionControl.h index 7093e8bd96..bfe7f552d1 100644 --- a/libraries/networking/src/udt/CongestionControl.h +++ b/libraries/networking/src/udt/CongestionControl.h @@ -45,8 +45,10 @@ public: virtual void onTimeout() {} virtual void onPacketSent(int wireSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint) {} + virtual void onPacketReSent(int wireSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint) {} virtual int estimatedTimeout() const = 0; + protected: void setMSS(int mss) { _mss = mss; } virtual void setInitialSendSequenceNumber(SequenceNumber seqNum) = 0; diff --git a/libraries/networking/src/udt/Connection.cpp b/libraries/networking/src/udt/Connection.cpp index 24e294881a..4798288a18 100644 --- a/libraries/networking/src/udt/Connection.cpp +++ b/libraries/networking/src/udt/Connection.cpp @@ -195,7 +195,7 @@ void Connection::recordSentPackets(int wireSize, int payloadSize, void Connection::recordRetransmission(int wireSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint) { _stats.record(ConnectionStats::Stats::Retransmission); - _congestionControl->onPacketSent(wireSize, seqNum, timePoint); + _congestionControl->onPacketReSent(wireSize, seqNum, timePoint); } void Connection::sendACK() { @@ -303,7 +303,7 @@ void Connection::processControl(ControlPacketPointer controlPacket) { // where the other end expired our connection. Let's reset. #ifdef UDT_CONNECTION_DEBUG - qCDebug(networking) << "Got HandshakeRequest from" << _destination << ", stopping SendQueue"; + qCDebug(networking) << "Got HandshakeRequest from" << _destination << ", stopping SendQueue"; #endif _hasReceivedHandshakeACK = false; stopSendQueue(); @@ -327,19 +327,19 @@ void Connection::processACK(ControlPacketPointer controlPacket) { return; } - if (ack <= _lastReceivedACK) { + if (ack < _lastReceivedACK) { // this is an out of order ACK, bail - // or - // processing an already received ACK, bail return; } - - _lastReceivedACK = ack; - - // ACK the send queue so it knows what was received - getSendQueue().ack(ack); - + if (ack > _lastReceivedACK) { + // this is not a repeated ACK, so update our member and tell the send queue + _lastReceivedACK = ack; + + // ACK the send queue so it knows what was received + getSendQueue().ack(ack); + } + // give this ACK to the congestion control and update the send queue parameters updateCongestionControlAndSendQueue([this, ack, &controlPacket] { if (_congestionControl->onACK(ack, controlPacket->getReceiveTime())) { diff --git a/libraries/networking/src/udt/SendQueue.cpp b/libraries/networking/src/udt/SendQueue.cpp index b1dfb9a8cf..9cba4970ac 100644 --- a/libraries/networking/src/udt/SendQueue.cpp +++ b/libraries/networking/src/udt/SendQueue.cpp @@ -481,6 +481,7 @@ bool SendQueue::isInactive(bool attemptedToSendPacket) { auto cvStatus = _emptyCondition.wait_for(locker, EMPTY_QUEUES_INACTIVE_TIMEOUT); if (cvStatus == std::cv_status::timeout && (_packets.isEmpty() || isFlowWindowFull()) && _naks.isEmpty()) { + #ifdef UDT_CONNECTION_DEBUG qCDebug(networking) << "SendQueue to" << _destination << "has been empty for" << EMPTY_QUEUES_INACTIVE_TIMEOUT.count() diff --git a/libraries/networking/src/udt/TCPVegasCC.cpp b/libraries/networking/src/udt/TCPVegasCC.cpp index 4842e5a204..f2119237c2 100644 --- a/libraries/networking/src/udt/TCPVegasCC.cpp +++ b/libraries/networking/src/udt/TCPVegasCC.cpp @@ -27,112 +27,106 @@ TCPVegasCC::TCPVegasCC() { _baseRTT = std::numeric_limits::max(); } +bool TCPVegasCC::calculateRTT(p_high_resolution_clock::time_point sendTime, p_high_resolution_clock::time_point receiveTime) { + // calculate the RTT (receive time - time ACK sent) + int lastRTT = duration_cast(receiveTime - sendTime).count(); + + const int MAX_RTT_SAMPLE_MICROSECONDS = 10000000; + + if (lastRTT < 0) { + Q_ASSERT_X(false, __FUNCTION__, "calculated an RTT that is not > 0"); + return false; + } else if (lastRTT == 0) { + // we do not allow a zero microsecond RTT (as per the UNIX kernel implementation of TCP Vegas) + lastRTT = 1; + } else if (lastRTT > MAX_RTT_SAMPLE_MICROSECONDS) { + // we cap the lastRTT to MAX_RTT_SAMPLE_MICROSECONDS to avoid overflows in window size calculations + lastRTT = MAX_RTT_SAMPLE_MICROSECONDS; + } + + if (_ewmaRTT == -1) { + // first RTT sample - set _ewmaRTT to the value and set the variance to half the value + _ewmaRTT = lastRTT; + _rttVariance = lastRTT / 2; + } else { + // This updates the RTT using exponential weighted moving average + // This is the Jacobson's forumla for RTT estimation + // http://www.mathcs.emory.edu/~cheung/Courses/455/Syllabus/7-transport/Jacobson-88.pdf + + // Estimated RTT = (1 - x)(estimatedRTT) + (x)(sampleRTT) + // (where x = 0.125 via Jacobson) + + // Deviation = (1 - x)(deviation) + x |sampleRTT - estimatedRTT| + // (where x = 0.25 via Jacobson) + + static const int RTT_ESTIMATION_ALPHA = 8; + static const int RTT_ESTIMATION_VARIANCE_ALPHA = 4; + + _ewmaRTT = (_ewmaRTT * (RTT_ESTIMATION_ALPHA - 1) + lastRTT) / RTT_ESTIMATION_ALPHA; + _rttVariance = (_rttVariance * (RTT_ESTIMATION_VARIANCE_ALPHA- 1) + + abs(lastRTT - _ewmaRTT)) / RTT_ESTIMATION_VARIANCE_ALPHA; + } + + // keep track of the lowest RTT during connection + _baseRTT = std::min(_baseRTT, lastRTT); + + // find the min RTT during the last RTT + _currentMinRTT = std::min(_currentMinRTT, lastRTT); + + // add 1 to the number of RTT samples collected during this RTT window + ++_numRTTs; + + return true; +} + bool TCPVegasCC::onACK(SequenceNumber ack, p_high_resolution_clock::time_point receiveTime) { - auto it = _sentPacketTimes.find(ack); auto previousAck = _lastACK; _lastACK = ack; - if (it != _sentPacketTimes.end()) { + bool wasDuplicateACK = (ack == previousAck); - // calculate the RTT (receive time - time ACK sent) - int lastRTT = duration_cast(receiveTime - it->second).count(); + auto it = std::find_if(_sentPacketDatas.begin(), _sentPacketDatas.end(), [ack](SentPacketData& packetTime){ + return packetTime.sequenceNumber == ack; + }); - const int MAX_RTT_SAMPLE_MICROSECONDS = 10000000; + if (!wasDuplicateACK && it != _sentPacketDatas.end()) { + // check if we can unambigiously calculate an RTT from this ACK - if (lastRTT < 0) { - Q_ASSERT_X(false, __FUNCTION__, "calculated an RTT that is not > 0"); + // for that to be the case, + // any of the packets this ACK covers (from the current ACK back to our previous ACK) + // must not have been re-sent + bool canBeUsedForRTT = std::none_of(_sentPacketDatas.begin(), _sentPacketDatas.end(), + [ack, previousAck](SentPacketData& sentPacketData) + { + return sentPacketData.sequenceNumber > previousAck + && sentPacketData.sequenceNumber <= ack + && sentPacketData.wasResent; + }); + + auto sendTime = it->timePoint; + + // remove all sent packet times up to this sequence number + it = _sentPacketDatas.erase(_sentPacketDatas.begin(), it + 1); + + // if we can use this ACK for an RTT calculation then do so + // returning false if we calculate an invalid RTT + if (canBeUsedForRTT && !calculateRTT(sendTime, receiveTime)) { return false; - } else if (lastRTT == 0) { - // we do not allow a zero microsecond RTT (as per the UNIX kernel implementation of TCP Vegas) - lastRTT = 1; - } else if (lastRTT > MAX_RTT_SAMPLE_MICROSECONDS) { - // we cap the lastRTT to MAX_RTT_SAMPLE_MICROSECONDS to avoid overflows in window size calculations - lastRTT = MAX_RTT_SAMPLE_MICROSECONDS; } + } - if (_ewmaRTT == -1) { - // first RTT sample - set _ewmaRTT to the value and set the variance to half the value - _ewmaRTT = lastRTT; - _rttVariance = lastRTT / 2; - } else { - // This updates the RTT using exponential weighted moving average - // This is the Jacobson's forumla for RTT estimation - // http://www.mathcs.emory.edu/~cheung/Courses/455/Syllabus/7-transport/Jacobson-88.pdf - - // Estimated RTT = (1 - x)(estimatedRTT) + (x)(sampleRTT) - // (where x = 0.125 via Jacobson) - - // Deviation = (1 - x)(deviation) + x |sampleRTT - estimatedRTT| - // (where x = 0.25 via Jacobson) - - static const int RTT_ESTIMATION_ALPHA = 8; - static const int RTT_ESTIMATION_VARIANCE_ALPHA = 4; - - _ewmaRTT = (_ewmaRTT * (RTT_ESTIMATION_ALPHA - 1) + lastRTT) / RTT_ESTIMATION_ALPHA; - _rttVariance = (_rttVariance * (RTT_ESTIMATION_VARIANCE_ALPHA- 1) - + abs(lastRTT - _ewmaRTT)) / RTT_ESTIMATION_VARIANCE_ALPHA; - } - - // add 1 to the number of ACKs during this RTT - ++_numACKs; - - // keep track of the lowest RTT during connection - _baseRTT = std::min(_baseRTT, lastRTT); - - // find the min RTT during the last RTT - _currentMinRTT = std::min(_currentMinRTT, lastRTT); - - auto sinceLastAdjustment = duration_cast(p_high_resolution_clock::now() - _lastAdjustmentTime).count(); - if (sinceLastAdjustment >= _ewmaRTT) { - performCongestionAvoidance(ack); - } - - // remove this sent packet time from the hash - _sentPacketTimes.erase(it); + auto sinceLastAdjustment = duration_cast(p_high_resolution_clock::now() - _lastAdjustmentTime).count(); + if (sinceLastAdjustment >= _ewmaRTT) { + performCongestionAvoidance(ack); } ++_numACKSinceFastRetransmit; // perform the fast re-transmit check if this is a duplicate ACK or if this is the first or second ACK // after a previous fast re-transmit - if (ack == previousAck || _numACKSinceFastRetransmit < 3) { - // we may need to re-send ackNum + 1 if it has been more than our estimated timeout since it was sent - - auto it = _sentPacketTimes.find(ack + 1); - if (it != _sentPacketTimes.end()) { - - auto now = p_high_resolution_clock::now(); - auto sinceSend = duration_cast(now - it->second).count(); - - if (sinceSend >= estimatedTimeout()) { - // break out of slow start, we've decided this is loss - _slowStart = false; - - // reset the fast re-transmit counter - _numACKSinceFastRetransmit = 0; - - // return true so the caller knows we needed a fast re-transmit - return true; - } - } - - // if this is the 3rd duplicate ACK, we fallback to Reno's fast re-transmit - static const int RENO_FAST_RETRANSMIT_DUPLICATE_COUNT = 3; - - ++_duplicateACKCount; - - if (ack == previousAck && _duplicateACKCount == RENO_FAST_RETRANSMIT_DUPLICATE_COUNT) { - // break out of slow start, we just hit loss - _slowStart = false; - - // reset our fast re-transmit counters - _numACKSinceFastRetransmit = 0; - _duplicateACKCount = 0; - - // return true so the caller knows we needed a fast re-transmit - return true; - } + if (wasDuplicateACK || _numACKSinceFastRetransmit < 3) { + return needsFastRetransmit(ack, wasDuplicateACK); } else { _duplicateACKCount = 0; } @@ -141,6 +135,49 @@ bool TCPVegasCC::onACK(SequenceNumber ack, p_high_resolution_clock::time_point r return false; } +bool TCPVegasCC::needsFastRetransmit(SequenceNumber ack, bool wasDuplicateACK) { + // we may need to re-send ackNum + 1 if it has been more than our estimated timeout since it was sent + + auto nextIt = std::find_if(_sentPacketDatas.begin(), _sentPacketDatas.end(), [ack](SentPacketData& packetTime){ + return packetTime.sequenceNumber == ack + 1; + }); + + if (nextIt != _sentPacketDatas.end()) { + auto now = p_high_resolution_clock::now(); + auto sinceSend = duration_cast(now - nextIt->timePoint).count(); + + if (sinceSend >= estimatedTimeout()) { + // break out of slow start, we've decided this is loss + _slowStart = false; + + // reset the fast re-transmit counter + _numACKSinceFastRetransmit = 0; + + // return true so the caller knows we needed a fast re-transmit + return true; + } + } + + // if this is the 3rd duplicate ACK, we fallback to Reno's fast re-transmit + static const int RENO_FAST_RETRANSMIT_DUPLICATE_COUNT = 3; + + ++_duplicateACKCount; + + if (wasDuplicateACK && _duplicateACKCount == RENO_FAST_RETRANSMIT_DUPLICATE_COUNT) { + // break out of slow start, we just hit loss + _slowStart = false; + + // reset our fast re-transmit counters + _numACKSinceFastRetransmit = 0; + _duplicateACKCount = 0; + + // return true so the caller knows we needed a fast re-transmit + return true; + } + + return false; +} + void TCPVegasCC::performCongestionAvoidance(udt::SequenceNumber ack) { static int VEGAS_ALPHA_SEGMENTS = 4; static int VEGAS_BETA_SEGMENTS = 6; @@ -158,7 +195,7 @@ void TCPVegasCC::performCongestionAvoidance(udt::SequenceNumber ack) { int64_t windowSizeDiff = (int64_t) _congestionWindowSize * (rtt - _baseRTT) / _baseRTT; - if (_numACKs <= 2) { + if (_numRTTs <= 2) { performRenoCongestionAvoidance(ack); } else { if (_slowStart) { @@ -209,7 +246,7 @@ void TCPVegasCC::performCongestionAvoidance(udt::SequenceNumber ack) { _currentMinRTT = std::numeric_limits::max(); // reset our count of collected RTT samples - _numACKs = 0; + _numRTTs = 0; } @@ -230,29 +267,29 @@ void TCPVegasCC::performRenoCongestionAvoidance(SequenceNumber ack) { return; } - int numAcked = _numACKs; + int numRTTCollected = _numRTTs; if (_slowStart) { // while in slow start we grow the congestion window by the number of ACKed packets // allowing it to grow as high as the slow start threshold - int congestionWindow = _congestionWindowSize + numAcked; + int congestionWindow = _congestionWindowSize + numRTTCollected; if (congestionWindow > udt::MAX_PACKETS_IN_FLIGHT) { // we're done with slow start, set the congestion window to the slow start threshold _congestionWindowSize = udt::MAX_PACKETS_IN_FLIGHT; // figure out how many left over ACKs we should apply using the regular reno congestion avoidance - numAcked = congestionWindow - udt::MAX_PACKETS_IN_FLIGHT; + numRTTCollected = congestionWindow - udt::MAX_PACKETS_IN_FLIGHT; } else { _congestionWindowSize = congestionWindow; - numAcked = 0; + numRTTCollected = 0; } } // grab the size of the window prior to reno additive increase int preAIWindowSize = _congestionWindowSize; - if (numAcked > 0) { + if (numRTTCollected > 0) { // Once we are out of slow start, we use additive increase to grow the window slowly. // We grow the congestion window by a single packet everytime the entire congestion window is sent. @@ -263,7 +300,7 @@ void TCPVegasCC::performRenoCongestionAvoidance(SequenceNumber ack) { } // increase the window size by (1 / window size) for every ACK received - _ackAICount += numAcked; + _ackAICount += numRTTCollected; if (_ackAICount >= preAIWindowSize) { // when _ackAICount % preAIWindowSize == 0 then _ackAICount is 0 // when _ackAICount % preAIWindowSize != 0 then _ackAICount is _ackAICount - (_ackAICount % preAIWindowSize) @@ -277,8 +314,19 @@ void TCPVegasCC::performRenoCongestionAvoidance(SequenceNumber ack) { } void TCPVegasCC::onPacketSent(int wireSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint) { - if (_sentPacketTimes.find(seqNum) == _sentPacketTimes.end()) { - _sentPacketTimes[seqNum] = timePoint; + _sentPacketDatas.emplace_back(seqNum, timePoint); +} + +void TCPVegasCC::onPacketReSent(int wireSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint) { + // look for our information for this sent packet + auto it = std::find_if(_sentPacketDatas.begin(), _sentPacketDatas.end(), [seqNum](SentPacketData& sentPacketInfo){ + return sentPacketInfo.sequenceNumber == seqNum; + }); + + // if we found information for this packet (it hasn't been erased because it hasn't yet been ACKed) + // then mark it as re-sent so we know it cannot be used for RTT calculations + if (it != _sentPacketDatas.end()) { + it->wasResent = true; } } diff --git a/libraries/networking/src/udt/TCPVegasCC.h b/libraries/networking/src/udt/TCPVegasCC.h index bb14728d4b..1d83c4c992 100644 --- a/libraries/networking/src/udt/TCPVegasCC.h +++ b/libraries/networking/src/udt/TCPVegasCC.h @@ -30,6 +30,7 @@ public: virtual void onTimeout() override {}; virtual void onPacketSent(int wireSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint) override; + virtual void onPacketReSent(int wireSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint) override; virtual int estimatedTimeout() const override; @@ -37,11 +38,23 @@ protected: virtual void performCongestionAvoidance(SequenceNumber ack); virtual void setInitialSendSequenceNumber(SequenceNumber seqNum) override { _lastACK = seqNum - 1; } private: + bool calculateRTT(p_high_resolution_clock::time_point sendTime, p_high_resolution_clock::time_point receiveTime); + bool needsFastRetransmit(SequenceNumber ack, bool wasDuplicateACK); + bool isCongestionWindowLimited(); void performRenoCongestionAvoidance(SequenceNumber ack); - using PacketTimeList = std::map; - PacketTimeList _sentPacketTimes; // Map of sequence numbers to sent time + struct SentPacketData { + SentPacketData(SequenceNumber seqNum, p_high_resolution_clock::time_point tPoint) + : sequenceNumber(seqNum), timePoint(tPoint) {}; + + SequenceNumber sequenceNumber; + p_high_resolution_clock::time_point timePoint; + bool wasResent { false }; + }; + + using PacketTimeList = std::vector; + PacketTimeList _sentPacketDatas; // association of sequence numbers to sent time, for RTT calc p_high_resolution_clock::time_point _lastAdjustmentTime; // Time of last congestion control adjustment @@ -56,7 +69,7 @@ private: int _ewmaRTT { -1 }; // Exponential weighted moving average RTT int _rttVariance { 0 }; // Variance in collected RTT values - int _numACKs { 0 }; // Number of ACKs received during the last RTT (since last performed congestion avoidance) + int _numRTTs { 0 }; // Number of RTTs calculated during the last RTT (since last performed congestion avoidance) int _ackAICount { 0 }; // Counter for number of ACKs received for Reno additive increase int _duplicateACKCount { 0 }; // Counter for duplicate ACKs received From 34fb849536daa4a3a30d0a387946e114e9aea526 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 31 Oct 2018 13:53:18 -0700 Subject: [PATCH 292/362] Cleanup FIXME --- .../qml/hifi/commerce/purchases/Purchases.qml | 2 +- scripts/system/html/js/marketplacesInject.js | 18 +----------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 108a57d1f5..41453c9790 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -161,7 +161,7 @@ Rectangle { id: titleBarContainer; z: 997; visible: false; - height: 100; // HRS FIXME: get rid of the header and associated code entirely? + height: 100; // Size width: parent.width; // Anchors diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index fd228e2596..eda37c06b7 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -251,7 +251,6 @@ } cost = $(this).closest('.col-xs-3').find('.item-cost').text(); var costInt = parseInt(cost, 10); - var disable = limitedCommerce && (costInt > 0); $(this).closest('.col-xs-3').prev().attr("class", 'col-xs-6'); $(this).closest('.col-xs-3').attr("class", 'col-xs-6'); @@ -260,23 +259,11 @@ priceElement.css({ "padding": "3px 5px", "height": "40px", - "background": disable ? "grey" : "linear-gradient(#00b4ef, #0093C5)", + "background": "linear-gradient(#00b4ef, #0093C5)", "color": "#FFF", "font-weight": "600", "line-height": "34px" }); - - if (parseInt(cost) > 0) { - if (disable) { - priceElement.html('N/A'); // In case the following fails - $(this).parent().parent().parent().parent().parent().css({"display": "none"}); // HRS FIXME, oh and do I have to set display non-none in the other branch? - } else { - priceElement.css({ "width": "auto" }); - priceElement.html(' ' + cost); - priceElement.css({ "min-width": priceElement.width() + 30 }); - } - } }); // change pricing to GET/BUY on button hover @@ -395,9 +382,6 @@ var cost = $('.item-cost').text(); var costInt = parseInt(cost, 10); var availability = $.trim($('.item-availability').text()); - if (limitedCommerce && (costInt > 0)) { - availability = ''; - } if (availability === 'available') { purchaseButton.css({ "background": "linear-gradient(#00b4ef, #0093C5)", From ce0ad48a28de45c9bba040fb94eb5675a27223a0 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 31 Oct 2018 13:58:26 -0700 Subject: [PATCH 293/362] tabless wallet --- .../resources/qml/hifi/commerce/purchases/Purchases.qml | 2 +- interface/resources/qml/hifi/commerce/wallet/Wallet.qml | 8 ++++---- scripts/system/commerce/wallet.js | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 7000b632e2..d9ece98a84 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -452,7 +452,7 @@ Rectangle { anchors.left: parent.left; anchors.leftMargin: 16; width: paintedWidth; - text: "Inventory"; + text: "Items"; color: hifi.colors.black; size: 22; } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index a958e62aad..d63b9c722e 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -135,7 +135,7 @@ Rectangle { // Title Bar text RalewaySemiBold { id: titleBarText; - text: "ASSETS"; + text: "INVENTORY"; // Text size size: hifi.fontSizes.overlayTitle; // Anchors @@ -376,7 +376,7 @@ Rectangle { id: walletInventory; visible: root.activeView === "walletInventory"; anchors.top: titleBarContainer.bottom; - anchors.bottom: tabButtonsContainer.top; + anchors.bottom: !WalletScriptingInterface.limitedCommerce ? tabButtonsContainer.top : parent.bottom; anchors.left: parent.left; anchors.right: parent.right; Connections { @@ -475,7 +475,7 @@ Rectangle { // Item { id: tabButtonsContainer; - visible: !needsLogIn.visible && root.activeView !== "passphraseChange" && root.activeView !== "securityImageChange" && sendMoney.currentActiveView !== "sendAssetStep"; + visible: !needsLogIn.visible && root.activeView !== "passphraseChange" && root.activeView !== "securityImageChange" && sendMoney.currentActiveView !== "sendAssetStep" && !WalletScriptingInterface.limitedCommerce; property int numTabs: 5; // Size width: root.width; @@ -585,7 +585,7 @@ Rectangle { } RalewaySemiBold { - text: "INVENTORY"; + text: "ITEMS"; // Text size size: 16; // Anchors diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 353145035e..a8eca5f60d 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -644,14 +644,13 @@ function uninstallMarketplaceItemTester() { } } -var BUTTON_NAME = "ASSETS"; +var BUTTON_NAME = "INVENTORY"; var WALLET_QML_SOURCE = "hifi/commerce/wallet/Wallet.qml"; var NOTIFICATION_POLL_TIMEOUT = 300000; var ui; function startup() { ui = new AppUi({ buttonName: BUTTON_NAME, - buttonPrefix: "wallet-", sortOrder: 10, home: WALLET_QML_SOURCE, onOpened: walletOpened, From 0b7ddca5f66058e5df4fe7d356362f1358dcfcc7 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 31 Oct 2018 14:03:20 -0700 Subject: [PATCH 294/362] Change naming for straggler names in model loading debug dumps --- libraries/fbx/src/GLTFReader.cpp | 60 ++++++++++++++++---------------- libraries/fbx/src/GLTFReader.h | 2 +- libraries/fbx/src/OBJReader.cpp | 18 +++++----- libraries/fbx/src/OBJReader.h | 2 +- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/libraries/fbx/src/GLTFReader.cpp b/libraries/fbx/src/GLTFReader.cpp index 05534b5264..7ee13c5cdf 100644 --- a/libraries/fbx/src/GLTFReader.cpp +++ b/libraries/fbx/src/GLTFReader.cpp @@ -929,7 +929,7 @@ HFMGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping buildGeometry(geometry, url); - //fbxDebugDump(geometry); + //hfmDebugDump(geometry); return geometryPtr; } @@ -1181,37 +1181,37 @@ void GLTFReader::retriangulate(const QVector& inIndices, const QVector Date: Wed, 31 Oct 2018 14:15:30 -0700 Subject: [PATCH 295/362] Change local variable in TestFbx for clarity --- tests-manual/gpu/src/TestFbx.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests-manual/gpu/src/TestFbx.cpp b/tests-manual/gpu/src/TestFbx.cpp index ba94fb810c..9890e4fbe7 100644 --- a/tests-manual/gpu/src/TestFbx.cpp +++ b/tests-manual/gpu/src/TestFbx.cpp @@ -100,12 +100,12 @@ bool TestFbx::isReady() const { void TestFbx::parseFbx(const QByteArray& fbxData) { QVariantHash mapping; - HFMGeometry* fbx = readFBX(fbxData, mapping); + HFMGeometry* geometry = readFBX(fbxData, mapping); size_t totalVertexCount = 0; size_t totalIndexCount = 0; size_t totalPartCount = 0; size_t highestIndex = 0; - for (const auto& mesh : fbx->meshes) { + for (const auto& mesh : geometry->meshes) { size_t vertexCount = mesh.vertices.size(); totalVertexCount += mesh.vertices.size(); highestIndex = std::max(highestIndex, vertexCount); @@ -123,7 +123,7 @@ void TestFbx::parseFbx(const QByteArray& fbxData) { std::vector parts; parts.reserve(totalPartCount); _partCount = totalPartCount; - for (const auto& mesh : fbx->meshes) { + for (const auto& mesh : geometry->meshes) { baseVertex = vertices.size(); vec3 color; @@ -133,7 +133,7 @@ void TestFbx::parseFbx(const QByteArray& fbxData) { partIndirect.firstIndex = (uint)indices.size(); partIndirect.baseInstance = (uint)parts.size(); _partTransforms.push_back(mesh.modelTransform); - auto material = fbx->materials[part.materialID]; + auto material = geometry->materials[part.materialID]; color = material.diffuseColor; for (auto index : part.quadTrianglesIndices) { indices.push_back(index); @@ -163,7 +163,7 @@ void TestFbx::parseFbx(const QByteArray& fbxData) { _vertexBuffer->append(vertices); _indexBuffer->append(indices); _indirectBuffer->append(parts); - delete fbx; + delete geometry; } void TestFbx::renderTest(size_t testId, RenderArgs* args) { From 41a0d093896cc4c08b103a92d8473bc689f598d2 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 31 Oct 2018 15:31:51 -0700 Subject: [PATCH 296/362] First pass --- .../qml/LoginDialog/LinkAccountBody.qml | 14 +- .../hifi/commerce/common/CommerceLightbox.qml | 8 +- .../qml/hifi/commerce/wallet/Security.qml | 246 -------------- .../qml/hifi/dialogs/security/Security.qml | 306 ++++++++++++++++++ .../security}/SecurityImageChange.qml | 32 +- .../security}/SecurityImageModel.qml | 8 +- .../security}/SecurityImageSelection.qml | 16 +- .../hifi/dialogs/security/SecurityWrapper.qml | 30 ++ .../wallet => dialogs/security}/images/01.jpg | Bin .../wallet => dialogs/security}/images/02.jpg | Bin .../wallet => dialogs/security}/images/03.jpg | Bin .../wallet => dialogs/security}/images/04.jpg | Bin .../wallet => dialogs/security}/images/05.jpg | Bin .../wallet => dialogs/security}/images/06.jpg | Bin .../wallet => dialogs/security}/images/07.jpg | Bin .../wallet => dialogs/security}/images/08.jpg | Bin .../wallet => dialogs/security}/images/09.jpg | Bin .../wallet => dialogs/security}/images/10.jpg | Bin .../wallet => dialogs/security}/images/11.jpg | Bin .../wallet => dialogs/security}/images/12.jpg | Bin .../wallet => dialogs/security}/images/13.jpg | Bin .../wallet => dialogs/security}/images/14.jpg | Bin .../wallet => dialogs/security}/images/15.jpg | Bin .../wallet => dialogs/security}/images/16.jpg | Bin .../wallet => dialogs/security}/images/17.jpg | Bin .../wallet => dialogs/security}/images/18.jpg | Bin .../wallet => dialogs/security}/images/19.jpg | Bin .../wallet => dialogs/security}/images/20.jpg | Bin .../wallet => dialogs/security}/images/21.jpg | Bin .../wallet => dialogs/security}/images/22.jpg | Bin .../wallet => dialogs/security}/images/23.jpg | Bin .../wallet => dialogs/security}/images/24.jpg | Bin .../wallet => dialogs/security}/images/25.jpg | Bin .../wallet => dialogs/security}/images/26.jpg | Bin .../wallet => dialogs/security}/images/27.jpg | Bin .../wallet => dialogs/security}/images/28.jpg | Bin .../wallet => dialogs/security}/images/29.jpg | Bin .../wallet => dialogs/security}/images/30.jpg | Bin .../wallet => dialogs/security}/images/31.jpg | Bin .../wallet => dialogs/security}/images/32.jpg | Bin .../wallet => dialogs/security}/images/33.jpg | Bin .../wallet => dialogs/security}/images/34.jpg | Bin .../security}/images/lowerKeyboard.png | Bin .../security}/images/wallet-bg.jpg | Bin .../security}/images/wallet-tip-bg.png | Bin interface/src/Application.cpp | 12 +- interface/src/Menu.cpp | 7 + scripts/system/commerce/wallet.js | 6 +- 48 files changed, 388 insertions(+), 297 deletions(-) delete mode 100644 interface/resources/qml/hifi/commerce/wallet/Security.qml create mode 100644 interface/resources/qml/hifi/dialogs/security/Security.qml rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/SecurityImageChange.qml (89%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/SecurityImageModel.qml (89%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/SecurityImageSelection.qml (88%) create mode 100644 interface/resources/qml/hifi/dialogs/security/SecurityWrapper.qml rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/01.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/02.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/03.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/04.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/05.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/06.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/07.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/08.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/09.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/10.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/11.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/12.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/13.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/14.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/15.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/16.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/17.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/18.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/19.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/20.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/21.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/22.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/23.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/24.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/25.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/26.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/27.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/28.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/29.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/30.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/31.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/32.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/33.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/34.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/lowerKeyboard.png (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/wallet-bg.jpg (100%) rename interface/resources/qml/hifi/{commerce/wallet => dialogs/security}/images/wallet-tip-bg.png (100%) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 48cf124127..d5d89cd0b4 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -136,7 +136,7 @@ Item { TextField { id: usernameField - text: Settings.getValue("wallet/savedUsername", ""); + text: Settings.getValue("keepMeLoggedIn/savedUsername", ""); width: parent.width focus: true placeholderText: "Username or Email" @@ -165,7 +165,7 @@ Item { root.text = ""; } Component.onCompleted: { - var savedUsername = Settings.getValue("wallet/savedUsername", ""); + var savedUsername = Settings.getValue("keepMeLoggedIn/savedUsername", ""); usernameField.text = savedUsername === "Unknown user" ? "" : savedUsername; } } @@ -263,21 +263,21 @@ Item { CheckBox { id: autoLogoutCheckbox - checked: !Settings.getValue("wallet/autoLogout", true) + checked: Settings.getValue("keepMeLoggedIn", false) text: "Keep me signed in" boxSize: 20; labelFontSize: 15 color: hifi.colors.black onCheckedChanged: { - Settings.setValue("wallet/autoLogout", !checked); + Settings.setValue("keepMeLoggedIn", checked); if (checked) { - Settings.setValue("wallet/savedUsername", Account.username); + Settings.setValue("keepMeLoggedIn/savedUsername", Account.username); } else { - Settings.setValue("wallet/savedUsername", ""); + Settings.setValue("keepMeLoggedIn/savedUsername", ""); } } Component.onDestruction: { - Settings.setValue("wallet/autoLogout", !checked); + Settings.setValue("keepMeLoggedIn", checked); } } diff --git a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml index 9d9216c461..5ddfa98923 100644 --- a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml +++ b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml @@ -14,9 +14,9 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 import QtGraphicalEffects 1.0 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit -import "../../../controls" as HifiControls +import "qrc:////qml//styles-uit" +import "qrc:////qml//controls-uit" as HifiControlsUit +import "qrc:////qml//controls" as HifiControls // references XXX from root context @@ -40,6 +40,8 @@ Rectangle { anchors.fill: parent; color: Qt.rgba(0, 0, 0, 0.5); z: 999; + + HifiConstants { id: hifi; } onVisibleChanged: { if (!visible) { diff --git a/interface/resources/qml/hifi/commerce/wallet/Security.qml b/interface/resources/qml/hifi/commerce/wallet/Security.qml deleted file mode 100644 index 14ac696ef7..0000000000 --- a/interface/resources/qml/hifi/commerce/wallet/Security.qml +++ /dev/null @@ -1,246 +0,0 @@ -// -// Security.qml -// qml/hifi/commerce/wallet -// -// Security -// -// Created by Zach Fox on 2017-08-18 -// 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 -// - -import Hifi 1.0 as Hifi -import QtQuick 2.5 -import QtGraphicalEffects 1.0 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit -import "../../../controls" as HifiControls - -// references XXX from root context - -Item { - HifiConstants { id: hifi; } - - id: root; - property string keyFilePath; - - Connections { - target: Commerce; - - onKeyFilePathIfExistsResult: { - root.keyFilePath = path; - } - } - - // Username Text - RalewayRegular { - id: usernameText; - text: Account.username; - // Text size - size: 24; - // Style - color: hifi.colors.white; - elide: Text.ElideRight; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: parent.width/2; - height: 80; - } - - Item { - id: securityContainer; - anchors.top: usernameText.bottom; - anchors.topMargin: 20; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.bottom: parent.bottom; - - RalewaySemiBold { - id: securityText; - text: "Security"; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 20; - anchors.right: parent.right; - height: 30; - // Text size - size: 18; - // Style - color: hifi.colors.blueHighlight; - } - - Rectangle { - id: securityTextSeparator; - // Size - width: parent.width; - height: 1; - // Anchors - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: securityText.bottom; - anchors.topMargin: 8; - // Style - color: hifi.colors.faintGray; - } - - Item { - id: changeSecurityImageContainer; - anchors.top: securityTextSeparator.bottom; - anchors.topMargin: 8; - anchors.left: parent.left; - anchors.leftMargin: 40; - anchors.right: parent.right; - anchors.rightMargin: 55; - height: 75; - - HiFiGlyphs { - id: changeSecurityImageImage; - text: hifi.glyphs.securityImage; - // Size - size: 80; - // Anchors - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - // Style - color: hifi.colors.white; - } - - RalewaySemiBold { - text: "Security Pic"; - // Anchors - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.left: changeSecurityImageImage.right; - anchors.leftMargin: 30; - width: 50; - // Text size - size: 18; - // Style - color: hifi.colors.white; - } - - // "Change Security Pic" button - HifiControlsUit.Button { - id: changeSecurityImageButton; - color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.dark; - anchors.right: parent.right; - anchors.verticalCenter: parent.verticalCenter; - width: 140; - height: 40; - text: "Change"; - onClicked: { - sendSignalToWallet({method: 'walletSecurity_changeSecurityImage'}); - } - } - } - - Item { - id: autoLogoutContainer; - anchors.top: changeSecurityImageContainer.bottom; - anchors.topMargin: 8; - anchors.left: parent.left; - anchors.leftMargin: 40; - anchors.right: parent.right; - anchors.rightMargin: 55; - height: 75; - - HiFiGlyphs { - id: autoLogoutImage; - text: hifi.glyphs.walletKey; - // Size - size: 80; - // Anchors - anchors.top: parent.top; - anchors.topMargin: 20; - anchors.left: parent.left; - // Style - color: hifi.colors.white; - } - - HifiControlsUit.CheckBox { - id: autoLogoutCheckbox; - checked: Settings.getValue("wallet/autoLogout", false); - text: "Automatically Log Out when Exiting Interface" - // Anchors - anchors.verticalCenter: autoLogoutImage.verticalCenter; - anchors.left: autoLogoutImage.right; - anchors.leftMargin: 20; - anchors.right: autoLogoutHelp.left; - anchors.rightMargin: 12; - boxSize: 28; - labelFontSize: 18; - color: hifi.colors.white; - onCheckedChanged: { - Settings.setValue("wallet/autoLogout", checked); - if (checked) { - Settings.setValue("wallet/savedUsername", Account.username); - } else { - Settings.setValue("wallet/savedUsername", ""); - } - } - } - - RalewaySemiBold { - id: autoLogoutHelp; - text: '[?]'; - // Anchors - anchors.verticalCenter: autoLogoutImage.verticalCenter; - anchors.right: parent.right; - width: 30; - height: 30; - // Text size - size: 18; - // Style - color: hifi.colors.blueHighlight; - - MouseArea { - anchors.fill: parent; - hoverEnabled: true; - onEntered: { - parent.color = hifi.colors.blueAccent; - } - onExited: { - parent.color = hifi.colors.blueHighlight; - } - onClicked: { - sendSignalToWallet({method: 'walletSecurity_autoLogoutHelp'}); - } - } - } - } - } - - // - // FUNCTION DEFINITIONS START - // - // - // Function Name: fromScript() - // - // Relevant Variables: - // None - // - // Arguments: - // message: The message sent from the JavaScript. - // Messages are in format "{method, params}", like json-rpc. - // - // Description: - // Called when a message is received from a script. - // - function fromScript(message) { - switch (message.method) { - default: - console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); - } - } - signal sendSignalToWallet(var msg); - // - // FUNCTION DEFINITIONS END - // -} diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml new file mode 100644 index 0000000000..d1dc2a9e11 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/security/Security.qml @@ -0,0 +1,306 @@ +// +// Security.qml +// qml\hifi\dialogs\security +// +// Security +// +// Created by Zach Fox on 2018-10-31 +// 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 +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtGraphicalEffects 1.0 +import "qrc:////qml//styles-uit" as HifiStylesUit +import "qrc:////qml//controls-uit" as HifiControlsUit +import "qrc:////qml//controls" as HifiControls +import "qrc:////qml//hifi//commerce//common" as HifiCommerceCommon + +// references XXX from root context + +Rectangle { + HifiStylesUit.HifiConstants { id: hifi; } + + id: root; + color: hifi.colors.darkGray; + + property string title: "Security Settings"; + property bool walletSetUp; + + Connections { + target: Commerce; + + onWalletStatusResult: { + if (walletStatus === 5) { + Commerce.getSecurityImage(); + root.walletSetUp = true; + } else { + root.walletSetUp = false; + } + } + + onSecurityImageResult: { + if (exists) { + currentSecurityPicture.source = ""; + currentSecurityPicture.source = "image://security/securityImage"; + } + } + } + + HifiCommerceCommon.CommerceLightbox { + id: lightboxPopup; + visible: false; + anchors.fill: parent; + } + + // Username Text + HifiStylesUit.RalewayRegular { + id: usernameText; + text: Account.username === "" ? Account.username : "Please Log In"; + // Text size + size: 24; + // Style + color: hifi.colors.white; + elide: Text.ElideRight; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: 80; + } + + Item { + id: pleaseLogInContainer; + visible: Account.username === ""; + anchors.top: usernameText.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + + HifiStylesUit.RalewayRegular { + text: "Please log in for security settings." + // Text size + size: 24; + // Style + color: hifi.colors.white; + // Anchors + anchors.bottom: openLoginButton.top; + anchors.left: parent.left; + anchors.right: parent.right; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + height: 80; + } + + HifiControlsUit.Button { + id: openLoginButton; + color: hifi.buttons.white; + colorScheme: hifi.colorSchemes.dark; + anchors.centerIn: parent; + width: 140; + height: 40; + text: "Change"; + onClicked: { + DialogsManager.showLoginDialog(); + } + } + } + + Item { + id: securitySettingsContainer; + visible: !pleaseLogInContainer.visible; + anchors.top: usernameText.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + + Item { + id: accountContainer; + anchors.top: securitySettingsContainer.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + + Rectangle { + id: accountHeaderContainer; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 80; + color: hifi.colors.baseGrayHighlight; + + HifiStylesUit.RalewaySemiBold { + text: "Account"; + anchors.fill: parent; + anchors.leftMargin: 20; + } + } + + Item { + id: keepMeLoggedInContainer; + anchors.top: accountHeaderContainer.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: 80; + + HifiControlsUit.CheckBox { + id: autoLogoutCheckbox; + checked: Settings.getValue("keepMeLoggedIn", false); + text: "Keep Me Logged In" + // Anchors + anchors.verticalCenter: parent.verticalCenter; + anchors.left: parent.left; + anchors.leftMargin: 20; + boxSize: 28; + labelFontSize: 18; + color: hifi.colors.white; + onCheckedChanged: { + Settings.setValue("keepMeLoggedIn", checked); + if (checked) { + Settings.setValue("keepMeLoggedIn/savedUsername", Account.username); + } else { + Settings.setValue("keepMeLoggedIn/savedUsername", ""); + } + } + } + + HifiStylesUit.RalewaySemiBold { + id: autoLogoutHelp; + text: '[?]'; + // Anchors + anchors.verticalCenter: parent.verticalCenter; + anchors.right: autoLogoutCheckbox.right; + width: 30; + height: 30; + // Text size + size: 18; + // Style + color: hifi.colors.blueHighlight; + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + parent.color = hifi.colors.blueAccent; + } + onExited: { + parent.color = hifi.colors.blueHighlight; + } + onClicked: { + lightboxPopup.titleText = "Keep Me Logged In"; + lightboxPopup.bodyText = "If you choose to stay logged in, ensure that this is a trusted device.\n\n" + + "Also, remember that logging out may not disconnect you from a domain."; + lightboxPopup.button1text = "OK"; + lightboxPopup.button1method = function() { + lightboxPopup.visible = false; + } + lightboxPopup.visible = true; + } + } + } + } + } + + Item { + id: walletContainer; + anchors.top: accountContainer.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + + Rectangle { + id: walletHeaderContainer; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 80; + color: hifi.colors.baseGrayHighlight; + + HifiStylesUit.RalewaySemiBold { + text: "Wallet"; + anchors.fill: parent; + anchors.leftMargin: 20; + } + } + + Item { + id: walletSecurityPictureContainer; + visible: root.walletSetUp; + anchors.top: walletHeaderContainer.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: 80; + + Image { + id: currentSecurityPicture; + source: ""; + visible: true; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.verticalCenter: parent.verticalCenter; + height: 40; + width: height; + mipmap: true; + cache: false; + } + + HifiStylesUit.RalewayRegular { + id: securityPictureText; + text: "Wallet Security Picture"; + // Anchors + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: currentSecurityPicture.right; + anchors.leftMargin: 12; + width: paintedWidth; + // Text size + size: 18; + // Style + color: hifi.colors.white; + } + + // "Change Security Pic" button + HifiControlsUit.Button { + id: changeSecurityImageButton; + color: hifi.buttons.white; + colorScheme: hifi.colorSchemes.dark; + anchors.left: securityPictureText.right; + anchors.verticalCenter: parent.verticalCenter; + width: 140; + height: 40; + text: "Change"; + onClicked: { + + } + } + } + + Item { + id: walletNotSetUpContainer; + visible: !root.walletSetUp; + anchors.top: walletHeaderContainer.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: 80; + + HifiStylesUit.RalewayRegular { + text: "Your wallet is not set up.\n" + + "Open the WALLET app to get started."; + // Anchors + anchors.fill: parent; + // Text size + size: 18; + // Style + color: hifi.colors.white; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + } + } + } +} diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml b/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml similarity index 89% rename from interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml rename to interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml index 01df18352b..d953a764fd 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml +++ b/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml @@ -1,11 +1,11 @@ // // SecurityImageChange.qml -// qml/hifi/commerce/wallet +// qml\hifi\dialogs\security // -// SecurityImageChange +// Security // -// Created by Zach Fox on 2017-08-18 -// Copyright 2017 High Fidelity, Inc. +// Created by Zach Fox on 2018-10-31 +// 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 @@ -13,9 +13,9 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit -import "../../../controls" as HifiControls +import "qrc:////qml//styles-uit" as HifiStylesUit +import "qrc:////qml//controls-uit" as HifiControlsUit +import "qrc:////qml//controls" as HifiControls // references XXX from root context @@ -33,7 +33,7 @@ Item { securityImageChangePageSecurityImage.source = "image://security/securityImage"; if (exists) { // Success submitting new security image if (root.justSubmitted) { - sendSignalToWallet({method: "walletSecurity_changeSecurityImageSuccess"}); + sendSignalToParent({method: "walletSecurity_changeSecurityImageSuccess"}); root.justSubmitted = false; } } else if (root.justSubmitted) { @@ -83,10 +83,10 @@ Item { verticalAlignment: Text.AlignBottom; color: hifi.colors.white; } - // "Security image" text below pic + // "Security image" text below image RalewayRegular { id: securityImageText; - text: "SECURITY PIC"; + text: "SECURITY IMAGE"; // Text size size: 12; // Anchors @@ -118,7 +118,7 @@ Item { // "Change Security Image" text RalewaySemiBold { id: securityImageTitle; - text: "Change Security Pic:"; + text: "Change Security Image:"; // Text size size: 18; anchors.top: parent.top; @@ -139,12 +139,6 @@ Item { anchors.right: parent.right; anchors.rightMargin: 16; height: 300; - - Connections { - onSendSignalToWallet: { - sendSignalToWallet(msg); - } - } } // Navigation Bar @@ -169,7 +163,7 @@ Item { width: 150; text: "Cancel" onClicked: { - sendSignalToWallet({method: "walletSecurity_changeSecurityImageCancelled"}); + sendSignalToParent({method: "walletSecurity_changeSecurityImageCancelled"}); } } @@ -197,7 +191,7 @@ Item { // SECURITY IMAGE SELECTION END // - signal sendSignalToWallet(var msg); + signal sendSignalToParent(var msg); function initModel() { securityImageSelection.initModel(); diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageModel.qml b/interface/resources/qml/hifi/dialogs/security/SecurityImageModel.qml similarity index 89% rename from interface/resources/qml/hifi/commerce/wallet/SecurityImageModel.qml rename to interface/resources/qml/hifi/dialogs/security/SecurityImageModel.qml index b8e9db09ab..946f979c1a 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageModel.qml +++ b/interface/resources/qml/hifi/dialogs/security/SecurityImageModel.qml @@ -1,11 +1,11 @@ // // SecurityImageModel.qml -// qml/hifi/commerce +// qml\hifi\dialogs\security // -// SecurityImageModel +// Security // -// Created by Zach Fox on 2017-08-17 -// Copyright 2017 High Fidelity, Inc. +// Created by Zach Fox on 2018-10-31 +// 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 diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml b/interface/resources/qml/hifi/dialogs/security/SecurityImageSelection.qml similarity index 88% rename from interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml rename to interface/resources/qml/hifi/dialogs/security/SecurityImageSelection.qml index 599c6a1851..5a05a28ba3 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml +++ b/interface/resources/qml/hifi/dialogs/security/SecurityImageSelection.qml @@ -1,11 +1,11 @@ // // SecurityImageSelection.qml -// qml/hifi/commerce/wallet +// qml\hifi\dialogs\security // -// SecurityImageSelection +// Security // -// Created by Zach Fox on 2017-08-17 -// Copyright 2017 High Fidelity, Inc. +// Created by Zach Fox on 2018-10-31 +// 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 @@ -13,9 +13,9 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit -import "../../../controls" as HifiControls +import "qrc:////qml//styles-uit" as HifiStylesUit +import "qrc:////qml//controls-uit" as HifiControlsUit +import "qrc:////qml//controls" as HifiControls // references XXX from root context @@ -73,8 +73,6 @@ Item { // // FUNCTION DEFINITIONS START // - signal sendSignalToWallet(var msg); - function getImagePathFromImageID(imageID) { return (imageID ? gridModel.getImagePathFromImageID(imageID) : ""); } diff --git a/interface/resources/qml/hifi/dialogs/security/SecurityWrapper.qml b/interface/resources/qml/hifi/dialogs/security/SecurityWrapper.qml new file mode 100644 index 0000000000..7c17818225 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/security/SecurityWrapper.qml @@ -0,0 +1,30 @@ +// +// SecurityWrapper.qml +// qml\hifi\dialogs\security +// +// SecurityWrapper +// +// Created by Zach Fox on 2018-10-31 +// 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 +// + +import "qrc:////qml//windows" +import "./" + +ScrollingWindow { + id: root; + + resizable: true; + destroyOnHidden: true; + width: 400; + height: 577; + minSize: Qt.vector2d(400, 500); + + Security { id: security; width: root.width } + + objectName: "SecurityDialog"; + title: security.title; +} diff --git a/interface/resources/qml/hifi/commerce/wallet/images/01.jpg b/interface/resources/qml/hifi/dialogs/security/images/01.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/01.jpg rename to interface/resources/qml/hifi/dialogs/security/images/01.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/02.jpg b/interface/resources/qml/hifi/dialogs/security/images/02.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/02.jpg rename to interface/resources/qml/hifi/dialogs/security/images/02.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/03.jpg b/interface/resources/qml/hifi/dialogs/security/images/03.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/03.jpg rename to interface/resources/qml/hifi/dialogs/security/images/03.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/04.jpg b/interface/resources/qml/hifi/dialogs/security/images/04.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/04.jpg rename to interface/resources/qml/hifi/dialogs/security/images/04.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/05.jpg b/interface/resources/qml/hifi/dialogs/security/images/05.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/05.jpg rename to interface/resources/qml/hifi/dialogs/security/images/05.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/06.jpg b/interface/resources/qml/hifi/dialogs/security/images/06.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/06.jpg rename to interface/resources/qml/hifi/dialogs/security/images/06.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/07.jpg b/interface/resources/qml/hifi/dialogs/security/images/07.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/07.jpg rename to interface/resources/qml/hifi/dialogs/security/images/07.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/08.jpg b/interface/resources/qml/hifi/dialogs/security/images/08.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/08.jpg rename to interface/resources/qml/hifi/dialogs/security/images/08.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/09.jpg b/interface/resources/qml/hifi/dialogs/security/images/09.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/09.jpg rename to interface/resources/qml/hifi/dialogs/security/images/09.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/10.jpg b/interface/resources/qml/hifi/dialogs/security/images/10.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/10.jpg rename to interface/resources/qml/hifi/dialogs/security/images/10.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/11.jpg b/interface/resources/qml/hifi/dialogs/security/images/11.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/11.jpg rename to interface/resources/qml/hifi/dialogs/security/images/11.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/12.jpg b/interface/resources/qml/hifi/dialogs/security/images/12.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/12.jpg rename to interface/resources/qml/hifi/dialogs/security/images/12.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/13.jpg b/interface/resources/qml/hifi/dialogs/security/images/13.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/13.jpg rename to interface/resources/qml/hifi/dialogs/security/images/13.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/14.jpg b/interface/resources/qml/hifi/dialogs/security/images/14.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/14.jpg rename to interface/resources/qml/hifi/dialogs/security/images/14.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/15.jpg b/interface/resources/qml/hifi/dialogs/security/images/15.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/15.jpg rename to interface/resources/qml/hifi/dialogs/security/images/15.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/16.jpg b/interface/resources/qml/hifi/dialogs/security/images/16.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/16.jpg rename to interface/resources/qml/hifi/dialogs/security/images/16.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/17.jpg b/interface/resources/qml/hifi/dialogs/security/images/17.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/17.jpg rename to interface/resources/qml/hifi/dialogs/security/images/17.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/18.jpg b/interface/resources/qml/hifi/dialogs/security/images/18.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/18.jpg rename to interface/resources/qml/hifi/dialogs/security/images/18.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/19.jpg b/interface/resources/qml/hifi/dialogs/security/images/19.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/19.jpg rename to interface/resources/qml/hifi/dialogs/security/images/19.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/20.jpg b/interface/resources/qml/hifi/dialogs/security/images/20.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/20.jpg rename to interface/resources/qml/hifi/dialogs/security/images/20.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/21.jpg b/interface/resources/qml/hifi/dialogs/security/images/21.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/21.jpg rename to interface/resources/qml/hifi/dialogs/security/images/21.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/22.jpg b/interface/resources/qml/hifi/dialogs/security/images/22.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/22.jpg rename to interface/resources/qml/hifi/dialogs/security/images/22.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/23.jpg b/interface/resources/qml/hifi/dialogs/security/images/23.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/23.jpg rename to interface/resources/qml/hifi/dialogs/security/images/23.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/24.jpg b/interface/resources/qml/hifi/dialogs/security/images/24.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/24.jpg rename to interface/resources/qml/hifi/dialogs/security/images/24.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/25.jpg b/interface/resources/qml/hifi/dialogs/security/images/25.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/25.jpg rename to interface/resources/qml/hifi/dialogs/security/images/25.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/26.jpg b/interface/resources/qml/hifi/dialogs/security/images/26.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/26.jpg rename to interface/resources/qml/hifi/dialogs/security/images/26.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/27.jpg b/interface/resources/qml/hifi/dialogs/security/images/27.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/27.jpg rename to interface/resources/qml/hifi/dialogs/security/images/27.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/28.jpg b/interface/resources/qml/hifi/dialogs/security/images/28.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/28.jpg rename to interface/resources/qml/hifi/dialogs/security/images/28.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/29.jpg b/interface/resources/qml/hifi/dialogs/security/images/29.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/29.jpg rename to interface/resources/qml/hifi/dialogs/security/images/29.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/30.jpg b/interface/resources/qml/hifi/dialogs/security/images/30.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/30.jpg rename to interface/resources/qml/hifi/dialogs/security/images/30.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/31.jpg b/interface/resources/qml/hifi/dialogs/security/images/31.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/31.jpg rename to interface/resources/qml/hifi/dialogs/security/images/31.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/32.jpg b/interface/resources/qml/hifi/dialogs/security/images/32.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/32.jpg rename to interface/resources/qml/hifi/dialogs/security/images/32.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/33.jpg b/interface/resources/qml/hifi/dialogs/security/images/33.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/33.jpg rename to interface/resources/qml/hifi/dialogs/security/images/33.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/34.jpg b/interface/resources/qml/hifi/dialogs/security/images/34.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/34.jpg rename to interface/resources/qml/hifi/dialogs/security/images/34.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/lowerKeyboard.png b/interface/resources/qml/hifi/dialogs/security/images/lowerKeyboard.png similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/lowerKeyboard.png rename to interface/resources/qml/hifi/dialogs/security/images/lowerKeyboard.png diff --git a/interface/resources/qml/hifi/commerce/wallet/images/wallet-bg.jpg b/interface/resources/qml/hifi/dialogs/security/images/wallet-bg.jpg similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/wallet-bg.jpg rename to interface/resources/qml/hifi/dialogs/security/images/wallet-bg.jpg diff --git a/interface/resources/qml/hifi/commerce/wallet/images/wallet-tip-bg.png b/interface/resources/qml/hifi/dialogs/security/images/wallet-tip-bg.png similarity index 100% rename from interface/resources/qml/hifi/commerce/wallet/images/wallet-tip-bg.png rename to interface/resources/qml/hifi/dialogs/security/images/wallet-tip-bg.png diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7cd91d76a0..1ca71a70d9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -382,7 +382,7 @@ static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin"; static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; -static const QString AUTO_LOGOUT_SETTING_NAME = "wallet/autoLogout"; +static const QString KEEP_ME_LOGGED_IN_SETTING_NAME = "keepMeLoggedIn"; const std::vector> Application::_acceptedExtensions { { SVO_EXTENSION, &Application::importSVOFromURL }, @@ -2567,7 +2567,7 @@ void Application::cleanupBeforeQuit() { } DependencyManager::destroy(); - bool autoLogout = Setting::Handle(AUTO_LOGOUT_SETTING_NAME, false).get(); + bool autoLogout = Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, false).get(); if (autoLogout) { DependencyManager::get()->removeAccountFromFile(); } @@ -2944,13 +2944,13 @@ void Application::initializeUi() { QUrl{ "hifi/commerce/wallet/PassphraseChange.qml" }, QUrl{ "hifi/commerce/wallet/PassphraseModal.qml" }, QUrl{ "hifi/commerce/wallet/PassphraseSelection.qml" }, - QUrl{ "hifi/commerce/wallet/Security.qml" }, - QUrl{ "hifi/commerce/wallet/SecurityImageChange.qml" }, - QUrl{ "hifi/commerce/wallet/SecurityImageModel.qml" }, - QUrl{ "hifi/commerce/wallet/SecurityImageSelection.qml" }, QUrl{ "hifi/commerce/wallet/Wallet.qml" }, QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, QUrl{ "hifi/commerce/wallet/WalletSetup.qml" }, + QUrl{ "hifi/dialogs/security/Security.qml" }, + QUrl{ "hifi/dialogs/security/SecurityImageChange.qml" }, + QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" }, + QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, }, callback); qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); qmlRegisterType("Hifi", 1, 0, "Preference"); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 539bdabe7d..e16da6781d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -265,6 +265,13 @@ Menu::Menu() { QString("hifi/tablet/TabletGraphicsPreferences.qml"), "GraphicsPreferencesDialog"); }); + // Settings > Security... + action = addActionToQMenuAndActionHash(settingsMenu, "Security..."); + connect(action, &QAction::triggered, [] { + qApp->showDialog(QString("hifi/dialogs/security/SecurityWrapper.qml"), + QString("hifi/dialogs/security/Security.qml"), "SecurityDialog"); + }); + // Settings > Developer Menu addCheckableActionToQMenuAndActionHash(settingsMenu, "Developer Menu", 0, false, this, SLOT(toggleDeveloperMenus())); diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 5b91afea33..2d44650d9c 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -376,9 +376,9 @@ function deleteSendMoneyParticleEffect() { } function onUsernameChanged() { - if (Account.username !== Settings.getValue("wallet/savedUsername")) { - Settings.setValue("wallet/autoLogout", false); - Settings.setValue("wallet/savedUsername", ""); + if (Account.username !== Settings.getValue("keepMeLoggedIn/savedUsername")) { + Settings.setValue("keepMeLoggedIn", false); + Settings.setValue("keepMeLoggedIn/savedUsername", ""); } } From e6c3e7e67f31b30613bf1a86dd60ece38731bfb7 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 31 Oct 2018 14:41:49 -0700 Subject: [PATCH 297/362] initial purge of security tab from wallet --- .../qml/hifi/commerce/wallet/Help.qml | 7 +- .../qml/hifi/commerce/wallet/Wallet.qml | 132 +----------------- 2 files changed, 8 insertions(+), 131 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/Help.qml b/interface/resources/qml/hifi/commerce/wallet/Help.qml index 6d8fc3c33f..6cf00f78eb 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Help.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Help.qml @@ -74,8 +74,7 @@ In your Wallet's Send Money tab, choose from your list of connections, or choose isExpanded: false; question: "What is a Security Pic?" answer: "Your Security Pic acts as an extra layer of Wallet security. \ -When you see your Security Pic, you know that your actions and data are securely making use of your account. \ -

Tap here to change your Security Pic."; +When you see your Security Pic, you know that your actions and data are securely making use of your account."; } ListElement { isExpanded: false; @@ -137,7 +136,7 @@ At the moment, there is currently no way to convert HFC to other currencies. Sta anchors.left: parent.left; width: parent.width; height: questionText.paintedHeight + 50; - + RalewaySemiBold { id: plusMinusButton; text: model.isExpanded ? "-" : "+"; @@ -217,8 +216,6 @@ At the moment, there is currently no way to convert HFC to other currencies. Sta } } else if (link === "#support") { Qt.openUrlExternally("mailto:support@highfidelity.com"); - } else if (link === "#securitypic") { - sendSignalToWallet({method: 'walletSecurity_changeSecurityImage'}); } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index cbb77883df..f2119a1a8f 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -232,10 +232,6 @@ Rectangle { root.isPassword = msg.isPasswordField; } else if (msg.method === 'walletSetup_lowerKeyboard') { root.keyboardRaised = false; - } else if (msg.method === 'walletSecurity_changePassphraseCancelled') { - root.activeView = "security"; - } else if (msg.method === 'walletSecurity_changePassphraseSuccess') { - root.activeView = "security"; } else { sendToScript(msg); } @@ -245,27 +241,6 @@ Rectangle { } } } - SecurityImageChange { - id: securityImageChange; - visible: root.activeView === "securityImageChange"; - z: 997; - anchors.top: titleBarContainer.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.bottom: parent.bottom; - - Connections { - onSendSignalToWallet: { - if (msg.method === 'walletSecurity_changeSecurityImageCancelled') { - root.activeView = "security"; - } else if (msg.method === 'walletSecurity_changeSecurityImageSuccess') { - root.activeView = "security"; - } else { - sendToScript(msg); - } - } - } - } // // TAB CONTENTS START @@ -366,39 +341,6 @@ Rectangle { } } - Security { - id: security; - visible: root.activeView === "security"; - anchors.top: titleBarContainer.bottom; - anchors.bottom: tabButtonsContainer.top; - anchors.left: parent.left; - anchors.right: parent.right; - - Connections { - onSendSignalToWallet: { - if (msg.method === 'walletSecurity_changePassphrase') { - root.activeView = "passphraseChange"; - passphraseChange.clearPassphraseFields(); - passphraseChange.resetSubmitButton(); - } else if (msg.method === 'walletSecurity_changeSecurityImage') { - securityImageChange.initModel(); - root.activeView = "securityImageChange"; - } else if (msg.method === 'walletSecurity_autoLogoutHelp') { - lightboxPopup.titleText = "Automatically Log Out"; - lightboxPopup.bodyText = "By default, after you log in to High Fidelity, you will stay logged in to your High Fidelity " + - "account even after you close and re-open Interface. This means anyone who opens Interface on your computer " + - "could make purchases with your Wallet.\n\n" + - "If you do not want to stay logged in across Interface sessions, check this box."; - lightboxPopup.button1text = "CLOSE"; - lightboxPopup.button1method = function() { - lightboxPopup.visible = false; - } - lightboxPopup.visible = true; - } - } - } - } - Help { id: help; visible: root.activeView === "help"; @@ -407,14 +349,6 @@ Rectangle { anchors.left: parent.left; anchors.right: parent.right; - Connections { - onSendSignalToWallet: { - if (msg.method === 'walletSecurity_changeSecurityImage') { - securityImageChange.initModel(); - root.activeView = "securityImageChange"; - } - } - } } @@ -428,7 +362,7 @@ Rectangle { Item { id: tabButtonsContainer; visible: !needsLogIn.visible && root.activeView !== "passphraseChange" && root.activeView !== "securityImageChange" && sendMoney.currentActiveView !== "sendAssetStep"; - property int numTabs: 5; + property int numTabs: 4; // Size width: root.width; height: 90; @@ -452,7 +386,7 @@ Rectangle { anchors.left: parent.left; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; - + HiFiGlyphs { id: homeTabIcon; text: hifi.glyphs.home2; @@ -506,7 +440,7 @@ Rectangle { anchors.left: walletHomeButtonContainer.right; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; - + HiFiGlyphs { id: exchangeMoneyTabIcon; text: hifi.glyphs.leftRightArrows; @@ -550,7 +484,7 @@ Rectangle { anchors.left: exchangeMoneyButtonContainer.right; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; - + HiFiGlyphs { id: sendMoneyTabIcon; text: hifi.glyphs.paperPlane; @@ -596,70 +530,16 @@ Rectangle { } } - // "SECURITY" tab button - Rectangle { - id: securityButtonContainer; - visible: !walletSetup.visible; - color: root.activeView === "security" ? hifi.colors.blueAccent : hifi.colors.black; - anchors.top: parent.top; - anchors.left: sendMoneyButtonContainer.right; - anchors.bottom: parent.bottom; - width: parent.width / tabButtonsContainer.numTabs; - - HiFiGlyphs { - id: securityTabIcon; - text: hifi.glyphs.lock; - // Size - size: 38; - // Anchors - anchors.horizontalCenter: parent.horizontalCenter; - anchors.top: parent.top; - anchors.topMargin: 2; - // Style - color: root.activeView === "security" || securityTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; - } - - RalewaySemiBold { - text: "SECURITY"; - // Text size - size: 16; - // Anchors - anchors.bottom: parent.bottom; - height: parent.height/2; - anchors.left: parent.left; - anchors.leftMargin: 4; - anchors.right: parent.right; - anchors.rightMargin: 4; - // Style - color: root.activeView === "security" || securityTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignTop; - } - MouseArea { - id: securityTabMouseArea; - anchors.fill: parent; - hoverEnabled: enabled; - onClicked: { - root.activeView = "security"; - tabButtonsContainer.resetTabButtonColors(); - } - onEntered: parent.color = hifi.colors.blueHighlight; - onExited: parent.color = root.activeView === "security" ? hifi.colors.blueAccent : hifi.colors.black; - } - } - // "HELP" tab button Rectangle { id: helpButtonContainer; visible: !walletSetup.visible; color: root.activeView === "help" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; - anchors.left: securityButtonContainer.right; + anchors.left: sendMoneyButtonContainer.right; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; - + HiFiGlyphs { id: helpTabIcon; text: hifi.glyphs.question; From 317f4d28ba5e0ac11d19e1aaa8c141df21df5024 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 31 Oct 2018 15:41:51 -0700 Subject: [PATCH 298/362] always keep a workload view around MyAvatar --- libraries/workload/src/workload/ViewTask.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/libraries/workload/src/workload/ViewTask.cpp b/libraries/workload/src/workload/ViewTask.cpp index 9d0c693149..0db24f19ba 100644 --- a/libraries/workload/src/workload/ViewTask.cpp +++ b/libraries/workload/src/workload/ViewTask.cpp @@ -31,13 +31,25 @@ void SetupViews::run(const WorkloadContextPointer& renderContext, const Input& i auto& outViews = outputs; outViews.clear(); - // Filter the first view centerer on the avatar head if needed if (_views.size() >= 2) { + // when inputs contains two or more views: + // index 0 = view from avatar's head + // index 1 = view from camera + // index 2 and higher = secondary camera and whatever if (data.useAvatarView) { + // for debug purposes we keep the head view and skip that of the camera outViews.push_back(_views[0]); outViews.insert(outViews.end(), _views.begin() + 2, _views.end()); } else { - outViews.insert(outViews.end(), _views.begin() + 1, _views.end()); + // otherwise we use all of the views... + const float MIN_HEAD_CAMERA_SEPARATION_SQUARED = 0.2f; + if (glm::distance2(_views[0].origin, _views[1].origin) < MIN_HEAD_CAMERA_SEPARATION_SQUARED) { + // ... unless the first two are close enough to be considered the same + // in which case we only keep one of them + outViews.insert(outViews.end(), _views.begin() + 1, _views.end()); + } else { + outViews = _views; + } } } else { outViews = _views; @@ -177,7 +189,6 @@ void ControlViews::regulateViews(workload::Views& outViews, const workload::Timi outView.regionBackFronts[workload::Region::R1] = regionBackFronts[workload::Region::R1]; outView.regionBackFronts[workload::Region::R2] = regionBackFronts[workload::Region::R2]; outView.regionBackFronts[workload::Region::R3] = regionBackFronts[workload::Region::R3]; - workload::View::updateRegionsFromBackFronts(outView); } } From d4e9a7056409e7e071af495917d8be91312c5c19 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 31 Oct 2018 16:12:16 -0700 Subject: [PATCH 299/362] Almost there --- .../qml/LoginDialog/LinkAccountBody.qml | 15 ++- .../qml/hifi/commerce/wallet/Wallet.qml | 4 +- .../qml/hifi/commerce/wallet/WalletSetup.qml | 120 ------------------ .../qml/hifi/dialogs/security/Security.qml | 62 +++++++-- .../dialogs/security/SecurityImageChange.qml | 13 +- .../security/SecurityImageSelection.qml | 10 +- .../hifi/dialogs/security/SecurityWrapper.qml | 30 ----- interface/src/Application.cpp | 4 +- interface/src/Menu.cpp | 5 +- interface/src/commerce/Wallet.cpp | 2 +- 10 files changed, 82 insertions(+), 183 deletions(-) delete mode 100644 interface/resources/qml/hifi/dialogs/security/SecurityWrapper.qml diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index d5d89cd0b4..3c0577532a 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -239,7 +239,10 @@ Item { } - Keys.onReturnPressed: linkAccountBody.login() + Keys.onReturnPressed: { + Settings.setValue("keepMeLoggedIn/savedUsername", usernameField.text); + linkAccountBody.login(); + } } InfoItem { @@ -264,14 +267,14 @@ Item { CheckBox { id: autoLogoutCheckbox checked: Settings.getValue("keepMeLoggedIn", false) - text: "Keep me signed in" + text: "Keep me logged in" boxSize: 20; labelFontSize: 15 color: hifi.colors.black onCheckedChanged: { Settings.setValue("keepMeLoggedIn", checked); if (checked) { - Settings.setValue("keepMeLoggedIn/savedUsername", Account.username); + Settings.setValue("keepMeLoggedIn/savedUsername", usernameField.text); } else { Settings.setValue("keepMeLoggedIn/savedUsername", ""); } @@ -289,7 +292,10 @@ Item { text: qsTr(loginDialog.isSteamRunning() ? "Link Account" : "Log in") color: hifi.buttons.blue - onClicked: linkAccountBody.login() + onClicked: { + Settings.setValue("keepMeLoggedIn/savedUsername", usernameField.text); + linkAccountBody.login(); + } } } @@ -403,6 +409,7 @@ Item { case Qt.Key_Enter: case Qt.Key_Return: event.accepted = true + Settings.setValue("keepMeLoggedIn/savedUsername", usernameField.text); linkAccountBody.login() break } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index f2119a1a8f..d032f060e2 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -142,7 +142,7 @@ Rectangle { Image { id: titleBarSecurityImage; source: ""; - visible: titleBarSecurityImage.source !== "" && !securityImageChange.visible; + visible: titleBarSecurityImage.source !== ""; anchors.right: parent.right; anchors.rightMargin: 6; anchors.top: parent.top; @@ -361,7 +361,7 @@ Rectangle { // Item { id: tabButtonsContainer; - visible: !needsLogIn.visible && root.activeView !== "passphraseChange" && root.activeView !== "securityImageChange" && sendMoney.currentActiveView !== "sendAssetStep"; + visible: !needsLogIn.visible && root.activeView !== "passphraseChange" && sendMoney.currentActiveView !== "sendAssetStep"; property int numTabs: 4; // Size width: root.width; diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml index dc6ce45a74..ecd7234400 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -243,7 +243,6 @@ Item { height: 50; text: "Set Up Wallet"; onClicked: { - securityImageSelection.initModel(); root.activeView = "step_2"; } } @@ -267,124 +266,6 @@ Item { // FIRST PAGE END // - // - // SECURITY IMAGE SELECTION START - // - Item { - id: securityImageContainer; - visible: root.activeView === "step_2"; - // Anchors - anchors.top: titleBarContainer.bottom; - anchors.topMargin: 30; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - - // Text below title bar - RalewayRegular { - id: securityImageTitleHelper; - text: "Choose a Security Pic:"; - // Text size - size: 24; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - height: 50; - width: paintedWidth; - // Style - color: hifi.colors.white; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - SecurityImageSelection { - id: securityImageSelection; - // Anchors - anchors.top: securityImageTitleHelper.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - height: 300; - - Connections { - onSendSignalToWallet: { - sendSignalToWallet(msg); - } - } - } - - // Text below security images - RalewayRegular { - text: "Your security picture shows you that the service asking for your passphrase is authorized. You can change your secure picture at any time."; - // Text size - size: 18; - // Anchors - anchors.top: securityImageSelection.bottom; - anchors.topMargin: 40; - anchors.left: parent.left; - anchors.right: parent.right; - height: paintedHeight; - // Style - color: hifi.colors.white; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - // Navigation Bar - Item { - // Size - width: parent.width; - height: 50; - // Anchors: - anchors.left: parent.left; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 50; - - // "Back" button - HifiControlsUit.Button { - color: hifi.buttons.noneBorderlessWhite; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: 200; - text: "Back" - onClicked: { - securityImageSelection.resetSelection(); - root.activeView = "step_1"; - } - } - - // "Next" button - HifiControlsUit.Button { - enabled: securityImageSelection.currentIndex !== -1; - color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: 200; - text: "Next"; - onClicked: { - root.lastPage = "step_2"; - var securityImagePath = securityImageSelection.getImagePathFromImageID(securityImageSelection.getSelectedImageIndex()) - Commerce.chooseSecurityImage(securityImagePath); - root.activeView = "step_3"; - passphraseSelection.clearPassphraseFields(); - } - } - } - } - // - // SECURITY IMAGE SELECTION END - // - // // SECURE PASSPHRASE SELECTION START // @@ -525,7 +406,6 @@ Item { width: 200; text: "Back" onClicked: { - securityImageSelection.resetSelection(); root.lastPage = "step_3"; root.activeView = "step_2"; } diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml index d1dc2a9e11..3f4d17e838 100644 --- a/interface/resources/qml/hifi/dialogs/security/Security.qml +++ b/interface/resources/qml/hifi/dialogs/security/Security.qml @@ -29,6 +29,17 @@ Rectangle { property string title: "Security Settings"; property bool walletSetUp; + + QtObject { + id: margins + property real paddings: root.width / 20.25 + + property real sizeCheckBox: root.width / 13.5 + property real sizeText: root.width / 2.5 + property real sizeLevel: root.width / 5.8 + property real sizeDesktop: root.width / 5.8 + property real sizeVR: root.width / 13.5 + } Connections { target: Commerce; @@ -50,16 +61,37 @@ Rectangle { } } + Component.onCompleted: { + Commerce.getWalletStatus(); + } + HifiCommerceCommon.CommerceLightbox { + z: 996; id: lightboxPopup; visible: false; anchors.fill: parent; } + SecurityImageChange { + id: securityImageChange; + visible: false; + z: 997; + anchors.top: usernameText.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + + Connections { + onSendSignalToParent: { + securityImageChange.visible = false; + } + } + } + // Username Text HifiStylesUit.RalewayRegular { id: usernameText; - text: Account.username === "" ? Account.username : "Please Log In"; + text: Account.username === "Unknown user" ? "Please Log In" : Account.username; // Text size size: 24; // Style @@ -71,12 +103,12 @@ Rectangle { anchors.leftMargin: 20; anchors.right: parent.right; anchors.rightMargin: 20; - height: 80; + height: 60; } Item { id: pleaseLogInContainer; - visible: Account.username === ""; + visible: Account.username === "Unknown user"; anchors.top: usernameText.bottom; anchors.left: parent.left; anchors.right: parent.right; @@ -94,7 +126,7 @@ Rectangle { anchors.right: parent.right; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; - height: 80; + height: 60; } HifiControlsUit.Button { @@ -104,7 +136,7 @@ Rectangle { anchors.centerIn: parent; width: 140; height: 40; - text: "Change"; + text: "Log In"; onClicked: { DialogsManager.showLoginDialog(); } @@ -131,13 +163,15 @@ Rectangle { anchors.top: parent.top; anchors.left: parent.left; anchors.right: parent.right; - height: 80; + height: 70; color: hifi.colors.baseGrayHighlight; HifiStylesUit.RalewaySemiBold { text: "Account"; anchors.fill: parent; anchors.leftMargin: 20; + color: hifi.colors.white; + size: 18; } } @@ -156,9 +190,11 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter; anchors.left: parent.left; anchors.leftMargin: 20; - boxSize: 28; + boxSize: 24; labelFontSize: 18; + colorScheme: hifi.colorSchemes.dark color: hifi.colors.white; + width: 240; onCheckedChanged: { Settings.setValue("keepMeLoggedIn", checked); if (checked) { @@ -218,13 +254,15 @@ Rectangle { anchors.top: parent.top; anchors.left: parent.left; anchors.right: parent.right; - height: 80; + height: 70; color: hifi.colors.baseGrayHighlight; HifiStylesUit.RalewaySemiBold { text: "Wallet"; anchors.fill: parent; anchors.leftMargin: 20; + color: hifi.colors.white; + size: 18; } } @@ -249,7 +287,7 @@ Rectangle { cache: false; } - HifiStylesUit.RalewayRegular { + HifiStylesUit.RalewaySemiBold { id: securityPictureText; text: "Wallet Security Picture"; // Anchors @@ -270,12 +308,14 @@ Rectangle { color: hifi.buttons.white; colorScheme: hifi.colorSchemes.dark; anchors.left: securityPictureText.right; + anchors.leftMargin: 12; anchors.verticalCenter: parent.verticalCenter; width: 140; height: 40; text: "Change"; onClicked: { - + securityImageChange.visible = true; + securityImageChange.initModel(); } } } @@ -286,7 +326,7 @@ Rectangle { anchors.top: walletHeaderContainer.bottom; anchors.left: parent.left; anchors.right: parent.right; - height: 80; + height: 60; HifiStylesUit.RalewayRegular { text: "Your wallet is not set up.\n" + diff --git a/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml b/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml index d953a764fd..0007b98291 100644 --- a/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml +++ b/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml @@ -19,10 +19,11 @@ import "qrc:////qml//controls" as HifiControls // references XXX from root context -Item { - HifiConstants { id: hifi; } +Rectangle { + HifiStylesUit.HifiConstants { id: hifi; } id: root; + color: hifi.colors.darkGray; property bool justSubmitted: false; Connections { @@ -72,7 +73,7 @@ Item { anchors.bottom: parent.bottom; height: 22; // Lock icon - HiFiGlyphs { + HifiStylesUit.HiFiGlyphs { id: lockIcon; text: hifi.glyphs.lock; anchors.bottom: parent.bottom; @@ -84,9 +85,9 @@ Item { color: hifi.colors.white; } // "Security image" text below image - RalewayRegular { + HifiStylesUit.RalewayRegular { id: securityImageText; - text: "SECURITY IMAGE"; + text: "SECURITY PIC"; // Text size size: 12; // Anchors @@ -116,7 +117,7 @@ Item { anchors.bottom: parent.bottom; // "Change Security Image" text - RalewaySemiBold { + HifiStylesUit.RalewaySemiBold { id: securityImageTitle; text: "Change Security Image:"; // Text size diff --git a/interface/resources/qml/hifi/dialogs/security/SecurityImageSelection.qml b/interface/resources/qml/hifi/dialogs/security/SecurityImageSelection.qml index 5a05a28ba3..f058aad40a 100644 --- a/interface/resources/qml/hifi/dialogs/security/SecurityImageSelection.qml +++ b/interface/resources/qml/hifi/dialogs/security/SecurityImageSelection.qml @@ -20,7 +20,7 @@ import "qrc:////qml//controls" as HifiControls // references XXX from root context Item { - HifiConstants { id: hifi; } + HifiStylesUit.HifiConstants { id: hifi; } id: root; property alias currentIndex: securityImageGrid.currentIndex; @@ -64,10 +64,10 @@ Item { } } highlight: Rectangle { - width: securityImageGrid.cellWidth; - height: securityImageGrid.cellHeight; - color: hifi.colors.blueHighlight; - } + width: securityImageGrid.cellWidth; + height: securityImageGrid.cellHeight; + color: hifi.colors.blueHighlight; + } } // diff --git a/interface/resources/qml/hifi/dialogs/security/SecurityWrapper.qml b/interface/resources/qml/hifi/dialogs/security/SecurityWrapper.qml deleted file mode 100644 index 7c17818225..0000000000 --- a/interface/resources/qml/hifi/dialogs/security/SecurityWrapper.qml +++ /dev/null @@ -1,30 +0,0 @@ -// -// SecurityWrapper.qml -// qml\hifi\dialogs\security -// -// SecurityWrapper -// -// Created by Zach Fox on 2018-10-31 -// 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 -// - -import "qrc:////qml//windows" -import "./" - -ScrollingWindow { - id: root; - - resizable: true; - destroyOnHidden: true; - width: 400; - height: 577; - minSize: Qt.vector2d(400, 500); - - Security { id: security; width: root.width } - - objectName: "SecurityDialog"; - title: security.title; -} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1ca71a70d9..245e6c0017 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2567,8 +2567,8 @@ void Application::cleanupBeforeQuit() { } DependencyManager::destroy(); - bool autoLogout = Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, false).get(); - if (autoLogout) { + bool keepMeLoggedIn = Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, false).get(); + if (!keepMeLoggedIn) { DependencyManager::get()->removeAccountFromFile(); } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index e16da6781d..3636a2f45a 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -268,8 +268,9 @@ Menu::Menu() { // Settings > Security... action = addActionToQMenuAndActionHash(settingsMenu, "Security..."); connect(action, &QAction::triggered, [] { - qApp->showDialog(QString("hifi/dialogs/security/SecurityWrapper.qml"), - QString("hifi/dialogs/security/Security.qml"), "SecurityDialog"); + auto tablet = dynamic_cast( + DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + tablet->loadQMLSource(QString("hifi/dialogs/security/Security.qml")); }); // Settings > Developer Menu diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 5b8417be7c..0e9ad7d79a 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -687,7 +687,7 @@ void Wallet::chooseSecurityImage(const QString& filename) { delete _securityImage; } QString path = PathUtils::resourcesPath(); - path.append("/qml/hifi/commerce/wallet/"); + path.append("/qml/hifi/dialogs/security/"); path.append(filename); // now create a new security image pixmap From 162a08b9935986a46caaf86648191186e1637d95 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 31 Oct 2018 16:14:26 -0700 Subject: [PATCH 300/362] Restore certain images --- .../wallet}/images/lowerKeyboard.png | Bin .../wallet}/images/wallet-bg.jpg | Bin .../wallet}/images/wallet-tip-bg.png | Bin 3 files changed, 0 insertions(+), 0 deletions(-) rename interface/resources/qml/hifi/{dialogs/security => commerce/wallet}/images/lowerKeyboard.png (100%) rename interface/resources/qml/hifi/{dialogs/security => commerce/wallet}/images/wallet-bg.jpg (100%) rename interface/resources/qml/hifi/{dialogs/security => commerce/wallet}/images/wallet-tip-bg.png (100%) diff --git a/interface/resources/qml/hifi/dialogs/security/images/lowerKeyboard.png b/interface/resources/qml/hifi/commerce/wallet/images/lowerKeyboard.png similarity index 100% rename from interface/resources/qml/hifi/dialogs/security/images/lowerKeyboard.png rename to interface/resources/qml/hifi/commerce/wallet/images/lowerKeyboard.png diff --git a/interface/resources/qml/hifi/dialogs/security/images/wallet-bg.jpg b/interface/resources/qml/hifi/commerce/wallet/images/wallet-bg.jpg similarity index 100% rename from interface/resources/qml/hifi/dialogs/security/images/wallet-bg.jpg rename to interface/resources/qml/hifi/commerce/wallet/images/wallet-bg.jpg diff --git a/interface/resources/qml/hifi/dialogs/security/images/wallet-tip-bg.png b/interface/resources/qml/hifi/commerce/wallet/images/wallet-tip-bg.png similarity index 100% rename from interface/resources/qml/hifi/dialogs/security/images/wallet-tip-bg.png rename to interface/resources/qml/hifi/commerce/wallet/images/wallet-tip-bg.png From a1eee351e94e292cc34d0c752f859cef4d3f8af8 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Wed, 31 Oct 2018 16:24:57 -0700 Subject: [PATCH 301/362] Change "Proofs" to "My Submissions" in assets filter. --- interface/resources/qml/hifi/commerce/purchases/Purchases.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 41453c9790..ced5450290 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -498,7 +498,7 @@ Rectangle { "filterName": "updated" }, { - "displayName": "Proofs", + "displayName": "My Submissions", "filterName": "proofs" } ] From a4b345bb583eca092e625dd0ef93c6cca6660bb3 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 1 Nov 2018 09:25:46 -0700 Subject: [PATCH 302/362] fix interstitial domain loading due to bad texture url --- libraries/model-networking/src/model-networking/ModelCache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index e96815d391..2686f1c9a8 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -551,7 +551,7 @@ graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel) { auto textureCache = DependencyManager::get(); - if (textureCache) { + if (textureCache && !url.isEmpty()) { auto texture = textureCache->getTexture(url, type); _textures[channel].texture = texture; From cfec94f3fffe56e7a184b7674fa9e5abf690970c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 1 Nov 2018 09:28:31 -0700 Subject: [PATCH 303/362] larger threshold btw 1 vs 2 workload views for 3rd person --- libraries/workload/src/workload/ViewTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/workload/src/workload/ViewTask.cpp b/libraries/workload/src/workload/ViewTask.cpp index 0db24f19ba..0a268df9fc 100644 --- a/libraries/workload/src/workload/ViewTask.cpp +++ b/libraries/workload/src/workload/ViewTask.cpp @@ -42,7 +42,7 @@ void SetupViews::run(const WorkloadContextPointer& renderContext, const Input& i outViews.insert(outViews.end(), _views.begin() + 2, _views.end()); } else { // otherwise we use all of the views... - const float MIN_HEAD_CAMERA_SEPARATION_SQUARED = 0.2f; + const float MIN_HEAD_CAMERA_SEPARATION_SQUARED = MIN_VIEW_BACK_FRONTS[0][1] * MIN_VIEW_BACK_FRONTS[0][1]; if (glm::distance2(_views[0].origin, _views[1].origin) < MIN_HEAD_CAMERA_SEPARATION_SQUARED) { // ... unless the first two are close enough to be considered the same // in which case we only keep one of them From d481f081bbf163086a1835e8095b5110e86a1092 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 1 Nov 2018 10:29:38 -0700 Subject: [PATCH 304/362] Fix four bugs; truncate some logs --- .../hifi/commerce/purchases/PurchasedItem.qml | 5 +-- .../qml/hifi/commerce/wallet/Wallet.qml | 6 --- .../qml/hifi/commerce/wallet/WalletHome.qml | 22 ---------- scripts/modules/appUi.js | 41 ++++++++++++------- scripts/system/commerce/wallet.js | 12 ++++++ 5 files changed, 40 insertions(+), 46 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index d13473af22..0828d86eff 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -47,8 +47,7 @@ Item { property string wornEntityID; property string upgradeUrl; property string upgradeTitle; - property bool updateAvailable: root.upgradeUrl !== "" && !root.isShowingMyItems; - property bool isShowingMyItems; + property bool updateAvailable: root.upgradeUrl !== ""; property bool valid; property string originalStatusText; @@ -231,7 +230,7 @@ Item { Loader { id: giftButton; - visible: !root.isShowingMyItems; + visible: root.itemEdition > 0; sourceComponent: contextCardButton; anchors.right: parent.right; anchors.top: parent.top; diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index e1e58bf7c7..e17faef194 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -355,10 +355,6 @@ Rectangle { if (msg.method === 'transactionHistory_usernameLinkClicked') { userInfoViewer.url = msg.usernameLink; userInfoViewer.visible = true; - } else if (msg.method === 'goToPurchases_fromWalletHome') { - root.activeView = "walletInventory"; - walletInventory.isShowingMyItems = false; - tabButtonsContainer.resetTabButtonColors(); } else { sendToScript(msg); } @@ -630,7 +626,6 @@ Rectangle { hoverEnabled: enabled; onClicked: { root.activeView = "walletInventory"; - walletInventory.isShowingMyItems = false; tabButtonsContainer.resetTabButtonColors(); } onEntered: parent.color = hifi.colors.blueHighlight; @@ -961,7 +956,6 @@ Rectangle { Commerce.getWalletStatus(); } else if (msg.referrer === 'purchases') { root.activeView = "walletInventory"; - walletInventory.isShowingMyItems = false; tabButtonsContainer.resetTabButtonColors(); } else if (msg.referrer === 'marketplace cta' || msg.referrer === 'mainPage') { sendToScript({method: 'goToMarketplaceMainPage', itemId: msg.referrer}); diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 246773ff07..d32017189e 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -179,28 +179,6 @@ Item { color: hifi.colors.baseGrayHighlight; } - RalewaySemiBold { - id: myPurchasesLink; - text: 'Inventory'; - // Anchors - anchors.top: parent.top; - anchors.topMargin: 26; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: paintedWidth; - height: 30; - y: 4; - // Text size - size: 18; - // Style - color: hifi.colors.baseGrayHighlight; - horizontalAlignment: Text.AlignRight; - - onLinkActivated: { - sendSignalToWallet({method: 'goToPurchases_fromWalletHome'}); - } - } - HifiModels.PSFListModel { id: transactionHistoryModel; property int lastPendingCount: 0; diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js index c340dfecd2..e267293977 100644 --- a/scripts/modules/appUi.js +++ b/scripts/modules/appUi.js @@ -129,7 +129,9 @@ function AppUi(properties) { } that.isOpen = true; } - } else { // Not us. Should we do something for type Home, Menu, and particularly Closed (meaning tablet hidden? + } 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); @@ -139,12 +141,12 @@ function AppUi(properties) { that.isOpen = false; } } - // console.debug(that.buttonName + " app reports: Tablet screen changed.\nNew screen type: " + type + - // "\nNew screen URL: " + url + "\nCurrent app open status: " + that.isOpen + "\n"); }; // Overwrite with the given properties: - Object.keys(properties).forEach(function (key) { that[key] = properties[key]; }); + Object.keys(properties).forEach(function (key) { + that[key] = properties[key]; + }); // // START Notification Handling @@ -157,22 +159,20 @@ function AppUi(properties) { 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 get", urlOfRequest, error || response.status); - that.notificationPollTimeout[indexOfRequest] = Script.setTimeout( - that.notificationPoll(indexOfRequest), that.notificationPollTimeoutMs[indexOfRequest]); + startNotificationTimer(indexOfRequest); return; } if (!that.notificationPollStopPaginatingConditionMet[indexOfRequest] || that.notificationPollStopPaginatingConditionMet[indexOfRequest](response)) { - that.notificationPollTimeout[indexOfRequest] = Script.setTimeout(function () { - that.notificationPoll(indexOfRequest); - }, that.notificationPollTimeoutMs[indexOfRequest]); + startNotificationTimer(indexOfRequest); var notificationData; if (concatenatedServerResponse[indexOfRequest].length) { @@ -181,7 +181,8 @@ function AppUi(properties) { notificationData = that.notificationDataProcessPage[indexOfRequest](response); } console.debug(that.buttonName, that.notificationPollEndpoint[indexOfRequest], - 'notification data for processing:', JSON.stringify(notificationData)); + '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; @@ -199,15 +200,19 @@ function AppUi(properties) { 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 offline - if (GlobalServices.findableBy === "none" || Account.username === "") { - that.notificationPollTimeout[i] = Script.setTimeout( - that.notificationPoll(i), that.notificationPollTimeoutMs[i]); + // 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; } @@ -217,7 +222,7 @@ function AppUi(properties) { var currentTimestamp = new Date().getTime(); var lastPollTimestamp = Settings.getValue(settingsKey, currentTimestamp); if (that.notificationPollCaresAboutSince[i]) { - url = url + "&since=" + lastPollTimestamp / 1000; + url = url + "&since=" + lastPollTimestamp / MS_IN_SEC; } Settings.setValue(settingsKey, currentTimestamp); @@ -239,6 +244,12 @@ function AppUi(properties) { 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; diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 2ae30b5b35..bc7bccd94f 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -401,6 +401,14 @@ function openMarketplace(optionalItemOrUrl) { ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL); } +function setCertificateInfo(itemCertificateId) { + ui.tablet.sendToQml({ + method: 'inspectionCertificate_setCertificateId', + entityId: "", + certificateId: itemCertificateId + }); +} + // Function Name: fromQml() // // Description: @@ -521,6 +529,9 @@ function fromQml(message) { openMarketplace(itemId); } break; + case 'purchases_itemCertificateClicked': + setCertificateInfo(message.itemCertificateId); + break; case 'clearShouldShowDotHistory': shouldShowDotHistory = false; ui.messagesWaiting(shouldShowDotUpdates || shouldShowDotHistory); @@ -698,6 +709,7 @@ function off() { Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); triggerMapping.disable(); triggerPressMapping.disable(); + isWired = false; } if (isUpdateOverlaysWired) { From 4a56f769e5b133e2ce8c67cf87274939a732ffcb Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 1 Nov 2018 11:04:59 -0700 Subject: [PATCH 305/362] Re-add some code I shouldn't have removed --- scripts/system/html/js/marketplacesInject.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index eda37c06b7..db433c5058 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -264,6 +264,13 @@ "font-weight": "600", "line-height": "34px" }); + + if (parseInt(cost) > 0) { + priceElement.css({ "width": "auto" }); + priceElement.html(' ' + cost); + priceElement.css({ "min-width": priceElement.width() + 30 }); + } }); // change pricing to GET/BUY on button hover @@ -382,6 +389,9 @@ var cost = $('.item-cost').text(); var costInt = parseInt(cost, 10); var availability = $.trim($('.item-availability').text()); + if (limitedCommerce && (costInt > 0)) { + availability = ''; + } if (availability === 'available') { purchaseButton.css({ "background": "linear-gradient(#00b4ef, #0093C5)", From 3e2d6c341b6b2cfcf142664d99b3400bff01f8e5 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 1 Nov 2018 11:45:25 -0700 Subject: [PATCH 306/362] placeholder svgs --- .../tablet-icons/{wallet-a-msg.svg => inventory-a-msg.svg} | 0 interface/resources/icons/tablet-icons/inventory-a.svg | 3 +++ .../tablet-icons/{wallet-i-msg.svg => inventory-i-msg.svg} | 0 .../icons/tablet-icons/{wallet-i.svg => inventory-i.svg} | 0 interface/resources/icons/tablet-icons/wallet-a.svg | 1 - 5 files changed, 3 insertions(+), 1 deletion(-) rename interface/resources/icons/tablet-icons/{wallet-a-msg.svg => inventory-a-msg.svg} (100%) create mode 100644 interface/resources/icons/tablet-icons/inventory-a.svg rename interface/resources/icons/tablet-icons/{wallet-i-msg.svg => inventory-i-msg.svg} (100%) rename interface/resources/icons/tablet-icons/{wallet-i.svg => inventory-i.svg} (100%) delete mode 100644 interface/resources/icons/tablet-icons/wallet-a.svg diff --git a/interface/resources/icons/tablet-icons/wallet-a-msg.svg b/interface/resources/icons/tablet-icons/inventory-a-msg.svg similarity index 100% rename from interface/resources/icons/tablet-icons/wallet-a-msg.svg rename to interface/resources/icons/tablet-icons/inventory-a-msg.svg diff --git a/interface/resources/icons/tablet-icons/inventory-a.svg b/interface/resources/icons/tablet-icons/inventory-a.svg new file mode 100644 index 0000000000..8b6f34eaa3 --- /dev/null +++ b/interface/resources/icons/tablet-icons/inventory-a.svg @@ -0,0 +1,3 @@ + + + diff --git a/interface/resources/icons/tablet-icons/wallet-i-msg.svg b/interface/resources/icons/tablet-icons/inventory-i-msg.svg similarity index 100% rename from interface/resources/icons/tablet-icons/wallet-i-msg.svg rename to interface/resources/icons/tablet-icons/inventory-i-msg.svg diff --git a/interface/resources/icons/tablet-icons/wallet-i.svg b/interface/resources/icons/tablet-icons/inventory-i.svg similarity index 100% rename from interface/resources/icons/tablet-icons/wallet-i.svg rename to interface/resources/icons/tablet-icons/inventory-i.svg diff --git a/interface/resources/icons/tablet-icons/wallet-a.svg b/interface/resources/icons/tablet-icons/wallet-a.svg deleted file mode 100644 index 50ea64848f..0000000000 --- a/interface/resources/icons/tablet-icons/wallet-a.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 01b29185ca50c7cf2ce6d966193a99c9afcdbf7b Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 1 Nov 2018 09:22:56 -0700 Subject: [PATCH 307/362] Update the default edit.js collidesWith to include myAvatar --- scripts/system/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 6425806771..c3f8f1f4e7 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -278,7 +278,7 @@ const DEFAULT_ENTITY_PROPERTIES = { All: { description: "", rotation: { x: 0, y: 0, z: 0, w: 1 }, - collidesWith: "static,dynamic,kinematic,otherAvatar", + collidesWith: "static,dynamic,kinematic,otherAvatar,myAvatar", collisionSoundURL: "", cloneable: false, ignoreIK: true, From d6477993a160e7f8c667849a645c523260c0173b Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 1 Nov 2018 13:08:00 -0700 Subject: [PATCH 308/362] Make it work better with the tablet --- interface/src/Menu.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 3636a2f45a..16e8af5683 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -268,9 +268,13 @@ Menu::Menu() { // Settings > Security... action = addActionToQMenuAndActionHash(settingsMenu, "Security..."); connect(action, &QAction::triggered, [] { - auto tablet = dynamic_cast( - DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); - tablet->loadQMLSource(QString("hifi/dialogs/security/Security.qml")); + auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); + auto hmd = DependencyManager::get(); + tablet->pushOntoStack("hifi/dialogs/security/Security.qml"); + + if (!hmd->getShouldShowTablet()) { + hmd->toggleShouldShowTablet(); + } }); // Settings > Developer Menu From 0a15e94fe4de1088806af17489859066c7856171 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 1 Nov 2018 13:27:31 -0700 Subject: [PATCH 309/362] New colors --- .../resources/qml/hifi/dialogs/security/Security.qml | 8 +++----- .../qml/hifi/dialogs/security/SecurityImageChange.qml | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml index 3f4d17e838..8baff0ac13 100644 --- a/interface/resources/qml/hifi/dialogs/security/Security.qml +++ b/interface/resources/qml/hifi/dialogs/security/Security.qml @@ -19,13 +19,11 @@ import "qrc:////qml//controls-uit" as HifiControlsUit import "qrc:////qml//controls" as HifiControls import "qrc:////qml//hifi//commerce//common" as HifiCommerceCommon -// references XXX from root context - Rectangle { HifiStylesUit.HifiConstants { id: hifi; } id: root; - color: hifi.colors.darkGray; + color: hifi.colors.baseGray; property string title: "Security Settings"; property bool walletSetUp; @@ -163,7 +161,7 @@ Rectangle { anchors.top: parent.top; anchors.left: parent.left; anchors.right: parent.right; - height: 70; + height: 55; color: hifi.colors.baseGrayHighlight; HifiStylesUit.RalewaySemiBold { @@ -254,7 +252,7 @@ Rectangle { anchors.top: parent.top; anchors.left: parent.left; anchors.right: parent.right; - height: 70; + height: 55; color: hifi.colors.baseGrayHighlight; HifiStylesUit.RalewaySemiBold { diff --git a/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml b/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml index 0007b98291..96d75e340b 100644 --- a/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml +++ b/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml @@ -23,7 +23,7 @@ Rectangle { HifiStylesUit.HifiConstants { id: hifi; } id: root; - color: hifi.colors.darkGray; + color: hifi.colors.baseGray; property bool justSubmitted: false; Connections { From 4593b33435ea1a73d7c011e0ac1e33f692cf876f Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 1 Nov 2018 14:23:21 -0700 Subject: [PATCH 310/362] Fix new models not obeying dynamic checkbox --- scripts/system/edit.js | 36 +++++++++--------------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 6425806771..43b82c8615 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -484,23 +484,28 @@ var toolBar = (function () { originalProperties[key] = newProperties[key]; } } - function createNewEntity(properties) { - var dimensions = properties.dimensions ? properties.dimensions : DEFAULT_DIMENSIONS; + function createNewEntity(requestedProperties) { + var dimensions = requestedProperties.dimensions ? requestedProperties.dimensions : DEFAULT_DIMENSIONS; var position = getPositionToCreateEntity(); var entityID = null; + var properties = {}; + applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.All); - var type = properties.type; + var type = requestedProperties.type; if (type == "Box" || type == "Sphere") { applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.Shape); } else if (type == "Image") { - properties.type = "Model"; + requestedProperties.type = "Model"; applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.Image); } else { applyProperties(properties, DEFAULT_ENTITY_PROPERTIES[type]); } + // We apply the requested properties first so that they take priority over any default properties. + applyProperties(properties, requestedProperties); + if (position !== null && position !== undefined) { var direction; @@ -845,41 +850,18 @@ var toolBar = (function () { addButton("newCubeButton", function () { createNewEntity({ type: "Box", - dimensions: DEFAULT_DIMENSIONS, - color: { - red: 255, - green: 0, - blue: 0 - } }); }); addButton("newSphereButton", function () { createNewEntity({ type: "Sphere", - dimensions: DEFAULT_DIMENSIONS, - color: { - red: 255, - green: 0, - blue: 0 - } }); }); addButton("newLightButton", function () { createNewEntity({ type: "Light", - isSpotlight: false, - color: { - red: 150, - green: 150, - blue: 150 - }, - constantAttenuation: 1, - linearAttenuation: 0, - quadraticAttenuation: 0, - exponent: 0, - cutoff: 180 // in degrees }); }); From bccb3f1de9ea68e6d722d77924be490854ca04e6 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 1 Nov 2018 14:53:10 -0700 Subject: [PATCH 311/362] Prepare for further work --- scripts/defaultScripts.js | 3 +-- scripts/system/tts/TTS.js | 28 ---------------------------- scripts/system/tts/tts-a.svg | 9 --------- scripts/system/tts/tts-i.svg | 9 --------- 4 files changed, 1 insertion(+), 48 deletions(-) delete mode 100644 scripts/system/tts/TTS.js delete mode 100644 scripts/system/tts/tts-a.svg delete mode 100644 scripts/system/tts/tts-i.svg diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 5ed74fd833..5df1b3e511 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,8 +32,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/emote.js", - "system/miniTablet.js", - "system/tts/TTS.js" + "system/miniTablet.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/system/tts/TTS.js b/scripts/system/tts/TTS.js deleted file mode 100644 index 36259cfda0..0000000000 --- a/scripts/system/tts/TTS.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; -/*jslint vars:true, plusplus:true, forin:true*/ -/*global Tablet, Script, */ -/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ -// -// TTS.js -// -// Created by Zach Fox on 2018-10-10 -// 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 -var AppUi = Script.require('appUi'); - -var ui; -function startup() { - ui = new AppUi({ - buttonName: "TTS", - //home: Script.resolvePath("TTS.qml") - home: "hifi/tts/TTS.qml", - graphicsDirectory: Script.resolvePath("./") // speech by Danil Polshin from the Noun Project - }); -} -startup(); -}()); // END LOCAL_SCOPE diff --git a/scripts/system/tts/tts-a.svg b/scripts/system/tts/tts-a.svg deleted file mode 100644 index 9dac3a2d53..0000000000 --- a/scripts/system/tts/tts-a.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/scripts/system/tts/tts-i.svg b/scripts/system/tts/tts-i.svg deleted file mode 100644 index 1c52ec3193..0000000000 --- a/scripts/system/tts/tts-i.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - From 8256bfbc554b06d2d752f55403f2239729801d6c Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 1 Nov 2018 15:20:50 -0700 Subject: [PATCH 312/362] icons --- .../icons/tablet-icons/inventory-a-msg.svg | 10 ++--- .../icons/tablet-icons/inventory-i-msg.svg | 18 ++------- .../icons/tablet-icons/inventory-i.svg | 17 ++------- .../qml/hifi/commerce/wallet/Wallet.qml | 38 +++++++++++-------- .../commerce/wallet/images/items-tab-a.svg | 8 ++++ 5 files changed, 40 insertions(+), 51 deletions(-) create mode 100644 interface/resources/qml/hifi/commerce/wallet/images/items-tab-a.svg diff --git a/interface/resources/icons/tablet-icons/inventory-a-msg.svg b/interface/resources/icons/tablet-icons/inventory-a-msg.svg index d51c3e99a2..794bd1e414 100644 --- a/interface/resources/icons/tablet-icons/inventory-a-msg.svg +++ b/interface/resources/icons/tablet-icons/inventory-a-msg.svg @@ -1,6 +1,4 @@ - - - - \ No newline at end of file + + + + diff --git a/interface/resources/icons/tablet-icons/inventory-i-msg.svg b/interface/resources/icons/tablet-icons/inventory-i-msg.svg index 676f97a966..35d4fb54ae 100644 --- a/interface/resources/icons/tablet-icons/inventory-i-msg.svg +++ b/interface/resources/icons/tablet-icons/inventory-i-msg.svg @@ -1,16 +1,4 @@ - - - - - - - - + + + diff --git a/interface/resources/icons/tablet-icons/inventory-i.svg b/interface/resources/icons/tablet-icons/inventory-i.svg index 4e27e41b44..071fabce88 100644 --- a/interface/resources/icons/tablet-icons/inventory-i.svg +++ b/interface/resources/icons/tablet-icons/inventory-i.svg @@ -1,14 +1,3 @@ - - - - - - - - + + + diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 4124200535..6383677b84 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -126,16 +126,19 @@ Rectangle { anchors.top: parent.top; // Wallet icon - HiFiGlyphs { + Image { id: walletIcon; - text: hifi.glyphs.wallet; - // Size - size: parent.height * 0.8; - // Anchors + source: "../../../../icons/tablet-icons/inventory-a.svg"; + height: parent.height * 0.5; + width: walletIcon.height; anchors.left: parent.left; anchors.leftMargin: 8; anchors.verticalCenter: parent.verticalCenter; - // Style + visible: false; // When we use a white .svg instead of a glyph with color property, we set to invisible and use the following ColorOverlay. + } + ColorOverlay { + anchors.fill: walletIcon; + source: walletIcon; color: hifi.colors.blueHighlight; } @@ -148,7 +151,7 @@ Rectangle { // Anchors anchors.top: parent.top; anchors.left: walletIcon.right; - anchors.leftMargin: 4; + anchors.leftMargin: 6; anchors.bottom: parent.bottom; width: paintedWidth; // Style @@ -575,16 +578,19 @@ Rectangle { anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; - HiFiGlyphs { + Image { id: exchangeMoneyTabIcon; - text: hifi.glyphs.home2; - // Size - size: 50; - // Anchors + source: "images/items-tab-a.svg"; + height: 25; + width: exchangeMoneyTabIcon.height; anchors.horizontalCenter: parent.horizontalCenter; anchors.top: parent.top; - anchors.topMargin: -2; - // Style + anchors.topMargin: 10; + visible: false; // When we use a white .svg instead of a glyph with color property, we set to invisible and use the following ColorOverlay. + } + ColorOverlay { + anchors.fill: exchangeMoneyTabIcon; + source: exchangeMoneyTabIcon; color: root.activeView === "walletInventory" || inventoryTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; } @@ -592,9 +598,9 @@ Rectangle { id: exchangeMoneyMessagesWaitingLight; visible: parent.messagesWaiting; anchors.right: exchangeMoneyTabIcon.left; - anchors.rightMargin: -4; + anchors.rightMargin: 10; anchors.top: exchangeMoneyTabIcon.top; - anchors.topMargin: 16; + anchors.topMargin: 4; height: 10; width: height; radius: height/2; diff --git a/interface/resources/qml/hifi/commerce/wallet/images/items-tab-a.svg b/interface/resources/qml/hifi/commerce/wallet/images/items-tab-a.svg new file mode 100644 index 0000000000..7474f4ed57 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/images/items-tab-a.svg @@ -0,0 +1,8 @@ + + + + + + + + From 1f8dde67568f80e70cc888200535f2442cc1792c Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 1 Nov 2018 15:37:16 -0700 Subject: [PATCH 313/362] Transaction string was assuming items given to the marketplace/highfidelity were Gifts and was reporting them as such. Items given to the marketplace are in exchange for updates, and the memo indicates such. --- interface/src/commerce/Ledger.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 67303f2a9b..60cd96c0ca 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -219,7 +219,11 @@ QString transactionString(const QJsonObject& valueObject) { if (!message.isEmpty()) { result += QString("
with memo: \"%1\"").arg(message); } - } else if (sentMoney <= 0 && receivedMoney <= 0 && (sentCerts > 0 || receivedCerts > 0) && !KNOWN_USERS.contains(valueObject["sender_name"].toString())) { + } else if (sentMoney <= 0 && receivedMoney <= 0 && + (sentCerts > 0 || receivedCerts > 0) && + !KNOWN_USERS.contains(valueObject["sender_name"].toString()) && + !KNOWN_USERS.contains(valueObject["recipient_name"].toString()) + ) { // this is a non-HFC asset transfer. if (sentCerts > 0) { QString recipient = userLink(valueObject["recipient_name"].toString(), valueObject["place_name"].toString()); From 20cd1df22cf1f48bbf3c27a7347518c1022afae7 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 1 Nov 2018 16:48:08 -0700 Subject: [PATCH 314/362] Lotsa changes --- interface/resources/qml/hifi/tts/TTS.qml | 12 +++- interface/src/Application.cpp | 4 -- .../src/scripting/TTSScriptingInterface.cpp | 55 +++++++------------ .../src/scripting/TTSScriptingInterface.h | 15 ++--- libraries/audio-client/src/AudioClient.cpp | 41 -------------- libraries/audio-client/src/AudioClient.h | 5 -- 6 files changed, 36 insertions(+), 96 deletions(-) diff --git a/interface/resources/qml/hifi/tts/TTS.qml b/interface/resources/qml/hifi/tts/TTS.qml index 114efd0cca..d9507f6084 100644 --- a/interface/resources/qml/hifi/tts/TTS.qml +++ b/interface/resources/qml/hifi/tts/TTS.qml @@ -202,7 +202,6 @@ Rectangle { TextArea { id: messageToSpeak; - placeholderText: "Message to Speak"; font.family: "Fira Sans SemiBold"; font.pixelSize: 20; // Anchors @@ -229,6 +228,17 @@ Rectangle { event.accepted = true; } } + + HifiStylesUit.FiraSansRegular { + text: "Input Text to Speak..."; + size: 20; + anchors.fill: parent; + anchors.topMargin: 4; + anchors.leftMargin: 4; + color: hifi.colors.lightGrayText; + visible: !parent.activeFocus && messageToSpeak.text === ""; + verticalAlignment: Text.AlignTop; + } } HifiControlsUit.Button { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index dfb4ef1c32..967a31135c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1187,10 +1187,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo recording::Frame::registerFrameHandler(AudioConstants::getAudioFrameName(), [&audioIO](recording::Frame::ConstPointer frame) { audioIO->handleRecordedAudioInput(frame->data); }); - - auto TTS = DependencyManager::get().data(); - connect(TTS, &TTSScriptingInterface::ttsSampleCreated, audioIO, &AudioClient::handleTTSAudioInput); - connect(TTS, &TTSScriptingInterface::clearTTSBuffer, audioIO, &AudioClient::clearTTSBuffer); connect(audioIO, &AudioClient::inputReceived, [](const QByteArray& audio) { static auto recorder = DependencyManager::get(); diff --git a/interface/src/scripting/TTSScriptingInterface.cpp b/interface/src/scripting/TTSScriptingInterface.cpp index f51a638471..6a5b72ea5f 100644 --- a/interface/src/scripting/TTSScriptingInterface.cpp +++ b/interface/src/scripting/TTSScriptingInterface.cpp @@ -37,6 +37,9 @@ TTSScriptingInterface::TTSScriptingInterface() { if (FAILED(hr)) { qDebug() << "Can't set default voice."; } + + _lastSoundAudioInjectorUpdateTimer.setSingleShot(true); + connect(&_lastSoundAudioInjectorUpdateTimer, &QTimer::timeout, this, &TTSScriptingInterface::updateLastSoundAudioInjector); #endif } @@ -58,38 +61,22 @@ private: }; #endif -void TTSScriptingInterface::testTone(const bool& alsoInject) { - QByteArray byteArray(480000, 0); - _lastSoundByteArray.resize(0); - _lastSoundByteArray.resize(480000); - - int32_t a = 0; - int16_t* samples = reinterpret_cast(byteArray.data()); - for (a = 0; a < 240000; a++) { - int16_t temp = (glm::sin(glm::radians((float)a))) * 32768; - samples[a] = temp; - } - emit ttsSampleCreated(_lastSoundByteArray, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * 50, 96); - - if (alsoInject) { +const std::chrono::milliseconds INJECTOR_INTERVAL_MS = std::chrono::milliseconds(100); +void TTSScriptingInterface::updateLastSoundAudioInjector() { + if (_lastSoundAudioInjector) { AudioInjectorOptions options; options.position = DependencyManager::get()->getMyAvatarPosition(); - - _lastSoundAudioInjector = AudioInjector::playSound(_lastSoundByteArray, options); + _lastSoundAudioInjector->setOptions(options); + _lastSoundAudioInjectorUpdateTimer.start(INJECTOR_INTERVAL_MS); } } -void TTSScriptingInterface::speakText(const QString& textToSpeak, - const int& newChunkSize, - const int& timerInterval, - const int& sampleRate, - const int& bitsPerSample, - const bool& alsoInject) { +void TTSScriptingInterface::speakText(const QString& textToSpeak) { #ifdef WIN32 WAVEFORMATEX fmt; fmt.wFormatTag = WAVE_FORMAT_PCM; - fmt.nSamplesPerSec = sampleRate; - fmt.wBitsPerSample = bitsPerSample; + fmt.nSamplesPerSec = 24000; + fmt.wBitsPerSample = 16; fmt.nChannels = 1; fmt.nBlockAlign = fmt.nChannels * fmt.wBitsPerSample / 8; fmt.nAvgBytesPerSec = fmt.nSamplesPerSec * fmt.nBlockAlign; @@ -156,16 +143,17 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak, _lastSoundByteArray.resize(0); _lastSoundByteArray.append(buf1, dwSize); - // Commented out because this doesn't work completely :) - // Obviously, commenting this out isn't fit for production, but it's fine for a test PR - //emit ttsSampleCreated(_lastSoundByteArray, newChunkSize, timerInterval); + AudioInjectorOptions options; + options.position = DependencyManager::get()->getMyAvatarPosition(); - if (alsoInject) { - AudioInjectorOptions options; - options.position = DependencyManager::get()->getMyAvatarPosition(); - - _lastSoundAudioInjector = AudioInjector::playSound(_lastSoundByteArray, options); + if (_lastSoundAudioInjector) { + _lastSoundAudioInjector->stop(); + _lastSoundAudioInjectorUpdateTimer.stop(); } + + _lastSoundAudioInjector = AudioInjector::playSoundAndDelete(_lastSoundByteArray, options); + + _lastSoundAudioInjectorUpdateTimer.start(INJECTOR_INTERVAL_MS); #else qDebug() << "Text-to-Speech isn't currently supported on non-Windows platforms."; #endif @@ -174,7 +162,6 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak, void TTSScriptingInterface::stopLastSpeech() { if (_lastSoundAudioInjector) { _lastSoundAudioInjector->stop(); + _lastSoundAudioInjector = NULL; } - - emit clearTTSBuffer(); } diff --git a/interface/src/scripting/TTSScriptingInterface.h b/interface/src/scripting/TTSScriptingInterface.h index 7e4f3afa9d..0f1e723885 100644 --- a/interface/src/scripting/TTSScriptingInterface.h +++ b/interface/src/scripting/TTSScriptingInterface.h @@ -12,6 +12,7 @@ #define hifi_SpeechScriptingInterface_h #include +#include #include #ifdef WIN32 #pragma warning(disable : 4996) @@ -31,19 +32,9 @@ public: TTSScriptingInterface(); ~TTSScriptingInterface(); - Q_INVOKABLE void testTone(const bool& alsoInject = false); - Q_INVOKABLE void speakText(const QString& textToSpeak, - const int& newChunkSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * 50), - const int& timerInterval = 96, - const int& sampleRate = 24000, - const int& bitsPerSample = 16, - const bool& alsoInject = false); + Q_INVOKABLE void speakText(const QString& textToSpeak); Q_INVOKABLE void stopLastSpeech(); -signals: - void ttsSampleCreated(QByteArray outputArray, const int& newChunkSize, const int& timerInterval); - void clearTTSBuffer(); - private: #ifdef WIN32 class CComAutoInit { @@ -90,6 +81,8 @@ private: QByteArray _lastSoundByteArray; AudioInjectorPointer _lastSoundAudioInjector; + QTimer _lastSoundAudioInjectorUpdateTimer; + void updateLastSoundAudioInjector(); }; #endif // hifi_SpeechScriptingInterface_h diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 1ce6c15951..b7557681a5 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -245,8 +245,6 @@ AudioClient::AudioClient() : packetReceiver.registerListener(PacketType::NoisyMute, this, "handleNoisyMutePacket"); packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket"); packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat"); - - connect(&_TTSTimer, &QTimer::timeout, this, &AudioClient::processTTSBuffer); } AudioClient::~AudioClient() { @@ -1202,45 +1200,6 @@ int rawToWav(const char* rawData, const int& rawLength, const char* wavfn, long return 0; } -void AudioClient::processTTSBuffer() { - Lock lock(_TTSMutex); - if (_TTSAudioBuffer.size() > 0) { - QByteArray part; - part.append(_TTSAudioBuffer.data(), _TTSChunkSize); - _TTSAudioBuffer.remove(0, _TTSChunkSize); - handleAudioInput(part); - } else { - _isProcessingTTS = false; - _TTSTimer.stop(); - } -} - -void AudioClient::handleTTSAudioInput(const QByteArray& audio, const int& newChunkSize, const int& timerInterval) { - _TTSChunkSize = newChunkSize; - _TTSAudioBuffer.append(audio); - - handleLocalEchoAndReverb(_TTSAudioBuffer, 48000, 1); - - //QString filename = QString::number(usecTimestampNow()); - //QString path = PathUtils::getAppDataPath() + "Audio/" + filename + "-before.wav"; - //rawToWav(_TTSAudioBuffer.data(), _TTSAudioBuffer.size(), path.toLocal8Bit(), 24000, 1); - - //QByteArray temp; - - _isProcessingTTS = true; - _TTSTimer.start(timerInterval); - - //filename = QString::number(usecTimestampNow()); - //path = PathUtils::getAppDataPath() + "Audio/" + filename + "-after.wav"; - //rawToWav(temp.data(), temp.size(), path.toLocal8Bit(), 12000, 1); -} - -void AudioClient::clearTTSBuffer() { - _TTSAudioBuffer.resize(0); - _isProcessingTTS = false; - _TTSTimer.stop(); -} - void AudioClient::prepareLocalAudioInjectors(std::unique_ptr localAudioLock) { bool doSynchronously = localAudioLock.operator bool(); if (!localAudioLock) { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 9b50d3eccb..788b764903 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -197,11 +197,6 @@ public slots: void checkInputTimeout(); void handleDummyAudioInput(); void handleRecordedAudioInput(const QByteArray& audio); - void handleTTSAudioInput(const QByteArray& audio, - const int& newChunkSize, - const int& timerInterval); - void clearTTSBuffer(); - void processTTSBuffer(); void reset(); void audioMixerKilled(); From cbca77b12fa3f87d70dfc22de92a97b875c77257 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 1 Nov 2018 17:17:48 -0700 Subject: [PATCH 315/362] Rename HFMGeometry to HFMModel and adjust related variables --- .../src/avatars/ScriptableAvatar.cpp | 6 +- interface/src/ModelPackager.cpp | 34 +-- interface/src/ModelPackager.h | 6 +- interface/src/ModelPropertiesDialog.cpp | 20 +- interface/src/ModelPropertiesDialog.h | 4 +- interface/src/avatar/MyAvatar.cpp | 14 +- interface/src/avatar/MySkeletonModel.cpp | 14 +- interface/src/raypick/CollisionPick.cpp | 14 +- interface/src/ui/overlays/ModelOverlay.cpp | 14 +- libraries/animation/src/AnimClip.cpp | 10 +- libraries/animation/src/AnimSkeleton.cpp | 6 +- libraries/animation/src/AnimSkeleton.h | 2 +- libraries/animation/src/AnimationCache.cpp | 24 +- libraries/animation/src/AnimationCache.h | 8 +- libraries/animation/src/Rig.cpp | 68 ++--- libraries/animation/src/Rig.h | 6 +- .../src/avatars-renderer/Avatar.cpp | 6 +- .../src/avatars-renderer/SkeletonModel.cpp | 46 ++-- .../src/avatars-renderer/SkeletonModel.h | 4 +- libraries/baking/src/FBXBaker.cpp | 6 +- libraries/baking/src/FBXBaker.h | 2 +- libraries/baking/src/OBJBaker.cpp | 20 +- libraries/baking/src/OBJBaker.h | 4 +- .../src/RenderableModelEntityItem.cpp | 24 +- libraries/fbx/src/FBX.h | 14 +- libraries/fbx/src/FBXReader.cpp | 234 +++++++++--------- libraries/fbx/src/FBXReader.h | 12 +- libraries/fbx/src/GLTFReader.cpp | 112 ++++----- libraries/fbx/src/GLTFReader.h | 8 +- libraries/fbx/src/OBJReader.cpp | 70 +++--- libraries/fbx/src/OBJReader.h | 6 +- .../src/model-networking/ModelCache.cpp | 40 +-- .../src/model-networking/ModelCache.h | 8 +- .../render-utils/src/CauterizedModel.cpp | 20 +- .../render-utils/src/MeshPartPayload.cpp | 4 +- libraries/render-utils/src/Model.cpp | 90 +++---- libraries/render-utils/src/Model.h | 8 +- .../render-utils/src/SoftAttachmentModel.cpp | 6 +- tests-manual/gpu/src/TestFbx.cpp | 10 +- tests-manual/gpu/src/TestFbx.h | 2 +- .../src/AnimInverseKinematicsTests.cpp | 24 +- tools/skeleton-dump/src/SkeletonDumpApp.cpp | 2 +- tools/vhacd-util/src/VHACDUtil.cpp | 34 +-- tools/vhacd-util/src/VHACDUtil.h | 8 +- tools/vhacd-util/src/VHACDUtilApp.cpp | 10 +- tools/vhacd-util/src/VHACDUtilApp.h | 2 +- 46 files changed, 543 insertions(+), 543 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index 385f94d9f3..51038a782f 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -84,7 +84,7 @@ void ScriptableAvatar::update(float deltatime) { // Run animation if (_animation && _animation->isLoaded() && _animation->getFrames().size() > 0 && !_bind.isNull() && _bind->isLoaded()) { if (!_animSkeleton) { - _animSkeleton = std::make_shared(_bind->getGeometry()); + _animSkeleton = std::make_shared(_bind->getHFMModel()); } float currentFrame = _animationDetails.currentFrame + deltatime * _animationDetails.fps; if (_animationDetails.loop || currentFrame < _animationDetails.lastFrame) { @@ -93,7 +93,7 @@ void ScriptableAvatar::update(float deltatime) { } _animationDetails.currentFrame = currentFrame; - const QVector& modelJoints = _bind->getGeometry().joints; + const QVector& modelJoints = _bind->getHFMModel().joints; QStringList animationJointNames = _animation->getJointNames(); const int nJoints = modelJoints.size(); @@ -113,7 +113,7 @@ void ScriptableAvatar::update(float deltatime) { const QString& name = animationJointNames[i]; // As long as we need the model preRotations anyway, let's get the jointIndex from the bind skeleton rather than // trusting the .fst (which is sometimes not updated to match changes to .fbx). - int mapping = _bind->getGeometry().getJointIndex(name); + int mapping = _bind->getHFMModel().getJointIndex(name); if (mapping != -1 && !_maskedJoints.contains(name)) { AnimPose floorPose = composeAnimPose(modelJoints[mapping], floorFrame.rotations[i], floorFrame.translations[i] * UNIT_SCALE); diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index 0b2846006b..f3a69bbad2 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -109,10 +109,10 @@ bool ModelPackager::loadModel() { qCDebug(interfaceapp) << "Reading FBX file : " << _fbxInfo.filePath(); QByteArray fbxContents = fbx.readAll(); - _geometry.reset(readFBX(fbxContents, QVariantHash(), _fbxInfo.filePath())); + _model.reset(readFBX(fbxContents, QVariantHash(), _fbxInfo.filePath())); // make sure we have some basic mappings - populateBasicMapping(_mapping, _fbxInfo.filePath(), *_geometry); + populateBasicMapping(_mapping, _fbxInfo.filePath(), *_model); } catch (const QString& error) { qCDebug(interfaceapp) << "Error reading " << _fbxInfo.filePath() << ": " << error; return false; @@ -122,7 +122,7 @@ bool ModelPackager::loadModel() { bool ModelPackager::editProperties() { // open the dialog to configure the rest - ModelPropertiesDialog properties(_modelType, _mapping, _modelFile.path(), *_geometry); + ModelPropertiesDialog properties(_modelType, _mapping, _modelFile.path(), *_model); if (properties.exec() == QDialog::Rejected) { return false; } @@ -235,18 +235,18 @@ bool ModelPackager::zipModel() { return true; } -void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const HFMGeometry& geometry) { +void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const HFMModel& model) { bool isBodyType = _modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL; // mixamo files - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will // be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file - bool likelyMixamoFile = geometry.applicationName == "mixamo.com" || - (geometry.blendshapeChannelNames.contains("BrowsDown_Right") && - geometry.blendshapeChannelNames.contains("MouthOpen") && - geometry.blendshapeChannelNames.contains("Blink_Left") && - geometry.blendshapeChannelNames.contains("Blink_Right") && - geometry.blendshapeChannelNames.contains("Squint_Right")); + bool likelyMixamoFile = model.applicationName == "mixamo.com" || + (model.blendshapeChannelNames.contains("BrowsDown_Right") && + model.blendshapeChannelNames.contains("MouthOpen") && + model.blendshapeChannelNames.contains("Blink_Left") && + model.blendshapeChannelNames.contains("Blink_Right") && + model.blendshapeChannelNames.contains("Squint_Right")); if (!mapping.contains(NAME_FIELD)) { mapping.insert(NAME_FIELD, QFileInfo(filename).baseName()); @@ -268,15 +268,15 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename } QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); if (!joints.contains("jointEyeLeft")) { - joints.insert("jointEyeLeft", geometry.jointIndices.contains("jointEyeLeft") ? "jointEyeLeft" : - (geometry.jointIndices.contains("EyeLeft") ? "EyeLeft" : "LeftEye")); + joints.insert("jointEyeLeft", model.jointIndices.contains("jointEyeLeft") ? "jointEyeLeft" : + (model.jointIndices.contains("EyeLeft") ? "EyeLeft" : "LeftEye")); } if (!joints.contains("jointEyeRight")) { - joints.insert("jointEyeRight", geometry.jointIndices.contains("jointEyeRight") ? "jointEyeRight" : - geometry.jointIndices.contains("EyeRight") ? "EyeRight" : "RightEye"); + joints.insert("jointEyeRight", model.jointIndices.contains("jointEyeRight") ? "jointEyeRight" : + model.jointIndices.contains("EyeRight") ? "EyeRight" : "RightEye"); } if (!joints.contains("jointNeck")) { - joints.insert("jointNeck", geometry.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck"); + joints.insert("jointNeck", model.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck"); } if (isBodyType) { @@ -296,7 +296,7 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename if (!joints.contains("jointHead")) { const char* topName = likelyMixamoFile ? "HeadTop_End" : "HeadEnd"; - joints.insert("jointHead", geometry.jointIndices.contains(topName) ? topName : "Head"); + joints.insert("jointHead", model.jointIndices.contains(topName) ? topName : "Head"); } mapping.insert(JOINT_FIELD, joints); @@ -370,7 +370,7 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename void ModelPackager::listTextures() { _textures.clear(); - foreach (const HFMMaterial mat, _geometry->materials) { + foreach (const HFMMaterial mat, _model->materials) { if (!mat.albedoTexture.filename.isEmpty() && mat.albedoTexture.content.isEmpty() && !_textures.contains(mat.albedoTexture.filename)) { _textures << mat.albedoTexture.filename; diff --git a/interface/src/ModelPackager.h b/interface/src/ModelPackager.h index b68d9e746d..2ec629a363 100644 --- a/interface/src/ModelPackager.h +++ b/interface/src/ModelPackager.h @@ -19,7 +19,7 @@ #include "ui/ModelsBrowser.h" -class HFMGeometry; +class HFMModel; class ModelPackager : public QObject { public: @@ -32,7 +32,7 @@ private: bool editProperties(); bool zipModel(); - void populateBasicMapping(QVariantHash& mapping, QString filename, const HFMGeometry& geometry); + void populateBasicMapping(QVariantHash& mapping, QString filename, const HFMModel& model); void listTextures(); bool copyTextures(const QString& oldDir, const QDir& newDir); @@ -44,7 +44,7 @@ private: QString _scriptDir; QVariantHash _mapping; - std::unique_ptr _geometry; + std::unique_ptr _model; QStringList _textures; QStringList _scripts; }; diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp index dcda85d117..0b1638a5bc 100644 --- a/interface/src/ModelPropertiesDialog.cpp +++ b/interface/src/ModelPropertiesDialog.cpp @@ -27,11 +27,11 @@ ModelPropertiesDialog::ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, - const QString& basePath, const HFMGeometry& geometry) : + const QString& basePath, const HFMModel& model) : _modelType(modelType), _originalMapping(originalMapping), _basePath(basePath), -_geometry(geometry) +_model(model) { setWindowTitle("Set Model Properties"); @@ -108,8 +108,8 @@ QVariantHash ModelPropertiesDialog::getMapping() const { // update the joint indices QVariantHash jointIndices; - for (int i = 0; i < _geometry.joints.size(); i++) { - jointIndices.insert(_geometry.joints.at(i).name, QString::number(i)); + for (int i = 0; i < _model.joints.size(); i++) { + jointIndices.insert(_model.joints.at(i).name, QString::number(i)); } mapping.insert(JOINT_INDEX_FIELD, jointIndices); @@ -118,10 +118,10 @@ QVariantHash ModelPropertiesDialog::getMapping() const { if (_modelType == FSTReader::ATTACHMENT_MODEL) { glm::vec3 pivot; if (_pivotAboutCenter->isChecked()) { - pivot = (_geometry.meshExtents.minimum + _geometry.meshExtents.maximum) * 0.5f; + pivot = (_model.meshExtents.minimum + _model.meshExtents.maximum) * 0.5f; } else if (_pivotJoint->currentIndex() != 0) { - pivot = extractTranslation(_geometry.joints.at(_pivotJoint->currentIndex() - 1).transform); + pivot = extractTranslation(_model.joints.at(_pivotJoint->currentIndex() - 1).transform); } mapping.insert(TRANSLATION_X_FIELD, -pivot.x * (float)_scale->value() + (float)_translationX->value()); mapping.insert(TRANSLATION_Y_FIELD, -pivot.y * (float)_scale->value() + (float)_translationY->value()); @@ -191,7 +191,7 @@ void ModelPropertiesDialog::reset() { } foreach (const QVariant& joint, _originalMapping.values(FREE_JOINT_FIELD)) { QString jointName = joint.toString(); - if (_geometry.jointIndices.contains(jointName)) { + if (_model.jointIndices.contains(jointName)) { createNewFreeJoint(jointName); } } @@ -249,8 +249,8 @@ QComboBox* ModelPropertiesDialog::createJointBox(bool withNone) const { if (withNone) { box->addItem("(none)"); } - foreach (const HFMJoint& joint, _geometry.joints) { - if (joint.isSkeletonJoint || !_geometry.hasSkeletonJoints) { + foreach (const HFMJoint& joint, _model.joints) { + if (joint.isSkeletonJoint || !_model.hasSkeletonJoints) { box->addItem(joint.name); } } @@ -266,7 +266,7 @@ QDoubleSpinBox* ModelPropertiesDialog::createTranslationBox() const { } void ModelPropertiesDialog::insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const { - if (_geometry.jointIndices.contains(name)) { + if (_model.jointIndices.contains(name)) { joints.insert(joint, name); } else { joints.remove(joint); diff --git a/interface/src/ModelPropertiesDialog.h b/interface/src/ModelPropertiesDialog.h index d1a020bec3..b979768a2d 100644 --- a/interface/src/ModelPropertiesDialog.h +++ b/interface/src/ModelPropertiesDialog.h @@ -30,7 +30,7 @@ class ModelPropertiesDialog : public QDialog { public: ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, - const QString& basePath, const HFMGeometry& geometry); + const QString& basePath, const HFMModel& model); QVariantHash getMapping() const; @@ -50,7 +50,7 @@ private: FSTReader::ModelType _modelType; QVariantHash _originalMapping; QString _basePath; - HFMGeometry _geometry; + HFMModel _model; QLineEdit* _name = nullptr; QPushButton* _textureDirectory = nullptr; QPushButton* _scriptDirectory = nullptr; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b3a66f70b8..8d47591d40 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -155,8 +155,8 @@ MyAvatar::MyAvatar(QThread* thread) : }); connect(_skeletonModel.get(), &Model::rigReady, this, [this]() { if (_shouldLoadScripts) { - auto geometry = getSkeletonModel()->getHFMGeometry(); - qApp->loadAvatarScripts(geometry.scripts); + auto hfmModel = getSkeletonModel()->getHFMModel(); + qApp->loadAvatarScripts(hfmModel.scripts); _shouldLoadScripts = false; } // Load and convert old attachments to avatar entities @@ -2429,10 +2429,10 @@ void MyAvatar::attachmentDataToEntityProperties(const AttachmentData& data, Enti void MyAvatar::initHeadBones() { int neckJointIndex = -1; if (_skeletonModel->isLoaded()) { - neckJointIndex = _skeletonModel->getHFMGeometry().neckJointIndex; + neckJointIndex = _skeletonModel->getHFMModel().neckJointIndex; } if (neckJointIndex == -1) { - neckJointIndex = (_skeletonModel->getHFMGeometry().headJointIndex - 1); + neckJointIndex = (_skeletonModel->getHFMModel().headJointIndex - 1); if (neckJointIndex < 0) { // return if the head is not even there. can't cauterize!! return; @@ -2592,11 +2592,11 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { if (_skeletonModel && _skeletonModel->isLoaded()) { const Rig& rig = _skeletonModel->getRig(); - const HFMGeometry& geometry = _skeletonModel->getHFMGeometry(); + const HFMModel& hfmModel = _skeletonModel->getHFMModel(); for (int i = 0; i < rig.getJointStateCount(); i++) { AnimPose jointPose; rig.getAbsoluteJointPoseInRigFrame(i, jointPose); - const HFMJointShapeInfo& shapeInfo = geometry.joints[i].shapeInfo; + const HFMJointShapeInfo& shapeInfo = hfmModel.joints[i].shapeInfo; const AnimPose pose = rigToWorldPose * jointPose; for (size_t j = 0; j < shapeInfo.debugLines.size() / 2; j++) { glm::vec3 pointA = pose.xformPoint(shapeInfo.debugLines[2 * j]); @@ -4012,7 +4012,7 @@ float MyAvatar::getSitStandStateChange() const { } QVector MyAvatar::getScriptUrls() { - QVector scripts = _skeletonModel->isLoaded() ? _skeletonModel->getHFMGeometry().scripts : QVector(); + QVector scripts = _skeletonModel->isLoaded() ? _skeletonModel->getHFMModel().scripts : QVector(); return scripts; } diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 3ec40d372b..2a21f78b21 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -90,7 +90,7 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { // Called within Model::simulate call, below. void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { - const HFMGeometry& geometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); Head* head = _owningAvatar->getHead(); @@ -268,19 +268,19 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { // pass detailed torso k-dops to rig. int hipsJoint = _rig.indexOfJoint("Hips"); if (hipsJoint >= 0) { - params.hipsShapeInfo = geometry.joints[hipsJoint].shapeInfo; + params.hipsShapeInfo = hfmModel.joints[hipsJoint].shapeInfo; } int spineJoint = _rig.indexOfJoint("Spine"); if (spineJoint >= 0) { - params.spineShapeInfo = geometry.joints[spineJoint].shapeInfo; + params.spineShapeInfo = hfmModel.joints[spineJoint].shapeInfo; } int spine1Joint = _rig.indexOfJoint("Spine1"); if (spine1Joint >= 0) { - params.spine1ShapeInfo = geometry.joints[spine1Joint].shapeInfo; + params.spine1ShapeInfo = hfmModel.joints[spine1Joint].shapeInfo; } int spine2Joint = _rig.indexOfJoint("Spine2"); if (spine2Joint >= 0) { - params.spine2ShapeInfo = geometry.joints[spine2Joint].shapeInfo; + params.spine2ShapeInfo = hfmModel.joints[spine2Joint].shapeInfo; } _rig.updateFromControllerParameters(params, deltaTime); @@ -300,8 +300,8 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { eyeParams.eyeSaccade = head->getSaccade(); eyeParams.modelRotation = getRotation(); eyeParams.modelTranslation = getTranslation(); - eyeParams.leftEyeJointIndex = geometry.leftEyeJointIndex; - eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex; + eyeParams.leftEyeJointIndex = hfmModel.leftEyeJointIndex; + eyeParams.rightEyeJointIndex = hfmModel.rightEyeJointIndex; _rig.updateFromEyeParameters(eyeParams); diff --git a/interface/src/raypick/CollisionPick.cpp b/interface/src/raypick/CollisionPick.cpp index e8a53aa9b6..2b75946e28 100644 --- a/interface/src/raypick/CollisionPick.cpp +++ b/interface/src/raypick/CollisionPick.cpp @@ -131,7 +131,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha // should never fall in here when collision model not fully loaded // TODO: assert that all geometries exist and are loaded //assert(_model && _model->isLoaded() && _compoundShapeResource && _compoundShapeResource->isLoaded()); - const HFMGeometry& collisionGeometry = resource->getHFMGeometry(); + const HFMModel& collisionModel = resource->getHFMModel(); ShapeInfo::PointCollection& pointCollection = shapeInfo.getPointCollection(); pointCollection.clear(); @@ -139,7 +139,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha // the way OBJ files get read, each section under a "g" line is its own meshPart. We only expect // to find one actual "mesh" (with one or more meshParts in it), but we loop over the meshes, just in case. - foreach (const HFMMesh& mesh, collisionGeometry.meshes) { + foreach (const HFMMesh& mesh, collisionModel.meshes) { // each meshPart is a convex hull foreach (const HFMMeshPart &meshPart, mesh.parts) { pointCollection.push_back(QVector()); @@ -206,7 +206,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha // to the visual model and apply them to the collision model (without regard for the // collision model's extents). - glm::vec3 scaleToFit = dimensions / resource->getHFMGeometry().getUnscaledMeshExtents().size(); + glm::vec3 scaleToFit = dimensions / resource->getHFMModel().getUnscaledMeshExtents().size(); // multiply each point by scale for (int32_t i = 0; i < pointCollection.size(); i++) { for (int32_t j = 0; j < pointCollection[i].size(); j++) { @@ -216,11 +216,11 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha } shapeInfo.setParams(type, dimensions, resource->getURL().toString()); } else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) { - const HFMGeometry& hfmGeometry = resource->getHFMGeometry(); - int numHFMMeshes = hfmGeometry.meshes.size(); + const HFMModel& hfmModel = resource->getHFMModel(); + int numHFMMeshes = hfmModel.meshes.size(); int totalNumVertices = 0; for (int i = 0; i < numHFMMeshes; i++) { - const HFMMesh& mesh = hfmGeometry.meshes.at(i); + const HFMMesh& mesh = hfmModel.meshes.at(i); totalNumVertices += mesh.vertices.size(); } const int32_t MAX_VERTICES_PER_STATIC_MESH = 1e6; @@ -230,7 +230,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha return; } - auto& meshes = resource->getHFMGeometry().meshes; + auto& meshes = resource->getHFMModel().meshes; int32_t numMeshes = (int32_t)(meshes.size()); const int MAX_ALLOWED_MESH_COUNT = 1000; diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 1b66ff08ad..190d9c3895 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -446,7 +446,7 @@ QVariant ModelOverlay::getProperty(const QString& property) { if (property == "jointNames") { if (_model && _model->isActive()) { - // note: going through Rig because Model::getJointNames() (which proxies to HFMGeometry) was always empty + // note: going through Rig because Model::getJointNames() (which proxies to HFMModel) was always empty const Rig* rig = &(_model->getRig()); return mapJoints([rig](int jointIndex) -> QString { return rig->nameOfJoint(jointIndex); @@ -605,11 +605,11 @@ void ModelOverlay::animate() { return; } - QStringList animationJointNames = _animation->getGeometry().getJointNames(); - auto& hfmJoints = _animation->getGeometry().joints; + QStringList animationJointNames = _animation->getHFMModel().getJointNames(); + auto& hfmJoints = _animation->getHFMModel().joints; - auto& originalHFMJoints = _model->getHFMGeometry().joints; - auto& originalFbxIndices = _model->getHFMGeometry().jointIndices; + auto& originalHFMJoints = _model->getHFMModel().joints; + auto& originalHFMIndices = _model->getHFMModel().jointIndices; const QVector& rotations = frames[_lastKnownCurrentFrame].rotations; const QVector& translations = frames[_lastKnownCurrentFrame].translations; @@ -628,9 +628,9 @@ void ModelOverlay::animate() { } else if (index < animationJointNames.size()) { QString jointName = hfmJoints[index].name; - if (originalFbxIndices.contains(jointName)) { + if (originalHFMIndices.contains(jointName)) { // Making sure the joint names exist in the original model the animation is trying to apply onto. If they do, then remap and get its translation. - int remappedIndex = originalFbxIndices[jointName] - 1; // JointIndeces seem to always start from 1 and the found index is always 1 higher than actual. + int remappedIndex = originalHFMIndices[jointName] - 1; // JointIndeces seem to always start from 1 and the found index is always 1 higher than actual. translationMat = glm::translate(originalHFMJoints[remappedIndex].translation); } } diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index d630218165..7380ca8a13 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -101,8 +101,8 @@ void AnimClip::copyFromNetworkAnim() { // build a mapping from animation joint indices to skeleton joint indices. // by matching joints with the same name. - const HFMGeometry& geom = _networkAnim->getGeometry(); - AnimSkeleton animSkeleton(geom); + const HFMModel& model = _networkAnim->getHFMModel(); + AnimSkeleton animSkeleton(model); const auto animJointCount = animSkeleton.getNumJoints(); const auto skeletonJointCount = _skeleton->getNumJoints(); std::vector jointMap; @@ -115,12 +115,12 @@ void AnimClip::copyFromNetworkAnim() { jointMap.push_back(skeletonJoint); } - const int frameCount = geom.animationFrames.size(); + const int frameCount = model.animationFrames.size(); _anim.resize(frameCount); for (int frame = 0; frame < frameCount; frame++) { - const HFMAnimationFrame& hfmAnimFrame = geom.animationFrames[frame]; + const HFMAnimationFrame& hfmAnimFrame = model.animationFrames[frame]; // init all joints in animation to default pose // this will give us a resonable result for bones in the model skeleton but not in the animation. @@ -150,7 +150,7 @@ void AnimClip::copyFromNetworkAnim() { // adjust translation offsets, so large translation animatons on the reference skeleton // will be adjusted when played on a skeleton with short limbs. - const glm::vec3& hfmZeroTrans = geom.animationFrames[0].translations[animJoint]; + const glm::vec3& hfmZeroTrans = model.animationFrames[0].translations[animJoint]; const AnimPose& relDefaultPose = _skeleton->getRelativeDefaultPose(skeletonJoint); float boneLengthScale = 1.0f; const float EPSILON = 0.0001f; diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index fc4114ac7b..bc179739a0 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -16,11 +16,11 @@ #include "AnimationLogging.h" -AnimSkeleton::AnimSkeleton(const HFMGeometry& geometry) { +AnimSkeleton::AnimSkeleton(const HFMModel& model) { // convert to std::vector of joints std::vector joints; - joints.reserve(geometry.joints.size()); - for (auto& joint : geometry.joints) { + joints.reserve(model.joints.size()); + for (auto& joint : model.joints) { joints.push_back(joint); } buildSkeletonFromJoints(joints); diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 1717d75985..80353523d1 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -23,7 +23,7 @@ public: using Pointer = std::shared_ptr; using ConstPointer = std::shared_ptr; - explicit AnimSkeleton(const HFMGeometry& geometry); + explicit AnimSkeleton(const HFMModel& model); explicit AnimSkeleton(const std::vector& joints); int nameToJointIndex(const QString& jointName) const; const QString& getJointName(int jointIndex) const; diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index b5b27c3a24..06dfc0262a 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -69,14 +69,14 @@ void AnimationReader::run() { if (urlValid) { // Parse the FBX directly from the QNetworkReply - HFMGeometry::Pointer fbxgeo; + HFMModel::Pointer hfmModel; if (_url.path().toLower().endsWith(".fbx")) { - fbxgeo.reset(readFBX(_data, QVariantHash(), _url.path())); + hfmModel.reset(readFBX(_data, QVariantHash(), _url.path())); } else { QString errorStr("usupported format"); emit onError(299, errorStr); } - emit onSuccess(fbxgeo); + emit onSuccess(hfmModel); } else { throw QString("url is invalid"); } @@ -88,7 +88,7 @@ void AnimationReader::run() { } bool Animation::isLoaded() const { - return _loaded && _geometry; + return _loaded && _hfmModel; } QStringList Animation::getJointNames() const { @@ -99,8 +99,8 @@ QStringList Animation::getJointNames() const { return result; } QStringList names; - if (_geometry) { - foreach (const HFMJoint& joint, _geometry->joints) { + if (_hfmModel) { + foreach (const HFMJoint& joint, _hfmModel->joints) { names.append(joint.name); } } @@ -114,30 +114,30 @@ QVector Animation::getFrames() const { Q_RETURN_ARG(QVector, result)); return result; } - if (_geometry) { - return _geometry->animationFrames; + if (_hfmModel) { + return _hfmModel->animationFrames; } else { return QVector(); } } const QVector& Animation::getFramesReference() const { - return _geometry->animationFrames; + return _hfmModel->animationFrames; } void Animation::downloadFinished(const QByteArray& data) { // parse the animation/fbx file on a background thread. AnimationReader* animationReader = new AnimationReader(_url, data); - connect(animationReader, SIGNAL(onSuccess(HFMGeometry::Pointer)), SLOT(animationParseSuccess(HFMGeometry::Pointer))); + connect(animationReader, SIGNAL(onSuccess(HFMModel::Pointer)), SLOT(animationParseSuccess(HFMModel::Pointer))); connect(animationReader, SIGNAL(onError(int, QString)), SLOT(animationParseError(int, QString))); QThreadPool::globalInstance()->start(animationReader); } -void Animation::animationParseSuccess(HFMGeometry::Pointer geometry) { +void Animation::animationParseSuccess(HFMModel::Pointer hfmModel) { qCDebug(animation) << "Animation parse success" << _url.toDisplayString(); - _geometry = geometry; + _hfmModel = hfmModel; finishedLoading(true); } diff --git a/libraries/animation/src/AnimationCache.h b/libraries/animation/src/AnimationCache.h index 302f23a4e7..4423e8f18d 100644 --- a/libraries/animation/src/AnimationCache.h +++ b/libraries/animation/src/AnimationCache.h @@ -66,7 +66,7 @@ public: QString getType() const override { return "Animation"; } - const HFMGeometry& getGeometry() const { return *_geometry; } + const HFMModel& getHFMModel() const { return *_hfmModel; } virtual bool isLoaded() const override; @@ -88,12 +88,12 @@ protected: virtual void downloadFinished(const QByteArray& data) override; protected slots: - void animationParseSuccess(HFMGeometry::Pointer geometry); + void animationParseSuccess(HFMModel::Pointer hfmModel); void animationParseError(int error, QString str); private: - HFMGeometry::Pointer _geometry; + HFMModel::Pointer _hfmModel; }; /// Reads geometry in a worker thread. @@ -105,7 +105,7 @@ public: virtual void run() override; signals: - void onSuccess(HFMGeometry::Pointer geometry); + void onSuccess(HFMModel::Pointer hfmModel); void onError(int error, QString str); private: diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 2641be92da..c2f909dd24 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -260,14 +260,14 @@ void Rig::destroyAnimGraph() { _rightEyeJointChildren.clear(); } -void Rig::initJointStates(const HFMGeometry& geometry, const glm::mat4& modelOffset) { - _geometryOffset = AnimPose(geometry.offset); +void Rig::initJointStates(const HFMModel& hfmModel, const glm::mat4& modelOffset) { + _geometryOffset = AnimPose(hfmModel.offset); _invGeometryOffset = _geometryOffset.inverse(); - _geometryToRigTransform = modelOffset * geometry.offset; + _geometryToRigTransform = modelOffset * hfmModel.offset; _rigToGeometryTransform = glm::inverse(_geometryToRigTransform); setModelOffset(modelOffset); - _animSkeleton = std::make_shared(geometry); + _animSkeleton = std::make_shared(hfmModel); _internalPoseSet._relativePoses.clear(); _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); @@ -293,24 +293,24 @@ void Rig::initJointStates(const HFMGeometry& geometry, const glm::mat4& modelOff buildAbsoluteRigPoses(_animSkeleton->getRelativeDefaultPoses(), _absoluteDefaultPoses); - _rootJointIndex = geometry.rootJointIndex; - _leftEyeJointIndex = geometry.leftEyeJointIndex; - _rightEyeJointIndex = geometry.rightEyeJointIndex; - _leftHandJointIndex = geometry.leftHandJointIndex; - _leftElbowJointIndex = _leftHandJointIndex >= 0 ? geometry.joints.at(_leftHandJointIndex).parentIndex : -1; - _leftShoulderJointIndex = _leftElbowJointIndex >= 0 ? geometry.joints.at(_leftElbowJointIndex).parentIndex : -1; - _rightHandJointIndex = geometry.rightHandJointIndex; - _rightElbowJointIndex = _rightHandJointIndex >= 0 ? geometry.joints.at(_rightHandJointIndex).parentIndex : -1; - _rightShoulderJointIndex = _rightElbowJointIndex >= 0 ? geometry.joints.at(_rightElbowJointIndex).parentIndex : -1; + _rootJointIndex = hfmModel.rootJointIndex; + _leftEyeJointIndex = hfmModel.leftEyeJointIndex; + _rightEyeJointIndex = hfmModel.rightEyeJointIndex; + _leftHandJointIndex = hfmModel.leftHandJointIndex; + _leftElbowJointIndex = _leftHandJointIndex >= 0 ? hfmModel.joints.at(_leftHandJointIndex).parentIndex : -1; + _leftShoulderJointIndex = _leftElbowJointIndex >= 0 ? hfmModel.joints.at(_leftElbowJointIndex).parentIndex : -1; + _rightHandJointIndex = hfmModel.rightHandJointIndex; + _rightElbowJointIndex = _rightHandJointIndex >= 0 ? hfmModel.joints.at(_rightHandJointIndex).parentIndex : -1; + _rightShoulderJointIndex = _rightElbowJointIndex >= 0 ? hfmModel.joints.at(_rightElbowJointIndex).parentIndex : -1; - _leftEyeJointChildren = _animSkeleton->getChildrenOfJoint(geometry.leftEyeJointIndex); - _rightEyeJointChildren = _animSkeleton->getChildrenOfJoint(geometry.rightEyeJointIndex); + _leftEyeJointChildren = _animSkeleton->getChildrenOfJoint(hfmModel.leftEyeJointIndex); + _rightEyeJointChildren = _animSkeleton->getChildrenOfJoint(hfmModel.rightEyeJointIndex); } -void Rig::reset(const HFMGeometry& geometry) { - _geometryOffset = AnimPose(geometry.offset); +void Rig::reset(const HFMModel& hfmModel) { + _geometryOffset = AnimPose(hfmModel.offset); _invGeometryOffset = _geometryOffset.inverse(); - _animSkeleton = std::make_shared(geometry); + _animSkeleton = std::make_shared(hfmModel); _internalPoseSet._relativePoses.clear(); _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); @@ -338,18 +338,18 @@ void Rig::reset(const HFMGeometry& geometry) { buildAbsoluteRigPoses(_animSkeleton->getRelativeDefaultPoses(), _absoluteDefaultPoses); - _rootJointIndex = geometry.rootJointIndex; - _leftEyeJointIndex = geometry.leftEyeJointIndex; - _rightEyeJointIndex = geometry.rightEyeJointIndex; - _leftHandJointIndex = geometry.leftHandJointIndex; - _leftElbowJointIndex = _leftHandJointIndex >= 0 ? geometry.joints.at(_leftHandJointIndex).parentIndex : -1; - _leftShoulderJointIndex = _leftElbowJointIndex >= 0 ? geometry.joints.at(_leftElbowJointIndex).parentIndex : -1; - _rightHandJointIndex = geometry.rightHandJointIndex; - _rightElbowJointIndex = _rightHandJointIndex >= 0 ? geometry.joints.at(_rightHandJointIndex).parentIndex : -1; - _rightShoulderJointIndex = _rightElbowJointIndex >= 0 ? geometry.joints.at(_rightElbowJointIndex).parentIndex : -1; + _rootJointIndex = hfmModel.rootJointIndex; + _leftEyeJointIndex = hfmModel.leftEyeJointIndex; + _rightEyeJointIndex = hfmModel.rightEyeJointIndex; + _leftHandJointIndex = hfmModel.leftHandJointIndex; + _leftElbowJointIndex = _leftHandJointIndex >= 0 ? hfmModel.joints.at(_leftHandJointIndex).parentIndex : -1; + _leftShoulderJointIndex = _leftElbowJointIndex >= 0 ? hfmModel.joints.at(_leftElbowJointIndex).parentIndex : -1; + _rightHandJointIndex = hfmModel.rightHandJointIndex; + _rightElbowJointIndex = _rightHandJointIndex >= 0 ? hfmModel.joints.at(_rightHandJointIndex).parentIndex : -1; + _rightShoulderJointIndex = _rightElbowJointIndex >= 0 ? hfmModel.joints.at(_rightElbowJointIndex).parentIndex : -1; - _leftEyeJointChildren = _animSkeleton->getChildrenOfJoint(geometry.leftEyeJointIndex); - _rightEyeJointChildren = _animSkeleton->getChildrenOfJoint(geometry.rightEyeJointIndex); + _leftEyeJointChildren = _animSkeleton->getChildrenOfJoint(hfmModel.leftEyeJointIndex); + _rightEyeJointChildren = _animSkeleton->getChildrenOfJoint(hfmModel.rightEyeJointIndex); if (!_animGraphURL.isEmpty()) { _animNode.reset(); @@ -1938,7 +1938,7 @@ void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { data.rotationIsDefaultPose = isEqual(data.rotation, defaultAbsRot); // translations are in relative frame but scaled so that they are in meters, - // instead of geometry units. + // instead of model units. glm::vec3 defaultRelTrans = _geometryOffset.scale() * _animSkeleton->getRelativeDefaultPose(i).trans(); data.translation = _geometryOffset.scale() * (!_sendNetworkNode ? _internalPoseSet._relativePoses[i].trans() : _networkPoseSet._relativePoses[i].trans()); data.translationIsDefaultPose = isEqual(data.translation, defaultRelTrans); @@ -1963,7 +1963,7 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { return; } - // make a vector of rotations in absolute-geometry-frame + // make a vector of rotations in absolute-model-frame std::vector rotations; rotations.reserve(numJoints); const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform)); @@ -1972,7 +1972,7 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { if (data.rotationIsDefaultPose) { rotations.push_back(absoluteDefaultPoses[i].rot()); } else { - // JointData rotations are in absolute rig-frame so we rotate them to absolute geometry-frame + // JointData rotations are in absolute rig-frame so we rotate them to absolute model-frame rotations.push_back(rigToGeometryRot * data.rotation); } } @@ -2008,7 +2008,7 @@ void Rig::computeExternalPoses(const glm::mat4& modelOffsetMat) { } void Rig::computeAvatarBoundingCapsule( - const HFMGeometry& geometry, + const HFMModel& hfmModel, float& radiusOut, float& heightOut, glm::vec3& localOffsetOut) const { @@ -2041,7 +2041,7 @@ void Rig::computeAvatarBoundingCapsule( // from the head to the hips when computing the rest of the bounding capsule. int index = indexOfJoint("Head"); while (index != -1) { - const HFMJointShapeInfo& shapeInfo = geometry.joints.at(index).shapeInfo; + const HFMJointShapeInfo& shapeInfo = hfmModel.joints.at(index).shapeInfo; AnimPose pose = _animSkeleton->getAbsoluteDefaultPose(index); if (shapeInfo.points.size() > 0) { for (auto& point : shapeInfo.points) { diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 61e8672972..345f335f88 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -122,8 +122,8 @@ public: void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); void restoreRoleAnimation(const QString& role); - void initJointStates(const HFMGeometry& geometry, const glm::mat4& modelOffset); - void reset(const HFMGeometry& geometry); + void initJointStates(const HFMModel& hfmModel, const glm::mat4& modelOffset); + void reset(const HFMModel& hfmModel); bool jointStatesEmpty(); int getJointStateCount() const; int indexOfJoint(const QString& jointName) const; @@ -210,7 +210,7 @@ public: void copyJointsFromJointData(const QVector& jointDataVec); void computeExternalPoses(const glm::mat4& modelOffsetMat); - void computeAvatarBoundingCapsule(const HFMGeometry& geometry, float& radiusOut, float& heightOut, glm::vec3& offsetOut) const; + void computeAvatarBoundingCapsule(const HFMModel& hfmModel, float& radiusOut, float& heightOut, glm::vec3& offsetOut) const; void setEnableInverseKinematics(bool enable); void setEnableAnimations(bool enable); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index b8448e389d..78f994f462 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1311,7 +1311,7 @@ glm::quat Avatar::getAbsoluteJointRotationInObjectFrame(int index) const { case CAMERA_MATRIX_INDEX: { glm::quat rotation; if (_skeletonModel && _skeletonModel->isActive()) { - int headJointIndex = _skeletonModel->getHFMGeometry().headJointIndex; + int headJointIndex = _skeletonModel->getHFMModel().headJointIndex; if (headJointIndex >= 0) { _skeletonModel->getAbsoluteJointRotationInRigFrame(headJointIndex, rotation); } @@ -1360,7 +1360,7 @@ glm::vec3 Avatar::getAbsoluteJointTranslationInObjectFrame(int index) const { case CAMERA_MATRIX_INDEX: { glm::vec3 translation; if (_skeletonModel && _skeletonModel->isActive()) { - int headJointIndex = _skeletonModel->getHFMGeometry().headJointIndex; + int headJointIndex = _skeletonModel->getHFMModel().headJointIndex; if (headJointIndex >= 0) { _skeletonModel->getAbsoluteJointTranslationInRigFrame(headJointIndex, translation); } @@ -1416,7 +1416,7 @@ void Avatar::withValidJointIndicesCache(std::function const& worker) con if (!_modelJointsCached) { _modelJointIndicesCache.clear(); if (_skeletonModel && _skeletonModel->isActive()) { - _modelJointIndicesCache = _skeletonModel->getHFMGeometry().jointIndices; + _modelJointIndicesCache = _skeletonModel->getHFMModel().jointIndices; _modelJointsCached = true; } } diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index a41cff528b..13ee5854bf 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -54,9 +54,9 @@ void SkeletonModel::setTextures(const QVariantMap& textures) { } void SkeletonModel::initJointStates() { - const HFMGeometry& geometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); glm::mat4 modelOffset = glm::scale(_scale) * glm::translate(_offset); - _rig.initJointStates(geometry, modelOffset); + _rig.initJointStates(hfmModel, modelOffset); { // initialize _jointData with proper values for default joints @@ -66,7 +66,7 @@ void SkeletonModel::initJointStates() { } // Determine the default eye position for avatar scale = 1.0 - int headJointIndex = geometry.headJointIndex; + int headJointIndex = hfmModel.headJointIndex; if (0 > headJointIndex || headJointIndex >= _rig.getJointStateCount()) { qCWarning(avatars_renderer) << "Bad head joint! Got:" << headJointIndex << "jointCount:" << _rig.getJointStateCount(); } @@ -74,7 +74,7 @@ void SkeletonModel::initJointStates() { getEyeModelPositions(leftEyePosition, rightEyePosition); glm::vec3 midEyePosition = (leftEyePosition + rightEyePosition) / 2.0f; - int rootJointIndex = geometry.rootJointIndex; + int rootJointIndex = hfmModel.rootJointIndex; glm::vec3 rootModelPosition; getJointPosition(rootJointIndex, rootModelPosition); @@ -96,7 +96,7 @@ void SkeletonModel::initJointStates() { // Called within Model::simulate call, below. void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { assert(!_owningAvatar->isMyAvatar()); - const HFMGeometry& geometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); Head* head = _owningAvatar->getHead(); @@ -124,7 +124,7 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { // If the head is not positioned, updateEyeJoints won't get the math right glm::quat headOrientation; - _rig.getJointRotation(geometry.headJointIndex, headOrientation); + _rig.getJointRotation(hfmModel.headJointIndex, headOrientation); glm::vec3 eulers = safeEulerAngles(headOrientation); head->setBasePitch(glm::degrees(-eulers.x)); head->setBaseYaw(glm::degrees(eulers.y)); @@ -135,8 +135,8 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { eyeParams.eyeSaccade = glm::vec3(0.0f); eyeParams.modelRotation = getRotation(); eyeParams.modelTranslation = getTranslation(); - eyeParams.leftEyeJointIndex = geometry.leftEyeJointIndex; - eyeParams.rightEyeJointIndex = geometry.rightEyeJointIndex; + eyeParams.leftEyeJointIndex = hfmModel.leftEyeJointIndex; + eyeParams.rightEyeJointIndex = hfmModel.rightEyeJointIndex; _rig.updateFromEyeParameters(eyeParams); } @@ -259,45 +259,45 @@ bool SkeletonModel::getRightShoulderPosition(glm::vec3& position) const { } bool SkeletonModel::getHeadPosition(glm::vec3& headPosition) const { - return isActive() && getJointPositionInWorldFrame(getHFMGeometry().headJointIndex, headPosition); + return isActive() && getJointPositionInWorldFrame(getHFMModel().headJointIndex, headPosition); } bool SkeletonModel::getNeckPosition(glm::vec3& neckPosition) const { - return isActive() && getJointPositionInWorldFrame(getHFMGeometry().neckJointIndex, neckPosition); + return isActive() && getJointPositionInWorldFrame(getHFMModel().neckJointIndex, neckPosition); } bool SkeletonModel::getLocalNeckPosition(glm::vec3& neckPosition) const { - return isActive() && getJointPosition(getHFMGeometry().neckJointIndex, neckPosition); + return isActive() && getJointPosition(getHFMModel().neckJointIndex, neckPosition); } bool SkeletonModel::getEyeModelPositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const { if (!isActive()) { return false; } - const HFMGeometry& geometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); - if (getJointPosition(geometry.leftEyeJointIndex, firstEyePosition) && - getJointPosition(geometry.rightEyeJointIndex, secondEyePosition)) { + if (getJointPosition(hfmModel.leftEyeJointIndex, firstEyePosition) && + getJointPosition(hfmModel.rightEyeJointIndex, secondEyePosition)) { return true; } // no eye joints; try to estimate based on head/neck joints glm::vec3 neckPosition, headPosition; - if (getJointPosition(geometry.neckJointIndex, neckPosition) && - getJointPosition(geometry.headJointIndex, headPosition)) { + if (getJointPosition(hfmModel.neckJointIndex, neckPosition) && + getJointPosition(hfmModel.headJointIndex, headPosition)) { const float EYE_PROPORTION = 0.6f; glm::vec3 baseEyePosition = glm::mix(neckPosition, headPosition, EYE_PROPORTION); glm::quat headRotation; - getJointRotation(geometry.headJointIndex, headRotation); + getJointRotation(hfmModel.headJointIndex, headRotation); const float EYES_FORWARD = 0.25f; const float EYE_SEPARATION = 0.1f; float headHeight = glm::distance(neckPosition, headPosition); firstEyePosition = baseEyePosition + headRotation * glm::vec3(EYE_SEPARATION, 0.0f, EYES_FORWARD) * headHeight; secondEyePosition = baseEyePosition + headRotation * glm::vec3(-EYE_SEPARATION, 0.0f, EYES_FORWARD) * headHeight; return true; - } else if (getJointPosition(geometry.headJointIndex, headPosition)) { + } else if (getJointPosition(hfmModel.headJointIndex, headPosition)) { glm::vec3 baseEyePosition = headPosition; glm::quat headRotation; - getJointRotation(geometry.headJointIndex, headRotation); + getJointRotation(hfmModel.headJointIndex, headRotation); const float EYES_FORWARD_HEAD_ONLY = 0.30f; const float EYE_SEPARATION = 0.1f; firstEyePosition = baseEyePosition + headRotation * glm::vec3(EYE_SEPARATION, 0.0f, EYES_FORWARD_HEAD_ONLY); @@ -330,15 +330,15 @@ void SkeletonModel::computeBoundingShape() { return; } - const HFMGeometry& geometry = getHFMGeometry(); - if (geometry.joints.isEmpty() || geometry.rootJointIndex == -1) { + const HFMModel& model = getHFMModel(); + if (model.joints.isEmpty() || model.rootJointIndex == -1) { // rootJointIndex == -1 if the avatar model has no skeleton return; } float radius, height; glm::vec3 offset; - _rig.computeAvatarBoundingCapsule(geometry, radius, height, offset); + _rig.computeAvatarBoundingCapsule(model, radius, height, offset); float invScale = 1.0f / _owningAvatar->getModelScale(); _boundingCapsuleRadius = invScale * radius; _boundingCapsuleHeight = invScale * height; @@ -369,7 +369,7 @@ void SkeletonModel::renderBoundingCollisionShapes(RenderArgs* args, gpu::Batch& } bool SkeletonModel::hasSkeleton() { - return isActive() ? getHFMGeometry().rootJointIndex != -1 : false; + return isActive() ? getHFMModel().rootJointIndex != -1 : false; } void SkeletonModel::onInvalidate() { diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h index 6c533a5941..c53cf8d333 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h @@ -41,10 +41,10 @@ public: void updateAttitude(const glm::quat& orientation); /// Returns the index of the left hand joint, or -1 if not found. - int getLeftHandJointIndex() const { return isActive() ? getHFMGeometry().leftHandJointIndex : -1; } + int getLeftHandJointIndex() const { return isActive() ? getHFMModel().leftHandJointIndex : -1; } /// Returns the index of the right hand joint, or -1 if not found. - int getRightHandJointIndex() const { return isActive() ? getHFMGeometry().rightHandJointIndex : -1; } + int getRightHandJointIndex() const { return isActive() ? getHFMModel().rightHandJointIndex : -1; } bool getLeftGrabPosition(glm::vec3& position) const; bool getRightGrabPosition(glm::vec3& position) const; diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index 0b76c275d4..9898651268 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -206,7 +206,7 @@ void FBXBaker::importScene() { } #endif - _geometry = reader.extractHFMGeometry({}, _modelURL.toString()); + _model = reader.extractHFMModel({}, _modelURL.toString()); _textureContentMap = reader._textureContent; } @@ -231,7 +231,7 @@ void FBXBaker::rewriteAndBakeSceneModels() { for (FBXNode& objectChild : rootChild.children) { if (objectChild.name == "Geometry") { - // TODO Pull this out of _geometry instead so we don't have to reprocess it + // TODO Pull this out of _model instead so we don't have to reprocess it auto extractedMesh = FBXReader::extractMesh(objectChild, meshIndex, false); // Callback to get MaterialID @@ -293,7 +293,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { QHash textureTypes; // enumerate the materials in the extracted geometry so we can determine the texture type for each texture ID - for (const auto& material : _geometry->materials) { + for (const auto& material : _model->materials) { if (material.normalTexture.isBumpmap) { textureTypes[material.normalTexture.id] = BUMP_TEXTURE; } else { diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 8edaf91c79..8dfde12b31 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -53,7 +53,7 @@ private: void rewriteAndBakeSceneModels(); void rewriteAndBakeSceneTextures(); - HFMGeometry* _geometry; + HFMModel* _model; QHash _textureNameMatchCount; QHash _remappedTexturePaths; diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index e9130e3fbd..67bd2d5d62 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -153,7 +153,7 @@ void OBJBaker::bakeOBJ() { checkIfTexturesFinished(); } -void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMGeometry& geometry) { +void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& model) { // Generating FBX Header Node FBXNode headerNode; headerNode.name = FBX_HEADER_EXTENSION; @@ -199,7 +199,7 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMGeometry& geometry) { // Compress the mesh information and store in dracoNode bool hasDeformers = false; // No concept of deformers for an OBJ FBXNode dracoNode; - compressMesh(geometry.meshes[0], hasDeformers, dracoNode); + compressMesh(model.meshes[0], hasDeformers, dracoNode); geometryNode.children.append(dracoNode); // Generating Object node's child - Model node @@ -214,17 +214,17 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMGeometry& geometry) { objectNode.children = { geometryNode, modelNode }; // Generating Objects node's child - Material node - auto& meshParts = geometry.meshes[0].parts; + auto& meshParts = model.meshes[0].parts; for (auto& meshPart : meshParts) { FBXNode materialNode; materialNode.name = MATERIAL_NODE_NAME; - if (geometry.materials.size() == 1) { + if (model.materials.size() == 1) { // case when no material information is provided, OBJReader considers it as a single default material - for (auto& materialID : geometry.materials.keys()) { - setMaterialNodeProperties(materialNode, materialID, geometry); + for (auto& materialID : model.materials.keys()) { + setMaterialNodeProperties(materialNode, materialID, model); } } else { - setMaterialNodeProperties(materialNode, meshPart.materialID, geometry); + setMaterialNodeProperties(materialNode, meshPart.materialID, model); } objectNode.children.append(materialNode); @@ -235,7 +235,7 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMGeometry& geometry) { auto size = meshParts.size(); for (int i = 0; i < size; i++) { QString material = meshParts[i].materialID; - HFMMaterial currentMaterial = geometry.materials[material]; + HFMMaterial currentMaterial = model.materials[material]; if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) { auto textureID = nextNodeID(); _mapTextureMaterial.emplace_back(textureID, i); @@ -325,12 +325,12 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMGeometry& geometry) { } // Set properties for material nodes -void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMGeometry& geometry) { +void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& model) { auto materialID = nextNodeID(); _materialIDs.push_back(materialID); materialNode.properties = { materialID, material, MESH }; - HFMMaterial currentMaterial = geometry.materials[material]; + HFMMaterial currentMaterial = model.materials[material]; // Setting the hierarchy: Material -> Properties70 -> P -> Properties FBXNode properties70Node; diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index 875a500129..f889730ffa 100644 --- a/libraries/baking/src/OBJBaker.h +++ b/libraries/baking/src/OBJBaker.h @@ -39,8 +39,8 @@ private slots: private: void loadOBJ(); - void createFBXNodeTree(FBXNode& rootNode, HFMGeometry& geometry); - void setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMGeometry& geometry); + void createFBXNodeTree(FBXNode& rootNode, HFMModel& model); + void setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& model); NodeID nextNodeID() { return _nodeID++; } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index c36f60600f..f60bf20e3d 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -268,7 +268,7 @@ EntityItemProperties RenderableModelEntityItem::getProperties(const EntityProper if (model->isLoaded()) { // TODO: improve naturalDimensions in the future, // for now we've added this hack for setting natural dimensions of models - Extents meshExtents = model->getHFMGeometry().getUnscaledMeshExtents(); + Extents meshExtents = model->getHFMModel().getUnscaledMeshExtents(); properties.setNaturalDimensions(meshExtents.maximum - meshExtents.minimum); properties.calculateNaturalPosition(meshExtents.minimum, meshExtents.maximum); } @@ -403,7 +403,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // should never fall in here when collision model not fully loaded // TODO: assert that all geometries exist and are loaded //assert(_model && _model->isLoaded() && _compoundShapeResource && _compoundShapeResource->isLoaded()); - const HFMGeometry& collisionGeometry = _compoundShapeResource->getHFMGeometry(); + const HFMModel& collisionGeometry = _compoundShapeResource->getHFMModel(); ShapeInfo::PointCollection& pointCollection = shapeInfo.getPointCollection(); pointCollection.clear(); @@ -478,7 +478,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // to the visual model and apply them to the collision model (without regard for the // collision model's extents). - glm::vec3 scaleToFit = dimensions / model->getHFMGeometry().getUnscaledMeshExtents().size(); + glm::vec3 scaleToFit = dimensions / model->getHFMModel().getUnscaledMeshExtents().size(); // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. glm::vec3 registrationOffset = dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint()); @@ -498,12 +498,12 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // compute meshPart local transforms QVector localTransforms; - const HFMGeometry& hfmGeometry = model->getHFMGeometry(); - int numHFMMeshes = hfmGeometry.meshes.size(); + const HFMModel& hfmModel = model->getHFMModel(); + int numHFMMeshes = hfmModel.meshes.size(); int totalNumVertices = 0; glm::mat4 invRegistraionOffset = glm::translate(dimensions * (getRegistrationPoint() - ENTITY_ITEM_DEFAULT_REGISTRATION_POINT)); for (int i = 0; i < numHFMMeshes; i++) { - const HFMMesh& mesh = hfmGeometry.meshes.at(i); + const HFMMesh& mesh = hfmModel.meshes.at(i); if (mesh.clusters.size() > 0) { const HFMCluster& cluster = mesh.clusters.at(0); auto jointMatrix = model->getRig().getJointTransform(cluster.jointIndex); @@ -524,7 +524,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { std::vector> meshes; if (type == SHAPE_TYPE_SIMPLE_COMPOUND) { - auto& hfmMeshes = _compoundShapeResource->getHFMGeometry().meshes; + auto& hfmMeshes = _compoundShapeResource->getHFMModel().meshes; meshes.reserve(hfmMeshes.size()); for (auto& hfmMesh : hfmMeshes) { meshes.push_back(hfmMesh._mesh); @@ -755,7 +755,7 @@ int RenderableModelEntityItem::avatarJointIndex(int modelJointIndex) { bool RenderableModelEntityItem::contains(const glm::vec3& point) const { auto model = getModel(); if (EntityItem::contains(point) && model && _compoundShapeResource && _compoundShapeResource->isLoaded()) { - return _compoundShapeResource->getHFMGeometry().convexHullContains(worldToEntity(point)); + return _compoundShapeResource->getHFMModel().convexHullContains(worldToEntity(point)); } return false; @@ -1159,11 +1159,11 @@ void ModelEntityRenderer::animate(const TypedEntityPointer& entity) { return; } - QStringList animationJointNames = _animation->getGeometry().getJointNames(); - auto& hfmJoints = _animation->getGeometry().joints; + QStringList animationJointNames = _animation->getHFMModel().getJointNames(); + auto& hfmJoints = _animation->getHFMModel().joints; - auto& originalHFMJoints = _model->getHFMGeometry().joints; - auto& originalHFMIndices = _model->getHFMGeometry().jointIndices; + auto& originalHFMJoints = _model->getHFMModel().joints; + auto& originalHFMIndices = _model->getHFMModel().jointIndices; bool allowTranslation = entity->getAnimationAllowTranslation(); diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index 8051dbafea..84caa98ace 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -231,7 +231,7 @@ public: bool needTangentSpace() const; }; -/// A single mesh (with optional blendshapes) extracted from an FBX document. +/// A single mesh (with optional blendshapes). class HFMMesh { public: @@ -277,7 +277,7 @@ public: * @property {Quat[]} rotations * @property {Vec3[]} translations */ -/// A single animation frame extracted from an FBX document. +/// A single animation frame. class HFMAnimationFrame { public: QVector rotations; @@ -305,10 +305,10 @@ public: Q_DECLARE_METATYPE(HFMAnimationFrame) Q_DECLARE_METATYPE(QVector) -/// A set of meshes extracted from an FBX document. -class HFMGeometry { +/// The runtime model format. +class HFMModel { public: - using Pointer = std::shared_ptr; + using Pointer = std::shared_ptr; QString originalURL; QString author; @@ -368,7 +368,7 @@ public: QList blendshapeChannelNames; }; -Q_DECLARE_METATYPE(HFMGeometry) -Q_DECLARE_METATYPE(HFMGeometry::Pointer) +Q_DECLARE_METATYPE(HFMModel) +Q_DECLARE_METATYPE(HFMModel::Pointer) #endif // hifi_FBX_h_ diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index df6abbfdf2..15ba5f0101 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -40,9 +40,9 @@ using namespace std; -int HFMGeometryPointerMetaTypeId = qRegisterMetaType(); +int HFMModelPointerMetaTypeId = qRegisterMetaType(); -QStringList HFMGeometry::getJointNames() const { +QStringList HFMModel::getJointNames() const { QStringList names; foreach (const HFMJoint& joint, joints) { names.append(joint.name); @@ -50,7 +50,7 @@ QStringList HFMGeometry::getJointNames() const { return names; } -bool HFMGeometry::hasBlendedMeshes() const { +bool HFMModel::hasBlendedMeshes() const { if (!meshes.isEmpty()) { foreach (const HFMMesh& mesh, meshes) { if (!mesh.blendshapes.isEmpty()) { @@ -61,7 +61,7 @@ bool HFMGeometry::hasBlendedMeshes() const { return false; } -Extents HFMGeometry::getUnscaledMeshExtents() const { +Extents HFMModel::getUnscaledMeshExtents() const { const Extents& extents = meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which @@ -74,7 +74,7 @@ Extents HFMGeometry::getUnscaledMeshExtents() const { } // TODO: Move to graphics::Mesh when Sam's ready -bool HFMGeometry::convexHullContains(const glm::vec3& point) const { +bool HFMModel::convexHullContains(const glm::vec3& point) const { if (!getUnscaledMeshExtents().containsPoint(point)) { return false; } @@ -124,16 +124,16 @@ bool HFMGeometry::convexHullContains(const glm::vec3& point) const { return false; } -QString HFMGeometry::getModelNameOfMesh(int meshIndex) const { +QString HFMModel::getModelNameOfMesh(int meshIndex) const { if (meshIndicesToModelNames.contains(meshIndex)) { return meshIndicesToModelNames.value(meshIndex); } return QString(); } -int hfmGeometryMetaTypeId = qRegisterMetaType(); +int hfmModelMetaTypeId = qRegisterMetaType(); int hfmAnimationFrameMetaTypeId = qRegisterMetaType(); -int hfmAnimationFrameVectorMetaTypeId = qRegisterMetaType >(); +int hfmAnimationFrameVectorMetaTypeId = qRegisterMetaType>(); glm::vec3 parseVec3(const QString& string) { @@ -403,7 +403,7 @@ static void createTangents(const HFMMesh& mesh, bool generateFromTexCoords, setTangents(mesh, accessor, part.triangleIndices.at(i + 2), part.triangleIndices.at(i), vertices, normals, tangents); } if ((part.triangleIndices.size() % 3) != 0) { - qCDebug(modelformat) << "Error in extractHFMGeometry part.triangleIndices.size() is not divisible by three "; + qCDebug(modelformat) << "Error in extractHFMModel part.triangleIndices.size() is not divisible by three "; } } } @@ -615,7 +615,7 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { return filepath.mid(filepath.lastIndexOf('/') + 1); } -HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QString& url) { +HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; QMap meshes; QHash modelIDsToNames; @@ -624,7 +624,7 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS QVector blendshapes; - QHash models; + QHash fbxModels; QHash clusters; QHash animationCurves; @@ -689,10 +689,10 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS #if defined(DEBUG_FBXREADER) int unknown = 0; #endif - HFMGeometry* geometryPtr = new HFMGeometry; - HFMGeometry& geometry = *geometryPtr; + HFMModel* modelPtr = new HFMModel; + HFMModel& model = *modelPtr; - geometry.originalURL = url; + model.originalURL = url; float unitScaleFactor = 1.0f; glm::vec3 ambientColor; @@ -708,7 +708,7 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS if (subobject.name == "MetaData") { foreach (const FBXNode& subsubobject, subobject.children) { if (subsubobject.name == "Author") { - geometry.author = subsubobject.properties.at(0).toString(); + model.author = subsubobject.properties.at(0).toString(); } } } else if (subobject.name == "Properties70") { @@ -716,7 +716,7 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS static const QVariant APPLICATION_NAME = QVariant(QByteArray("Original|ApplicationName")); if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 && subsubobject.properties.at(0) == APPLICATION_NAME) { - geometry.applicationName = subsubobject.properties.at(4).toString(); + model.applicationName = subsubobject.properties.at(4).toString(); } } } @@ -814,7 +814,7 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS glm::vec3 geometricRotation; glm::vec3 rotationMin, rotationMax; - FBXModel model = { name, -1, glm::vec3(), glm::mat4(), glm::quat(), glm::quat(), glm::quat(), + FBXModel fbxModel = { name, -1, glm::vec3(), glm::mat4(), glm::quat(), glm::quat(), glm::quat(), glm::mat4(), glm::vec3(), glm::vec3(), false, glm::vec3(), glm::quat(), glm::vec3(1.0f) }; ExtractedMesh* mesh = NULL; @@ -963,27 +963,27 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS } // see FBX documentation, http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html - model.translation = translation; + fbxModel.translation = translation; - model.preTransform = glm::translate(rotationOffset) * glm::translate(rotationPivot); - model.preRotation = glm::quat(glm::radians(preRotation)); - model.rotation = glm::quat(glm::radians(rotation)); - model.postRotation = glm::inverse(glm::quat(glm::radians(postRotation))); - model.postTransform = glm::translate(-rotationPivot) * glm::translate(scaleOffset) * + fbxModel.preTransform = glm::translate(rotationOffset) * glm::translate(rotationPivot); + fbxModel.preRotation = glm::quat(glm::radians(preRotation)); + fbxModel.rotation = glm::quat(glm::radians(rotation)); + fbxModel.postRotation = glm::inverse(glm::quat(glm::radians(postRotation))); + fbxModel.postTransform = glm::translate(-rotationPivot) * glm::translate(scaleOffset) * glm::translate(scalePivot) * glm::scale(scale) * glm::translate(-scalePivot); // NOTE: angles from the FBX file are in degrees // so we convert them to radians for the FBXModel class - model.rotationMin = glm::radians(glm::vec3(rotationMinX ? rotationMin.x : -180.0f, + fbxModel.rotationMin = glm::radians(glm::vec3(rotationMinX ? rotationMin.x : -180.0f, rotationMinY ? rotationMin.y : -180.0f, rotationMinZ ? rotationMin.z : -180.0f)); - model.rotationMax = glm::radians(glm::vec3(rotationMaxX ? rotationMax.x : 180.0f, + fbxModel.rotationMax = glm::radians(glm::vec3(rotationMaxX ? rotationMax.x : 180.0f, rotationMaxY ? rotationMax.y : 180.0f, rotationMaxZ ? rotationMax.z : 180.0f)); - model.hasGeometricOffset = hasGeometricOffset; - model.geometricTranslation = geometricTranslation; - model.geometricRotation = glm::quat(glm::radians(geometricRotation)); - model.geometricScaling = geometricScaling; + fbxModel.hasGeometricOffset = hasGeometricOffset; + fbxModel.geometricTranslation = geometricTranslation; + fbxModel.geometricRotation = glm::quat(glm::radians(geometricRotation)); + fbxModel.geometricScaling = geometricScaling; - models.insert(getID(object.properties), model); + fbxModels.insert(getID(object.properties), fbxModel); } else if (object.name == "Texture") { TextureParam tex; foreach (const FBXNode& subobject, object.children) { @@ -1307,7 +1307,7 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS name = name.mid(name.lastIndexOf('.') + 1); } QString id = getID(object.properties); - geometry.blendshapeChannelNames << name; + model.blendshapeChannelNames << name; foreach (const WeightedIndex& index, blendshapeIndices.values(name)) { blendshapeChannelIndices.insert(id, index); } @@ -1454,26 +1454,26 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS float offsetScale = mapping.value("scale", 1.0f).toFloat() * unitScaleFactor * METERS_PER_CENTIMETER; glm::quat offsetRotation = glm::quat(glm::radians(glm::vec3(mapping.value("rx").toFloat(), mapping.value("ry").toFloat(), mapping.value("rz").toFloat()))); - geometry.offset = glm::translate(glm::vec3(mapping.value("tx").toFloat(), mapping.value("ty").toFloat(), + model.offset = glm::translate(glm::vec3(mapping.value("tx").toFloat(), mapping.value("ty").toFloat(), mapping.value("tz").toFloat())) * glm::mat4_cast(offsetRotation) * glm::scale(glm::vec3(offsetScale, offsetScale, offsetScale)); // get the list of models in depth-first traversal order QVector modelIDs; - QSet remainingModels; - for (QHash::const_iterator model = models.constBegin(); model != models.constEnd(); model++) { + QSet remainingFBXModels; + for (QHash::const_iterator fbxModel = fbxModels.constBegin(); fbxModel != fbxModels.constEnd(); fbxModel++) { // models with clusters must be parented to the cluster top // Unless the model is a root node. - bool isARootNode = !modelIDs.contains(_connectionParentMap.value(model.key())); + bool isARootNode = !modelIDs.contains(_connectionParentMap.value(fbxModel.key())); if (!isARootNode) { - foreach(const QString& deformerID, _connectionChildMap.values(model.key())) { + foreach(const QString& deformerID, _connectionChildMap.values(fbxModel.key())) { foreach(const QString& clusterID, _connectionChildMap.values(deformerID)) { if (!clusters.contains(clusterID)) { continue; } - QString topID = getTopModelID(_connectionParentMap, models, _connectionChildMap.value(clusterID), url); - _connectionChildMap.remove(_connectionParentMap.take(model.key()), model.key()); - _connectionParentMap.insert(model.key(), topID); + QString topID = getTopModelID(_connectionParentMap, fbxModels, _connectionChildMap.value(clusterID), url); + _connectionChildMap.remove(_connectionParentMap.take(fbxModel.key()), fbxModel.key()); + _connectionParentMap.insert(fbxModel.key(), topID); goto outerBreak; } } @@ -1481,21 +1481,21 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS } // make sure the parent is in the child map - QString parent = _connectionParentMap.value(model.key()); - if (!_connectionChildMap.contains(parent, model.key())) { - _connectionChildMap.insert(parent, model.key()); + QString parent = _connectionParentMap.value(fbxModel.key()); + if (!_connectionChildMap.contains(parent, fbxModel.key())) { + _connectionChildMap.insert(parent, fbxModel.key()); } - remainingModels.insert(model.key()); + remainingFBXModels.insert(fbxModel.key()); } - while (!remainingModels.isEmpty()) { - QString first = *remainingModels.constBegin(); - foreach (const QString& id, remainingModels) { + while (!remainingFBXModels.isEmpty()) { + QString first = *remainingFBXModels.constBegin(); + foreach (const QString& id, remainingFBXModels) { if (id < first) { first = id; } } - QString topID = getTopModelID(_connectionParentMap, models, first, url); - appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, models, remainingModels, modelIDs, true); + QString topID = getTopModelID(_connectionParentMap, fbxModels, first, url); + appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, fbxModels, remainingFBXModels, modelIDs, true); } // figure the number of animation frames from the curves @@ -1507,53 +1507,53 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS HFMAnimationFrame frame; frame.rotations.resize(modelIDs.size()); frame.translations.resize(modelIDs.size()); - geometry.animationFrames.append(frame); + model.animationFrames.append(frame); } // convert the models to joints QVariantList freeJoints = mapping.values("freeJoint"); - geometry.hasSkeletonJoints = false; + model.hasSkeletonJoints = false; foreach (const QString& modelID, modelIDs) { - const FBXModel& model = models[modelID]; + const FBXModel& fbxModel = fbxModels[modelID]; HFMJoint joint; - joint.isFree = freeJoints.contains(model.name); - joint.parentIndex = model.parentIndex; + joint.isFree = freeJoints.contains(fbxModel.name); + joint.parentIndex = fbxModel.parentIndex; // get the indices of all ancestors starting with the first free one (if any) - int jointIndex = geometry.joints.size(); + int jointIndex = model.joints.size(); joint.freeLineage.append(jointIndex); int lastFreeIndex = joint.isFree ? 0 : -1; - for (int index = joint.parentIndex; index != -1; index = geometry.joints.at(index).parentIndex) { - if (geometry.joints.at(index).isFree) { + for (int index = joint.parentIndex; index != -1; index = model.joints.at(index).parentIndex) { + if (model.joints.at(index).isFree) { lastFreeIndex = joint.freeLineage.size(); } joint.freeLineage.append(index); } joint.freeLineage.remove(lastFreeIndex + 1, joint.freeLineage.size() - lastFreeIndex - 1); - joint.translation = model.translation; // these are usually in centimeters - joint.preTransform = model.preTransform; - joint.preRotation = model.preRotation; - joint.rotation = model.rotation; - joint.postRotation = model.postRotation; - joint.postTransform = model.postTransform; - joint.rotationMin = model.rotationMin; - joint.rotationMax = model.rotationMax; + joint.translation = fbxModel.translation; // these are usually in centimeters + joint.preTransform = fbxModel.preTransform; + joint.preRotation = fbxModel.preRotation; + joint.rotation = fbxModel.rotation; + joint.postRotation = fbxModel.postRotation; + joint.postTransform = fbxModel.postTransform; + joint.rotationMin = fbxModel.rotationMin; + joint.rotationMax = fbxModel.rotationMax; - joint.hasGeometricOffset = model.hasGeometricOffset; - joint.geometricTranslation = model.geometricTranslation; - joint.geometricRotation = model.geometricRotation; - joint.geometricScaling = model.geometricScaling; + joint.hasGeometricOffset = fbxModel.hasGeometricOffset; + joint.geometricTranslation = fbxModel.geometricTranslation; + joint.geometricRotation = fbxModel.geometricRotation; + joint.geometricScaling = fbxModel.geometricScaling; glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation; if (joint.parentIndex == -1) { - joint.transform = geometry.offset * glm::translate(joint.translation) * joint.preTransform * + joint.transform = model.offset * glm::translate(joint.translation) * joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform; joint.inverseDefaultRotation = glm::inverse(combinedRotation); joint.distanceToParent = 0.0f; } else { - const HFMJoint& parentJoint = geometry.joints.at(joint.parentIndex); + const HFMJoint& parentJoint = model.joints.at(joint.parentIndex); joint.transform = parentJoint.transform * glm::translate(joint.translation) * joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform; joint.inverseDefaultRotation = glm::inverse(combinedRotation) * parentJoint.inverseDefaultRotation; @@ -1561,20 +1561,20 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS extractTranslation(joint.transform)); } joint.inverseBindRotation = joint.inverseDefaultRotation; - joint.name = model.name; + joint.name = fbxModel.name; foreach (const QString& childID, _connectionChildMap.values(modelID)) { QString type = typeFlags.value(childID); if (!type.isEmpty()) { - geometry.hasSkeletonJoints |= (joint.isSkeletonJoint = type.toLower().contains("Skeleton")); + model.hasSkeletonJoints |= (joint.isSkeletonJoint = type.toLower().contains("Skeleton")); break; } } joint.bindTransformFoundInCluster = false; - geometry.joints.append(joint); - geometry.jointIndices.insert(model.name, geometry.joints.size()); + model.joints.append(joint); + model.jointIndices.insert(fbxModel.name, model.joints.size()); QString rotationID = localRotations.value(modelID); AnimationCurve xRotCurve = animationCurves.value(xComponents.value(rotationID)); @@ -1590,11 +1590,11 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS glm::vec3 defaultPosValues = joint.translation; for (int i = 0; i < frameCount; i++) { - geometry.animationFrames[i].rotations[jointIndex] = glm::quat(glm::radians(glm::vec3( + model.animationFrames[i].rotations[jointIndex] = glm::quat(glm::radians(glm::vec3( xRotCurve.values.isEmpty() ? defaultRotValues.x : xRotCurve.values.at(i % xRotCurve.values.size()), yRotCurve.values.isEmpty() ? defaultRotValues.y : yRotCurve.values.at(i % yRotCurve.values.size()), zRotCurve.values.isEmpty() ? defaultRotValues.z : zRotCurve.values.at(i % zRotCurve.values.size())))); - geometry.animationFrames[i].translations[jointIndex] = glm::vec3( + model.animationFrames[i].translations[jointIndex] = glm::vec3( xPosCurve.values.isEmpty() ? defaultPosValues.x : xPosCurve.values.at(i % xPosCurve.values.size()), yPosCurve.values.isEmpty() ? defaultPosValues.y : yPosCurve.values.at(i % yPosCurve.values.size()), zPosCurve.values.isEmpty() ? defaultPosValues.z : zPosCurve.values.at(i % zPosCurve.values.size())); @@ -1603,32 +1603,32 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS // NOTE: shapeVertices are in joint-frame std::vector shapeVertices; - shapeVertices.resize(std::max(1, geometry.joints.size()) ); + shapeVertices.resize(std::max(1, model.joints.size()) ); // find our special joints - geometry.leftEyeJointIndex = modelIDs.indexOf(jointEyeLeftID); - geometry.rightEyeJointIndex = modelIDs.indexOf(jointEyeRightID); - geometry.neckJointIndex = modelIDs.indexOf(jointNeckID); - geometry.rootJointIndex = modelIDs.indexOf(jointRootID); - geometry.leanJointIndex = modelIDs.indexOf(jointLeanID); - geometry.headJointIndex = modelIDs.indexOf(jointHeadID); - geometry.leftHandJointIndex = modelIDs.indexOf(jointLeftHandID); - geometry.rightHandJointIndex = modelIDs.indexOf(jointRightHandID); - geometry.leftToeJointIndex = modelIDs.indexOf(jointLeftToeID); - geometry.rightToeJointIndex = modelIDs.indexOf(jointRightToeID); + model.leftEyeJointIndex = modelIDs.indexOf(jointEyeLeftID); + model.rightEyeJointIndex = modelIDs.indexOf(jointEyeRightID); + model.neckJointIndex = modelIDs.indexOf(jointNeckID); + model.rootJointIndex = modelIDs.indexOf(jointRootID); + model.leanJointIndex = modelIDs.indexOf(jointLeanID); + model.headJointIndex = modelIDs.indexOf(jointHeadID); + model.leftHandJointIndex = modelIDs.indexOf(jointLeftHandID); + model.rightHandJointIndex = modelIDs.indexOf(jointRightHandID); + model.leftToeJointIndex = modelIDs.indexOf(jointLeftToeID); + model.rightToeJointIndex = modelIDs.indexOf(jointRightToeID); foreach (const QString& id, humanIKJointIDs) { - geometry.humanIKJointIndices.append(modelIDs.indexOf(id)); + model.humanIKJointIndices.append(modelIDs.indexOf(id)); } // extract the translation component of the neck transform - if (geometry.neckJointIndex != -1) { - const glm::mat4& transform = geometry.joints.at(geometry.neckJointIndex).transform; - geometry.neckPivot = glm::vec3(transform[3][0], transform[3][1], transform[3][2]); + if (model.neckJointIndex != -1) { + const glm::mat4& transform = model.joints.at(model.neckJointIndex).transform; + model.neckPivot = glm::vec3(transform[3][0], transform[3][1], transform[3][2]); } - geometry.bindExtents.reset(); - geometry.meshExtents.reset(); + model.bindExtents.reset(); + model.meshExtents.reset(); // Create the Material Library consolidateHFMMaterials(mapping); @@ -1664,7 +1664,7 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS } } #endif - geometry.materials = _hfmMaterials; + model.materials = _hfmMaterials; // see if any materials have texture children bool materialsHaveTextures = checkMaterialsHaveTextures(_hfmMaterials, _textureFilenames, _connectionChildMap); @@ -1675,14 +1675,14 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS extracted.mesh.meshExtents.reset(); // accumulate local transforms - QString modelID = models.contains(it.key()) ? it.key() : _connectionParentMap.value(it.key()); - glm::mat4 modelTransform = getGlobalTransform(_connectionParentMap, models, modelID, geometry.applicationName == "mixamo.com", url); + QString modelID = fbxModels.contains(it.key()) ? it.key() : _connectionParentMap.value(it.key()); + glm::mat4 modelTransform = getGlobalTransform(_connectionParentMap, fbxModels, modelID, model.applicationName == "mixamo.com", url); // compute the mesh extents from the transformed vertices foreach (const glm::vec3& vertex, extracted.mesh.vertices) { glm::vec3 transformedVertex = glm::vec3(modelTransform * glm::vec4(vertex, 1.0f)); - geometry.meshExtents.minimum = glm::min(geometry.meshExtents.minimum, transformedVertex); - geometry.meshExtents.maximum = glm::max(geometry.meshExtents.maximum, transformedVertex); + model.meshExtents.minimum = glm::min(model.meshExtents.minimum, transformedVertex); + model.meshExtents.maximum = glm::max(model.meshExtents.maximum, transformedVertex); extracted.mesh.meshExtents.minimum = glm::min(extracted.mesh.meshExtents.minimum, transformedVertex); extracted.mesh.meshExtents.maximum = glm::max(extracted.mesh.meshExtents.maximum, transformedVertex); @@ -1763,14 +1763,14 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS extracted.mesh.clusters.append(hfmCluster); // override the bind rotation with the transform link - HFMJoint& joint = geometry.joints[hfmCluster.jointIndex]; + HFMJoint& joint = model.joints[hfmCluster.jointIndex]; joint.inverseBindRotation = glm::inverse(extractRotation(cluster.transformLink)); joint.bindTransform = cluster.transformLink; joint.bindTransformFoundInCluster = true; // update the bind pose extents - glm::vec3 bindTranslation = extractTranslation(geometry.offset * joint.bindTransform); - geometry.bindExtents.addPoint(bindTranslation); + glm::vec3 bindTranslation = extractTranslation(model.offset * joint.bindTransform); + model.bindExtents.addPoint(bindTranslation); } } @@ -1801,14 +1801,14 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS const Cluster& cluster = clusters[clusterID]; const HFMCluster& hfmCluster = extracted.mesh.clusters.at(i); int jointIndex = hfmCluster.jointIndex; - HFMJoint& joint = geometry.joints[jointIndex]; + HFMJoint& joint = model.joints[jointIndex]; glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform; glm::vec3 boneEnd = extractTranslation(transformJointToMesh); glm::vec3 boneBegin = boneEnd; glm::vec3 boneDirection; float boneLength = 0.0f; if (joint.parentIndex != -1) { - boneBegin = extractTranslation(inverseModelTransform * geometry.joints[joint.parentIndex].bindTransform); + boneBegin = extractTranslation(inverseModelTransform * model.joints[joint.parentIndex].bindTransform); boneDirection = boneEnd - boneBegin; boneLength = glm::length(boneDirection); if (boneLength > EPSILON) { @@ -1882,7 +1882,7 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS } else { // this is a single-mesh joint int jointIndex = firstHFMCluster.jointIndex; - HFMJoint& joint = geometry.joints[jointIndex]; + HFMJoint& joint = model.joints[jointIndex]; // transform cluster vertices to joint-frame and save for later glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; @@ -1902,8 +1902,8 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS } buildModelMesh(extracted.mesh, url); - geometry.meshes.append(extracted.mesh); - int meshIndex = geometry.meshes.size() - 1; + model.meshes.append(extracted.mesh); + int meshIndex = model.meshes.size() - 1; if (extracted.mesh._mesh) { extracted.mesh._mesh->displayName = QString("%1#/mesh/%2").arg(url).arg(meshIndex).toStdString(); extracted.mesh._mesh->modelName = modelIDsToNames.value(modelID).toStdString(); @@ -1923,8 +1923,8 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS }; // now that all joints have been scanned compute a k-Dop bounding volume of mesh - for (int i = 0; i < geometry.joints.size(); ++i) { - HFMJoint& joint = geometry.joints[i]; + for (int i = 0; i < model.joints.size(); ++i) { + HFMJoint& joint = model.joints[i]; // NOTE: points are in joint-frame ShapeVertices& points = shapeVertices.at(i); @@ -1958,7 +1958,7 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS generateBoundryLinesForDop14(joint.shapeInfo.dots, joint.shapeInfo.avgPoint, joint.shapeInfo.debugLines); } } - geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); + model.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); // attempt to map any meshes to a named model for (QHash::const_iterator m = meshIDsToMeshIndices.constBegin(); @@ -1971,14 +1971,14 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS const QString& modelID = ooChildToParent.value(meshID); if (modelIDsToNames.contains(modelID)) { const QString& modelName = modelIDsToNames.value(modelID); - geometry.meshIndicesToModelNames.insert(meshIndex, modelName); + model.meshIndicesToModelNames.insert(meshIndex, modelName); } } } { int i = 0; - for (const auto& mesh : geometry.meshes) { - auto name = geometry.getModelNameOfMesh(i++); + for (const auto& mesh : model.meshes) { + auto name = model.getModelNameOfMesh(i++); if (!name.isEmpty()) { if (mesh._mesh) { mesh._mesh->modelName = name.toStdString(); @@ -1991,16 +1991,16 @@ HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QS } } } - return geometryPtr; + return modelPtr; } -HFMGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { - QBuffer buffer(const_cast(&model)); +HFMModel* readFBX(const QByteArray& data, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { + QBuffer buffer(const_cast(&data)); buffer.open(QIODevice::ReadOnly); return readFBX(&buffer, mapping, url, loadLightmaps, lightmapLevel); } -HFMGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { +HFMModel* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { FBXReader reader; reader._rootNode = FBXReader::parseFBX(device); reader._loadLightmaps = loadLightmaps; @@ -2008,5 +2008,5 @@ HFMGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QStri qCDebug(modelformat) << "Reading FBX: " << url; - return reader.extractHFMGeometry(mapping, url); + return reader.extractHFMModel(mapping, url); } diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index f95ba7fe73..d9a216eeb8 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -34,13 +34,13 @@ class QIODevice; class FBXNode; -/// Reads FBX geometry from the supplied model and mapping data. +/// Reads HFMModel from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -HFMGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); +HFMModel* readFBX(const QByteArray& data, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); -/// Reads FBX geometry from the supplied model and mapping data. +/// Reads HFMModel from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -HFMGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); +HFMModel* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); class TextureParam { public: @@ -103,12 +103,12 @@ class ExtractedMesh; class FBXReader { public: - HFMGeometry* _hfmGeometry; + HFMModel* _hfmModel; FBXNode _rootNode; static FBXNode parseFBX(QIODevice* device); - HFMGeometry* extractHFMGeometry(const QVariantHash& mapping, const QString& url); + HFMModel* extractHFMModel(const QVariantHash& mapping, const QString& url); static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true); QHash meshes; diff --git a/libraries/fbx/src/GLTFReader.cpp b/libraries/fbx/src/GLTFReader.cpp index 7ee13c5cdf..cc1f3bec1c 100644 --- a/libraries/fbx/src/GLTFReader.cpp +++ b/libraries/fbx/src/GLTFReader.cpp @@ -533,10 +533,10 @@ bool GLTFReader::addTexture(const QJsonObject& object) { return true; } -bool GLTFReader::parseGLTF(const QByteArray& model) { +bool GLTFReader::parseGLTF(const QByteArray& data) { PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr); - QJsonDocument d = QJsonDocument::fromJson(model); + QJsonDocument d = QJsonDocument::fromJson(data); QJsonObject jsFile = d.object(); bool isvalid = setAsset(jsFile); @@ -697,7 +697,7 @@ glm::mat4 GLTFReader::getModelTransform(const GLTFNode& node) { return tmat; } -bool GLTFReader::buildGeometry(HFMGeometry& geometry, const QUrl& url) { +bool GLTFReader::buildGeometry(HFMModel& model, const QUrl& url) { //Build dependencies QVector> nodeDependencies(_file.nodes.size()); @@ -727,17 +727,17 @@ bool GLTFReader::buildGeometry(HFMGeometry& geometry, const QUrl& url) { } //Build default joints - geometry.joints.resize(1); - geometry.joints[0].isFree = false; - geometry.joints[0].parentIndex = -1; - geometry.joints[0].distanceToParent = 0; - geometry.joints[0].translation = glm::vec3(0, 0, 0); - geometry.joints[0].rotationMin = glm::vec3(0, 0, 0); - geometry.joints[0].rotationMax = glm::vec3(0, 0, 0); - geometry.joints[0].name = "OBJ"; - geometry.joints[0].isSkeletonJoint = true; + model.joints.resize(1); + model.joints[0].isFree = false; + model.joints[0].parentIndex = -1; + model.joints[0].distanceToParent = 0; + model.joints[0].translation = glm::vec3(0, 0, 0); + model.joints[0].rotationMin = glm::vec3(0, 0, 0); + model.joints[0].rotationMax = glm::vec3(0, 0, 0); + model.joints[0].name = "OBJ"; + model.joints[0].isSkeletonJoint = true; - geometry.jointIndices["x"] = 1; + model.jointIndices["x"] = 1; //Build materials QVector materialIDs; @@ -750,8 +750,8 @@ bool GLTFReader::buildGeometry(HFMGeometry& geometry, const QUrl& url) { for (int i = 0; i < materialIDs.size(); i++) { QString& matid = materialIDs[i]; - geometry.materials[matid] = HFMMaterial(); - HFMMaterial& hfmMaterial = geometry.materials[matid]; + model.materials[matid] = HFMMaterial(); + HFMMaterial& hfmMaterial = model.materials[matid]; hfmMaterial._material = std::make_shared(); setHFMMaterial(hfmMaterial, _file.materials[i]); } @@ -765,8 +765,8 @@ bool GLTFReader::buildGeometry(HFMGeometry& geometry, const QUrl& url) { if (node.defined["mesh"]) { qCDebug(modelformat) << "node_transforms" << node.transforms; foreach(auto &primitive, _file.meshes[node.mesh].primitives) { - geometry.meshes.append(HFMMesh()); - HFMMesh& mesh = geometry.meshes[geometry.meshes.size() - 1]; + model.meshes.append(HFMMesh()); + HFMMesh& mesh = model.meshes[model.meshes.size() - 1]; HFMCluster cluster; cluster.jointIndex = 0; cluster.inverseBindMatrix = glm::mat4(1, 0, 0, 0, @@ -886,7 +886,7 @@ bool GLTFReader::buildGeometry(HFMGeometry& geometry, const QUrl& url) { mesh.meshExtents.reset(); foreach(const glm::vec3& vertex, mesh.vertices) { mesh.meshExtents.addPoint(vertex); - geometry.meshExtents.addPoint(vertex); + model.meshExtents.addPoint(vertex); } // since mesh.modelTransform seems to not have any effect I apply the transformation the model @@ -898,7 +898,7 @@ bool GLTFReader::buildGeometry(HFMGeometry& geometry, const QUrl& url) { } } - mesh.meshIndex = geometry.meshes.size(); + mesh.meshIndex = model.meshes.size(); FBXReader::buildModelMesh(mesh, url.toString()); } @@ -910,7 +910,7 @@ bool GLTFReader::buildGeometry(HFMGeometry& geometry, const QUrl& url) { return true; } -HFMGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping, +HFMModel* GLTFReader::readGLTF(QByteArray& data, const QVariantHash& mapping, const QUrl& url, bool loadLightmaps, float lightmapLevel) { _url = url; @@ -922,15 +922,15 @@ HFMGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping _url = QUrl(QFileInfo(localFileName).absoluteFilePath()); } - parseGLTF(model); + parseGLTF(data); //_file.dump(); - HFMGeometry* geometryPtr = new HFMGeometry(); - HFMGeometry& geometry = *geometryPtr; + HFMModel* modelPtr = new HFMModel(); + HFMModel& model = *modelPtr; - buildGeometry(geometry, url); + buildGeometry(model, url); - //hfmDebugDump(geometry); - return geometryPtr; + //hfmDebugDump(data); + return modelPtr; } @@ -1181,37 +1181,37 @@ void GLTFReader::retriangulate(const QVector& inIndices, const QVector(); graphics::MaterialPointer modelMaterial = hfmMaterial._material; @@ -988,15 +988,15 @@ HFMGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m modelMaterial->setOpacity(hfmMaterial.opacity); } - return geometryPtr; + return modelPtr; } -void hfmDebugDump(const HFMGeometry& hfmgeo) { - qCDebug(modelformat) << "---------------- hfmGeometry ----------------"; - qCDebug(modelformat) << " hasSkeletonJoints =" << hfmgeo.hasSkeletonJoints; - qCDebug(modelformat) << " offset =" << hfmgeo.offset; - qCDebug(modelformat) << " meshes.count() =" << hfmgeo.meshes.count(); - foreach (HFMMesh mesh, hfmgeo.meshes) { +void hfmDebugDump(const HFMModel& hfmModel) { + qCDebug(modelformat) << "---------------- hfmModel ----------------"; + qCDebug(modelformat) << " hasSkeletonJoints =" << hfmModel.hasSkeletonJoints; + qCDebug(modelformat) << " offset =" << hfmModel.offset; + qCDebug(modelformat) << " meshes.count() =" << hfmModel.meshes.count(); + foreach (HFMMesh mesh, hfmModel.meshes) { qCDebug(modelformat) << " vertices.count() =" << mesh.vertices.count(); qCDebug(modelformat) << " colors.count() =" << mesh.colors.count(); qCDebug(modelformat) << " normals.count() =" << mesh.normals.count(); @@ -1037,10 +1037,10 @@ void hfmDebugDump(const HFMGeometry& hfmgeo) { } } - qCDebug(modelformat) << " jointIndices =" << hfmgeo.jointIndices; - qCDebug(modelformat) << " joints.count() =" << hfmgeo.joints.count(); + qCDebug(modelformat) << " jointIndices =" << hfmModel.jointIndices; + qCDebug(modelformat) << " joints.count() =" << hfmModel.joints.count(); - foreach (HFMJoint joint, hfmgeo.joints) { + foreach (HFMJoint joint, hfmModel.joints) { qCDebug(modelformat) << " isFree =" << joint.isFree; qCDebug(modelformat) << " freeLineage" << joint.freeLineage; qCDebug(modelformat) << " parentIndex" << joint.parentIndex; diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 2eb039eba2..1b259d90df 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -87,13 +87,13 @@ public: QString currentMaterialName; QHash materials; - HFMGeometry::Pointer readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url = QUrl()); + HFMModel::Pointer readOBJ(QByteArray& data, const QVariantHash& mapping, bool combineParts, const QUrl& url = QUrl()); private: QUrl _url; QHash librariesSeen; - bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMGeometry& geometry, + bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& model, float& scaleGuess, bool combineParts); void parseMaterialLibrary(QIODevice* device); void parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions); @@ -104,4 +104,4 @@ private: // What are these utilities doing here? One is used by fbx loading code in VHACD Utils, and the other a general debugging utility. void setMeshPartDefaults(HFMMeshPart& meshPart, QString materialID); -void hfmDebugDump(const HFMGeometry& hfmgeo); +void hfmDebugDump(const HFMModel& hfmModel); diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index a950e1df3c..6430e4599e 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -128,7 +128,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { void GeometryMappingResource::onGeometryMappingLoaded(bool success) { if (success && _geometryResource) { - _hfmGeometry = _geometryResource->_hfmGeometry; + _hfmModel = _geometryResource->_hfmModel; _meshParts = _geometryResource->_meshParts; _meshes = _geometryResource->_meshes; _materials = _geometryResource->_materials; @@ -193,38 +193,38 @@ void GeometryReader::run() { _url.path().toLower().endsWith(".obj.gz") || _url.path().toLower().endsWith(".gltf"))) { - HFMGeometry::Pointer hfmGeometry; + HFMModel::Pointer hfmModel; if (_url.path().toLower().endsWith(".fbx")) { - hfmGeometry.reset(readFBX(_data, _mapping, _url.path())); - if (hfmGeometry->meshes.size() == 0 && hfmGeometry->joints.size() == 0) { + hfmModel.reset(readFBX(_data, _mapping, _url.path())); + if (hfmModel->meshes.size() == 0 && hfmModel->joints.size() == 0) { throw QString("empty geometry, possibly due to an unsupported FBX version"); } } else if (_url.path().toLower().endsWith(".obj")) { - hfmGeometry = OBJReader().readOBJ(_data, _mapping, _combineParts, _url); + hfmModel = OBJReader().readOBJ(_data, _mapping, _combineParts, _url); } else if (_url.path().toLower().endsWith(".obj.gz")) { QByteArray uncompressedData; if (gunzip(_data, uncompressedData)){ - hfmGeometry = OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url); + hfmModel = OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url); } else { throw QString("failed to decompress .obj.gz"); } } else if (_url.path().toLower().endsWith(".gltf")) { std::shared_ptr glreader = std::make_shared(); - hfmGeometry.reset(glreader->readGLTF(_data, _mapping, _url)); - if (hfmGeometry->meshes.size() == 0 && hfmGeometry->joints.size() == 0) { + hfmModel.reset(glreader->readGLTF(_data, _mapping, _url)); + if (hfmModel->meshes.size() == 0 && hfmModel->joints.size() == 0) { throw QString("empty geometry, possibly due to an unsupported GLTF version"); } } else { throw QString("unsupported format"); } - // Add scripts to hfmGeometry + // Add scripts to hfmModel if (!_mapping.value(SCRIPT_FIELD).isNull()) { QVariantList scripts = _mapping.values(SCRIPT_FIELD); for (auto &script : scripts) { - hfmGeometry->scripts.push_back(script.toString()); + hfmModel->scripts.push_back(script.toString()); } } @@ -234,7 +234,7 @@ void GeometryReader::run() { qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; } else { QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition", - Q_ARG(HFMGeometry::Pointer, hfmGeometry)); + Q_ARG(HFMModel::Pointer, hfmModel)); } } else { throw QString("url is invalid"); @@ -262,7 +262,7 @@ public: virtual void downloadFinished(const QByteArray& data) override; protected: - Q_INVOKABLE void setGeometryDefinition(HFMGeometry::Pointer hfmGeometry); + Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel); private: QVariantHash _mapping; @@ -277,13 +277,13 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { QThreadPool::globalInstance()->start(new GeometryReader(_self, _effectiveBaseURL, _mapping, data, _combineParts)); } -void GeometryDefinitionResource::setGeometryDefinition(HFMGeometry::Pointer hfmGeometry) { - // Assume ownership of the geometry pointer - _hfmGeometry = hfmGeometry; +void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel) { + // Assume ownership of the HFMModel pointer + _hfmModel = hfmModel; // Copy materials QHash materialIDAtlas; - for (const HFMMaterial& material : _hfmGeometry->materials) { + for (const HFMMaterial& material : _hfmModel->materials) { materialIDAtlas[material.materialID] = _materials.size(); _materials.push_back(std::make_shared(material, _textureBaseUrl)); } @@ -291,7 +291,7 @@ void GeometryDefinitionResource::setGeometryDefinition(HFMGeometry::Pointer hfmG std::shared_ptr meshes = std::make_shared(); std::shared_ptr parts = std::make_shared(); int meshID = 0; - for (const HFMMesh& mesh : _hfmGeometry->meshes) { + for (const HFMMesh& mesh : _hfmModel->meshes) { // Copy mesh pointers meshes->emplace_back(mesh._mesh); int partID = 0; @@ -371,7 +371,7 @@ const QVariantMap Geometry::getTextures() const { // FIXME: The materials should only be copied when modified, but the Model currently caches the original Geometry::Geometry(const Geometry& geometry) { - _hfmGeometry = geometry._hfmGeometry; + _hfmModel = geometry._hfmModel; _meshes = geometry._meshes; _meshParts = geometry._meshParts; @@ -444,8 +444,8 @@ void GeometryResource::deleter() { } void GeometryResource::setTextures() { - if (_hfmGeometry) { - for (const HFMMaterial& material : _hfmGeometry->materials) { + if (_hfmModel) { + for (const HFMMaterial& material : _hfmModel->materials) { _materials.push_back(std::make_shared(material, _textureBaseUrl)); } } diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 2283c355d8..1bb340b83c 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -45,9 +45,9 @@ public: // Mutable, but must retain structure of vector using NetworkMaterials = std::vector>; - bool isGeometryLoaded() const { return (bool)_hfmGeometry; } + bool isHFMModelLoaded() const { return (bool)_hfmModel; } - const HFMGeometry& getHFMGeometry() const { return *_hfmGeometry; } + const HFMModel& getHFMModel() const { return *_hfmModel; } const GeometryMeshes& getMeshes() const { return *_meshes; } const std::shared_ptr getShapeMaterial(int shapeID) const; @@ -62,7 +62,7 @@ protected: friend class GeometryMappingResource; // Shared across all geometries, constant throughout lifetime - std::shared_ptr _hfmGeometry; + std::shared_ptr _hfmModel; std::shared_ptr _meshes; std::shared_ptr _meshParts; @@ -94,7 +94,7 @@ protected: // Geometries may not hold onto textures while cached - that is for the texture cache // Instead, these methods clear and reset textures from the geometry when caching/loading - bool shouldSetTextures() const { return _hfmGeometry && _materials.empty(); } + bool shouldSetTextures() const { return _hfmModel && _materials.empty(); } void setTextures(); void resetTextures(); diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 31d6cef060..c31345bc55 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -32,8 +32,8 @@ bool CauterizedModel::updateGeometry() { bool needsFullUpdate = Model::updateGeometry(); if (_isCauterized && needsFullUpdate) { assert(_cauterizeMeshStates.empty()); - const HFMGeometry& hfmGeometry = getHFMGeometry(); - foreach (const HFMMesh& mesh, hfmGeometry.meshes) { + const HFMModel& hfmModel = getHFMModel(); + foreach (const HFMMesh& mesh, hfmModel.meshes) { Model::MeshState state; if (_useDualQuaternionSkinning) { state.clusterDualQuaternions.resize(mesh.clusters.size()); @@ -76,7 +76,7 @@ void CauterizedModel::createRenderItemSet() { // Run through all of the meshes, and place them into their segregated, but unsorted buckets int shapeID = 0; uint32_t numMeshes = (uint32_t)meshes.size(); - const HFMGeometry& hfmGeometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); for (uint32_t i = 0; i < numMeshes; i++) { const auto& mesh = meshes.at(i); if (!mesh) { @@ -86,7 +86,7 @@ void CauterizedModel::createRenderItemSet() { // Create the render payloads int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { - initializeBlendshapes(hfmGeometry.meshes[i], i); + initializeBlendshapes(hfmModel.meshes[i], i); auto ptr = std::make_shared(shared_from_this(), i, partIndex, shapeID, transform, offset); _modelMeshRenderItems << std::static_pointer_cast(ptr); @@ -109,11 +109,11 @@ void CauterizedModel::updateClusterMatrices() { return; } _needsUpdateClusterMatrices = false; - const HFMGeometry& geometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); for (int i = 0; i < (int)_meshStates.size(); i++) { Model::MeshState& state = _meshStates[i]; - const HFMMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = hfmModel.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { const HFMCluster& cluster = mesh.clusters.at(j); if (_useDualQuaternionSkinning) { @@ -133,7 +133,7 @@ void CauterizedModel::updateClusterMatrices() { // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. if (!_cauterizeBoneSet.empty()) { - AnimPose cauterizePose = _rig.getJointPose(geometry.neckJointIndex); + AnimPose cauterizePose = _rig.getJointPose(hfmModel.neckJointIndex); cauterizePose.scale() = glm::vec3(0.0001f, 0.0001f, 0.0001f); static const glm::mat4 zeroScale( @@ -141,11 +141,11 @@ void CauterizedModel::updateClusterMatrices() { glm::vec4(0.0f, 0.0001f, 0.0f, 0.0f), glm::vec4(0.0f, 0.0f, 0.0001f, 0.0f), glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); - auto cauterizeMatrix = _rig.getJointTransform(geometry.neckJointIndex) * zeroScale; + auto cauterizeMatrix = _rig.getJointTransform(hfmModel.neckJointIndex) * zeroScale; for (int i = 0; i < _cauterizeMeshStates.size(); i++) { Model::MeshState& state = _cauterizeMeshStates[i]; - const HFMMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = hfmModel.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { const HFMCluster& cluster = mesh.clusters.at(j); @@ -175,7 +175,7 @@ void CauterizedModel::updateClusterMatrices() { // post the blender if we're not currently waiting for one to finish auto modelBlender = DependencyManager::get(); - if (_blendshapeOffsetsInitialized && modelBlender->shouldComputeBlendshapes() && geometry.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) { + if (_blendshapeOffsetsInitialized && modelBlender->shouldComputeBlendshapes() && hfmModel.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) { _blendedBlendshapeCoefficients = _blendshapeCoefficients; modelBlender->noteRequiresBlend(getThisPointer()); } diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 8e2541fdda..b493780aff 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -260,8 +260,8 @@ void ModelMeshPartPayload::initCache(const ModelPointer& model) { _hasColorAttrib = vertexFormat->hasAttribute(gpu::Stream::COLOR); _isSkinned = vertexFormat->hasAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT) && vertexFormat->hasAttribute(gpu::Stream::SKIN_CLUSTER_INDEX); - const HFMGeometry& geometry = model->getHFMGeometry(); - const HFMMesh& mesh = geometry.meshes.at(_meshIndex); + const HFMModel& hfmModel = model->getHFMModel(); + const HFMMesh& mesh = hfmModel.meshes.at(_meshIndex); _isBlendShaped = !mesh.blendshapes.isEmpty(); _hasTangents = !mesh.tangents.isEmpty(); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 65b3fef7c0..6f285a9f0c 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -183,11 +183,11 @@ bool Model::shouldInvalidatePayloadShapeKey(int meshIndex) { return true; } - const HFMGeometry& geometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); const auto& networkMeshes = getGeometry()->getMeshes(); // if our index is ever out of range for either meshes or networkMeshes, then skip it, and set our _meshGroupsKnown // to false to rebuild out mesh groups. - if (meshIndex < 0 || meshIndex >= (int)networkMeshes.size() || meshIndex >= (int)geometry.meshes.size() || meshIndex >= (int)_meshStates.size()) { + if (meshIndex < 0 || meshIndex >= (int)networkMeshes.size() || meshIndex >= (int)hfmModel.meshes.size() || meshIndex >= (int)_meshStates.size()) { _needsFixupInScene = true; // trigger remove/add cycle invalidCalculatedMeshBoxes(); // if we have to reload, we need to assume our mesh boxes are all invalid return true; @@ -278,8 +278,8 @@ void Model::setRenderItemsNeedUpdate() { void Model::reset() { if (isLoaded()) { - const HFMGeometry& geometry = getHFMGeometry(); - _rig.reset(geometry); + const HFMModel& hfmModel = getHFMModel(); + _rig.reset(hfmModel); emit rigReset(); emit rigReady(); } @@ -295,13 +295,13 @@ bool Model::updateGeometry() { _needsReload = false; // TODO: should all Models have a valid _rig? - if (_rig.jointStatesEmpty() && getHFMGeometry().joints.size() > 0) { + if (_rig.jointStatesEmpty() && getHFMModel().joints.size() > 0) { initJointStates(); assert(_meshStates.empty()); - const HFMGeometry& hfmGeometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); int i = 0; - foreach (const HFMMesh& mesh, hfmGeometry.meshes) { + foreach (const HFMMesh& mesh, hfmModel.meshes) { MeshState state; state.clusterDualQuaternions.resize(mesh.clusters.size()); state.clusterMatrices.resize(mesh.clusters.size()); @@ -319,10 +319,10 @@ bool Model::updateGeometry() { // virtual void Model::initJointStates() { - const HFMGeometry& geometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); glm::mat4 modelOffset = glm::scale(_scale) * glm::translate(_offset); - _rig.initJointStates(geometry, modelOffset); + _rig.initJointStates(hfmModel, modelOffset); } bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, @@ -363,9 +363,9 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g int bestShapeID = 0; int bestSubMeshIndex = 0; - const HFMGeometry& geometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); if (!_triangleSetsValid) { - calculateTriangleSets(geometry); + calculateTriangleSets(hfmModel); } glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset); @@ -448,7 +448,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g extraInfo["shapeID"] = bestShapeID; if (pickAgainstTriangles) { extraInfo["subMeshIndex"] = bestSubMeshIndex; - extraInfo["subMeshName"] = geometry.getModelNameOfMesh(bestSubMeshIndex); + extraInfo["subMeshName"] = hfmModel.getModelNameOfMesh(bestSubMeshIndex); extraInfo["subMeshTriangleWorld"] = QVariantMap{ { "v0", vec3toVariant(bestWorldTriangle.v0) }, { "v1", vec3toVariant(bestWorldTriangle.v1) }, @@ -506,9 +506,9 @@ bool Model::findParabolaIntersectionAgainstSubMeshes(const glm::vec3& origin, co int bestShapeID = 0; int bestSubMeshIndex = 0; - const HFMGeometry& geometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); if (!_triangleSetsValid) { - calculateTriangleSets(geometry); + calculateTriangleSets(hfmModel); } glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset); @@ -595,7 +595,7 @@ bool Model::findParabolaIntersectionAgainstSubMeshes(const glm::vec3& origin, co extraInfo["shapeID"] = bestShapeID; if (pickAgainstTriangles) { extraInfo["subMeshIndex"] = bestSubMeshIndex; - extraInfo["subMeshName"] = geometry.getModelNameOfMesh(bestSubMeshIndex); + extraInfo["subMeshName"] = hfmModel.getModelNameOfMesh(bestSubMeshIndex); extraInfo["subMeshTriangleWorld"] = QVariantMap{ { "v0", vec3toVariant(bestWorldTriangle.v0) }, { "v1", vec3toVariant(bestWorldTriangle.v1) }, @@ -641,7 +641,7 @@ bool Model::convexHullContains(glm::vec3 point) { QMutexLocker locker(&_mutex); if (!_triangleSetsValid) { - calculateTriangleSets(getHFMGeometry()); + calculateTriangleSets(getHFMModel()); } // If we are inside the models box, then consider the submeshes... @@ -753,7 +753,7 @@ bool Model::replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointe } // update triangles for picking { - HFMGeometry geometry; + HFMModel hfmModel; for (const auto& newMesh : meshes) { HFMMesh mesh; mesh._mesh = newMesh.getMeshPointer(); @@ -767,15 +767,15 @@ bool Model::replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointe { foreach (const glm::vec3& vertex, mesh.vertices) { glm::vec3 transformedVertex = glm::vec3(mesh.modelTransform * glm::vec4(vertex, 1.0f)); - geometry.meshExtents.minimum = glm::min(geometry.meshExtents.minimum, transformedVertex); - geometry.meshExtents.maximum = glm::max(geometry.meshExtents.maximum, transformedVertex); + hfmModel.meshExtents.minimum = glm::min(hfmModel.meshExtents.minimum, transformedVertex); + hfmModel.meshExtents.maximum = glm::max(hfmModel.meshExtents.maximum, transformedVertex); mesh.meshExtents.minimum = glm::min(mesh.meshExtents.minimum, transformedVertex); mesh.meshExtents.maximum = glm::max(mesh.meshExtents.maximum, transformedVertex); } } - geometry.meshes << mesh; + hfmModel.meshes << mesh; } - calculateTriangleSets(geometry); + calculateTriangleSets(hfmModel); } return true; } @@ -789,11 +789,11 @@ scriptable::ScriptableModelBase Model::getScriptableModel() { return result; } - const HFMGeometry& geometry = getHFMGeometry(); - int numberOfMeshes = geometry.meshes.size(); + const HFMModel& hfmModel = getHFMModel(); + int numberOfMeshes = hfmModel.meshes.size(); int shapeID = 0; for (int i = 0; i < numberOfMeshes; i++) { - const HFMMesh& hfmMesh = geometry.meshes.at(i); + const HFMMesh& hfmMesh = hfmModel.meshes.at(i); if (auto mesh = hfmMesh._mesh) { result.append(mesh); @@ -808,17 +808,17 @@ scriptable::ScriptableModelBase Model::getScriptableModel() { return result; } -void Model::calculateTriangleSets(const HFMGeometry& geometry) { +void Model::calculateTriangleSets(const HFMModel& hfmModel) { PROFILE_RANGE(render, __FUNCTION__); - int numberOfMeshes = geometry.meshes.size(); + int numberOfMeshes = hfmModel.meshes.size(); _triangleSetsValid = true; _modelSpaceMeshTriangleSets.clear(); _modelSpaceMeshTriangleSets.resize(numberOfMeshes); for (int i = 0; i < numberOfMeshes; i++) { - const HFMMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = hfmModel.meshes.at(i); const int numberOfParts = mesh.parts.size(); auto& meshTriangleSets = _modelSpaceMeshTriangleSets[i]; @@ -839,7 +839,7 @@ void Model::calculateTriangleSets(const HFMGeometry& geometry) { int totalTriangles = (numberOfQuads * TRIANGLES_PER_QUAD) + numberOfTris; partTriangleSet.reserve(totalTriangles); - auto meshTransform = geometry.offset * mesh.modelTransform; + auto meshTransform = hfmModel.offset * mesh.modelTransform; if (part.quadIndices.size() > 0) { int vIndex = 0; @@ -1114,7 +1114,7 @@ Extents Model::getBindExtents() const { if (!isActive()) { return Extents(); } - const Extents& bindExtents = getHFMGeometry().bindExtents; + const Extents& bindExtents = getHFMModel().bindExtents; Extents scaledExtents = { bindExtents.minimum * _scale, bindExtents.maximum * _scale }; return scaledExtents; } @@ -1128,12 +1128,12 @@ Extents Model::getMeshExtents() const { if (!isActive()) { return Extents(); } - const Extents& extents = getHFMGeometry().meshExtents; + const Extents& extents = getHFMModel().meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which // is captured in the offset matrix - glm::vec3 minimum = glm::vec3(getHFMGeometry().offset * glm::vec4(extents.minimum, 1.0f)); - glm::vec3 maximum = glm::vec3(getHFMGeometry().offset * glm::vec4(extents.maximum, 1.0f)); + glm::vec3 minimum = glm::vec3(getHFMModel().offset * glm::vec4(extents.minimum, 1.0f)); + glm::vec3 maximum = glm::vec3(getHFMModel().offset * glm::vec4(extents.maximum, 1.0f)); Extents scaledExtents = { minimum * _scale, maximum * _scale }; return scaledExtents; } @@ -1143,12 +1143,12 @@ Extents Model::getUnscaledMeshExtents() const { return Extents(); } - const Extents& extents = getHFMGeometry().meshExtents; + const Extents& extents = getHFMModel().meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which // is captured in the offset matrix - glm::vec3 minimum = glm::vec3(getHFMGeometry().offset * glm::vec4(extents.minimum, 1.0f)); - glm::vec3 maximum = glm::vec3(getHFMGeometry().offset * glm::vec4(extents.maximum, 1.0f)); + glm::vec3 minimum = glm::vec3(getHFMModel().offset * glm::vec4(extents.minimum, 1.0f)); + glm::vec3 maximum = glm::vec3(getHFMModel().offset * glm::vec4(extents.maximum, 1.0f)); Extents scaledExtents = { minimum, maximum }; return scaledExtents; @@ -1171,11 +1171,11 @@ void Model::setJointTranslation(int index, bool valid, const glm::vec3& translat } int Model::getParentJointIndex(int jointIndex) const { - return (isActive() && jointIndex != -1) ? getHFMGeometry().joints.at(jointIndex).parentIndex : -1; + return (isActive() && jointIndex != -1) ? getHFMModel().joints.at(jointIndex).parentIndex : -1; } int Model::getLastFreeJointIndex(int jointIndex) const { - return (isActive() && jointIndex != -1) ? getHFMGeometry().joints.at(jointIndex).freeLineage.last() : -1; + return (isActive() && jointIndex != -1) ? getHFMModel().joints.at(jointIndex).freeLineage.last() : -1; } void Model::setTextures(const QVariantMap& textures) { @@ -1275,7 +1275,7 @@ QStringList Model::getJointNames() const { Q_RETURN_ARG(QStringList, result)); return result; } - return isActive() ? getHFMGeometry().getJointNames() : QStringList(); + return isActive() ? getHFMModel().getJointNames() : QStringList(); } void Model::setScaleToFit(bool scaleToFit, const glm::vec3& dimensions, bool forceRescale) { @@ -1415,10 +1415,10 @@ void Model::updateClusterMatrices() { } _needsUpdateClusterMatrices = false; - const HFMGeometry& geometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); for (int i = 0; i < (int) _meshStates.size(); i++) { MeshState& state = _meshStates[i]; - const HFMMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = hfmModel.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { const HFMCluster& cluster = mesh.clusters.at(j); if (_useDualQuaternionSkinning) { @@ -1436,7 +1436,7 @@ void Model::updateClusterMatrices() { // post the blender if we're not currently waiting for one to finish auto modelBlender = DependencyManager::get(); - if (_blendshapeOffsetsInitialized && modelBlender->shouldComputeBlendshapes() && geometry.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) { + if (_blendshapeOffsetsInitialized && modelBlender->shouldComputeBlendshapes() && hfmModel.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) { _blendedBlendshapeCoefficients = _blendshapeCoefficients; modelBlender->noteRequiresBlend(getThisPointer()); } @@ -1505,7 +1505,7 @@ void Model::createRenderItemSet() { // Run through all of the meshes, and place them into their segregated, but unsorted buckets int shapeID = 0; uint32_t numMeshes = (uint32_t)meshes.size(); - auto& hfmGeometry = getHFMGeometry(); + auto& hfmModel = getHFMModel(); for (uint32_t i = 0; i < numMeshes; i++) { const auto& mesh = meshes.at(i); if (!mesh) { @@ -1515,7 +1515,7 @@ void Model::createRenderItemSet() { // Create the render payloads int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { - initializeBlendshapes(hfmGeometry.meshes[i], i); + initializeBlendshapes(hfmModel.meshes[i], i); _modelMeshRenderItems << std::make_shared(shared_from_this(), i, partIndex, shapeID, transform, offset); auto material = getGeometry()->getShapeMaterial(shapeID); _modelMeshMaterialNames.push_back(material ? material->getName() : ""); @@ -1600,7 +1600,7 @@ void Model::removeMaterial(graphics::MaterialPointer material, const std::string class CollisionRenderGeometry : public Geometry { public: CollisionRenderGeometry(graphics::MeshPointer mesh) { - _hfmGeometry = std::make_shared(); + _hfmModel = std::make_shared(); std::shared_ptr meshes = std::make_shared(); meshes->push_back(mesh); _meshes = meshes; @@ -1656,7 +1656,7 @@ void Blender::run() { if (_model && _model->isLoaded()) { DETAILED_PROFILE_RANGE_EX(simulation_animation, __FUNCTION__, 0xFFFF0000, 0, { { "url", _model->getURL().toString() } }); int offset = 0; - auto meshes = _model->getHFMGeometry().meshes; + auto meshes = _model->getHFMModel().meshes; int meshIndex = 0; foreach(const HFMMesh& mesh, meshes) { auto modelMeshBlendshapeOffsets = _model->_blendshapeOffsets.find(meshIndex++); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index db5625b3d8..93a0626d28 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -163,7 +163,7 @@ public: bool maybeStartBlender(); - bool isLoaded() const { return (bool)_renderGeometry && _renderGeometry->isGeometryLoaded(); } + bool isLoaded() const { return (bool)_renderGeometry && _renderGeometry->isHFMModelLoaded(); } bool isAddedToScene() const { return _addedToScene; } void setIsWireframe(bool isWireframe) { _isWireframe = isWireframe; } @@ -184,8 +184,8 @@ public: Q_INVOKABLE virtual void setTextures(const QVariantMap& textures); /// Provided as a convenience, will crash if !isLoaded() - // And so that getGeometry() isn't chained everywhere - const HFMGeometry& getHFMGeometry() const { assert(isLoaded()); return _renderGeometry->getHFMGeometry(); } + // And so that getHFMModel() isn't chained everywhere + const HFMModel& getHFMModel() const { assert(isLoaded()); return _renderGeometry->getHFMModel(); } bool isActive() const { return isLoaded(); } @@ -450,7 +450,7 @@ protected: bool _overrideModelTransform { false }; bool _triangleSetsValid { false }; - void calculateTriangleSets(const HFMGeometry& geometry); + void calculateTriangleSets(const HFMModel& hfmModel); std::vector> _modelSpaceMeshTriangleSets; // model space triangles for all sub meshes virtual void createRenderItemSet(); diff --git a/libraries/render-utils/src/SoftAttachmentModel.cpp b/libraries/render-utils/src/SoftAttachmentModel.cpp index 77b09caa1d..f26bad86b0 100644 --- a/libraries/render-utils/src/SoftAttachmentModel.cpp +++ b/libraries/render-utils/src/SoftAttachmentModel.cpp @@ -41,11 +41,11 @@ void SoftAttachmentModel::updateClusterMatrices() { _needsUpdateClusterMatrices = false; - const HFMGeometry& geometry = getHFMGeometry(); + const HFMModel& hfmModel = getHFMModel(); for (int i = 0; i < (int) _meshStates.size(); i++) { MeshState& state = _meshStates[i]; - const HFMMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = hfmModel.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { const HFMCluster& cluster = mesh.clusters.at(j); @@ -78,7 +78,7 @@ void SoftAttachmentModel::updateClusterMatrices() { // post the blender if we're not currently waiting for one to finish auto modelBlender = DependencyManager::get(); - if (_blendshapeOffsetsInitialized && modelBlender->shouldComputeBlendshapes() && geometry.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) { + if (_blendshapeOffsetsInitialized && modelBlender->shouldComputeBlendshapes() && hfmModel.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) { _blendedBlendshapeCoefficients = _blendshapeCoefficients; modelBlender->noteRequiresBlend(getThisPointer()); } diff --git a/tests-manual/gpu/src/TestFbx.cpp b/tests-manual/gpu/src/TestFbx.cpp index 9890e4fbe7..9253f8bc91 100644 --- a/tests-manual/gpu/src/TestFbx.cpp +++ b/tests-manual/gpu/src/TestFbx.cpp @@ -100,12 +100,12 @@ bool TestFbx::isReady() const { void TestFbx::parseFbx(const QByteArray& fbxData) { QVariantHash mapping; - HFMGeometry* geometry = readFBX(fbxData, mapping); + HFMModel* hfmModel = readFBX(fbxData, mapping); size_t totalVertexCount = 0; size_t totalIndexCount = 0; size_t totalPartCount = 0; size_t highestIndex = 0; - for (const auto& mesh : geometry->meshes) { + for (const auto& mesh : hfmModel->meshes) { size_t vertexCount = mesh.vertices.size(); totalVertexCount += mesh.vertices.size(); highestIndex = std::max(highestIndex, vertexCount); @@ -123,7 +123,7 @@ void TestFbx::parseFbx(const QByteArray& fbxData) { std::vector parts; parts.reserve(totalPartCount); _partCount = totalPartCount; - for (const auto& mesh : geometry->meshes) { + for (const auto& mesh : hfmModel->meshes) { baseVertex = vertices.size(); vec3 color; @@ -133,7 +133,7 @@ void TestFbx::parseFbx(const QByteArray& fbxData) { partIndirect.firstIndex = (uint)indices.size(); partIndirect.baseInstance = (uint)parts.size(); _partTransforms.push_back(mesh.modelTransform); - auto material = geometry->materials[part.materialID]; + auto material = hfmModel->materials[part.materialID]; color = material.diffuseColor; for (auto index : part.quadTrianglesIndices) { indices.push_back(index); @@ -163,7 +163,7 @@ void TestFbx::parseFbx(const QByteArray& fbxData) { _vertexBuffer->append(vertices); _indexBuffer->append(indices); _indirectBuffer->append(parts); - delete geometry; + delete hfmModel; } void TestFbx::renderTest(size_t testId, RenderArgs* args) { diff --git a/tests-manual/gpu/src/TestFbx.h b/tests-manual/gpu/src/TestFbx.h index 4e22928460..8056af21ec 100644 --- a/tests-manual/gpu/src/TestFbx.h +++ b/tests-manual/gpu/src/TestFbx.h @@ -11,7 +11,7 @@ #include -class HFMGeometry; +class HFMModel; class TestFbx : public GpuTestBase { size_t _partCount { 0 }; diff --git a/tests/animation/src/AnimInverseKinematicsTests.cpp b/tests/animation/src/AnimInverseKinematicsTests.cpp index f51fe12ecb..910770bb0d 100644 --- a/tests/animation/src/AnimInverseKinematicsTests.cpp +++ b/tests/animation/src/AnimInverseKinematicsTests.cpp @@ -28,7 +28,7 @@ const glm::quat identity = glm::quat(); const glm::quat quaterTurnAroundZ = glm::angleAxis(0.5f * PI, zAxis); -void makeTestFBXJoints(HFMGeometry& geometry) { +void makeTestFBXJoints(HFMModel& hfmModel) { HFMJoint joint; joint.isFree = false; joint.freeLineage.clear(); @@ -60,29 +60,29 @@ void makeTestFBXJoints(HFMGeometry& geometry) { joint.name = "A"; joint.parentIndex = -1; joint.translation = origin; - geometry.joints.push_back(joint); + hfmModel.joints.push_back(joint); joint.name = "B"; joint.parentIndex = 0; joint.translation = xAxis; - geometry.joints.push_back(joint); + hfmModel.joints.push_back(joint); joint.name = "C"; joint.parentIndex = 1; joint.translation = xAxis; - geometry.joints.push_back(joint); + hfmModel.joints.push_back(joint); joint.name = "D"; joint.parentIndex = 2; joint.translation = xAxis; - geometry.joints.push_back(joint); + hfmModel.joints.push_back(joint); // compute each joint's transform - for (int i = 1; i < (int)geometry.joints.size(); ++i) { - HFMJoint& j = geometry.joints[i]; + for (int i = 1; i < (int)hfmModel.joints.size(); ++i) { + HFMJoint& j = hfmModel.joints[i]; int parentIndex = j.parentIndex; // World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1) - j.transform = geometry.joints[parentIndex].transform * + j.transform = hfmModel.joints[parentIndex].transform * glm::translate(j.translation) * j.preTransform * glm::mat4_cast(j.preRotation * j.rotation * j.postRotation) * @@ -96,12 +96,12 @@ void AnimInverseKinematicsTests::testSingleChain() { AnimContext context(false, false, false, glm::mat4(), glm::mat4()); - HFMGeometry geometry; - makeTestFBXJoints(geometry); + HFMModel hfmModel; + makeTestFBXJoints(hfmModel); // create a skeleton and doll AnimPose offset; - AnimSkeleton::Pointer skeletonPtr = std::make_shared(geometry); + AnimSkeleton::Pointer skeletonPtr = std::make_shared(hfmModel); AnimInverseKinematics ikDoll("doll"); ikDoll.setSkeleton(skeletonPtr); @@ -119,7 +119,7 @@ void AnimInverseKinematicsTests::testSingleChain() { poses.push_back(pose); pose.trans() = xAxis; - for (int i = 1; i < (int)geometry.joints.size(); ++i) { + for (int i = 1; i < (int)hfmModel.joints.size(); ++i) { poses.push_back(pose); } ikDoll.loadPoses(poses); diff --git a/tools/skeleton-dump/src/SkeletonDumpApp.cpp b/tools/skeleton-dump/src/SkeletonDumpApp.cpp index 5107931da1..10b13aef36 100644 --- a/tools/skeleton-dump/src/SkeletonDumpApp.cpp +++ b/tools/skeleton-dump/src/SkeletonDumpApp.cpp @@ -54,7 +54,7 @@ SkeletonDumpApp::SkeletonDumpApp(int argc, char* argv[]) : QCoreApplication(argc return; } QByteArray blob = file.readAll(); - std::unique_ptr geometry(readFBX(blob, QVariantHash())); + std::unique_ptr geometry(readFBX(blob, QVariantHash())); std::unique_ptr skeleton(new AnimSkeleton(*geometry)); skeleton->dump(verbose); } diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index bb2958e11d..b5367a77ca 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -19,16 +19,16 @@ // FBXReader jumbles the order of the meshes by reading them back out of a hashtable. This will put // them back in the order in which they appeared in the file. -bool HFMGeometryLessThan(const HFMMesh& e1, const HFMMesh& e2) { +bool HFMModelLessThan(const HFMMesh& e1, const HFMMesh& e2) { return e1.meshIndex < e2.meshIndex; } -void reSortHFMGeometryMeshes(HFMGeometry& geometry) { - qSort(geometry.meshes.begin(), geometry.meshes.end(), HFMGeometryLessThan); +void reSortHFMModelMeshes(HFMModel& model) { + qSort(model.meshes.begin(), model.meshes.end(), HFMModelLessThan); } // Read all the meshes from provided FBX file -bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMGeometry& result) { +bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMModel& result) { if (_verbose) { qDebug() << "reading FBX file =" << filename << "..."; } @@ -41,19 +41,19 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMGeometry& result) { } try { QByteArray fbxContents = fbx.readAll(); - HFMGeometry::Pointer geom; + HFMModel::Pointer model; if (filename.toLower().endsWith(".obj")) { bool combineParts = false; - geom = OBJReader().readOBJ(fbxContents, QVariantHash(), combineParts); + model = OBJReader().readOBJ(fbxContents, QVariantHash(), combineParts); } else if (filename.toLower().endsWith(".fbx")) { - geom.reset(readFBX(fbxContents, QVariantHash(), filename)); + model.reset(readFBX(fbxContents, QVariantHash(), filename)); } else { qWarning() << "file has unknown extension" << filename; return false; } - result = *geom; + result = *model; - reSortHFMGeometryMeshes(result); + reSortHFMModelMeshes(result); } catch (const QString& error) { qWarning() << "error reading" << filename << ":" << error; return false; @@ -88,7 +88,7 @@ void getTrianglesInMeshPart(const HFMMeshPart &meshPart, std::vector& trian } } -void vhacd::VHACDUtil::fattenMesh(const HFMMesh& mesh, const glm::mat4& geometryOffset, HFMMesh& result) const { +void vhacd::VHACDUtil::fattenMesh(const HFMMesh& mesh, const glm::mat4& modelOffset, HFMMesh& result) const { // this is used to make meshes generated from a highfield collidable. each triangle // is converted into a tetrahedron and made into its own mesh-part. @@ -104,7 +104,7 @@ void vhacd::VHACDUtil::fattenMesh(const HFMMesh& mesh, const glm::mat4& geometry int indexStartOffset = result.vertices.size(); // new mesh gets the transformed points from the original - glm::mat4 totalTransform = geometryOffset * mesh.modelTransform; + glm::mat4 totalTransform = modelOffset * mesh.modelTransform; for (int i = 0; i < mesh.vertices.size(); i++) { // apply the source mesh's transform to the points glm::vec4 v = totalTransform * glm::vec4(mesh.vertices[i], 1.0f); @@ -288,17 +288,17 @@ float computeDt(uint64_t start) { return (float)(usecTimestampNow() - start) / (float)USECS_PER_SECOND; } -bool vhacd::VHACDUtil::computeVHACD(HFMGeometry& geometry, +bool vhacd::VHACDUtil::computeVHACD(HFMModel& model, VHACD::IVHACD::Parameters params, - HFMGeometry& result, + HFMModel& result, float minimumMeshSize, float maximumMeshSize) { if (_verbose) { - qDebug() << "meshes =" << geometry.meshes.size(); + qDebug() << "meshes =" << model.meshes.size(); } // count the mesh-parts int numParts = 0; - foreach (const HFMMesh& mesh, geometry.meshes) { + foreach (const HFMMesh& mesh, model.meshes) { numParts += mesh.parts.size(); } if (_verbose) { @@ -316,7 +316,7 @@ bool vhacd::VHACDUtil::computeVHACD(HFMGeometry& geometry, int meshIndex = 0; int validPartsFound = 0; - foreach (const HFMMesh& mesh, geometry.meshes) { + foreach (const HFMMesh& mesh, model.meshes) { // find duplicate points int numDupes = 0; @@ -337,7 +337,7 @@ bool vhacd::VHACDUtil::computeVHACD(HFMGeometry& geometry, // each mesh has its own transform to move it to model-space std::vector vertices; - glm::mat4 totalTransform = geometry.offset * mesh.modelTransform; + glm::mat4 totalTransform = model.offset * mesh.modelTransform; foreach (glm::vec3 vertex, mesh.vertices) { vertices.push_back(glm::vec3(totalTransform * glm::vec4(vertex, 1.0f))); } diff --git a/tools/vhacd-util/src/VHACDUtil.h b/tools/vhacd-util/src/VHACDUtil.h index 64e86ed7df..6ef568d170 100644 --- a/tools/vhacd-util/src/VHACDUtil.h +++ b/tools/vhacd-util/src/VHACDUtil.h @@ -27,13 +27,13 @@ namespace vhacd { public: void setVerbose(bool verbose) { _verbose = verbose; } - bool loadFBX(const QString filename, HFMGeometry& result); + bool loadFBX(const QString filename, HFMModel& result); - void fattenMesh(const HFMMesh& mesh, const glm::mat4& gometryOffset, HFMMesh& result) const; + void fattenMesh(const HFMMesh& mesh, const glm::mat4& modelOffset, HFMMesh& result) const; - bool computeVHACD(HFMGeometry& geometry, + bool computeVHACD(HFMModel& model, VHACD::IVHACD::Parameters params, - HFMGeometry& result, + HFMModel& result, float minimumMeshSize, float maximumMeshSize); void getConvexResults(VHACD::IVHACD* convexifier, HFMMesh& resultMesh) const; diff --git a/tools/vhacd-util/src/VHACDUtilApp.cpp b/tools/vhacd-util/src/VHACDUtilApp.cpp index 0941198234..3d675f8baf 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.cpp +++ b/tools/vhacd-util/src/VHACDUtilApp.cpp @@ -36,7 +36,7 @@ QString formatFloat(double n) { } -bool VHACDUtilApp::writeOBJ(QString outFileName, HFMGeometry& geometry, bool outputCentimeters, int whichMeshPart) { +bool VHACDUtilApp::writeOBJ(QString outFileName, HFMModel& hfmModel, bool outputCentimeters, int whichMeshPart) { QFile file(outFileName); if (!file.open(QIODevice::WriteOnly)) { qWarning() << "unable to write to" << outFileName; @@ -56,7 +56,7 @@ bool VHACDUtilApp::writeOBJ(QString outFileName, HFMGeometry& geometry, bool out int vertexIndexOffset = 0; - foreach (const HFMMesh& mesh, geometry.meshes) { + foreach (const HFMMesh& mesh, hfmModel.meshes) { bool verticesHaveBeenOutput = false; foreach (const HFMMeshPart &meshPart, mesh.parts) { if (whichMeshPart >= 0 && nth != (unsigned int) whichMeshPart) { @@ -297,7 +297,7 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } // load the mesh - HFMGeometry fbx; + HFMModel fbx; auto begin = std::chrono::high_resolution_clock::now(); if (!vUtil.loadFBX(inputFilename, fbx)){ _returnCode = VHACD_RETURN_CODE_FAILURE_TO_READ; @@ -358,7 +358,7 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } begin = std::chrono::high_resolution_clock::now(); - HFMGeometry result; + HFMModel result; bool success = vUtil.computeVHACD(fbx, params, result, minimumMeshSize, maximumMeshSize); end = std::chrono::high_resolution_clock::now(); @@ -398,7 +398,7 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } if (fattenFaces) { - HFMGeometry newFbx; + HFMModel newFbx; HFMMesh result; // count the mesh-parts diff --git a/tools/vhacd-util/src/VHACDUtilApp.h b/tools/vhacd-util/src/VHACDUtilApp.h index 3db49456a0..72e8cfb7e4 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.h +++ b/tools/vhacd-util/src/VHACDUtilApp.h @@ -28,7 +28,7 @@ public: VHACDUtilApp(int argc, char* argv[]); ~VHACDUtilApp(); - bool writeOBJ(QString outFileName, HFMGeometry& geometry, bool outputCentimeters, int whichMeshPart = -1); + bool writeOBJ(QString outFileName, HFMModel& model, bool outputCentimeters, int whichMeshPart = -1); int getReturnCode() const { return _returnCode; } From 9ca862ad6bdaa6f6822976362df18b14906ffd60 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 1 Nov 2018 17:22:47 -0700 Subject: [PATCH 316/362] Case 19373 - Lowering volume on another user in PAL mutes them --- assignment-client/src/audio/AudioMixerClientData.cpp | 2 +- assignment-client/src/audio/AudioMixerClientData.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 7e1420ef60..7f0838d8a5 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -202,7 +202,7 @@ void AudioMixerClientData::parsePerAvatarGainSet(ReceivedMessage& message, const } } -void AudioMixerClientData::setGainForAvatar(QUuid nodeID, uint8_t gain) { +void AudioMixerClientData::setGainForAvatar(QUuid nodeID, float gain) { auto it = std::find_if(_streams.active.cbegin(), _streams.active.cend(), [nodeID](const MixableStream& mixableStream){ return mixableStream.nodeStreamID.nodeID == nodeID && mixableStream.nodeStreamID.streamID.isNull(); }); diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 610b258789..0a66a8164b 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -172,7 +172,7 @@ private: void optionallyReplicatePacket(ReceivedMessage& packet, const Node& node); - void setGainForAvatar(QUuid nodeID, uint8_t gain); + void setGainForAvatar(QUuid nodeID, float gain); bool containsValidPosition(ReceivedMessage& message) const; From 917fc4ad58c5fa368f2d429f86789f7445973827 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 1 Nov 2018 16:50:37 -0700 Subject: [PATCH 317/362] Update language --- .../resources/qml/hifi/commerce/common/CommerceLightbox.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml index 5ddfa98923..5901adc484 100644 --- a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml +++ b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml @@ -33,7 +33,7 @@ Rectangle { property string buttonLayout: "leftright"; readonly property string securityPicBodyText: "When you see your Security Pic, your actions and data are securely making use of your " + - "Wallet's private keys.

You can change your Security Pic in your Wallet."; + "private keys.

You can change your Security Pic via Settings > Security..."; id: root; visible: false; From 2b50c40224dcc3da5e29e5f8f7c2c93220cdef26 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 2 Nov 2018 09:33:06 -0700 Subject: [PATCH 318/362] Minor changes found by Chang --- .../qml/hifi/commerce/checkout/Checkout.qml | 10 ++-- .../InspectionCertificate.qml | 53 ++++++++++--------- scripts/system/marketplaces/marketplaces.js | 2 +- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 95b040eceb..db030d2fee 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -937,9 +937,9 @@ Rectangle { } } - // "Continue Shopping" button + // "Continue" button HifiControlsUit.Button { - id: continueShoppingButton; + id: continueButton; color: hifi.buttons.noneBorderlessGray; colorScheme: hifi.colorSchemes.light; anchors.bottom: parent.bottom; @@ -947,9 +947,9 @@ Rectangle { anchors.right: parent.right; width: 193; height: 44; - text: "Continue Shopping"; + text: "Continue"; onClicked: { - sendToScript({method: 'checkout_continueShopping', itemId: itemId}); + sendToScript({method: 'checkout_continue', itemId: itemId}); } } } @@ -1038,7 +1038,7 @@ Rectangle { width: parent.width/2 - anchors.leftMargin*2; text: "Back to Marketplace"; onClicked: { - sendToScript({method: 'checkout_continueShopping', itemId: itemId}); + sendToScript({method: 'checkout_continue', itemId: itemId}); } } } diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml index d24344b40a..734292d774 100644 --- a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -28,7 +28,7 @@ Rectangle { property string itemName: "--"; property string itemOwner: "--"; property string itemEdition: "--"; - property string dateOfPurchase: "--"; + property string dateAcquired: "--"; property string itemCost: "--"; property string certTitleTextColor: hifi.colors.darkGray; property string certTextColor: hifi.colors.white; @@ -64,7 +64,7 @@ Rectangle { root.itemName = ""; root.itemEdition = ""; root.itemOwner = ""; - root.dateOfPurchase = ""; + root.dateAcquired = ""; root.itemCost = ""; errorText.text = "Information about this certificate is currently unavailable. Please try again later."; } @@ -77,8 +77,9 @@ Rectangle { // "\u2022" is the Unicode character 'BULLET' - it's what's used in password fields on the web, etc root.itemOwner = root.isMyCert ? Account.username : "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"; - root.dateOfPurchase = root.isMyCert ? getFormattedDate(result.data.transfer_created_at * 1000) : "Undisclosed"; - root.itemCost = (root.isMyCert && result.data.cost !== undefined) ? result.data.cost : "Undisclosed"; + root.dateAcquired = root.isMyCert ? getFormattedDate(result.data.transfer_created_at * 1000) : "Undisclosed"; + root.itemCost = (root.isMyCert && result.data.cost !== undefined) ? + (parseInt(result.data.cost) > 0 ? result.data.cost : "Free") : "Undisclosed"; } if (root.certInfoReplaceMode > 4) { root.itemEdition = result.data.edition_number + "/" + (result.data.limited_run === -1 ? "\u221e" : result.data.limited_run); @@ -86,7 +87,7 @@ Rectangle { if (root.certificateStatus === 4) { // CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED if (root.isMyCert) { - errorText.text = "This item is an uncertified copy of an item you purchased."; + errorText.text = "This item is an uncertified copy of an item you acquired."; } else { errorText.text = "The person who placed this item doesn't own it."; } @@ -102,8 +103,8 @@ Rectangle { showInMarketplaceButton.visible = false; // "Edition" text previously set above in this function // "Owner" text previously set above in this function - // "Purchase Date" text previously set above in this function - // "Purchase Price" text previously set above in this function + // "Date Acquired" text previously set above in this function + // "Original Price" text previously set above in this function if (result.data.invalid_reason) { errorText.text = result.data.invalid_reason; } @@ -117,8 +118,8 @@ Rectangle { showInMarketplaceButton.visible = true; // "Edition" text previously set above in this function // "Owner" text previously set above in this function - // "Purchase Date" text previously set above in this function - // "Purchase Price" text previously set above in this function + // "Date Acquired" text previously set above in this function + // "Original Price" text previously set above in this function errorText.text = "The status of this item is still pending confirmation. If the purchase is not confirmed, " + "this entity will be cleaned up by the domain."; } @@ -145,8 +146,8 @@ Rectangle { // "Item Name" text will be set in "onCertificateInfoResult()" // "Edition" text will be set in "onCertificateInfoResult()" // "Owner" text will be set in "onCertificateInfoResult()" - // "Purchase Date" text will be set in "onCertificateInfoResult()" - // "Purchase Price" text will be set in "onCertificateInfoResult()" + // "Date Acquired" text will be set in "onCertificateInfoResult()" + // "Original Price" text will be set in "onCertificateInfoResult()" errorText.text = ""; } else if (root.certificateStatus === 2) { // CERTIFICATE_STATUS_VERIFICATION_TIMEOUT root.useGoldCert = false; @@ -160,7 +161,7 @@ Rectangle { root.itemName = ""; root.itemEdition = ""; root.itemOwner = ""; - root.dateOfPurchase = ""; + root.dateAcquired = ""; root.itemCost = ""; errorText.text = "Your request to inspect this item timed out. Please try again later."; } else if (root.certificateStatus === 3) { // CERTIFICATE_STATUS_STATIC_VERIFICATION_FAILED @@ -175,8 +176,8 @@ Rectangle { // "Item Name" text will be set in "onCertificateInfoResult()" // "Edition" text will be set in "onCertificateInfoResult()" // "Owner" text will be set in "onCertificateInfoResult()" - // "Purchase Date" text will be set in "onCertificateInfoResult()" - // "Purchase Price" text will be set in "onCertificateInfoResult()" + // "Date Acquired" text will be set in "onCertificateInfoResult()" + // "Original Price" text will be set in "onCertificateInfoResult()" errorText.text = "The information associated with this item has been modified and it no longer matches the original certified item."; } else if (root.certificateStatus === 4) { // CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED root.useGoldCert = false; @@ -190,8 +191,8 @@ Rectangle { // "Item Name" text will be set in "onCertificateInfoResult()" root.itemEdition = "Uncertified Copy" // "Owner" text will be set in "onCertificateInfoResult()" - // "Purchase Date" text will be set in "onCertificateInfoResult()" - // "Purchase Price" text will be set in "onCertificateInfoResult()" + // "Date Acquired" text will be set in "onCertificateInfoResult()" + // "Original Price" text will be set in "onCertificateInfoResult()" // "Error Text" text will be set in "onCertificateInfoResult()" } else { console.log("Unknown certificate status received from ledger signal!"); @@ -485,8 +486,8 @@ Rectangle { } RalewayRegular { - id: dateOfPurchaseHeader; - text: "PURCHASE DATE"; + id: dateAcquiredHeader; + text: "DATE ACQUIRED"; // Text size size: 16; // Anchors @@ -500,15 +501,15 @@ Rectangle { color: hifi.colors.darkGray; } AnonymousProRegular { - id: dateOfPurchase; - text: root.dateOfPurchase; + id: dateAcquired; + text: root.dateAcquired; // Text size size: 18; // Anchors - anchors.top: dateOfPurchaseHeader.bottom; + anchors.top: dateAcquiredHeader.bottom; anchors.topMargin: 8; - anchors.left: dateOfPurchaseHeader.left; - anchors.right: dateOfPurchaseHeader.right; + anchors.left: dateAcquiredHeader.left; + anchors.right: dateAcquiredHeader.right; height: paintedHeight; // Style color: root.infoTextColor; @@ -516,7 +517,7 @@ Rectangle { RalewayRegular { id: priceHeader; - text: "PURCHASE PRICE"; + text: "ORIGINAL PRICE"; // Text size size: 16; // Anchors @@ -530,7 +531,7 @@ Rectangle { } HiFiGlyphs { id: hfcGlyph; - visible: priceText.text !== "Undisclosed" && priceText.text !== ""; + visible: priceText.text !== "Undisclosed" && priceText.text !== "" && priceText.text !== "Free"; text: hifi.glyphs.hfc; // Size size: 24; @@ -618,7 +619,7 @@ Rectangle { root.itemName = "--"; root.itemOwner = "--"; root.itemEdition = "--"; - root.dateOfPurchase = "--"; + root.dateAcquired = "--"; root.marketplaceUrl = ""; root.itemCost = "--"; root.isMyCert = false; diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 9f55bade6a..01ab9b5639 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -554,7 +554,7 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'checkout_itemLinkClicked': openMarketplace(message.itemId); break; - case 'checkout_continueShopping': + case 'checkout_continue': openMarketplace(); break; case 'checkout_rezClicked': From b33934ec467005517562025e3548a195741a7a31 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 1 Nov 2018 15:43:56 -0700 Subject: [PATCH 319/362] fixing Image3DOverlay scaling --- interface/src/ui/overlays/Image3DOverlay.cpp | 14 ++++++++-- interface/src/ui/overlays/Image3DOverlay.h | 1 + scripts/system/interstitialPage.js | 29 ++++++++++---------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index 18da15f019..e24e3b3ed8 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -98,8 +98,8 @@ void Image3DOverlay::render(RenderArgs* args) { } float maxSize = glm::max(fromImage.width(), fromImage.height()); - float x = fromImage.width() / (2.0f * maxSize); - float y = -fromImage.height() / (2.0f * maxSize); + float x = _keepAspectRatio ? fromImage.width() / (2.0f * maxSize) : 0.5f; + float y = _keepAspectRatio ? -fromImage.height() / (2.0f * maxSize) : -0.5f; glm::vec2 topLeft(-x, -y); glm::vec2 bottomRight(x, y); @@ -176,6 +176,11 @@ void Image3DOverlay::setProperties(const QVariantMap& properties) { } } + auto keepAspectRatioValue = properties["keepAspectRatio"]; + if (keepAspectRatioValue.isValid()) { + _keepAspectRatio = keepAspectRatioValue.toBool(); + } + auto emissiveValue = properties["emissive"]; if (emissiveValue.isValid()) { _emissive = emissiveValue.toBool(); @@ -225,6 +230,8 @@ void Image3DOverlay::setProperties(const QVariantMap& properties) { * * @property {Vec2} dimensions=1,1 - The dimensions of the overlay. Synonyms: scale, size. * + * @property {bool} keepAspectRatio=true - overlays will maintain the aspect ratio when the subImage is applied. + * * @property {boolean} isFacingAvatar - If true, the overlay is rotated to face the user's camera about an axis * parallel to the user's avatar's "up" direction. * @@ -246,6 +253,9 @@ QVariant Image3DOverlay::getProperty(const QString& property) { if (property == "emissive") { return _emissive; } + if (property == "keepAspectRatio") { + return _keepAspectRatio; + } return Billboard3DOverlay::getProperty(property); } diff --git a/interface/src/ui/overlays/Image3DOverlay.h b/interface/src/ui/overlays/Image3DOverlay.h index 1ffa062d45..1000401abb 100644 --- a/interface/src/ui/overlays/Image3DOverlay.h +++ b/interface/src/ui/overlays/Image3DOverlay.h @@ -58,6 +58,7 @@ private: bool _textureIsLoaded { false }; bool _alphaTexture { false }; bool _emissive { false }; + bool _keepAspectRatio { true }; QRect _fromImage; // where from in the image to sample int _geometryId { 0 }; diff --git a/scripts/system/interstitialPage.js b/scripts/system/interstitialPage.js index e6e8d3d6d6..e2db032d8c 100644 --- a/scripts/system/interstitialPage.js +++ b/scripts/system/interstitialPage.js @@ -16,10 +16,7 @@ Script.include("/~/system/libraries/Xform.js"); Script.include("/~/system/libraries/globals.js"); var DEBUG = false; - var MIN_LOADING_PROGRESS = 3.6; var TOTAL_LOADING_PROGRESS = 3.7; - var FINAL_Y_DIMENSIONS = 2.8; - var BEGIN_Y_DIMENSIONS = 0.03; var EPSILON = 0.05; var TEXTURE_EPSILON = 0.01; var isVisible = false; @@ -191,14 +188,15 @@ localPosition: { x: 0.0, y: -0.86, z: 0.0 }, url: Script.resourcesPath() + "images/loadingBar_progress.png", alpha: 1, - dimensions: { x: 3.8, y: 2.8 }, + dimensions: { x: TOTAL_LOADING_PROGRESS, y: 0.3}, visible: isVisible, emissive: true, ignoreRayIntersection: false, drawInFront: true, grabbable: false, localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 180.0, z: 0.0 }), - parentID: anchorOverlay + parentID: anchorOverlay, + keepAspectRatio: false }); var loadingBarPlacard = Overlays.addOverlay("image3d", { @@ -259,10 +257,11 @@ target = 0; textureMemSizeStabilityCount = 0; textureMemSizeAtLastCheck = 0; - currentProgress = 0.1; + currentProgress = 0.0; connectionToDomainFailed = false; previousCameraMode = Camera.mode; Camera.mode = "first person"; + updateProgressBar(0.0); timer = Script.setTimeout(update, 2000); } } @@ -373,7 +372,7 @@ } } - var currentProgress = 0.1; + var currentProgress = 0.0; function updateOverlays(physicsEnabled) { @@ -396,7 +395,6 @@ }; var loadingBarProperties = { - dimensions: { x: 2.0, y: 2.8 }, visible: !physicsEnabled }; @@ -458,19 +456,22 @@ function updateProgressBar(progress) { var progressPercentage = progress / TOTAL_LOADING_PROGRESS; var subImageWidth = progressPercentage * LOADING_IMAGE_WIDTH_PIXELS; - var subImageWidthPercentage = subImageWidth / LOADING_IMAGE_WIDTH_PIXELS; + var start = TOTAL_LOADING_PROGRESS / 2; + var end = 0; + var xLocalPosition = (progressPercentage * (end - start)) + start; var properties = { - localPosition: { x: (TOTAL_LOADING_PROGRESS / 2) - (currentProgress / 2), y: -0.86, z: 0.0 }, + localPosition: { x: xLocalPosition, y: -0.93, z: 0.0 }, dimensions: { - x: currentProgress, - y: (subImageWidthPercentage * (FINAL_Y_DIMENSIONS - BEGIN_Y_DIMENSIONS)) + BEGIN_Y_DIMENSIONS + x: progress, + y: 0.3 }, + localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 180.0, z: 0.0 }), subImage: { x: 0.0, y: 0.0, width: subImageWidth, - height: 90 + height: 128 } }; @@ -479,7 +480,7 @@ var MAX_TEXTURE_STABILITY_COUNT = 30; var INTERVAL_PROGRESS = 0.04; - var INTERVAL_PROGRESS_PHYSICS_ENABLED = 0.2; + var INTERVAL_PROGRESS_PHYSICS_ENABLED = 0.09; function update() { var renderStats = Render.getConfig("Stats"); var physicsEnabled = Window.isPhysicsEnabled(); From 402c23e359f0a9cc6f4a12e1cc54c10e90d9635e Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 2 Nov 2018 16:07:29 -0700 Subject: [PATCH 320/362] Use useragent instead of queryparam to determine limitedCommerce --- interface/src/Application.cpp | 3 --- interface/src/scripting/WalletScriptingInterface.cpp | 9 ++++++++- interface/src/scripting/WalletScriptingInterface.h | 3 +-- interface/src/ui/overlays/ContextOverlayInterface.cpp | 7 +------ libraries/ui/src/ui/types/RequestFilters.cpp | 9 +++------ scripts/system/avatarapp.js | 2 +- scripts/system/commerce/wallet.js | 3 +-- scripts/system/edit.js | 2 +- scripts/system/html/js/marketplacesInject.js | 6 +++--- scripts/system/marketplaces/marketplace.js | 2 +- scripts/system/marketplaces/marketplaces.js | 5 +---- 11 files changed, 21 insertions(+), 30 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 70c25ce823..ff1822013b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -7881,9 +7881,6 @@ void Application::loadAvatarBrowser() const { auto tablet = dynamic_cast(DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); // construct the url to the marketplace item QString url = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace?category=avatars"; - if (DependencyManager::get()->getLimitedCommerce()) { - url += "&isFree=1"; - } QString MARKETPLACES_INJECT_SCRIPT_PATH = "file:///" + qApp->applicationDirPath() + "/scripts/system/html/js/marketplacesInject.js"; tablet->gotoWebScreen(url, MARKETPLACES_INJECT_SCRIPT_PATH); diff --git a/interface/src/scripting/WalletScriptingInterface.cpp b/interface/src/scripting/WalletScriptingInterface.cpp index 77ca232712..386cb4cba6 100644 --- a/interface/src/scripting/WalletScriptingInterface.cpp +++ b/interface/src/scripting/WalletScriptingInterface.cpp @@ -10,6 +10,7 @@ // #include "WalletScriptingInterface.h" +#include CheckoutProxy::CheckoutProxy(QObject* qmlObject, QObject* parent) : QmlWrapper(qmlObject, parent) { Q_ASSERT(QThread::currentThread() == qApp->thread()); @@ -38,4 +39,10 @@ void WalletScriptingInterface::proveAvatarEntityOwnershipVerification(const QUui } else { qCDebug(entities) << "Failed to prove ownership of:" << entityID << "is not an avatar entity"; } -} \ No newline at end of file +} + +void WalletScriptingInterface::setLimitedCommerce(bool isLimited) { + _limitedCommerce = isLimited; + Setting::Handle limitedCommerceSetting{ "limitedCommerce", false }; + limitedCommerceSetting.set(_limitedCommerce); +} diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h index 8aebb68a47..950fc9262a 100644 --- a/interface/src/scripting/WalletScriptingInterface.h +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -15,7 +15,6 @@ #include #include -#include "scripting/HMDScriptingInterface.h" #include #include #include @@ -68,7 +67,7 @@ public: void setWalletStatus(const uint& status); bool getLimitedCommerce() { return _limitedCommerce; } - void setLimitedCommerce(bool isLimited) { _limitedCommerce = isLimited; } + void setLimitedCommerce(bool isLimited); signals: diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index 9f1018f6c7..11c268dd48 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -223,12 +223,7 @@ bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID& bool ContextOverlayInterface::contextOverlayFilterPassed(const EntityItemID& entityItemID) { EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(entityItemID, _entityPropertyFlags); - Setting::Handle _settingSwitch{ "commerce", true }; - if (_settingSwitch.get()) { - return (entityProperties.getCertificateID().length() != 0); - } else { - return (entityProperties.getMarketplaceID().length() != 0); - } + return (entityProperties.getCertificateID().length() != 0); } bool ContextOverlayInterface::destroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { diff --git a/libraries/ui/src/ui/types/RequestFilters.cpp b/libraries/ui/src/ui/types/RequestFilters.cpp index 7f192d6e52..0e08874251 100644 --- a/libraries/ui/src/ui/types/RequestFilters.cpp +++ b/libraries/ui/src/ui/types/RequestFilters.cpp @@ -84,13 +84,10 @@ void RequestFilters::interceptHFWebEngineRequest(QWebEngineUrlRequestInfo& info, static const QString USER_AGENT = "User-Agent"; const QString tokenStringMobile{ "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36" }; const QString tokenStringMetaverse{ "Chrome/48.0 (HighFidelityInterface)" }; + const QString tokenStringLimitedCommerce{ "Chrome/48.0 (HighFidelityInterface limitedCommerce)" }; + Setting::Handle limitedCommerceSetting{ "limitedCommerce", false }; - // During the period in which we have HFC commerce in the system, but not applied everywhere: - const QString tokenStringCommerce{ "Chrome/48.0 (HighFidelityInterface WithHFC)" }; - Setting::Handle _settingSwitch{ "commerce", true }; - bool isMoney = _settingSwitch.get(); - - const QString tokenString = !isAuthable ? tokenStringMobile : (isMoney ? tokenStringCommerce : tokenStringMetaverse); + const QString tokenString = !isAuthable ? tokenStringMobile : (limitedCommerceSetting.get() ? tokenStringLimitedCommerce : tokenStringMetaverse); info.setHttpHeader(USER_AGENT.toLocal8Bit(), tokenString.toLocal8Bit()); } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 2f05b1b337..44f10396ca 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -160,7 +160,7 @@ var selectedAvatarEntityID = null; var grabbedAvatarEntityChangeNotifier = null; var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; -var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace" + (WalletScriptingInterface.limitedCommerce ? "?isFree=1" : ""); +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("html/js/marketplacesInject.js"); function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index bc7bccd94f..fd230de0e4 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -18,8 +18,7 @@ Script.include("/~/system/libraries/accountUtils.js"); Script.include("/~/system/libraries/connectionUtils.js"); var AppUi = Script.require('appUi'); -var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace" + - (WalletScriptingInterface.limitedCommerce ? "?isFree=1" : ""); + var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace";; // BEGIN AVATAR SELECTOR LOGIC var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 }; diff --git a/scripts/system/edit.js b/scripts/system/edit.js index ee1f5ecbec..6425806771 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -193,7 +193,7 @@ var importingSVOTextOverlay = Overlays.addOverlay("text", { visible: false }); -var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace" + (WalletScriptingInterface.limitedCommerce ? "?isFree=1" : ""); +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; var marketplaceWindow = new OverlayWebWindow({ title: 'Marketplace', source: "about:blank", diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index db433c5058..5cae42e33e 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -60,7 +60,7 @@ ); // Footer. - var isInitialHiFiPage = location.href === (marketplaceBaseURL + "/marketplace" + (limitedCommerce ? "?isFree=1" : "?")); + var isInitialHiFiPage = location.href === (marketplaceBaseURL + "/marketplace?"); $("body").append( '
' + (!isInitialHiFiPage ? '' : '') + @@ -72,7 +72,7 @@ // Footer actions. $("#back-button").on("click", function () { - (document.referrer !== "") ? window.history.back() : window.location = (marketplaceBaseURL + "/marketplace?") + (limitedCommerce ? "isFree=1" : ""); + (document.referrer !== "") ? window.history.back() : window.location = (marketplaceBaseURL + "/marketplace?"); }); $("#all-markets").on("click", function () { EventBridge.emitWebEvent(JSON.stringify({ @@ -93,7 +93,7 @@ window.location = "https://clara.io/library?gameCheck=true&public=true"; }); $('#exploreHifiMarketplace').on('click', function () { - window.location = marketplaceBaseURL + "/marketplace" + (limitedCommerce ? "?isFree=1" : "?"); + window.location = marketplaceBaseURL + "/marketplace?"; }); } diff --git a/scripts/system/marketplaces/marketplace.js b/scripts/system/marketplaces/marketplace.js index 2f749656d3..d90695c767 100644 --- a/scripts/system/marketplaces/marketplace.js +++ b/scripts/system/marketplaces/marketplace.js @@ -15,7 +15,7 @@ Script.include("../libraries/WebTablet.js"); var toolIconUrl = Script.resolvePath("../assets/images/tools/"); -var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace" + (WalletScriptingInterface.limitedCommerce ? "?isFree=1" : ""); +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; var marketplaceWindow = new OverlayWebWindow({ title: "Marketplace", source: "about:blank", diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 01ab9b5639..ed8ed9f288 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -165,9 +165,6 @@ function openMarketplace(optionalItemOrUrl) { if (optionalItemOrUrl && optionalItemOrUrl.indexOf(METAVERSE_SERVER_URL) === -1) { url = MARKETPLACE_URL + '/items/' + optionalItemOrUrl; } - if (WalletScriptingInterface.limitedCommerce) { - url += "?isFree=1"; - } ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL); } @@ -748,7 +745,7 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { var BUTTON_NAME = "MARKET"; -var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace" + (WalletScriptingInterface.limitedCommerce ? "?isFree=1" : ""); +var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; // Append "?" if necessary to signal injected script that it's the initial page. var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + (MARKETPLACE_URL.indexOf("?") > -1 ? "" : "?"); var ui; From c22df4dd947134fb7bd807cd3d843d935bacbb39 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 2 Nov 2018 16:20:35 -0700 Subject: [PATCH 321/362] Fix horizontal and vertical props in entity properties being swapped --- scripts/system/html/js/entityProperties.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 78de0d075a..1ce4626b8f 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -902,13 +902,13 @@ const GROUPS = [ { label: "Horizontal Angle Start", type: "slider", - min: -180, - max: 0, + min: 0, + max: 180, step: 1, decimals: 0, multiplier: DEGREES_TO_RADIANS, unit: "deg", - propertyID: "azimuthStart", + propertyID: "polarStart", }, { label: "Horizontal Angle Finish", @@ -919,18 +919,18 @@ const GROUPS = [ decimals: 0, multiplier: DEGREES_TO_RADIANS, unit: "deg", - propertyID: "azimuthFinish", + propertyID: "polarFinish", }, { label: "Vertical Angle Start", type: "slider", - min: 0, - max: 180, + min: -180, + max: 0, step: 1, decimals: 0, multiplier: DEGREES_TO_RADIANS, unit: "deg", - propertyID: "polarStart", + propertyID: "azimuthStart", }, { label: "Vertical Angle Finish", @@ -941,7 +941,7 @@ const GROUPS = [ decimals: 0, multiplier: DEGREES_TO_RADIANS, unit: "deg", - propertyID: "polarFinish", + propertyID: "azimuthFinish", }, ] }, From d0854ca2ab1219977068a55bb83558a1b0ed546c Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 2 Nov 2018 16:42:18 -0700 Subject: [PATCH 322/362] Use AccountManager instead of settings --- interface/src/scripting/WalletScriptingInterface.cpp | 11 +++++------ interface/src/scripting/WalletScriptingInterface.h | 8 +++++--- libraries/networking/src/AccountManager.cpp | 4 ++++ libraries/networking/src/AccountManager.h | 6 ++++++ libraries/ui/src/ui/types/RequestFilters.cpp | 5 ++--- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/interface/src/scripting/WalletScriptingInterface.cpp b/interface/src/scripting/WalletScriptingInterface.cpp index 386cb4cba6..93ee60ba5b 100644 --- a/interface/src/scripting/WalletScriptingInterface.cpp +++ b/interface/src/scripting/WalletScriptingInterface.cpp @@ -16,6 +16,11 @@ CheckoutProxy::CheckoutProxy(QObject* qmlObject, QObject* parent) : QmlWrapper(q Q_ASSERT(QThread::currentThread() == qApp->thread()); } +WalletScriptingInterface::WalletScriptingInterface() { + connect(DependencyManager::get().data(), + &AccountManager::limitedCommerceChanged, this, &WalletScriptingInterface::limitedCommerceChanged); +} + void WalletScriptingInterface::refreshWalletStatus() { auto wallet = DependencyManager::get(); wallet->getWalletStatus(); @@ -40,9 +45,3 @@ void WalletScriptingInterface::proveAvatarEntityOwnershipVerification(const QUui qCDebug(entities) << "Failed to prove ownership of:" << entityID << "is not an avatar entity"; } } - -void WalletScriptingInterface::setLimitedCommerce(bool isLimited) { - _limitedCommerce = isLimited; - Setting::Handle limitedCommerceSetting{ "limitedCommerce", false }; - limitedCommerceSetting.set(_limitedCommerce); -} diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h index 950fc9262a..36ee021b29 100644 --- a/interface/src/scripting/WalletScriptingInterface.h +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -21,6 +21,7 @@ #include "Application.h" #include "commerce/Wallet.h" #include "ui/overlays/ContextOverlayInterface.h" +#include class CheckoutProxy : public QmlWrapper { Q_OBJECT @@ -45,6 +46,8 @@ class WalletScriptingInterface : public QObject, public Dependency { public: + WalletScriptingInterface(); + /**jsdoc * @function WalletScriptingInterface.refreshWalletStatus */ @@ -66,8 +69,8 @@ public: // scripts could cause the Wallet to incorrectly report its status. void setWalletStatus(const uint& status); - bool getLimitedCommerce() { return _limitedCommerce; } - void setLimitedCommerce(bool isLimited); + bool getLimitedCommerce() { return DependencyManager::get()->getLimitedCommerce(); } + void setLimitedCommerce(bool isLimited) { DependencyManager::get()->setLimitedCommerce(isLimited); }; signals: @@ -105,7 +108,6 @@ signals: private: uint _walletStatus; - bool _limitedCommerce = false; }; #endif // hifi_WalletScriptingInterface_h diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index d99c0020da..2d1a0c0afb 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -837,3 +837,7 @@ void AccountManager::handleKeypairGenerationError() { // reset our waiting state for keypair response _isWaitingForKeypairResponse = false; } + +void AccountManager::setLimitedCommerce(bool isLimited) { + _limitedCommerce = isLimited; +} diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index f3b81cf1c9..093ac9a27c 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -98,6 +98,9 @@ public: void removeAccountFromFile(); + bool getLimitedCommerce() { return _limitedCommerce; } + void setLimitedCommerce(bool isLimited); + public slots: void requestAccessToken(const QString& login, const QString& password); void requestAccessTokenWithSteam(QByteArray authSessionTicket); @@ -121,6 +124,7 @@ signals: void loginFailed(); void logoutComplete(); void newKeypair(); + void limitedCommerceChanged(); private slots: void handleKeypairGenerationError(); @@ -150,6 +154,8 @@ private: QByteArray _pendingPrivateKey; QUuid _sessionID { QUuid::createUuid() }; + + bool _limitedCommerce { false }; }; #endif // hifi_AccountManager_h diff --git a/libraries/ui/src/ui/types/RequestFilters.cpp b/libraries/ui/src/ui/types/RequestFilters.cpp index 0e08874251..a3b3b7dc57 100644 --- a/libraries/ui/src/ui/types/RequestFilters.cpp +++ b/libraries/ui/src/ui/types/RequestFilters.cpp @@ -70,9 +70,9 @@ void RequestFilters::interceptHFWebEngineRequest(QWebEngineUrlRequestInfo& info, // check if this is a request to a highfidelity URL bool isAuthable = isAuthableHighFidelityURL(info.requestUrl()); + auto accountManager = DependencyManager::get(); if (isAuthable) { // if we have an access token, add it to the right HTTP header for authorization - auto accountManager = DependencyManager::get(); if (accountManager->hasValidAccessToken()) { static const QString OAUTH_AUTHORIZATION_HEADER = "Authorization"; @@ -85,9 +85,8 @@ void RequestFilters::interceptHFWebEngineRequest(QWebEngineUrlRequestInfo& info, const QString tokenStringMobile{ "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36" }; const QString tokenStringMetaverse{ "Chrome/48.0 (HighFidelityInterface)" }; const QString tokenStringLimitedCommerce{ "Chrome/48.0 (HighFidelityInterface limitedCommerce)" }; - Setting::Handle limitedCommerceSetting{ "limitedCommerce", false }; - const QString tokenString = !isAuthable ? tokenStringMobile : (limitedCommerceSetting.get() ? tokenStringLimitedCommerce : tokenStringMetaverse); + const QString tokenString = !isAuthable ? tokenStringMobile : (accountManager->getLimitedCommerce() ? tokenStringLimitedCommerce : tokenStringMetaverse); info.setHttpHeader(USER_AGENT.toLocal8Bit(), tokenString.toLocal8Bit()); } From a94fd2a1c97350deee39f90b896faa479c62bb6b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 3 Nov 2018 13:14:08 -0700 Subject: [PATCH 323/362] only equip an entity to a hand-controller joint if the hand-controllers are in use, else use avatar hand --- scripts/system/controllers/controllerModules/equipEntity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index 2f343ff84b..12a69d7b27 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -485,7 +485,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa } var handJointIndex; - if (grabData.grabFollowsController) { + if (HMD.mounted && HMD.isHandControllerAvailable() && grabData.grabFollowsController) { handJointIndex = this.controllerJointIndex; } else { handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); From ae51b95ca13f9d3f66a28f565d42c903dfc69276 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Sun, 4 Nov 2018 11:15:46 -0800 Subject: [PATCH 324/362] fix bad tab reference --- interface/resources/qml/hifi/commerce/wallet/Wallet.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index d882ef8d38..97736bf781 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -630,7 +630,7 @@ Rectangle { visible: !walletSetup.visible; color: root.activeView === "help" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; - anchors.left: securityButtonContainer.right; + anchors.left: sendMoneyButtonContainer.right; anchors.bottom: parent.bottom; width: parent.width / tabButtonsContainer.numTabs; From 8ad8398ef3d04f6f9b9f200f53227ea4cb3491d6 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Sun, 4 Nov 2018 11:17:56 -0800 Subject: [PATCH 325/362] remove 'purchase' from login notice. --- scripts/system/html/js/marketplacesInject.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 5cae42e33e..c4a4adebdf 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -170,7 +170,7 @@ var span = document.createElement('span'); span.style = "margin:10px;color:#1b6420;font-size:15px;"; - span.innerHTML = "to purchase items from the Marketplace."; + span.innerHTML = "to get items from the Marketplace."; var xButton = document.createElement('a'); xButton.id = "xButton"; From 100a17a2b9ee8293ca10a30747970ecfc096c1f1 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Sun, 4 Nov 2018 11:25:40 -0800 Subject: [PATCH 326/362] remove wallet reference in update notification --- scripts/system/commerce/wallet.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 641112b6b4..1fbf952dc6 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -582,7 +582,7 @@ function notificationPollCallbackUpdates(updatesArray) { if (!ui.notificationInitialCallbackMade[0]) { message = updatesArray.length + " of your purchased items " + (updatesArray.length === 1 ? "has an update " : "have updates ") + - "available. Open WALLET to update."; + "available. Open ASSETS to update."; ui.notificationDisplayBanner(message); ui.notificationPollCaresAboutSince[0] = true; @@ -590,7 +590,7 @@ function notificationPollCallbackUpdates(updatesArray) { for (var i = 0; i < updatesArray.length; i++) { message = "Update available for \"" + updatesArray[i].base_item_title + "\"." + - "Open WALLET to update."; + "Open ASSETS to update."; ui.notificationDisplayBanner(message); } } From 8906b909eb715fba8e20b1f941a5e9c4f2a7cee6 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Sun, 4 Nov 2018 12:08:29 -0800 Subject: [PATCH 327/362] close wallet app during login so that it doesn't tell to login after login --- scripts/system/commerce/wallet.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 1fbf952dc6..837cf6a78a 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -437,6 +437,7 @@ function fromQml(message) { } break; case 'needsLogIn_loginClicked': + ui.close(); openLoginWindow(); break; case 'disableHmdPreview': From 7115f0e6886783e9aab0d4342695c858218cacdc Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Sun, 4 Nov 2018 12:17:41 -0800 Subject: [PATCH 328/362] update notification refs inventory, not assets --- scripts/system/commerce/wallet.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 8b6fdbd1b1..4fb7ddd40b 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -583,7 +583,7 @@ function notificationPollCallbackUpdates(updatesArray) { if (!ui.notificationInitialCallbackMade[0]) { message = updatesArray.length + " of your purchased items " + (updatesArray.length === 1 ? "has an update " : "have updates ") + - "available. Open ASSETS to update."; + "available. Open INVENTORY to update."; ui.notificationDisplayBanner(message); ui.notificationPollCaresAboutSince[0] = true; @@ -591,7 +591,7 @@ function notificationPollCallbackUpdates(updatesArray) { for (var i = 0; i < updatesArray.length; i++) { message = "Update available for \"" + updatesArray[i].base_item_title + "\"." + - "Open ASSETS to update."; + "Open INVENTORY to update."; ui.notificationDisplayBanner(message); } } From c6cde2d412833ea0e6bdaf06c54c608d8572843a Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 5 Nov 2018 11:13:32 -0800 Subject: [PATCH 329/362] don't assign null Shape to RigidBody already in physics simulation --- libraries/physics/src/ObjectMotionState.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/libraries/physics/src/ObjectMotionState.cpp b/libraries/physics/src/ObjectMotionState.cpp index c814140930..acfb0c9236 100644 --- a/libraries/physics/src/ObjectMotionState.cpp +++ b/libraries/physics/src/ObjectMotionState.cpp @@ -305,15 +305,16 @@ bool ObjectMotionState::handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* } return true; } - } - if (_shape == newShape) { - // the shape didn't actually change, so we clear the DIRTY_SHAPE flag - flags &= ~Simulation::DIRTY_SHAPE; - // and clear the reference we just created - getShapeManager()->releaseShape(_shape); } else { - _body->setCollisionShape(const_cast(newShape)); - setShape(newShape); + if (_shape == newShape) { + // the shape didn't actually change, so we clear the DIRTY_SHAPE flag + flags &= ~Simulation::DIRTY_SHAPE; + // and clear the reference we just created + getShapeManager()->releaseShape(_shape); + } else { + _body->setCollisionShape(const_cast(newShape)); + setShape(newShape); + } } } if (flags & EASY_DIRTY_PHYSICS_FLAGS) { From 9e7b68feadd572f863bf1e03e5a8e1804b1ec11b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 5 Nov 2018 11:14:32 -0800 Subject: [PATCH 330/362] find and remove dangling pointers from _activeStaticBodies on remove --- libraries/physics/src/PhysicsEngine.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 8343919ae1..9e75dec475 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -289,6 +289,12 @@ void PhysicsEngine::processTransaction(PhysicsEngine::Transaction& transaction) bumpAndPruneContacts(object); btRigidBody* body = object->getRigidBody(); if (body) { + if (_activeStaticBodies.size() > 0) { + std::set::iterator itr = _activeStaticBodies.find(body); + if (itr != _activeStaticBodies.end()) { + _activeStaticBodies.erase(itr); + } + } removeDynamicsForBody(body); _dynamicsWorld->removeRigidBody(body); From 228847b5075b2d0ff1997bbc87f3eaa86f752ec1 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 5 Nov 2018 11:44:58 -0800 Subject: [PATCH 331/362] only look in _activeStaticBodies when body is static --- libraries/physics/src/PhysicsEngine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 9e75dec475..bf6e2463e5 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -289,7 +289,7 @@ void PhysicsEngine::processTransaction(PhysicsEngine::Transaction& transaction) bumpAndPruneContacts(object); btRigidBody* body = object->getRigidBody(); if (body) { - if (_activeStaticBodies.size() > 0) { + if (body->isStaticObject() && _activeStaticBodies.size() > 0) { std::set::iterator itr = _activeStaticBodies.find(body); if (itr != _activeStaticBodies.end()) { _activeStaticBodies.erase(itr); From 30eb360f626c0b3e91fb5fd5c22e708c4093bc80 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 5 Nov 2018 12:07:17 -0800 Subject: [PATCH 332/362] Convert remaining HFMModels called model to hfmModel, and rename some missed local variables --- interface/src/ModelPackager.cpp | 34 ++--- interface/src/ModelPackager.h | 4 +- interface/src/ModelPropertiesDialog.cpp | 20 +-- interface/src/ModelPropertiesDialog.h | 4 +- libraries/animation/src/AnimClip.cpp | 10 +- libraries/animation/src/AnimSkeleton.cpp | 6 +- libraries/animation/src/AnimSkeleton.h | 2 +- .../src/avatars-renderer/SkeletonModel.cpp | 6 +- libraries/baking/src/FBXBaker.cpp | 6 +- libraries/baking/src/FBXBaker.h | 2 +- libraries/baking/src/OBJBaker.cpp | 20 +-- libraries/baking/src/OBJBaker.h | 4 +- libraries/fbx/src/FBXReader.cpp | 138 +++++++++--------- libraries/fbx/src/GLTFReader.cpp | 42 +++--- libraries/fbx/src/GLTFReader.h | 4 +- libraries/fbx/src/OBJReader.cpp | 46 +++--- libraries/fbx/src/OBJReader.h | 2 +- tools/vhacd-util/src/VHACDUtil.cpp | 22 +-- tools/vhacd-util/src/VHACDUtil.h | 2 +- tools/vhacd-util/src/VHACDUtilApp.h | 2 +- 20 files changed, 188 insertions(+), 188 deletions(-) diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index f3a69bbad2..21d3477d7e 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -109,10 +109,10 @@ bool ModelPackager::loadModel() { qCDebug(interfaceapp) << "Reading FBX file : " << _fbxInfo.filePath(); QByteArray fbxContents = fbx.readAll(); - _model.reset(readFBX(fbxContents, QVariantHash(), _fbxInfo.filePath())); + _hfmModel.reset(readFBX(fbxContents, QVariantHash(), _fbxInfo.filePath())); // make sure we have some basic mappings - populateBasicMapping(_mapping, _fbxInfo.filePath(), *_model); + populateBasicMapping(_mapping, _fbxInfo.filePath(), *_hfmModel); } catch (const QString& error) { qCDebug(interfaceapp) << "Error reading " << _fbxInfo.filePath() << ": " << error; return false; @@ -122,7 +122,7 @@ bool ModelPackager::loadModel() { bool ModelPackager::editProperties() { // open the dialog to configure the rest - ModelPropertiesDialog properties(_modelType, _mapping, _modelFile.path(), *_model); + ModelPropertiesDialog properties(_modelType, _mapping, _modelFile.path(), *_hfmModel); if (properties.exec() == QDialog::Rejected) { return false; } @@ -235,18 +235,18 @@ bool ModelPackager::zipModel() { return true; } -void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const HFMModel& model) { +void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const HFMModel& hfmModel) { bool isBodyType = _modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL; // mixamo files - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will // be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file - bool likelyMixamoFile = model.applicationName == "mixamo.com" || - (model.blendshapeChannelNames.contains("BrowsDown_Right") && - model.blendshapeChannelNames.contains("MouthOpen") && - model.blendshapeChannelNames.contains("Blink_Left") && - model.blendshapeChannelNames.contains("Blink_Right") && - model.blendshapeChannelNames.contains("Squint_Right")); + bool likelyMixamoFile = hfmModel.applicationName == "mixamo.com" || + (hfmModel.blendshapeChannelNames.contains("BrowsDown_Right") && + hfmModel.blendshapeChannelNames.contains("MouthOpen") && + hfmModel.blendshapeChannelNames.contains("Blink_Left") && + hfmModel.blendshapeChannelNames.contains("Blink_Right") && + hfmModel.blendshapeChannelNames.contains("Squint_Right")); if (!mapping.contains(NAME_FIELD)) { mapping.insert(NAME_FIELD, QFileInfo(filename).baseName()); @@ -268,15 +268,15 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename } QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); if (!joints.contains("jointEyeLeft")) { - joints.insert("jointEyeLeft", model.jointIndices.contains("jointEyeLeft") ? "jointEyeLeft" : - (model.jointIndices.contains("EyeLeft") ? "EyeLeft" : "LeftEye")); + joints.insert("jointEyeLeft", hfmModel.jointIndices.contains("jointEyeLeft") ? "jointEyeLeft" : + (hfmModel.jointIndices.contains("EyeLeft") ? "EyeLeft" : "LeftEye")); } if (!joints.contains("jointEyeRight")) { - joints.insert("jointEyeRight", model.jointIndices.contains("jointEyeRight") ? "jointEyeRight" : - model.jointIndices.contains("EyeRight") ? "EyeRight" : "RightEye"); + joints.insert("jointEyeRight", hfmModel.jointIndices.contains("jointEyeRight") ? "jointEyeRight" : + hfmModel.jointIndices.contains("EyeRight") ? "EyeRight" : "RightEye"); } if (!joints.contains("jointNeck")) { - joints.insert("jointNeck", model.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck"); + joints.insert("jointNeck", hfmModel.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck"); } if (isBodyType) { @@ -296,7 +296,7 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename if (!joints.contains("jointHead")) { const char* topName = likelyMixamoFile ? "HeadTop_End" : "HeadEnd"; - joints.insert("jointHead", model.jointIndices.contains(topName) ? topName : "Head"); + joints.insert("jointHead", hfmModel.jointIndices.contains(topName) ? topName : "Head"); } mapping.insert(JOINT_FIELD, joints); @@ -370,7 +370,7 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename void ModelPackager::listTextures() { _textures.clear(); - foreach (const HFMMaterial mat, _model->materials) { + foreach (const HFMMaterial mat, _hfmModel->materials) { if (!mat.albedoTexture.filename.isEmpty() && mat.albedoTexture.content.isEmpty() && !_textures.contains(mat.albedoTexture.filename)) { _textures << mat.albedoTexture.filename; diff --git a/interface/src/ModelPackager.h b/interface/src/ModelPackager.h index 2ec629a363..ed86f15008 100644 --- a/interface/src/ModelPackager.h +++ b/interface/src/ModelPackager.h @@ -32,7 +32,7 @@ private: bool editProperties(); bool zipModel(); - void populateBasicMapping(QVariantHash& mapping, QString filename, const HFMModel& model); + void populateBasicMapping(QVariantHash& mapping, QString filename, const HFMModel& hfmModel); void listTextures(); bool copyTextures(const QString& oldDir, const QDir& newDir); @@ -44,7 +44,7 @@ private: QString _scriptDir; QVariantHash _mapping; - std::unique_ptr _model; + std::unique_ptr _hfmModel; QStringList _textures; QStringList _scripts; }; diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp index 0b1638a5bc..49c57744a9 100644 --- a/interface/src/ModelPropertiesDialog.cpp +++ b/interface/src/ModelPropertiesDialog.cpp @@ -27,11 +27,11 @@ ModelPropertiesDialog::ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, - const QString& basePath, const HFMModel& model) : + const QString& basePath, const HFMModel& hfmModel) : _modelType(modelType), _originalMapping(originalMapping), _basePath(basePath), -_model(model) +_hfmModel(hfmModel) { setWindowTitle("Set Model Properties"); @@ -108,8 +108,8 @@ QVariantHash ModelPropertiesDialog::getMapping() const { // update the joint indices QVariantHash jointIndices; - for (int i = 0; i < _model.joints.size(); i++) { - jointIndices.insert(_model.joints.at(i).name, QString::number(i)); + for (int i = 0; i < _hfmModel.joints.size(); i++) { + jointIndices.insert(_hfmModel.joints.at(i).name, QString::number(i)); } mapping.insert(JOINT_INDEX_FIELD, jointIndices); @@ -118,10 +118,10 @@ QVariantHash ModelPropertiesDialog::getMapping() const { if (_modelType == FSTReader::ATTACHMENT_MODEL) { glm::vec3 pivot; if (_pivotAboutCenter->isChecked()) { - pivot = (_model.meshExtents.minimum + _model.meshExtents.maximum) * 0.5f; + pivot = (_hfmModel.meshExtents.minimum + _hfmModel.meshExtents.maximum) * 0.5f; } else if (_pivotJoint->currentIndex() != 0) { - pivot = extractTranslation(_model.joints.at(_pivotJoint->currentIndex() - 1).transform); + pivot = extractTranslation(_hfmModel.joints.at(_pivotJoint->currentIndex() - 1).transform); } mapping.insert(TRANSLATION_X_FIELD, -pivot.x * (float)_scale->value() + (float)_translationX->value()); mapping.insert(TRANSLATION_Y_FIELD, -pivot.y * (float)_scale->value() + (float)_translationY->value()); @@ -191,7 +191,7 @@ void ModelPropertiesDialog::reset() { } foreach (const QVariant& joint, _originalMapping.values(FREE_JOINT_FIELD)) { QString jointName = joint.toString(); - if (_model.jointIndices.contains(jointName)) { + if (_hfmModel.jointIndices.contains(jointName)) { createNewFreeJoint(jointName); } } @@ -249,8 +249,8 @@ QComboBox* ModelPropertiesDialog::createJointBox(bool withNone) const { if (withNone) { box->addItem("(none)"); } - foreach (const HFMJoint& joint, _model.joints) { - if (joint.isSkeletonJoint || !_model.hasSkeletonJoints) { + foreach (const HFMJoint& joint, _hfmModel.joints) { + if (joint.isSkeletonJoint || !_hfmModel.hasSkeletonJoints) { box->addItem(joint.name); } } @@ -266,7 +266,7 @@ QDoubleSpinBox* ModelPropertiesDialog::createTranslationBox() const { } void ModelPropertiesDialog::insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const { - if (_model.jointIndices.contains(name)) { + if (_hfmModel.jointIndices.contains(name)) { joints.insert(joint, name); } else { joints.remove(joint); diff --git a/interface/src/ModelPropertiesDialog.h b/interface/src/ModelPropertiesDialog.h index b979768a2d..0bf8075197 100644 --- a/interface/src/ModelPropertiesDialog.h +++ b/interface/src/ModelPropertiesDialog.h @@ -30,7 +30,7 @@ class ModelPropertiesDialog : public QDialog { public: ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, - const QString& basePath, const HFMModel& model); + const QString& basePath, const HFMModel& hfmModel); QVariantHash getMapping() const; @@ -50,7 +50,7 @@ private: FSTReader::ModelType _modelType; QVariantHash _originalMapping; QString _basePath; - HFMModel _model; + HFMModel _hfmModel; QLineEdit* _name = nullptr; QPushButton* _textureDirectory = nullptr; QPushButton* _scriptDirectory = nullptr; diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 7380ca8a13..eeac8fadce 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -101,8 +101,8 @@ void AnimClip::copyFromNetworkAnim() { // build a mapping from animation joint indices to skeleton joint indices. // by matching joints with the same name. - const HFMModel& model = _networkAnim->getHFMModel(); - AnimSkeleton animSkeleton(model); + const HFMModel& hfmModel = _networkAnim->getHFMModel(); + AnimSkeleton animSkeleton(hfmModel); const auto animJointCount = animSkeleton.getNumJoints(); const auto skeletonJointCount = _skeleton->getNumJoints(); std::vector jointMap; @@ -115,12 +115,12 @@ void AnimClip::copyFromNetworkAnim() { jointMap.push_back(skeletonJoint); } - const int frameCount = model.animationFrames.size(); + const int frameCount = hfmModel.animationFrames.size(); _anim.resize(frameCount); for (int frame = 0; frame < frameCount; frame++) { - const HFMAnimationFrame& hfmAnimFrame = model.animationFrames[frame]; + const HFMAnimationFrame& hfmAnimFrame = hfmModel.animationFrames[frame]; // init all joints in animation to default pose // this will give us a resonable result for bones in the model skeleton but not in the animation. @@ -150,7 +150,7 @@ void AnimClip::copyFromNetworkAnim() { // adjust translation offsets, so large translation animatons on the reference skeleton // will be adjusted when played on a skeleton with short limbs. - const glm::vec3& hfmZeroTrans = model.animationFrames[0].translations[animJoint]; + const glm::vec3& hfmZeroTrans = hfmModel.animationFrames[0].translations[animJoint]; const AnimPose& relDefaultPose = _skeleton->getRelativeDefaultPose(skeletonJoint); float boneLengthScale = 1.0f; const float EPSILON = 0.0001f; diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index bc179739a0..73a0891fbe 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -16,11 +16,11 @@ #include "AnimationLogging.h" -AnimSkeleton::AnimSkeleton(const HFMModel& model) { +AnimSkeleton::AnimSkeleton(const HFMModel& hfmModel) { // convert to std::vector of joints std::vector joints; - joints.reserve(model.joints.size()); - for (auto& joint : model.joints) { + joints.reserve(hfmModel.joints.size()); + for (auto& joint : hfmModel.joints) { joints.push_back(joint); } buildSkeletonFromJoints(joints); diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 80353523d1..3a384388d0 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -23,7 +23,7 @@ public: using Pointer = std::shared_ptr; using ConstPointer = std::shared_ptr; - explicit AnimSkeleton(const HFMModel& model); + explicit AnimSkeleton(const HFMModel& hfmModel); explicit AnimSkeleton(const std::vector& joints); int nameToJointIndex(const QString& jointName) const; const QString& getJointName(int jointIndex) const; diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index 13ee5854bf..36e37dd3d4 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -330,15 +330,15 @@ void SkeletonModel::computeBoundingShape() { return; } - const HFMModel& model = getHFMModel(); - if (model.joints.isEmpty() || model.rootJointIndex == -1) { + const HFMModel& hfmModel = getHFMModel(); + if (hfmModel.joints.isEmpty() || hfmModel.rootJointIndex == -1) { // rootJointIndex == -1 if the avatar model has no skeleton return; } float radius, height; glm::vec3 offset; - _rig.computeAvatarBoundingCapsule(model, radius, height, offset); + _rig.computeAvatarBoundingCapsule(hfmModel, radius, height, offset); float invScale = 1.0f / _owningAvatar->getModelScale(); _boundingCapsuleRadius = invScale * radius; _boundingCapsuleHeight = invScale * height; diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index 9898651268..cef6c9b900 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -206,7 +206,7 @@ void FBXBaker::importScene() { } #endif - _model = reader.extractHFMModel({}, _modelURL.toString()); + _hfmModel = reader.extractHFMModel({}, _modelURL.toString()); _textureContentMap = reader._textureContent; } @@ -231,7 +231,7 @@ void FBXBaker::rewriteAndBakeSceneModels() { for (FBXNode& objectChild : rootChild.children) { if (objectChild.name == "Geometry") { - // TODO Pull this out of _model instead so we don't have to reprocess it + // TODO Pull this out of _hfmModel instead so we don't have to reprocess it auto extractedMesh = FBXReader::extractMesh(objectChild, meshIndex, false); // Callback to get MaterialID @@ -293,7 +293,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { QHash textureTypes; // enumerate the materials in the extracted geometry so we can determine the texture type for each texture ID - for (const auto& material : _model->materials) { + for (const auto& material : _hfmModel->materials) { if (material.normalTexture.isBumpmap) { textureTypes[material.normalTexture.id] = BUMP_TEXTURE; } else { diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 8dfde12b31..2af51b2190 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -53,7 +53,7 @@ private: void rewriteAndBakeSceneModels(); void rewriteAndBakeSceneTextures(); - HFMModel* _model; + HFMModel* _hfmModel; QHash _textureNameMatchCount; QHash _remappedTexturePaths; diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index 67bd2d5d62..d9f56b393e 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -153,7 +153,7 @@ void OBJBaker::bakeOBJ() { checkIfTexturesFinished(); } -void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& model) { +void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { // Generating FBX Header Node FBXNode headerNode; headerNode.name = FBX_HEADER_EXTENSION; @@ -199,7 +199,7 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& model) { // Compress the mesh information and store in dracoNode bool hasDeformers = false; // No concept of deformers for an OBJ FBXNode dracoNode; - compressMesh(model.meshes[0], hasDeformers, dracoNode); + compressMesh(hfmModel.meshes[0], hasDeformers, dracoNode); geometryNode.children.append(dracoNode); // Generating Object node's child - Model node @@ -214,17 +214,17 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& model) { objectNode.children = { geometryNode, modelNode }; // Generating Objects node's child - Material node - auto& meshParts = model.meshes[0].parts; + auto& meshParts = hfmModel.meshes[0].parts; for (auto& meshPart : meshParts) { FBXNode materialNode; materialNode.name = MATERIAL_NODE_NAME; - if (model.materials.size() == 1) { + if (hfmModel.materials.size() == 1) { // case when no material information is provided, OBJReader considers it as a single default material - for (auto& materialID : model.materials.keys()) { - setMaterialNodeProperties(materialNode, materialID, model); + for (auto& materialID : hfmModel.materials.keys()) { + setMaterialNodeProperties(materialNode, materialID, hfmModel); } } else { - setMaterialNodeProperties(materialNode, meshPart.materialID, model); + setMaterialNodeProperties(materialNode, meshPart.materialID, hfmModel); } objectNode.children.append(materialNode); @@ -235,7 +235,7 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& model) { auto size = meshParts.size(); for (int i = 0; i < size; i++) { QString material = meshParts[i].materialID; - HFMMaterial currentMaterial = model.materials[material]; + HFMMaterial currentMaterial = hfmModel.materials[material]; if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) { auto textureID = nextNodeID(); _mapTextureMaterial.emplace_back(textureID, i); @@ -325,12 +325,12 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& model) { } // Set properties for material nodes -void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& model) { +void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& hfmModel) { auto materialID = nextNodeID(); _materialIDs.push_back(materialID); materialNode.properties = { materialID, material, MESH }; - HFMMaterial currentMaterial = model.materials[material]; + HFMMaterial currentMaterial = hfmModel.materials[material]; // Setting the hierarchy: Material -> Properties70 -> P -> Properties FBXNode properties70Node; diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index f889730ffa..5aaae49d4a 100644 --- a/libraries/baking/src/OBJBaker.h +++ b/libraries/baking/src/OBJBaker.h @@ -39,8 +39,8 @@ private slots: private: void loadOBJ(); - void createFBXNodeTree(FBXNode& rootNode, HFMModel& model); - void setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& model); + void createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel); + void setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& hfmModel); NodeID nextNodeID() { return _nodeID++; } diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 15ba5f0101..2cf57e54b4 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -264,17 +264,17 @@ public: }; glm::mat4 getGlobalTransform(const QMultiMap& _connectionParentMap, - const QHash& models, QString nodeID, bool mixamoHack, const QString& url) { + const QHash& fbxModels, QString nodeID, bool mixamoHack, const QString& url) { glm::mat4 globalTransform; QVector visitedNodes; // Used to prevent following a cycle while (!nodeID.isNull()) { visitedNodes.append(nodeID); // Append each node we visit - const FBXModel& model = models.value(nodeID); - globalTransform = glm::translate(model.translation) * model.preTransform * glm::mat4_cast(model.preRotation * - model.rotation * model.postRotation) * model.postTransform * globalTransform; - if (model.hasGeometricOffset) { - glm::mat4 geometricOffset = createMatFromScaleQuatAndPos(model.geometricScaling, model.geometricRotation, model.geometricTranslation); + const FBXModel& fbxModel = fbxModels.value(nodeID); + globalTransform = glm::translate(fbxModel.translation) * fbxModel.preTransform * glm::mat4_cast(fbxModel.preRotation * + fbxModel.rotation * fbxModel.postRotation) * fbxModel.postTransform * globalTransform; + if (fbxModel.hasGeometricOffset) { + glm::mat4 geometricOffset = createMatFromScaleQuatAndPos(fbxModel.geometricScaling, fbxModel.geometricRotation, fbxModel.geometricTranslation); globalTransform = globalTransform * geometricOffset; } @@ -290,7 +290,7 @@ glm::mat4 getGlobalTransform(const QMultiMap& _connectionParen continue; } - if (models.contains(parentID)) { + if (fbxModels.contains(parentID)) { nodeID = parentID; break; } @@ -329,7 +329,7 @@ public: }; void appendModelIDs(const QString& parentID, const QMultiMap& connectionChildMap, - QHash& models, QSet& remainingModels, QVector& modelIDs, bool isRootNode = false) { + QHash& fbxModels, QSet& remainingModels, QVector& modelIDs, bool isRootNode = false) { if (remainingModels.contains(parentID)) { modelIDs.append(parentID); remainingModels.remove(parentID); @@ -337,10 +337,10 @@ void appendModelIDs(const QString& parentID, const QMultiMap& int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; foreach (const QString& childID, connectionChildMap.values(parentID)) { if (remainingModels.contains(childID)) { - FBXModel& model = models[childID]; - if (model.parentIndex == -1) { - model.parentIndex = parentIndex; - appendModelIDs(childID, connectionChildMap, models, remainingModels, modelIDs); + FBXModel& fbxModel = fbxModels[childID]; + if (fbxModel.parentIndex == -1) { + fbxModel.parentIndex = parentIndex; + appendModelIDs(childID, connectionChildMap, fbxModels, remainingModels, modelIDs); } } } @@ -503,7 +503,7 @@ void addBlendshapes(const ExtractedBlendshape& extracted, const QList& connectionParentMap, - const QHash& models, const QString& modelID, const QString& url) { + const QHash& fbxModels, const QString& modelID, const QString& url) { QString topID = modelID; QVector visitedNodes; // Used to prevent following a cycle forever { @@ -515,7 +515,7 @@ QString getTopModelID(const QMultiMap& connectionParentMap, continue; } - if (models.contains(parentID)) { + if (fbxModels.contains(parentID)) { topID = parentID; goto outerContinue; } @@ -689,10 +689,10 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& #if defined(DEBUG_FBXREADER) int unknown = 0; #endif - HFMModel* modelPtr = new HFMModel; - HFMModel& model = *modelPtr; + HFMModel* hfmModelPtr = new HFMModel; + HFMModel& hfmModel = *hfmModelPtr; - model.originalURL = url; + hfmModel.originalURL = url; float unitScaleFactor = 1.0f; glm::vec3 ambientColor; @@ -708,7 +708,7 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& if (subobject.name == "MetaData") { foreach (const FBXNode& subsubobject, subobject.children) { if (subsubobject.name == "Author") { - model.author = subsubobject.properties.at(0).toString(); + hfmModel.author = subsubobject.properties.at(0).toString(); } } } else if (subobject.name == "Properties70") { @@ -716,7 +716,7 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& static const QVariant APPLICATION_NAME = QVariant(QByteArray("Original|ApplicationName")); if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 && subsubobject.properties.at(0) == APPLICATION_NAME) { - model.applicationName = subsubobject.properties.at(4).toString(); + hfmModel.applicationName = subsubobject.properties.at(4).toString(); } } } @@ -1307,7 +1307,7 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& name = name.mid(name.lastIndexOf('.') + 1); } QString id = getID(object.properties); - model.blendshapeChannelNames << name; + hfmModel.blendshapeChannelNames << name; foreach (const WeightedIndex& index, blendshapeIndices.values(name)) { blendshapeChannelIndices.insert(id, index); } @@ -1454,7 +1454,7 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& float offsetScale = mapping.value("scale", 1.0f).toFloat() * unitScaleFactor * METERS_PER_CENTIMETER; glm::quat offsetRotation = glm::quat(glm::radians(glm::vec3(mapping.value("rx").toFloat(), mapping.value("ry").toFloat(), mapping.value("rz").toFloat()))); - model.offset = glm::translate(glm::vec3(mapping.value("tx").toFloat(), mapping.value("ty").toFloat(), + hfmModel.offset = glm::translate(glm::vec3(mapping.value("tx").toFloat(), mapping.value("ty").toFloat(), mapping.value("tz").toFloat())) * glm::mat4_cast(offsetRotation) * glm::scale(glm::vec3(offsetScale, offsetScale, offsetScale)); @@ -1507,12 +1507,12 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& HFMAnimationFrame frame; frame.rotations.resize(modelIDs.size()); frame.translations.resize(modelIDs.size()); - model.animationFrames.append(frame); + hfmModel.animationFrames.append(frame); } // convert the models to joints QVariantList freeJoints = mapping.values("freeJoint"); - model.hasSkeletonJoints = false; + hfmModel.hasSkeletonJoints = false; foreach (const QString& modelID, modelIDs) { const FBXModel& fbxModel = fbxModels[modelID]; HFMJoint joint; @@ -1520,11 +1520,11 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& joint.parentIndex = fbxModel.parentIndex; // get the indices of all ancestors starting with the first free one (if any) - int jointIndex = model.joints.size(); + int jointIndex = hfmModel.joints.size(); joint.freeLineage.append(jointIndex); int lastFreeIndex = joint.isFree ? 0 : -1; - for (int index = joint.parentIndex; index != -1; index = model.joints.at(index).parentIndex) { - if (model.joints.at(index).isFree) { + for (int index = joint.parentIndex; index != -1; index = hfmModel.joints.at(index).parentIndex) { + if (hfmModel.joints.at(index).isFree) { lastFreeIndex = joint.freeLineage.size(); } joint.freeLineage.append(index); @@ -1547,13 +1547,13 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation; if (joint.parentIndex == -1) { - joint.transform = model.offset * glm::translate(joint.translation) * joint.preTransform * + joint.transform = hfmModel.offset * glm::translate(joint.translation) * joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform; joint.inverseDefaultRotation = glm::inverse(combinedRotation); joint.distanceToParent = 0.0f; } else { - const HFMJoint& parentJoint = model.joints.at(joint.parentIndex); + const HFMJoint& parentJoint = hfmModel.joints.at(joint.parentIndex); joint.transform = parentJoint.transform * glm::translate(joint.translation) * joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform; joint.inverseDefaultRotation = glm::inverse(combinedRotation) * parentJoint.inverseDefaultRotation; @@ -1566,15 +1566,15 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& foreach (const QString& childID, _connectionChildMap.values(modelID)) { QString type = typeFlags.value(childID); if (!type.isEmpty()) { - model.hasSkeletonJoints |= (joint.isSkeletonJoint = type.toLower().contains("Skeleton")); + hfmModel.hasSkeletonJoints |= (joint.isSkeletonJoint = type.toLower().contains("Skeleton")); break; } } joint.bindTransformFoundInCluster = false; - model.joints.append(joint); - model.jointIndices.insert(fbxModel.name, model.joints.size()); + hfmModel.joints.append(joint); + hfmModel.jointIndices.insert(fbxModel.name, hfmModel.joints.size()); QString rotationID = localRotations.value(modelID); AnimationCurve xRotCurve = animationCurves.value(xComponents.value(rotationID)); @@ -1590,11 +1590,11 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& glm::vec3 defaultPosValues = joint.translation; for (int i = 0; i < frameCount; i++) { - model.animationFrames[i].rotations[jointIndex] = glm::quat(glm::radians(glm::vec3( + hfmModel.animationFrames[i].rotations[jointIndex] = glm::quat(glm::radians(glm::vec3( xRotCurve.values.isEmpty() ? defaultRotValues.x : xRotCurve.values.at(i % xRotCurve.values.size()), yRotCurve.values.isEmpty() ? defaultRotValues.y : yRotCurve.values.at(i % yRotCurve.values.size()), zRotCurve.values.isEmpty() ? defaultRotValues.z : zRotCurve.values.at(i % zRotCurve.values.size())))); - model.animationFrames[i].translations[jointIndex] = glm::vec3( + hfmModel.animationFrames[i].translations[jointIndex] = glm::vec3( xPosCurve.values.isEmpty() ? defaultPosValues.x : xPosCurve.values.at(i % xPosCurve.values.size()), yPosCurve.values.isEmpty() ? defaultPosValues.y : yPosCurve.values.at(i % yPosCurve.values.size()), zPosCurve.values.isEmpty() ? defaultPosValues.z : zPosCurve.values.at(i % zPosCurve.values.size())); @@ -1603,32 +1603,32 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& // NOTE: shapeVertices are in joint-frame std::vector shapeVertices; - shapeVertices.resize(std::max(1, model.joints.size()) ); + shapeVertices.resize(std::max(1, hfmModel.joints.size()) ); // find our special joints - model.leftEyeJointIndex = modelIDs.indexOf(jointEyeLeftID); - model.rightEyeJointIndex = modelIDs.indexOf(jointEyeRightID); - model.neckJointIndex = modelIDs.indexOf(jointNeckID); - model.rootJointIndex = modelIDs.indexOf(jointRootID); - model.leanJointIndex = modelIDs.indexOf(jointLeanID); - model.headJointIndex = modelIDs.indexOf(jointHeadID); - model.leftHandJointIndex = modelIDs.indexOf(jointLeftHandID); - model.rightHandJointIndex = modelIDs.indexOf(jointRightHandID); - model.leftToeJointIndex = modelIDs.indexOf(jointLeftToeID); - model.rightToeJointIndex = modelIDs.indexOf(jointRightToeID); + hfmModel.leftEyeJointIndex = modelIDs.indexOf(jointEyeLeftID); + hfmModel.rightEyeJointIndex = modelIDs.indexOf(jointEyeRightID); + hfmModel.neckJointIndex = modelIDs.indexOf(jointNeckID); + hfmModel.rootJointIndex = modelIDs.indexOf(jointRootID); + hfmModel.leanJointIndex = modelIDs.indexOf(jointLeanID); + hfmModel.headJointIndex = modelIDs.indexOf(jointHeadID); + hfmModel.leftHandJointIndex = modelIDs.indexOf(jointLeftHandID); + hfmModel.rightHandJointIndex = modelIDs.indexOf(jointRightHandID); + hfmModel.leftToeJointIndex = modelIDs.indexOf(jointLeftToeID); + hfmModel.rightToeJointIndex = modelIDs.indexOf(jointRightToeID); foreach (const QString& id, humanIKJointIDs) { - model.humanIKJointIndices.append(modelIDs.indexOf(id)); + hfmModel.humanIKJointIndices.append(modelIDs.indexOf(id)); } // extract the translation component of the neck transform - if (model.neckJointIndex != -1) { - const glm::mat4& transform = model.joints.at(model.neckJointIndex).transform; - model.neckPivot = glm::vec3(transform[3][0], transform[3][1], transform[3][2]); + if (hfmModel.neckJointIndex != -1) { + const glm::mat4& transform = hfmModel.joints.at(hfmModel.neckJointIndex).transform; + hfmModel.neckPivot = glm::vec3(transform[3][0], transform[3][1], transform[3][2]); } - model.bindExtents.reset(); - model.meshExtents.reset(); + hfmModel.bindExtents.reset(); + hfmModel.meshExtents.reset(); // Create the Material Library consolidateHFMMaterials(mapping); @@ -1664,7 +1664,7 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& } } #endif - model.materials = _hfmMaterials; + hfmModel.materials = _hfmMaterials; // see if any materials have texture children bool materialsHaveTextures = checkMaterialsHaveTextures(_hfmMaterials, _textureFilenames, _connectionChildMap); @@ -1676,13 +1676,13 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& // accumulate local transforms QString modelID = fbxModels.contains(it.key()) ? it.key() : _connectionParentMap.value(it.key()); - glm::mat4 modelTransform = getGlobalTransform(_connectionParentMap, fbxModels, modelID, model.applicationName == "mixamo.com", url); + glm::mat4 modelTransform = getGlobalTransform(_connectionParentMap, fbxModels, modelID, hfmModel.applicationName == "mixamo.com", url); // compute the mesh extents from the transformed vertices foreach (const glm::vec3& vertex, extracted.mesh.vertices) { glm::vec3 transformedVertex = glm::vec3(modelTransform * glm::vec4(vertex, 1.0f)); - model.meshExtents.minimum = glm::min(model.meshExtents.minimum, transformedVertex); - model.meshExtents.maximum = glm::max(model.meshExtents.maximum, transformedVertex); + hfmModel.meshExtents.minimum = glm::min(hfmModel.meshExtents.minimum, transformedVertex); + hfmModel.meshExtents.maximum = glm::max(hfmModel.meshExtents.maximum, transformedVertex); extracted.mesh.meshExtents.minimum = glm::min(extracted.mesh.meshExtents.minimum, transformedVertex); extracted.mesh.meshExtents.maximum = glm::max(extracted.mesh.meshExtents.maximum, transformedVertex); @@ -1763,14 +1763,14 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& extracted.mesh.clusters.append(hfmCluster); // override the bind rotation with the transform link - HFMJoint& joint = model.joints[hfmCluster.jointIndex]; + HFMJoint& joint = hfmModel.joints[hfmCluster.jointIndex]; joint.inverseBindRotation = glm::inverse(extractRotation(cluster.transformLink)); joint.bindTransform = cluster.transformLink; joint.bindTransformFoundInCluster = true; // update the bind pose extents - glm::vec3 bindTranslation = extractTranslation(model.offset * joint.bindTransform); - model.bindExtents.addPoint(bindTranslation); + glm::vec3 bindTranslation = extractTranslation(hfmModel.offset * joint.bindTransform); + hfmModel.bindExtents.addPoint(bindTranslation); } } @@ -1801,14 +1801,14 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& const Cluster& cluster = clusters[clusterID]; const HFMCluster& hfmCluster = extracted.mesh.clusters.at(i); int jointIndex = hfmCluster.jointIndex; - HFMJoint& joint = model.joints[jointIndex]; + HFMJoint& joint = hfmModel.joints[jointIndex]; glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform; glm::vec3 boneEnd = extractTranslation(transformJointToMesh); glm::vec3 boneBegin = boneEnd; glm::vec3 boneDirection; float boneLength = 0.0f; if (joint.parentIndex != -1) { - boneBegin = extractTranslation(inverseModelTransform * model.joints[joint.parentIndex].bindTransform); + boneBegin = extractTranslation(inverseModelTransform * hfmModel.joints[joint.parentIndex].bindTransform); boneDirection = boneEnd - boneBegin; boneLength = glm::length(boneDirection); if (boneLength > EPSILON) { @@ -1882,7 +1882,7 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& } else { // this is a single-mesh joint int jointIndex = firstHFMCluster.jointIndex; - HFMJoint& joint = model.joints[jointIndex]; + HFMJoint& joint = hfmModel.joints[jointIndex]; // transform cluster vertices to joint-frame and save for later glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; @@ -1902,8 +1902,8 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& } buildModelMesh(extracted.mesh, url); - model.meshes.append(extracted.mesh); - int meshIndex = model.meshes.size() - 1; + hfmModel.meshes.append(extracted.mesh); + int meshIndex = hfmModel.meshes.size() - 1; if (extracted.mesh._mesh) { extracted.mesh._mesh->displayName = QString("%1#/mesh/%2").arg(url).arg(meshIndex).toStdString(); extracted.mesh._mesh->modelName = modelIDsToNames.value(modelID).toStdString(); @@ -1923,8 +1923,8 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& }; // now that all joints have been scanned compute a k-Dop bounding volume of mesh - for (int i = 0; i < model.joints.size(); ++i) { - HFMJoint& joint = model.joints[i]; + for (int i = 0; i < hfmModel.joints.size(); ++i) { + HFMJoint& joint = hfmModel.joints[i]; // NOTE: points are in joint-frame ShapeVertices& points = shapeVertices.at(i); @@ -1958,7 +1958,7 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& generateBoundryLinesForDop14(joint.shapeInfo.dots, joint.shapeInfo.avgPoint, joint.shapeInfo.debugLines); } } - model.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); + hfmModel.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); // attempt to map any meshes to a named model for (QHash::const_iterator m = meshIDsToMeshIndices.constBegin(); @@ -1971,14 +1971,14 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& const QString& modelID = ooChildToParent.value(meshID); if (modelIDsToNames.contains(modelID)) { const QString& modelName = modelIDsToNames.value(modelID); - model.meshIndicesToModelNames.insert(meshIndex, modelName); + hfmModel.meshIndicesToModelNames.insert(meshIndex, modelName); } } } { int i = 0; - for (const auto& mesh : model.meshes) { - auto name = model.getModelNameOfMesh(i++); + for (const auto& mesh : hfmModel.meshes) { + auto name = hfmModel.getModelNameOfMesh(i++); if (!name.isEmpty()) { if (mesh._mesh) { mesh._mesh->modelName = name.toStdString(); @@ -1991,7 +1991,7 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& } } } - return modelPtr; + return hfmModelPtr; } HFMModel* readFBX(const QByteArray& data, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { diff --git a/libraries/fbx/src/GLTFReader.cpp b/libraries/fbx/src/GLTFReader.cpp index cc1f3bec1c..9cd43ddf08 100644 --- a/libraries/fbx/src/GLTFReader.cpp +++ b/libraries/fbx/src/GLTFReader.cpp @@ -697,7 +697,7 @@ glm::mat4 GLTFReader::getModelTransform(const GLTFNode& node) { return tmat; } -bool GLTFReader::buildGeometry(HFMModel& model, const QUrl& url) { +bool GLTFReader::buildGeometry(HFMModel& hfmModel, const QUrl& url) { //Build dependencies QVector> nodeDependencies(_file.nodes.size()); @@ -727,17 +727,17 @@ bool GLTFReader::buildGeometry(HFMModel& model, const QUrl& url) { } //Build default joints - model.joints.resize(1); - model.joints[0].isFree = false; - model.joints[0].parentIndex = -1; - model.joints[0].distanceToParent = 0; - model.joints[0].translation = glm::vec3(0, 0, 0); - model.joints[0].rotationMin = glm::vec3(0, 0, 0); - model.joints[0].rotationMax = glm::vec3(0, 0, 0); - model.joints[0].name = "OBJ"; - model.joints[0].isSkeletonJoint = true; + hfmModel.joints.resize(1); + hfmModel.joints[0].isFree = false; + hfmModel.joints[0].parentIndex = -1; + hfmModel.joints[0].distanceToParent = 0; + hfmModel.joints[0].translation = glm::vec3(0, 0, 0); + hfmModel.joints[0].rotationMin = glm::vec3(0, 0, 0); + hfmModel.joints[0].rotationMax = glm::vec3(0, 0, 0); + hfmModel.joints[0].name = "OBJ"; + hfmModel.joints[0].isSkeletonJoint = true; - model.jointIndices["x"] = 1; + hfmModel.jointIndices["x"] = 1; //Build materials QVector materialIDs; @@ -750,8 +750,8 @@ bool GLTFReader::buildGeometry(HFMModel& model, const QUrl& url) { for (int i = 0; i < materialIDs.size(); i++) { QString& matid = materialIDs[i]; - model.materials[matid] = HFMMaterial(); - HFMMaterial& hfmMaterial = model.materials[matid]; + hfmModel.materials[matid] = HFMMaterial(); + HFMMaterial& hfmMaterial = hfmModel.materials[matid]; hfmMaterial._material = std::make_shared(); setHFMMaterial(hfmMaterial, _file.materials[i]); } @@ -765,8 +765,8 @@ bool GLTFReader::buildGeometry(HFMModel& model, const QUrl& url) { if (node.defined["mesh"]) { qCDebug(modelformat) << "node_transforms" << node.transforms; foreach(auto &primitive, _file.meshes[node.mesh].primitives) { - model.meshes.append(HFMMesh()); - HFMMesh& mesh = model.meshes[model.meshes.size() - 1]; + hfmModel.meshes.append(HFMMesh()); + HFMMesh& mesh = hfmModel.meshes[hfmModel.meshes.size() - 1]; HFMCluster cluster; cluster.jointIndex = 0; cluster.inverseBindMatrix = glm::mat4(1, 0, 0, 0, @@ -886,7 +886,7 @@ bool GLTFReader::buildGeometry(HFMModel& model, const QUrl& url) { mesh.meshExtents.reset(); foreach(const glm::vec3& vertex, mesh.vertices) { mesh.meshExtents.addPoint(vertex); - model.meshExtents.addPoint(vertex); + hfmModel.meshExtents.addPoint(vertex); } // since mesh.modelTransform seems to not have any effect I apply the transformation the model @@ -898,7 +898,7 @@ bool GLTFReader::buildGeometry(HFMModel& model, const QUrl& url) { } } - mesh.meshIndex = model.meshes.size(); + mesh.meshIndex = hfmModel.meshes.size(); FBXReader::buildModelMesh(mesh, url.toString()); } @@ -924,13 +924,13 @@ HFMModel* GLTFReader::readGLTF(QByteArray& data, const QVariantHash& mapping, parseGLTF(data); //_file.dump(); - HFMModel* modelPtr = new HFMModel(); - HFMModel& model = *modelPtr; + HFMModel* hfmModelPtr = new HFMModel(); + HFMModel& hfmModel = *hfmModelPtr; - buildGeometry(model, url); + buildGeometry(hfmModel, url); //hfmDebugDump(data); - return modelPtr; + return hfmModelPtr; } diff --git a/libraries/fbx/src/GLTFReader.h b/libraries/fbx/src/GLTFReader.h index 544c85f1c1..44186504f0 100644 --- a/libraries/fbx/src/GLTFReader.h +++ b/libraries/fbx/src/GLTFReader.h @@ -706,7 +706,7 @@ class GLTFReader : public QObject { Q_OBJECT public: GLTFReader(); - HFMModel* readGLTF(QByteArray& model, const QVariantHash& mapping, + HFMModel* readGLTF(QByteArray& data, const QVariantHash& mapping, const QUrl& url, bool loadLightmaps = true, float lightmapLevel = 1.0f); private: GLTFFile _file; @@ -714,7 +714,7 @@ private: glm::mat4 getModelTransform(const GLTFNode& node); - bool buildGeometry(HFMModel& model, const QUrl& url); + bool buildGeometry(HFMModel& hfmModel, const QUrl& url); bool parseGLTF(const QByteArray& data); bool getStringVal(const QJsonObject& object, const QString& fieldname, diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index a37359aa2a..87fae0e965 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -488,10 +488,10 @@ QNetworkReply* request(QUrl& url, bool isTest) { } -bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& model, +bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& hfmModel, float& scaleGuess, bool combineParts) { FaceGroup faces; - HFMMesh& mesh = model.meshes[0]; + HFMMesh& mesh = hfmModel.meshes[0]; mesh.parts.append(HFMMeshPart()); HFMMeshPart& meshPart = mesh.parts.last(); bool sawG = false; @@ -657,36 +657,36 @@ HFMModel::Pointer OBJReader::readOBJ(QByteArray& data, const QVariantHash& mappi QBuffer buffer { &data }; buffer.open(QIODevice::ReadOnly); - auto modelPtr { std::make_shared() }; - HFMModel& model { *modelPtr }; + auto hfmModelPtr { std::make_shared() }; + HFMModel& hfmModel { *hfmModelPtr }; OBJTokenizer tokenizer { &buffer }; float scaleGuess = 1.0f; bool needsMaterialLibrary = false; _url = url; - model.meshExtents.reset(); - model.meshes.append(HFMMesh()); + hfmModel.meshExtents.reset(); + hfmModel.meshes.append(HFMMesh()); try { // call parseOBJGroup as long as it's returning true. Each successful call will // add a new meshPart to the model's single mesh. - while (parseOBJGroup(tokenizer, mapping, model, scaleGuess, combineParts)) {} + while (parseOBJGroup(tokenizer, mapping, hfmModel, scaleGuess, combineParts)) {} - HFMMesh& mesh = model.meshes[0]; + HFMMesh& mesh = hfmModel.meshes[0]; mesh.meshIndex = 0; - model.joints.resize(1); - model.joints[0].isFree = false; - model.joints[0].parentIndex = -1; - model.joints[0].distanceToParent = 0; - model.joints[0].translation = glm::vec3(0, 0, 0); - model.joints[0].rotationMin = glm::vec3(0, 0, 0); - model.joints[0].rotationMax = glm::vec3(0, 0, 0); - model.joints[0].name = "OBJ"; - model.joints[0].isSkeletonJoint = true; + hfmModel.joints.resize(1); + hfmModel.joints[0].isFree = false; + hfmModel.joints[0].parentIndex = -1; + hfmModel.joints[0].distanceToParent = 0; + hfmModel.joints[0].translation = glm::vec3(0, 0, 0); + hfmModel.joints[0].rotationMin = glm::vec3(0, 0, 0); + hfmModel.joints[0].rotationMax = glm::vec3(0, 0, 0); + hfmModel.joints[0].name = "OBJ"; + hfmModel.joints[0].isSkeletonJoint = true; - model.jointIndices["x"] = 1; + hfmModel.jointIndices["x"] = 1; HFMCluster cluster; cluster.jointIndex = 0; @@ -818,13 +818,13 @@ HFMModel::Pointer OBJReader::readOBJ(QByteArray& data, const QVariantHash& mappi mesh.meshExtents.reset(); foreach(const glm::vec3& vertex, mesh.vertices) { mesh.meshExtents.addPoint(vertex); - model.meshExtents.addPoint(vertex); + hfmModel.meshExtents.addPoint(vertex); } // Build the single mesh. FBXReader::buildModelMesh(mesh, url.toString()); - // hfmDebugDump(model); + // hfmDebugDump(hfmModel); } catch(const std::exception& e) { qCDebug(modelformat) << "OBJ reader fail: " << e.what(); } @@ -885,12 +885,12 @@ HFMModel::Pointer OBJReader::readOBJ(QByteArray& data, const QVariantHash& mappi if (!objMaterial.used) { continue; } - model.materials[materialID] = HFMMaterial(objMaterial.diffuseColor, + hfmModel.materials[materialID] = HFMMaterial(objMaterial.diffuseColor, objMaterial.specularColor, objMaterial.emissiveColor, objMaterial.shininess, objMaterial.opacity); - HFMMaterial& hfmMaterial = model.materials[materialID]; + HFMMaterial& hfmMaterial = hfmModel.materials[materialID]; hfmMaterial.materialID = materialID; hfmMaterial._material = std::make_shared(); graphics::MaterialPointer modelMaterial = hfmMaterial._material; @@ -988,7 +988,7 @@ HFMModel::Pointer OBJReader::readOBJ(QByteArray& data, const QVariantHash& mappi modelMaterial->setOpacity(hfmMaterial.opacity); } - return modelPtr; + return hfmModelPtr; } void hfmDebugDump(const HFMModel& hfmModel) { diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 1b259d90df..0088e8e9d7 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -93,7 +93,7 @@ private: QUrl _url; QHash librariesSeen; - bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& model, + bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& hfmModel, float& scaleGuess, bool combineParts); void parseMaterialLibrary(QIODevice* device); void parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions); diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index b5367a77ca..8de9c39da9 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -22,8 +22,8 @@ bool HFMModelLessThan(const HFMMesh& e1, const HFMMesh& e2) { return e1.meshIndex < e2.meshIndex; } -void reSortHFMModelMeshes(HFMModel& model) { - qSort(model.meshes.begin(), model.meshes.end(), HFMModelLessThan); +void reSortHFMModelMeshes(HFMModel& hfmModel) { + qSort(hfmModel.meshes.begin(), hfmModel.meshes.end(), HFMModelLessThan); } @@ -41,17 +41,17 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMModel& result) { } try { QByteArray fbxContents = fbx.readAll(); - HFMModel::Pointer model; + HFMModel::Pointer hfmModel; if (filename.toLower().endsWith(".obj")) { bool combineParts = false; - model = OBJReader().readOBJ(fbxContents, QVariantHash(), combineParts); + hfmModel = OBJReader().readOBJ(fbxContents, QVariantHash(), combineParts); } else if (filename.toLower().endsWith(".fbx")) { - model.reset(readFBX(fbxContents, QVariantHash(), filename)); + hfmModel.reset(readFBX(fbxContents, QVariantHash(), filename)); } else { qWarning() << "file has unknown extension" << filename; return false; } - result = *model; + result = *hfmModel; reSortHFMModelMeshes(result); } catch (const QString& error) { @@ -288,17 +288,17 @@ float computeDt(uint64_t start) { return (float)(usecTimestampNow() - start) / (float)USECS_PER_SECOND; } -bool vhacd::VHACDUtil::computeVHACD(HFMModel& model, +bool vhacd::VHACDUtil::computeVHACD(HFMModel& hfmModel, VHACD::IVHACD::Parameters params, HFMModel& result, float minimumMeshSize, float maximumMeshSize) { if (_verbose) { - qDebug() << "meshes =" << model.meshes.size(); + qDebug() << "meshes =" << hfmModel.meshes.size(); } // count the mesh-parts int numParts = 0; - foreach (const HFMMesh& mesh, model.meshes) { + foreach (const HFMMesh& mesh, hfmModel.meshes) { numParts += mesh.parts.size(); } if (_verbose) { @@ -316,7 +316,7 @@ bool vhacd::VHACDUtil::computeVHACD(HFMModel& model, int meshIndex = 0; int validPartsFound = 0; - foreach (const HFMMesh& mesh, model.meshes) { + foreach (const HFMMesh& mesh, hfmModel.meshes) { // find duplicate points int numDupes = 0; @@ -337,7 +337,7 @@ bool vhacd::VHACDUtil::computeVHACD(HFMModel& model, // each mesh has its own transform to move it to model-space std::vector vertices; - glm::mat4 totalTransform = model.offset * mesh.modelTransform; + glm::mat4 totalTransform = hfmModel.offset * mesh.modelTransform; foreach (glm::vec3 vertex, mesh.vertices) { vertices.push_back(glm::vec3(totalTransform * glm::vec4(vertex, 1.0f))); } diff --git a/tools/vhacd-util/src/VHACDUtil.h b/tools/vhacd-util/src/VHACDUtil.h index 6ef568d170..dd8f606756 100644 --- a/tools/vhacd-util/src/VHACDUtil.h +++ b/tools/vhacd-util/src/VHACDUtil.h @@ -31,7 +31,7 @@ namespace vhacd { void fattenMesh(const HFMMesh& mesh, const glm::mat4& modelOffset, HFMMesh& result) const; - bool computeVHACD(HFMModel& model, + bool computeVHACD(HFMModel& hfmModel, VHACD::IVHACD::Parameters params, HFMModel& result, float minimumMeshSize, float maximumMeshSize); diff --git a/tools/vhacd-util/src/VHACDUtilApp.h b/tools/vhacd-util/src/VHACDUtilApp.h index 72e8cfb7e4..7dadad20b3 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.h +++ b/tools/vhacd-util/src/VHACDUtilApp.h @@ -28,7 +28,7 @@ public: VHACDUtilApp(int argc, char* argv[]); ~VHACDUtilApp(); - bool writeOBJ(QString outFileName, HFMModel& model, bool outputCentimeters, int whichMeshPart = -1); + bool writeOBJ(QString outFileName, HFMModel& hfmModel, bool outputCentimeters, int whichMeshPart = -1); int getReturnCode() const { return _returnCode; } From 515ff016a46c079d234705dc107cb21415a5d8a0 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 5 Nov 2018 13:03:34 -0800 Subject: [PATCH 333/362] Fix seven bugs --- .../qml/hifi/commerce/checkout/Checkout.qml | 7 +---- .../qml/hifi/commerce/wallet/Help.qml | 2 +- .../qml/hifi/commerce/wallet/Wallet.qml | 20 ++++++++------ .../qml/hifi/dialogs/security/Security.qml | 2 +- scripts/system/commerce/wallet.js | 8 +++--- scripts/system/marketplaces/marketplaces.js | 27 ++++++++++++++++--- 6 files changed, 42 insertions(+), 24 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index db030d2fee..271aab87d1 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -240,11 +240,6 @@ Rectangle { lightboxPopup.button1method = function() { lightboxPopup.visible = false; } - lightboxPopup.button2text = "GO TO WALLET"; - lightboxPopup.button2method = function() { - lightboxPopup.visible = false; - sendToScript({method: 'checkout_openWallet'}); - }; lightboxPopup.visible = true; } else { sendToScript(msg); @@ -903,7 +898,7 @@ Rectangle { horizontalAlignment: Text.AlignLeft; verticalAlignment: Text.AlignVCenter; onLinkActivated: { - sendToScript({method: 'checkout_openWallet'}); + sendToScript({method: 'checkout_openRecentActivity'}); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/Help.qml b/interface/resources/qml/hifi/commerce/wallet/Help.qml index 6cf00f78eb..575edfc34d 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Help.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Help.qml @@ -62,7 +62,7 @@ Item { isExpanded: false; question: "How can I get HFC?"; answer: "High Fidelity commerce is in open beta right now. Want more HFC? \ -Get it by going to

BankOfHighFidelity. and meeting with the banker!"; +Get it by going to BankOfHighFidelity and meeting with the banker!"; } ListElement { isExpanded: false; diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 97736bf781..76c2484f0b 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -29,6 +29,7 @@ Rectangle { id: root; property string activeView: "initialize"; + property string initialActiveViewAfterStatus5: "walletInventory"; property bool keyboardRaised: false; property bool isPassword: false; @@ -66,7 +67,7 @@ Rectangle { } } else if (walletStatus === 5) { if (root.activeView !== "walletSetup") { - root.activeView = "walletInventory"; + root.activeView = root.initialActiveViewAfterStatus5; Commerce.getAvailableUpdates(); Commerce.getSecurityImage(); } @@ -526,7 +527,7 @@ Rectangle { id: exchangeMoneyMessagesWaitingLight; visible: parent.messagesWaiting; anchors.right: exchangeMoneyTabIcon.left; - anchors.rightMargin: -4; + anchors.rightMargin: 9; anchors.top: exchangeMoneyTabIcon.top; anchors.topMargin: 16; height: 10; @@ -682,15 +683,12 @@ Rectangle { function resetTabButtonColors() { walletHomeButtonContainer.color = hifi.colors.black; sendMoneyButtonContainer.color = hifi.colors.black; - securityButtonContainer.color = hifi.colors.black; helpButtonContainer.color = hifi.colors.black; exchangeMoneyButtonContainer.color = hifi.colors.black; if (root.activeView === "walletHome") { walletHomeButtonContainer.color = hifi.colors.blueAccent; } else if (root.activeView === "sendMoney") { sendMoneyButtonContainer.color = hifi.colors.blueAccent; - } else if (root.activeView === "security") { - securityButtonContainer.color = hifi.colors.blueAccent; } else if (root.activeView === "help") { helpButtonContainer.color = hifi.colors.blueAccent; } else if (root.activeView == "walletInventory") { @@ -760,10 +758,12 @@ Rectangle { break; case 'updateConnections': sendMoney.updateConnections(message.connections); + walletInventory.fromScript(message); break; case 'selectRecipient': case 'updateSelectedRecipientUsername': sendMoney.fromScript(message); + walletInventory.fromScript(message); break; case 'http.response': http.handleHttpResponse(message); @@ -779,15 +779,19 @@ Rectangle { break; case 'updatePurchases': case 'purchases_showMyItems': - case 'updateConnections': - case 'selectRecipient': - case 'updateSelectedRecipientUsername': case 'updateWearables': walletInventory.fromScript(message); break; case 'updateRecentActivityMessageLight': walletHomeButtonContainer.messagesWaiting = message.messagesWaiting; break; + case 'checkout_openRecentActivity': + if (root.activeView === "initialize") { + root.initialActiveViewAfterStatus5 = "walletHome"; + } else { + root.activeView = "walletHome"; + } + break; default: console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); } diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml index 8baff0ac13..96a554838f 100644 --- a/interface/resources/qml/hifi/dialogs/security/Security.qml +++ b/interface/resources/qml/hifi/dialogs/security/Security.qml @@ -328,7 +328,7 @@ Rectangle { HifiStylesUit.RalewayRegular { text: "Your wallet is not set up.\n" + - "Open the WALLET app to get started."; + "Open the ASSETS app to get started."; // Anchors anchors.fill: parent; // Text size diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 837cf6a78a..033ca638e4 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -18,7 +18,7 @@ Script.include("/~/system/libraries/accountUtils.js"); Script.include("/~/system/libraries/connectionUtils.js"); var AppUi = Script.require('appUi'); - var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace";; +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; // BEGIN AVATAR SELECTOR LOGIC var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 }; @@ -607,13 +607,13 @@ function notificationPollCallbackHistory(historyArray) { if (notificationCount > 0) { var message; if (!ui.notificationInitialCallbackMade[1]) { - message = "You have " + notificationCount + " unread wallet " + - "transaction" + (notificationCount === 1 ? "" : "s") + ". Open WALLET to see all activity."; + message = "You have " + notificationCount + " unread recent " + + "transaction" + (notificationCount === 1 ? "" : "s") + ". Open ASSETS to see all activity."; ui.notificationDisplayBanner(message); } else { for (var i = 0; i < notificationCount; i++) { message = '"' + (historyArray[i].message) + '" ' + - "Open WALLET to see all activity."; + "Open ASSETS to see all activity."; ui.notificationDisplayBanner(message); } } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index ed8ed9f288..8bfd776971 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -527,7 +527,13 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { Window.location = "hifi://BankOfHighFidelity"; } break; - case 'checkout_openWallet': + case 'checkout_openRecentActivity': + ui.open(MARKETPLACE_WALLET_QML_PATH); + wireQmlEventBridge(true); + ui.tablet.sendToQml({ + method: 'checkout_openRecentActivity' + }); + break; case 'checkout_setUpClicked': openWallet(); break; @@ -589,9 +595,6 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { break; case 'updateItemClicked': openMarketplace(message.upgradeUrl + "?edition=" + message.itemEdition); - break; - case 'giftAsset': - break; case 'passphrasePopup_cancelClicked': case 'needsLogIn_cancelClicked': @@ -635,6 +638,20 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'http.request': // Handled elsewhere, don't log. break; + // All of these are handled by wallet.js + case 'purchases_updateWearables': + case 'enable_ChooseRecipientNearbyMode': + case 'disable_ChooseRecipientNearbyMode': + case 'sendAsset_sendPublicly': + case 'refreshConnections': + case 'transactionHistory_goToBank': + case 'purchases_walletNotSetUp': + case 'purchases_openGoTo': + case 'purchases_itemInfoClicked': + case 'purchases_itemCertificateClicked': + case 'clearShouldShowDotHistory': + case 'giftAsset': + break; default: print('Unrecognized message from Checkout.qml: ' + JSON.stringify(message)); } @@ -705,6 +722,8 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { referrerURL: referrerURL, filterText: filterText }); + referrerURL = ""; + filterText = ""; } ui.isOpen = (onMarketplaceScreen || onCommerceScreen) && !onWalletScreen; From f10b9ae30cb234c6528edc91f2721f47355f89f8 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 5 Nov 2018 13:12:43 -0800 Subject: [PATCH 334/362] Squashed commit of the following: commit 515ff016a46c079d234705dc107cb21415a5d8a0 Author: Zach Fox Date: Mon Nov 5 13:03:34 2018 -0800 Fix seven bugs commit a4d36b384551abfa8d83e3ff560b9f271d4aea6b Merge: 8906b909eb d0181283dd Author: Zach Fox Date: Mon Nov 5 10:25:58 2018 -0800 Merge branch 'master' of github.com:highfidelity/hifi into oculus-store-commerce commit d0181283dda0f10856112e0d302258412ede36b3 Merge: be17ed0910 643026abd6 Author: Brad Hefta-Gaub Date: Fri Nov 2 16:44:31 2018 -0700 Merge pull request #14308 from sabrina-shanman/fbx2hfm (case 19302) Re-name FBXGeometry to HFMGeometry and do the same for related classes commit 643026abd61ed38f0907bb65da7693a13110a432 Author: sabrina-shanman Date: Wed Oct 31 14:15:30 2018 -0700 Change local variable in TestFbx for clarity commit 0b7ddca5f66058e5df4fe7d356362f1358dcfcc7 Author: sabrina-shanman Date: Wed Oct 31 14:03:20 2018 -0700 Change naming for straggler names in model loading debug dumps commit becee7f010571d3ffd61deeb062d16a73a06e2a1 Author: sabrina-shanman Date: Tue Oct 30 17:28:42 2018 -0700 Re-name FBXGeometry to HFMGeometry and do the same for related classes --- .../src/avatars/ScriptableAvatar.cpp | 12 +- .../qml/hifi/commerce/checkout/Checkout.qml | 7 +- .../qml/hifi/commerce/wallet/Help.qml | 2 +- .../qml/hifi/commerce/wallet/Wallet.qml | 18 ++- .../qml/hifi/dialogs/security/Security.qml | 2 +- interface/src/ModelPackager.cpp | 4 +- interface/src/ModelPackager.h | 6 +- interface/src/ModelPropertiesDialog.cpp | 4 +- interface/src/ModelPropertiesDialog.h | 4 +- interface/src/avatar/MyAvatar.cpp | 14 +- interface/src/avatar/MySkeletonModel.cpp | 2 +- interface/src/raypick/CollisionPick.cpp | 30 ++-- interface/src/ui/overlays/ModelOverlay.cpp | 22 +-- libraries/animation/src/AnimClip.cpp | 18 +-- libraries/animation/src/AnimSkeleton.cpp | 16 +- libraries/animation/src/AnimSkeleton.h | 8 +- libraries/animation/src/AnimationCache.cpp | 18 +-- libraries/animation/src/AnimationCache.h | 12 +- libraries/animation/src/AnimationObject.cpp | 8 +- libraries/animation/src/AnimationObject.h | 4 +- libraries/animation/src/Rig.cpp | 18 +-- libraries/animation/src/Rig.h | 22 +-- .../src/avatars-renderer/Avatar.cpp | 6 +- .../src/avatars-renderer/SkeletonModel.cpp | 16 +- .../src/avatars-renderer/SkeletonModel.h | 4 +- libraries/baking/src/FBXBaker.cpp | 6 +- libraries/baking/src/FBXBaker.h | 2 +- libraries/baking/src/ModelBaker.cpp | 2 +- libraries/baking/src/ModelBaker.h | 2 +- libraries/baking/src/OBJBaker.cpp | 8 +- libraries/baking/src/OBJBaker.h | 4 +- .../src/RenderableModelEntityItem.cpp | 66 ++++---- libraries/fbx/src/FBX.h | 90 +++++------ libraries/fbx/src/FBXReader.cpp | 150 +++++++++--------- libraries/fbx/src/FBXReader.h | 16 +- libraries/fbx/src/FBXReader_Material.cpp | 40 ++--- libraries/fbx/src/FBXReader_Mesh.cpp | 76 ++++----- libraries/fbx/src/GLTFReader.cpp | 112 ++++++------- libraries/fbx/src/GLTFReader.h | 10 +- libraries/fbx/src/OBJReader.cpp | 96 +++++------ libraries/fbx/src/OBJReader.h | 12 +- .../src/model-networking/ModelCache.cpp | 54 +++---- .../src/model-networking/ModelCache.h | 14 +- .../render-utils/src/CauterizedModel.cpp | 18 +-- .../render-utils/src/MeshPartPayload.cpp | 4 +- libraries/render-utils/src/Model.cpp | 76 ++++----- libraries/render-utils/src/Model.h | 6 +- .../render-utils/src/SoftAttachmentModel.cpp | 6 +- scripts/system/commerce/wallet.js | 8 +- scripts/system/marketplaces/marketplaces.js | 27 +++- tests-manual/gpu/src/TestFbx.cpp | 10 +- tests-manual/gpu/src/TestFbx.h | 2 +- .../src/AnimInverseKinematicsTests.cpp | 8 +- tools/skeleton-dump/src/SkeletonDumpApp.cpp | 4 +- tools/vhacd-util/src/VHACDUtil.cpp | 44 ++--- tools/vhacd-util/src/VHACDUtil.h | 12 +- tools/vhacd-util/src/VHACDUtilApp.cpp | 26 +-- tools/vhacd-util/src/VHACDUtilApp.h | 2 +- 58 files changed, 654 insertions(+), 636 deletions(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index 7d2b267a05..385f94d9f3 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -69,10 +69,10 @@ void ScriptableAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { AvatarData::setSkeletonModelURL(skeletonModelURL); } -static AnimPose composeAnimPose(const FBXJoint& fbxJoint, const glm::quat rotation, const glm::vec3 translation) { +static AnimPose composeAnimPose(const HFMJoint& joint, const glm::quat rotation, const glm::vec3 translation) { glm::mat4 translationMat = glm::translate(translation); - glm::mat4 rotationMat = glm::mat4_cast(fbxJoint.preRotation * rotation * fbxJoint.postRotation); - glm::mat4 finalMat = translationMat * fbxJoint.preTransform * rotationMat * fbxJoint.postTransform; + glm::mat4 rotationMat = glm::mat4_cast(joint.preRotation * rotation * joint.postRotation); + glm::mat4 finalMat = translationMat * joint.preTransform * rotationMat * joint.postTransform; return AnimPose(finalMat); } @@ -93,7 +93,7 @@ void ScriptableAvatar::update(float deltatime) { } _animationDetails.currentFrame = currentFrame; - const QVector& modelJoints = _bind->getGeometry().joints; + const QVector& modelJoints = _bind->getGeometry().joints; QStringList animationJointNames = _animation->getJointNames(); const int nJoints = modelJoints.size(); @@ -102,8 +102,8 @@ void ScriptableAvatar::update(float deltatime) { } const int frameCount = _animation->getFrames().size(); - const FBXAnimationFrame& floorFrame = _animation->getFrames().at((int)glm::floor(currentFrame) % frameCount); - const FBXAnimationFrame& ceilFrame = _animation->getFrames().at((int)glm::ceil(currentFrame) % frameCount); + const HFMAnimationFrame& floorFrame = _animation->getFrames().at((int)glm::floor(currentFrame) % frameCount); + const HFMAnimationFrame& ceilFrame = _animation->getFrames().at((int)glm::ceil(currentFrame) % frameCount); const float frameFraction = glm::fract(currentFrame); std::vector poses = _animSkeleton->getRelativeDefaultPoses(); diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index db030d2fee..271aab87d1 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -240,11 +240,6 @@ Rectangle { lightboxPopup.button1method = function() { lightboxPopup.visible = false; } - lightboxPopup.button2text = "GO TO WALLET"; - lightboxPopup.button2method = function() { - lightboxPopup.visible = false; - sendToScript({method: 'checkout_openWallet'}); - }; lightboxPopup.visible = true; } else { sendToScript(msg); @@ -903,7 +898,7 @@ Rectangle { horizontalAlignment: Text.AlignLeft; verticalAlignment: Text.AlignVCenter; onLinkActivated: { - sendToScript({method: 'checkout_openWallet'}); + sendToScript({method: 'checkout_openRecentActivity'}); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/Help.qml b/interface/resources/qml/hifi/commerce/wallet/Help.qml index 6cf00f78eb..575edfc34d 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Help.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Help.qml @@ -62,7 +62,7 @@ Item { isExpanded: false; question: "How can I get HFC?"; answer: "High Fidelity commerce is in open beta right now. Want more HFC? \ -Get it by going to

BankOfHighFidelity. and meeting with the banker!"; +Get it by going to BankOfHighFidelity and meeting with the banker!"; } ListElement { isExpanded: false; diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index ad472f85f8..1f8490b7c7 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -29,6 +29,7 @@ Rectangle { id: root; property string activeView: "initialize"; + property string initialActiveViewAfterStatus5: "walletInventory"; property bool keyboardRaised: false; property bool isPassword: false; @@ -66,7 +67,7 @@ Rectangle { } } else if (walletStatus === 5) { if (root.activeView !== "walletSetup") { - root.activeView = "walletInventory"; + root.activeView = root.initialActiveViewAfterStatus5; Commerce.getAvailableUpdates(); Commerce.getSecurityImage(); } @@ -688,15 +689,12 @@ Rectangle { function resetTabButtonColors() { walletHomeButtonContainer.color = hifi.colors.black; sendMoneyButtonContainer.color = hifi.colors.black; - securityButtonContainer.color = hifi.colors.black; helpButtonContainer.color = hifi.colors.black; exchangeMoneyButtonContainer.color = hifi.colors.black; if (root.activeView === "walletHome") { walletHomeButtonContainer.color = hifi.colors.blueAccent; } else if (root.activeView === "sendMoney") { sendMoneyButtonContainer.color = hifi.colors.blueAccent; - } else if (root.activeView === "security") { - securityButtonContainer.color = hifi.colors.blueAccent; } else if (root.activeView === "help") { helpButtonContainer.color = hifi.colors.blueAccent; } else if (root.activeView == "walletInventory") { @@ -766,10 +764,12 @@ Rectangle { break; case 'updateConnections': sendMoney.updateConnections(message.connections); + walletInventory.fromScript(message); break; case 'selectRecipient': case 'updateSelectedRecipientUsername': sendMoney.fromScript(message); + walletInventory.fromScript(message); break; case 'http.response': http.handleHttpResponse(message); @@ -785,15 +785,19 @@ Rectangle { break; case 'updatePurchases': case 'purchases_showMyItems': - case 'updateConnections': - case 'selectRecipient': - case 'updateSelectedRecipientUsername': case 'updateWearables': walletInventory.fromScript(message); break; case 'updateRecentActivityMessageLight': walletHomeButtonContainer.messagesWaiting = message.messagesWaiting; break; + case 'checkout_openRecentActivity': + if (root.activeView === "initialize") { + root.initialActiveViewAfterStatus5 = "walletHome"; + } else { + root.activeView = "walletHome"; + } + break; default: console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); } diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml index 8baff0ac13..96a554838f 100644 --- a/interface/resources/qml/hifi/dialogs/security/Security.qml +++ b/interface/resources/qml/hifi/dialogs/security/Security.qml @@ -328,7 +328,7 @@ Rectangle { HifiStylesUit.RalewayRegular { text: "Your wallet is not set up.\n" + - "Open the WALLET app to get started."; + "Open the ASSETS app to get started."; // Anchors anchors.fill: parent; // Text size diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index 3a5d92eb8c..0b2846006b 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -235,7 +235,7 @@ bool ModelPackager::zipModel() { return true; } -void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const FBXGeometry& geometry) { +void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const HFMGeometry& geometry) { bool isBodyType = _modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL; @@ -370,7 +370,7 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename void ModelPackager::listTextures() { _textures.clear(); - foreach (const FBXMaterial mat, _geometry->materials) { + foreach (const HFMMaterial mat, _geometry->materials) { if (!mat.albedoTexture.filename.isEmpty() && mat.albedoTexture.content.isEmpty() && !_textures.contains(mat.albedoTexture.filename)) { _textures << mat.albedoTexture.filename; diff --git a/interface/src/ModelPackager.h b/interface/src/ModelPackager.h index 76295e5a85..b68d9e746d 100644 --- a/interface/src/ModelPackager.h +++ b/interface/src/ModelPackager.h @@ -19,7 +19,7 @@ #include "ui/ModelsBrowser.h" -class FBXGeometry; +class HFMGeometry; class ModelPackager : public QObject { public: @@ -32,7 +32,7 @@ private: bool editProperties(); bool zipModel(); - void populateBasicMapping(QVariantHash& mapping, QString filename, const FBXGeometry& geometry); + void populateBasicMapping(QVariantHash& mapping, QString filename, const HFMGeometry& geometry); void listTextures(); bool copyTextures(const QString& oldDir, const QDir& newDir); @@ -44,7 +44,7 @@ private: QString _scriptDir; QVariantHash _mapping; - std::unique_ptr _geometry; + std::unique_ptr _geometry; QStringList _textures; QStringList _scripts; }; diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp index 8984f89d07..dcda85d117 100644 --- a/interface/src/ModelPropertiesDialog.cpp +++ b/interface/src/ModelPropertiesDialog.cpp @@ -27,7 +27,7 @@ ModelPropertiesDialog::ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, - const QString& basePath, const FBXGeometry& geometry) : + const QString& basePath, const HFMGeometry& geometry) : _modelType(modelType), _originalMapping(originalMapping), _basePath(basePath), @@ -249,7 +249,7 @@ QComboBox* ModelPropertiesDialog::createJointBox(bool withNone) const { if (withNone) { box->addItem("(none)"); } - foreach (const FBXJoint& joint, _geometry.joints) { + foreach (const HFMJoint& joint, _geometry.joints) { if (joint.isSkeletonJoint || !_geometry.hasSkeletonJoints) { box->addItem(joint.name); } diff --git a/interface/src/ModelPropertiesDialog.h b/interface/src/ModelPropertiesDialog.h index e3c2d8ed6a..d1a020bec3 100644 --- a/interface/src/ModelPropertiesDialog.h +++ b/interface/src/ModelPropertiesDialog.h @@ -30,7 +30,7 @@ class ModelPropertiesDialog : public QDialog { public: ModelPropertiesDialog(FSTReader::ModelType modelType, const QVariantHash& originalMapping, - const QString& basePath, const FBXGeometry& geometry); + const QString& basePath, const HFMGeometry& geometry); QVariantHash getMapping() const; @@ -50,7 +50,7 @@ private: FSTReader::ModelType _modelType; QVariantHash _originalMapping; QString _basePath; - FBXGeometry _geometry; + HFMGeometry _geometry; QLineEdit* _name = nullptr; QPushButton* _textureDirectory = nullptr; QPushButton* _scriptDirectory = nullptr; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 3299bd10e7..b3a66f70b8 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -155,7 +155,7 @@ MyAvatar::MyAvatar(QThread* thread) : }); connect(_skeletonModel.get(), &Model::rigReady, this, [this]() { if (_shouldLoadScripts) { - auto geometry = getSkeletonModel()->getFBXGeometry(); + auto geometry = getSkeletonModel()->getHFMGeometry(); qApp->loadAvatarScripts(geometry.scripts); _shouldLoadScripts = false; } @@ -2429,10 +2429,10 @@ void MyAvatar::attachmentDataToEntityProperties(const AttachmentData& data, Enti void MyAvatar::initHeadBones() { int neckJointIndex = -1; if (_skeletonModel->isLoaded()) { - neckJointIndex = _skeletonModel->getFBXGeometry().neckJointIndex; + neckJointIndex = _skeletonModel->getHFMGeometry().neckJointIndex; } if (neckJointIndex == -1) { - neckJointIndex = (_skeletonModel->getFBXGeometry().headJointIndex - 1); + neckJointIndex = (_skeletonModel->getHFMGeometry().headJointIndex - 1); if (neckJointIndex < 0) { // return if the head is not even there. can't cauterize!! return; @@ -2443,7 +2443,7 @@ void MyAvatar::initHeadBones() { q.push(neckJointIndex); _headBoneSet.insert(neckJointIndex); - // fbxJoints only hold links to parents not children, so we have to do a bit of extra work here. + // hfmJoints only hold links to parents not children, so we have to do a bit of extra work here. while (q.size() > 0) { int jointIndex = q.front(); for (int i = 0; i < _skeletonModel->getJointStateCount(); i++) { @@ -2592,11 +2592,11 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { if (_skeletonModel && _skeletonModel->isLoaded()) { const Rig& rig = _skeletonModel->getRig(); - const FBXGeometry& geometry = _skeletonModel->getFBXGeometry(); + const HFMGeometry& geometry = _skeletonModel->getHFMGeometry(); for (int i = 0; i < rig.getJointStateCount(); i++) { AnimPose jointPose; rig.getAbsoluteJointPoseInRigFrame(i, jointPose); - const FBXJointShapeInfo& shapeInfo = geometry.joints[i].shapeInfo; + const HFMJointShapeInfo& shapeInfo = geometry.joints[i].shapeInfo; const AnimPose pose = rigToWorldPose * jointPose; for (size_t j = 0; j < shapeInfo.debugLines.size() / 2; j++) { glm::vec3 pointA = pose.xformPoint(shapeInfo.debugLines[2 * j]); @@ -4012,7 +4012,7 @@ float MyAvatar::getSitStandStateChange() const { } QVector MyAvatar::getScriptUrls() { - QVector scripts = _skeletonModel->isLoaded() ? _skeletonModel->getFBXGeometry().scripts : QVector(); + QVector scripts = _skeletonModel->isLoaded() ? _skeletonModel->getHFMGeometry().scripts : QVector(); return scripts; } diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index c6aae6124a..3ec40d372b 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -90,7 +90,7 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { // Called within Model::simulate call, below. void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); Head* head = _owningAvatar->getHead(); diff --git a/interface/src/raypick/CollisionPick.cpp b/interface/src/raypick/CollisionPick.cpp index 25927c5b68..e8a53aa9b6 100644 --- a/interface/src/raypick/CollisionPick.cpp +++ b/interface/src/raypick/CollisionPick.cpp @@ -131,7 +131,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha // should never fall in here when collision model not fully loaded // TODO: assert that all geometries exist and are loaded //assert(_model && _model->isLoaded() && _compoundShapeResource && _compoundShapeResource->isLoaded()); - const FBXGeometry& collisionGeometry = resource->getFBXGeometry(); + const HFMGeometry& collisionGeometry = resource->getHFMGeometry(); ShapeInfo::PointCollection& pointCollection = shapeInfo.getPointCollection(); pointCollection.clear(); @@ -139,15 +139,15 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha // the way OBJ files get read, each section under a "g" line is its own meshPart. We only expect // to find one actual "mesh" (with one or more meshParts in it), but we loop over the meshes, just in case. - foreach (const FBXMesh& mesh, collisionGeometry.meshes) { + foreach (const HFMMesh& mesh, collisionGeometry.meshes) { // each meshPart is a convex hull - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { pointCollection.push_back(QVector()); ShapeInfo::PointList& pointsInPart = pointCollection[i]; // run through all the triangles and (uniquely) add each point to the hull uint32_t numIndices = (uint32_t)meshPart.triangleIndices.size(); - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices % TRIANGLE_STRIDE == 0); numIndices -= numIndices % TRIANGLE_STRIDE; // WORKAROUND lack of sanity checking in FBXReader @@ -168,7 +168,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha // run through all the quads and (uniquely) add each point to the hull numIndices = (uint32_t)meshPart.quadIndices.size(); - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices % QUAD_STRIDE == 0); numIndices -= numIndices % QUAD_STRIDE; // WORKAROUND lack of sanity checking in FBXReader @@ -206,7 +206,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha // to the visual model and apply them to the collision model (without regard for the // collision model's extents). - glm::vec3 scaleToFit = dimensions / resource->getFBXGeometry().getUnscaledMeshExtents().size(); + glm::vec3 scaleToFit = dimensions / resource->getHFMGeometry().getUnscaledMeshExtents().size(); // multiply each point by scale for (int32_t i = 0; i < pointCollection.size(); i++) { for (int32_t j = 0; j < pointCollection[i].size(); j++) { @@ -216,11 +216,11 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha } shapeInfo.setParams(type, dimensions, resource->getURL().toString()); } else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) { - const FBXGeometry& fbxGeometry = resource->getFBXGeometry(); - int numFbxMeshes = fbxGeometry.meshes.size(); + const HFMGeometry& hfmGeometry = resource->getHFMGeometry(); + int numHFMMeshes = hfmGeometry.meshes.size(); int totalNumVertices = 0; - for (int i = 0; i < numFbxMeshes; i++) { - const FBXMesh& mesh = fbxGeometry.meshes.at(i); + for (int i = 0; i < numHFMMeshes; i++) { + const HFMMesh& mesh = hfmGeometry.meshes.at(i); totalNumVertices += mesh.vertices.size(); } const int32_t MAX_VERTICES_PER_STATIC_MESH = 1e6; @@ -230,7 +230,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha return; } - auto& meshes = resource->getFBXGeometry().meshes; + auto& meshes = resource->getHFMGeometry().meshes; int32_t numMeshes = (int32_t)(meshes.size()); const int MAX_ALLOWED_MESH_COUNT = 1000; @@ -285,12 +285,12 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha if (type == SHAPE_TYPE_STATIC_MESH) { // copy into triangleIndices size_t triangleIndicesCount = 0; - for (const FBXMeshPart& meshPart : mesh.parts) { + for (const HFMMeshPart& meshPart : mesh.parts) { triangleIndicesCount += meshPart.triangleIndices.count(); } triangleIndices.reserve((int)triangleIndicesCount); - for (const FBXMeshPart& meshPart : mesh.parts) { + for (const HFMMeshPart& meshPart : mesh.parts) { const int* indexItr = meshPart.triangleIndices.cbegin(); while (indexItr != meshPart.triangleIndices.cend()) { triangleIndices.push_back(*indexItr); @@ -299,11 +299,11 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha } } else if (type == SHAPE_TYPE_SIMPLE_COMPOUND) { // for each mesh copy unique part indices, separated by special bogus (flag) index values - for (const FBXMeshPart& meshPart : mesh.parts) { + for (const HFMMeshPart& meshPart : mesh.parts) { // collect unique list of indices for this part std::set uniqueIndices; auto numIndices = meshPart.triangleIndices.count(); - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices% TRIANGLE_STRIDE == 0); numIndices -= numIndices % TRIANGLE_STRIDE; // WORKAROUND lack of sanity checking in FBXReader diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index eee8222051..1b66ff08ad 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -446,7 +446,7 @@ QVariant ModelOverlay::getProperty(const QString& property) { if (property == "jointNames") { if (_model && _model->isActive()) { - // note: going through Rig because Model::getJointNames() (which proxies to FBXGeometry) was always empty + // note: going through Rig because Model::getJointNames() (which proxies to HFMGeometry) was always empty const Rig* rig = &(_model->getRig()); return mapJoints([rig](int jointIndex) -> QString { return rig->nameOfJoint(jointIndex); @@ -574,7 +574,7 @@ void ModelOverlay::animate() { QVector jointsData; - const QVector& frames = _animation->getFramesReference(); // NOTE: getFrames() is too heavy + const QVector& frames = _animation->getFramesReference(); // NOTE: getFrames() is too heavy int frameCount = frames.size(); if (frameCount <= 0) { return; @@ -606,10 +606,10 @@ void ModelOverlay::animate() { } QStringList animationJointNames = _animation->getGeometry().getJointNames(); - auto& fbxJoints = _animation->getGeometry().joints; + auto& hfmJoints = _animation->getGeometry().joints; - auto& originalFbxJoints = _model->getFBXGeometry().joints; - auto& originalFbxIndices = _model->getFBXGeometry().jointIndices; + auto& originalHFMJoints = _model->getHFMGeometry().joints; + auto& originalFbxIndices = _model->getHFMGeometry().jointIndices; const QVector& rotations = frames[_lastKnownCurrentFrame].rotations; const QVector& translations = frames[_lastKnownCurrentFrame].translations; @@ -626,23 +626,23 @@ void ModelOverlay::animate() { translationMat = glm::translate(translations[index]); } } else if (index < animationJointNames.size()) { - QString jointName = fbxJoints[index].name; + QString jointName = hfmJoints[index].name; if (originalFbxIndices.contains(jointName)) { // Making sure the joint names exist in the original model the animation is trying to apply onto. If they do, then remap and get its translation. int remappedIndex = originalFbxIndices[jointName] - 1; // JointIndeces seem to always start from 1 and the found index is always 1 higher than actual. - translationMat = glm::translate(originalFbxJoints[remappedIndex].translation); + translationMat = glm::translate(originalHFMJoints[remappedIndex].translation); } } glm::mat4 rotationMat; if (index < rotations.size()) { - rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * rotations[index] * fbxJoints[index].postRotation); + rotationMat = glm::mat4_cast(hfmJoints[index].preRotation * rotations[index] * hfmJoints[index].postRotation); } else { - rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * fbxJoints[index].postRotation); + rotationMat = glm::mat4_cast(hfmJoints[index].preRotation * hfmJoints[index].postRotation); } - glm::mat4 finalMat = (translationMat * fbxJoints[index].preTransform * - rotationMat * fbxJoints[index].postTransform); + glm::mat4 finalMat = (translationMat * hfmJoints[index].preTransform * + rotationMat * hfmJoints[index].postTransform); auto& jointData = jointsData[j]; jointData.translation = extractTranslation(finalMat); jointData.translationIsDefaultPose = false; diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index f9195a608b..d630218165 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -101,7 +101,7 @@ void AnimClip::copyFromNetworkAnim() { // build a mapping from animation joint indices to skeleton joint indices. // by matching joints with the same name. - const FBXGeometry& geom = _networkAnim->getGeometry(); + const HFMGeometry& geom = _networkAnim->getGeometry(); AnimSkeleton animSkeleton(geom); const auto animJointCount = animSkeleton.getNumJoints(); const auto skeletonJointCount = _skeleton->getNumJoints(); @@ -120,7 +120,7 @@ void AnimClip::copyFromNetworkAnim() { for (int frame = 0; frame < frameCount; frame++) { - const FBXAnimationFrame& fbxAnimFrame = geom.animationFrames[frame]; + const HFMAnimationFrame& hfmAnimFrame = geom.animationFrames[frame]; // init all joints in animation to default pose // this will give us a resonable result for bones in the model skeleton but not in the animation. @@ -132,8 +132,8 @@ void AnimClip::copyFromNetworkAnim() { for (int animJoint = 0; animJoint < animJointCount; animJoint++) { int skeletonJoint = jointMap[animJoint]; - const glm::vec3& fbxAnimTrans = fbxAnimFrame.translations[animJoint]; - const glm::quat& fbxAnimRot = fbxAnimFrame.rotations[animJoint]; + const glm::vec3& hfmAnimTrans = hfmAnimFrame.translations[animJoint]; + const glm::quat& hfmAnimRot = hfmAnimFrame.rotations[animJoint]; // skip joints that are in the animation but not in the skeleton. if (skeletonJoint >= 0 && skeletonJoint < skeletonJointCount) { @@ -146,19 +146,19 @@ void AnimClip::copyFromNetworkAnim() { preRot.scale() = glm::vec3(1.0f); postRot.scale() = glm::vec3(1.0f); - AnimPose rot(glm::vec3(1.0f), fbxAnimRot, glm::vec3()); + AnimPose rot(glm::vec3(1.0f), hfmAnimRot, glm::vec3()); // adjust translation offsets, so large translation animatons on the reference skeleton // will be adjusted when played on a skeleton with short limbs. - const glm::vec3& fbxZeroTrans = geom.animationFrames[0].translations[animJoint]; + const glm::vec3& hfmZeroTrans = geom.animationFrames[0].translations[animJoint]; const AnimPose& relDefaultPose = _skeleton->getRelativeDefaultPose(skeletonJoint); float boneLengthScale = 1.0f; const float EPSILON = 0.0001f; - if (fabsf(glm::length(fbxZeroTrans)) > EPSILON) { - boneLengthScale = glm::length(relDefaultPose.trans()) / glm::length(fbxZeroTrans); + if (fabsf(glm::length(hfmZeroTrans)) > EPSILON) { + boneLengthScale = glm::length(relDefaultPose.trans()) / glm::length(hfmZeroTrans); } - AnimPose trans = AnimPose(glm::vec3(1.0f), glm::quat(), relDefaultPose.trans() + boneLengthScale * (fbxAnimTrans - fbxZeroTrans)); + AnimPose trans = AnimPose(glm::vec3(1.0f), glm::quat(), relDefaultPose.trans() + boneLengthScale * (hfmAnimTrans - hfmZeroTrans)); _anim[frame][skeletonJoint] = trans * preRot * rot * postRot; } diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index bed9c590be..fc4114ac7b 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -16,17 +16,17 @@ #include "AnimationLogging.h" -AnimSkeleton::AnimSkeleton(const FBXGeometry& fbxGeometry) { +AnimSkeleton::AnimSkeleton(const HFMGeometry& geometry) { // convert to std::vector of joints - std::vector joints; - joints.reserve(fbxGeometry.joints.size()); - for (auto& joint : fbxGeometry.joints) { + std::vector joints; + joints.reserve(geometry.joints.size()); + for (auto& joint : geometry.joints) { joints.push_back(joint); } buildSkeletonFromJoints(joints); } -AnimSkeleton::AnimSkeleton(const std::vector& joints) { +AnimSkeleton::AnimSkeleton(const std::vector& joints) { buildSkeletonFromJoints(joints); } @@ -166,7 +166,7 @@ void AnimSkeleton::mirrorAbsolutePoses(AnimPoseVec& poses) const { } } -void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) { +void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) { _joints = joints; _jointsSize = (int)joints.size(); // build a cache of bind poses @@ -177,7 +177,7 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) _relativePreRotationPoses.reserve(_jointsSize); _relativePostRotationPoses.reserve(_jointsSize); - // iterate over FBXJoints and extract the bind pose information. + // iterate over HFMJoints and extract the bind pose information. for (int i = 0; i < _jointsSize; i++) { // build pre and post transforms @@ -240,7 +240,7 @@ void AnimSkeleton::dump(bool verbose) const { qCDebug(animation) << " absDefaultPose =" << getAbsoluteDefaultPose(i); qCDebug(animation) << " relDefaultPose =" << getRelativeDefaultPose(i); if (verbose) { - qCDebug(animation) << " fbxJoint ="; + qCDebug(animation) << " hfmJoint ="; qCDebug(animation) << " isFree =" << _joints[i].isFree; qCDebug(animation) << " freeLineage =" << _joints[i].freeLineage; qCDebug(animation) << " parentIndex =" << _joints[i].parentIndex; diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 2ebf3f4f5d..1717d75985 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -23,8 +23,8 @@ public: using Pointer = std::shared_ptr; using ConstPointer = std::shared_ptr; - explicit AnimSkeleton(const FBXGeometry& fbxGeometry); - explicit AnimSkeleton(const std::vector& joints); + explicit AnimSkeleton(const HFMGeometry& geometry); + explicit AnimSkeleton(const std::vector& joints); int nameToJointIndex(const QString& jointName) const; const QString& getJointName(int jointIndex) const; int getNumJoints() const; @@ -64,9 +64,9 @@ public: std::vector lookUpJointIndices(const std::vector& jointNames) const; protected: - void buildSkeletonFromJoints(const std::vector& joints); + void buildSkeletonFromJoints(const std::vector& joints); - std::vector _joints; + std::vector _joints; int _jointsSize { 0 }; AnimPoseVec _relativeDefaultPoses; AnimPoseVec _absoluteDefaultPoses; diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index 04b7952ddb..b5b27c3a24 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -69,7 +69,7 @@ void AnimationReader::run() { if (urlValid) { // Parse the FBX directly from the QNetworkReply - FBXGeometry::Pointer fbxgeo; + HFMGeometry::Pointer fbxgeo; if (_url.path().toLower().endsWith(".fbx")) { fbxgeo.reset(readFBX(_data, QVariantHash(), _url.path())); } else { @@ -100,40 +100,40 @@ QStringList Animation::getJointNames() const { } QStringList names; if (_geometry) { - foreach (const FBXJoint& joint, _geometry->joints) { + foreach (const HFMJoint& joint, _geometry->joints) { names.append(joint.name); } } return names; } -QVector Animation::getFrames() const { +QVector Animation::getFrames() const { if (QThread::currentThread() != thread()) { - QVector result; + QVector result; BLOCKING_INVOKE_METHOD(const_cast(this), "getFrames", - Q_RETURN_ARG(QVector, result)); + Q_RETURN_ARG(QVector, result)); return result; } if (_geometry) { return _geometry->animationFrames; } else { - return QVector(); + return QVector(); } } -const QVector& Animation::getFramesReference() const { +const QVector& Animation::getFramesReference() const { return _geometry->animationFrames; } void Animation::downloadFinished(const QByteArray& data) { // parse the animation/fbx file on a background thread. AnimationReader* animationReader = new AnimationReader(_url, data); - connect(animationReader, SIGNAL(onSuccess(FBXGeometry::Pointer)), SLOT(animationParseSuccess(FBXGeometry::Pointer))); + connect(animationReader, SIGNAL(onSuccess(HFMGeometry::Pointer)), SLOT(animationParseSuccess(HFMGeometry::Pointer))); connect(animationReader, SIGNAL(onError(int, QString)), SLOT(animationParseError(int, QString))); QThreadPool::globalInstance()->start(animationReader); } -void Animation::animationParseSuccess(FBXGeometry::Pointer geometry) { +void Animation::animationParseSuccess(HFMGeometry::Pointer geometry) { qCDebug(animation) << "Animation parse success" << _url.toDisplayString(); diff --git a/libraries/animation/src/AnimationCache.h b/libraries/animation/src/AnimationCache.h index 483350e2b5..302f23a4e7 100644 --- a/libraries/animation/src/AnimationCache.h +++ b/libraries/animation/src/AnimationCache.h @@ -66,7 +66,7 @@ public: QString getType() const override { return "Animation"; } - const FBXGeometry& getGeometry() const { return *_geometry; } + const HFMGeometry& getGeometry() const { return *_geometry; } virtual bool isLoaded() const override; @@ -80,20 +80,20 @@ public: * @function AnimationObject.getFrames * @returns {FBXAnimationFrame[]} */ - Q_INVOKABLE QVector getFrames() const; + Q_INVOKABLE QVector getFrames() const; - const QVector& getFramesReference() const; + const QVector& getFramesReference() const; protected: virtual void downloadFinished(const QByteArray& data) override; protected slots: - void animationParseSuccess(FBXGeometry::Pointer geometry); + void animationParseSuccess(HFMGeometry::Pointer geometry); void animationParseError(int error, QString str); private: - FBXGeometry::Pointer _geometry; + HFMGeometry::Pointer _geometry; }; /// Reads geometry in a worker thread. @@ -105,7 +105,7 @@ public: virtual void run() override; signals: - void onSuccess(FBXGeometry::Pointer geometry); + void onSuccess(HFMGeometry::Pointer geometry); void onError(int error, QString str); private: diff --git a/libraries/animation/src/AnimationObject.cpp b/libraries/animation/src/AnimationObject.cpp index 7f0f35b104..bcbf497199 100644 --- a/libraries/animation/src/AnimationObject.cpp +++ b/libraries/animation/src/AnimationObject.cpp @@ -19,17 +19,17 @@ QStringList AnimationObject::getJointNames() const { return qscriptvalue_cast(thisObject())->getJointNames(); } -QVector AnimationObject::getFrames() const { +QVector AnimationObject::getFrames() const { return qscriptvalue_cast(thisObject())->getFrames(); } QVector AnimationFrameObject::getRotations() const { - return qscriptvalue_cast(thisObject()).rotations; + return qscriptvalue_cast(thisObject()).rotations; } void registerAnimationTypes(QScriptEngine* engine) { - qScriptRegisterSequenceMetaType >(engine); - engine->setDefaultPrototype(qMetaTypeId(), engine->newQObject( + qScriptRegisterSequenceMetaType >(engine); + engine->setDefaultPrototype(qMetaTypeId(), engine->newQObject( new AnimationFrameObject(), QScriptEngine::ScriptOwnership)); engine->setDefaultPrototype(qMetaTypeId(), engine->newQObject( new AnimationObject(), QScriptEngine::ScriptOwnership)); diff --git a/libraries/animation/src/AnimationObject.h b/libraries/animation/src/AnimationObject.h index aa69e78ceb..83880ed2ab 100644 --- a/libraries/animation/src/AnimationObject.h +++ b/libraries/animation/src/AnimationObject.h @@ -23,13 +23,13 @@ class QScriptEngine; class AnimationObject : public QObject, protected QScriptable { Q_OBJECT Q_PROPERTY(QStringList jointNames READ getJointNames) - Q_PROPERTY(QVector frames READ getFrames) + Q_PROPERTY(QVector frames READ getFrames) public: Q_INVOKABLE QStringList getJointNames() const; - Q_INVOKABLE QVector getFrames() const; + Q_INVOKABLE QVector getFrames() const; }; /// Scriptable wrapper for animation frames. diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 33b9569758..2641be92da 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -260,7 +260,7 @@ void Rig::destroyAnimGraph() { _rightEyeJointChildren.clear(); } -void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOffset) { +void Rig::initJointStates(const HFMGeometry& geometry, const glm::mat4& modelOffset) { _geometryOffset = AnimPose(geometry.offset); _invGeometryOffset = _geometryOffset.inverse(); _geometryToRigTransform = modelOffset * geometry.offset; @@ -307,7 +307,7 @@ void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOff _rightEyeJointChildren = _animSkeleton->getChildrenOfJoint(geometry.rightEyeJointIndex); } -void Rig::reset(const FBXGeometry& geometry) { +void Rig::reset(const HFMGeometry& geometry) { _geometryOffset = AnimPose(geometry.offset); _invGeometryOffset = _geometryOffset.inverse(); _animSkeleton = std::make_shared(geometry); @@ -1253,7 +1253,7 @@ const glm::vec3 DOP14_NORMALS[DOP14_COUNT] = { // returns true if the given point lies inside of the k-dop, specified by shapeInfo & shapePose. // if the given point does lie within the k-dop, it also returns the amount of displacement necessary to push that point outward // such that it lies on the surface of the kdop. -static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& shapePose, const FBXJointShapeInfo& shapeInfo, glm::vec3& displacementOut) { +static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& shapePose, const HFMJointShapeInfo& shapeInfo, glm::vec3& displacementOut) { // transform point into local space of jointShape. glm::vec3 localPoint = shapePose.inverse().xformPoint(point); @@ -1299,8 +1299,8 @@ static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& sh } } -glm::vec3 Rig::deflectHandFromTorso(const glm::vec3& handPosition, const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, - const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo) const { +glm::vec3 Rig::deflectHandFromTorso(const glm::vec3& handPosition, const HFMJointShapeInfo& hipsShapeInfo, const HFMJointShapeInfo& spineShapeInfo, + const HFMJointShapeInfo& spine1ShapeInfo, const HFMJointShapeInfo& spine2ShapeInfo) const { glm::vec3 position = handPosition; glm::vec3 displacement; int hipsJoint = indexOfJoint("Hips"); @@ -1349,8 +1349,8 @@ glm::vec3 Rig::deflectHandFromTorso(const glm::vec3& handPosition, const FBXJoin void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnabled, bool hipsEstimated, bool leftArmEnabled, bool rightArmEnabled, bool headEnabled, float dt, const AnimPose& leftHandPose, const AnimPose& rightHandPose, - const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, - const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo, + const HFMJointShapeInfo& hipsShapeInfo, const HFMJointShapeInfo& spineShapeInfo, + const HFMJointShapeInfo& spine1ShapeInfo, const HFMJointShapeInfo& spine2ShapeInfo, const glm::mat4& rigToSensorMatrix, const glm::mat4& sensorToRigMatrix) { const bool ENABLE_POLE_VECTORS = true; @@ -2008,7 +2008,7 @@ void Rig::computeExternalPoses(const glm::mat4& modelOffsetMat) { } void Rig::computeAvatarBoundingCapsule( - const FBXGeometry& geometry, + const HFMGeometry& geometry, float& radiusOut, float& heightOut, glm::vec3& localOffsetOut) const { @@ -2041,7 +2041,7 @@ void Rig::computeAvatarBoundingCapsule( // from the head to the hips when computing the rest of the bounding capsule. int index = indexOfJoint("Head"); while (index != -1) { - const FBXJointShapeInfo& shapeInfo = geometry.joints.at(index).shapeInfo; + const HFMJointShapeInfo& shapeInfo = geometry.joints.at(index).shapeInfo; AnimPose pose = _animSkeleton->getAbsoluteDefaultPose(index); if (shapeInfo.points.size() > 0) { for (auto& point : shapeInfo.points) { diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 7a090bd7bd..61e8672972 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -86,10 +86,10 @@ public: AnimPose secondaryControllerPoses[NumSecondaryControllerTypes]; // rig space uint8_t secondaryControllerFlags[NumSecondaryControllerTypes]; bool isTalking; - FBXJointShapeInfo hipsShapeInfo; - FBXJointShapeInfo spineShapeInfo; - FBXJointShapeInfo spine1ShapeInfo; - FBXJointShapeInfo spine2ShapeInfo; + HFMJointShapeInfo hipsShapeInfo; + HFMJointShapeInfo spineShapeInfo; + HFMJointShapeInfo spine1ShapeInfo; + HFMJointShapeInfo spine2ShapeInfo; }; struct EyeParameters { @@ -122,8 +122,8 @@ public: void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); void restoreRoleAnimation(const QString& role); - void initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOffset); - void reset(const FBXGeometry& geometry); + void initJointStates(const HFMGeometry& geometry, const glm::mat4& modelOffset); + void reset(const HFMGeometry& geometry); bool jointStatesEmpty(); int getJointStateCount() const; int indexOfJoint(const QString& jointName) const; @@ -210,7 +210,7 @@ public: void copyJointsFromJointData(const QVector& jointDataVec); void computeExternalPoses(const glm::mat4& modelOffsetMat); - void computeAvatarBoundingCapsule(const FBXGeometry& geometry, float& radiusOut, float& heightOut, glm::vec3& offsetOut) const; + void computeAvatarBoundingCapsule(const HFMGeometry& geometry, float& radiusOut, float& heightOut, glm::vec3& offsetOut) const; void setEnableInverseKinematics(bool enable); void setEnableAnimations(bool enable); @@ -245,8 +245,8 @@ protected: void updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnabled, bool hipsEstimated, bool leftArmEnabled, bool rightArmEnabled, bool headEnabled, float dt, const AnimPose& leftHandPose, const AnimPose& rightHandPose, - const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, - const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo, + const HFMJointShapeInfo& hipsShapeInfo, const HFMJointShapeInfo& spineShapeInfo, + const HFMJointShapeInfo& spine1ShapeInfo, const HFMJointShapeInfo& spine2ShapeInfo, const glm::mat4& rigToSensorMatrix, const glm::mat4& sensorToRigMatrix); void updateFeet(bool leftFootEnabled, bool rightFootEnabled, bool headEnabled, const AnimPose& leftFootPose, const AnimPose& rightFootPose, @@ -257,8 +257,8 @@ protected: bool calculateElbowPoleVector(int handIndex, int elbowIndex, int armIndex, int oppositeArmIndex, glm::vec3& poleVector) const; glm::vec3 calculateKneePoleVector(int footJointIndex, int kneeJoint, int upLegIndex, int hipsIndex, const AnimPose& targetFootPose) const; - glm::vec3 deflectHandFromTorso(const glm::vec3& handPosition, const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, - const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo) const; + glm::vec3 deflectHandFromTorso(const glm::vec3& handPosition, const HFMJointShapeInfo& hipsShapeInfo, const HFMJointShapeInfo& spineShapeInfo, + const HFMJointShapeInfo& spine1ShapeInfo, const HFMJointShapeInfo& spine2ShapeInfo) const; AnimPose _modelOffset; // model to rig space diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 06dd498af3..7c7c113d31 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1311,7 +1311,7 @@ glm::quat Avatar::getAbsoluteJointRotationInObjectFrame(int index) const { case CAMERA_MATRIX_INDEX: { glm::quat rotation; if (_skeletonModel && _skeletonModel->isActive()) { - int headJointIndex = _skeletonModel->getFBXGeometry().headJointIndex; + int headJointIndex = _skeletonModel->getHFMGeometry().headJointIndex; if (headJointIndex >= 0) { _skeletonModel->getAbsoluteJointRotationInRigFrame(headJointIndex, rotation); } @@ -1360,7 +1360,7 @@ glm::vec3 Avatar::getAbsoluteJointTranslationInObjectFrame(int index) const { case CAMERA_MATRIX_INDEX: { glm::vec3 translation; if (_skeletonModel && _skeletonModel->isActive()) { - int headJointIndex = _skeletonModel->getFBXGeometry().headJointIndex; + int headJointIndex = _skeletonModel->getHFMGeometry().headJointIndex; if (headJointIndex >= 0) { _skeletonModel->getAbsoluteJointTranslationInRigFrame(headJointIndex, translation); } @@ -1416,7 +1416,7 @@ void Avatar::withValidJointIndicesCache(std::function const& worker) con if (!_modelJointsCached) { _modelJointIndicesCache.clear(); if (_skeletonModel && _skeletonModel->isActive()) { - _modelJointIndicesCache = _skeletonModel->getFBXGeometry().jointIndices; + _modelJointIndicesCache = _skeletonModel->getHFMGeometry().jointIndices; _modelJointsCached = true; } } diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index 1ec58fd704..a41cff528b 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -54,7 +54,7 @@ void SkeletonModel::setTextures(const QVariantMap& textures) { } void SkeletonModel::initJointStates() { - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); glm::mat4 modelOffset = glm::scale(_scale) * glm::translate(_offset); _rig.initJointStates(geometry, modelOffset); @@ -96,7 +96,7 @@ void SkeletonModel::initJointStates() { // Called within Model::simulate call, below. void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { assert(!_owningAvatar->isMyAvatar()); - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); Head* head = _owningAvatar->getHead(); @@ -259,22 +259,22 @@ bool SkeletonModel::getRightShoulderPosition(glm::vec3& position) const { } bool SkeletonModel::getHeadPosition(glm::vec3& headPosition) const { - return isActive() && getJointPositionInWorldFrame(getFBXGeometry().headJointIndex, headPosition); + return isActive() && getJointPositionInWorldFrame(getHFMGeometry().headJointIndex, headPosition); } bool SkeletonModel::getNeckPosition(glm::vec3& neckPosition) const { - return isActive() && getJointPositionInWorldFrame(getFBXGeometry().neckJointIndex, neckPosition); + return isActive() && getJointPositionInWorldFrame(getHFMGeometry().neckJointIndex, neckPosition); } bool SkeletonModel::getLocalNeckPosition(glm::vec3& neckPosition) const { - return isActive() && getJointPosition(getFBXGeometry().neckJointIndex, neckPosition); + return isActive() && getJointPosition(getHFMGeometry().neckJointIndex, neckPosition); } bool SkeletonModel::getEyeModelPositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const { if (!isActive()) { return false; } - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); if (getJointPosition(geometry.leftEyeJointIndex, firstEyePosition) && getJointPosition(geometry.rightEyeJointIndex, secondEyePosition)) { @@ -330,7 +330,7 @@ void SkeletonModel::computeBoundingShape() { return; } - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); if (geometry.joints.isEmpty() || geometry.rootJointIndex == -1) { // rootJointIndex == -1 if the avatar model has no skeleton return; @@ -369,7 +369,7 @@ void SkeletonModel::renderBoundingCollisionShapes(RenderArgs* args, gpu::Batch& } bool SkeletonModel::hasSkeleton() { - return isActive() ? getFBXGeometry().rootJointIndex != -1 : false; + return isActive() ? getHFMGeometry().rootJointIndex != -1 : false; } void SkeletonModel::onInvalidate() { diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h index d82fce7412..6c533a5941 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h @@ -41,10 +41,10 @@ public: void updateAttitude(const glm::quat& orientation); /// Returns the index of the left hand joint, or -1 if not found. - int getLeftHandJointIndex() const { return isActive() ? getFBXGeometry().leftHandJointIndex : -1; } + int getLeftHandJointIndex() const { return isActive() ? getHFMGeometry().leftHandJointIndex : -1; } /// Returns the index of the right hand joint, or -1 if not found. - int getRightHandJointIndex() const { return isActive() ? getFBXGeometry().rightHandJointIndex : -1; } + int getRightHandJointIndex() const { return isActive() ? getHFMGeometry().rightHandJointIndex : -1; } bool getLeftGrabPosition(glm::vec3& position) const; bool getRightGrabPosition(glm::vec3& position) const; diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index b90082d969..0b76c275d4 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -206,7 +206,7 @@ void FBXBaker::importScene() { } #endif - _geometry = reader.extractFBXGeometry({}, _modelURL.toString()); + _geometry = reader.extractHFMGeometry({}, _modelURL.toString()); _textureContentMap = reader._textureContent; } @@ -329,7 +329,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { for (FBXNode& textureChild : object->children) { if (textureChild.name == "RelativeFilename") { - QString fbxTextureFileName { textureChild.properties.at(0).toString() }; + QString hfmTextureFileName { textureChild.properties.at(0).toString() }; // grab the ID for this texture so we can figure out the // texture type from the loaded materials @@ -337,7 +337,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { auto textureType = textureTypes[textureID]; // Compress the texture information and return the new filename to be added into the FBX scene - auto bakedTextureFile = compressTexture(fbxTextureFileName, textureType); + auto bakedTextureFile = compressTexture(hfmTextureFileName, textureType); // If no errors or warnings have occurred during texture compression add the filename to the FBX scene if (!bakedTextureFile.isNull()) { diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 9d41209d4c..8edaf91c79 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -53,7 +53,7 @@ private: void rewriteAndBakeSceneModels(); void rewriteAndBakeSceneTextures(); - FBXGeometry* _geometry; + HFMGeometry* _geometry; QHash _textureNameMatchCount; QHash _remappedTexturePaths; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 75e10c54ab..ca352cebae 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -75,7 +75,7 @@ void ModelBaker::abort() { } } -bool ModelBaker::compressMesh(FBXMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback) { +bool ModelBaker::compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback) { if (mesh.wasCompressed) { handleError("Cannot re-bake a file that contains compressed mesh"); return false; diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 1fd77ab761..cda4478b1d 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -39,7 +39,7 @@ public: const QString& bakedOutputDirectory, const QString& originalOutputDirectory = ""); virtual ~ModelBaker(); - bool compressMesh(FBXMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr); + bool compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr); QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE); virtual void setWasAborted(bool wasAborted) override; diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index cf62bc4fa8..e9130e3fbd 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -153,7 +153,7 @@ void OBJBaker::bakeOBJ() { checkIfTexturesFinished(); } -void OBJBaker::createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry) { +void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMGeometry& geometry) { // Generating FBX Header Node FBXNode headerNode; headerNode.name = FBX_HEADER_EXTENSION; @@ -235,7 +235,7 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry) { auto size = meshParts.size(); for (int i = 0; i < size; i++) { QString material = meshParts[i].materialID; - FBXMaterial currentMaterial = geometry.materials[material]; + HFMMaterial currentMaterial = geometry.materials[material]; if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) { auto textureID = nextNodeID(); _mapTextureMaterial.emplace_back(textureID, i); @@ -325,12 +325,12 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry) { } // Set properties for material nodes -void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, FBXGeometry& geometry) { +void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMGeometry& geometry) { auto materialID = nextNodeID(); _materialIDs.push_back(materialID); materialNode.properties = { materialID, material, MESH }; - FBXMaterial currentMaterial = geometry.materials[material]; + HFMMaterial currentMaterial = geometry.materials[material]; // Setting the hierarchy: Material -> Properties70 -> P -> Properties FBXNode properties70Node; diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index 8e49692d35..875a500129 100644 --- a/libraries/baking/src/OBJBaker.h +++ b/libraries/baking/src/OBJBaker.h @@ -39,8 +39,8 @@ private slots: private: void loadOBJ(); - void createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry); - void setMaterialNodeProperties(FBXNode& materialNode, QString material, FBXGeometry& geometry); + void createFBXNodeTree(FBXNode& rootNode, HFMGeometry& geometry); + void setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMGeometry& geometry); NodeID nextNodeID() { return _nodeID++; } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index c6337dc872..c36f60600f 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -268,7 +268,7 @@ EntityItemProperties RenderableModelEntityItem::getProperties(const EntityProper if (model->isLoaded()) { // TODO: improve naturalDimensions in the future, // for now we've added this hack for setting natural dimensions of models - Extents meshExtents = model->getFBXGeometry().getUnscaledMeshExtents(); + Extents meshExtents = model->getHFMGeometry().getUnscaledMeshExtents(); properties.setNaturalDimensions(meshExtents.maximum - meshExtents.minimum); properties.calculateNaturalPosition(meshExtents.minimum, meshExtents.maximum); } @@ -403,7 +403,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // should never fall in here when collision model not fully loaded // TODO: assert that all geometries exist and are loaded //assert(_model && _model->isLoaded() && _compoundShapeResource && _compoundShapeResource->isLoaded()); - const FBXGeometry& collisionGeometry = _compoundShapeResource->getFBXGeometry(); + const HFMGeometry& collisionGeometry = _compoundShapeResource->getHFMGeometry(); ShapeInfo::PointCollection& pointCollection = shapeInfo.getPointCollection(); pointCollection.clear(); @@ -411,15 +411,15 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // the way OBJ files get read, each section under a "g" line is its own meshPart. We only expect // to find one actual "mesh" (with one or more meshParts in it), but we loop over the meshes, just in case. - foreach (const FBXMesh& mesh, collisionGeometry.meshes) { + foreach (const HFMMesh& mesh, collisionGeometry.meshes) { // each meshPart is a convex hull - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { pointCollection.push_back(QVector()); ShapeInfo::PointList& pointsInPart = pointCollection[i]; // run through all the triangles and (uniquely) add each point to the hull uint32_t numIndices = (uint32_t)meshPart.triangleIndices.size(); - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices % TRIANGLE_STRIDE == 0); numIndices -= numIndices % TRIANGLE_STRIDE; // WORKAROUND lack of sanity checking in FBXReader @@ -440,7 +440,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // run through all the quads and (uniquely) add each point to the hull numIndices = (uint32_t)meshPart.quadIndices.size(); - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices % QUAD_STRIDE == 0); numIndices -= numIndices % QUAD_STRIDE; // WORKAROUND lack of sanity checking in FBXReader @@ -478,7 +478,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // to the visual model and apply them to the collision model (without regard for the // collision model's extents). - glm::vec3 scaleToFit = dimensions / model->getFBXGeometry().getUnscaledMeshExtents().size(); + glm::vec3 scaleToFit = dimensions / model->getHFMGeometry().getUnscaledMeshExtents().size(); // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. glm::vec3 registrationOffset = dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint()); @@ -498,14 +498,14 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // compute meshPart local transforms QVector localTransforms; - const FBXGeometry& fbxGeometry = model->getFBXGeometry(); - int numFbxMeshes = fbxGeometry.meshes.size(); + const HFMGeometry& hfmGeometry = model->getHFMGeometry(); + int numHFMMeshes = hfmGeometry.meshes.size(); int totalNumVertices = 0; glm::mat4 invRegistraionOffset = glm::translate(dimensions * (getRegistrationPoint() - ENTITY_ITEM_DEFAULT_REGISTRATION_POINT)); - for (int i = 0; i < numFbxMeshes; i++) { - const FBXMesh& mesh = fbxGeometry.meshes.at(i); + for (int i = 0; i < numHFMMeshes; i++) { + const HFMMesh& mesh = hfmGeometry.meshes.at(i); if (mesh.clusters.size() > 0) { - const FBXCluster& cluster = mesh.clusters.at(0); + const HFMCluster& cluster = mesh.clusters.at(0); auto jointMatrix = model->getRig().getJointTransform(cluster.jointIndex); // we backtranslate by the registration offset so we can apply that offset to the shapeInfo later localTransforms.push_back(invRegistraionOffset * jointMatrix * cluster.inverseBindMatrix); @@ -524,10 +524,10 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { std::vector> meshes; if (type == SHAPE_TYPE_SIMPLE_COMPOUND) { - auto& fbxMeshes = _compoundShapeResource->getFBXGeometry().meshes; - meshes.reserve(fbxMeshes.size()); - for (auto& fbxMesh : fbxMeshes) { - meshes.push_back(fbxMesh._mesh); + auto& hfmMeshes = _compoundShapeResource->getHFMGeometry().meshes; + meshes.reserve(hfmMeshes.size()); + for (auto& hfmMesh : hfmMeshes) { + meshes.push_back(hfmMesh._mesh); } } else { meshes = model->getGeometry()->getMeshes(); @@ -594,7 +594,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { while (partItr != parts.cend()) { auto numIndices = partItr->_numIndices; if (partItr->_topology == graphics::Mesh::TRIANGLES) { - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices % TRIANGLE_STRIDE == 0); numIndices -= numIndices % TRIANGLE_STRIDE; // WORKAROUND lack of sanity checking in FBXReader @@ -605,7 +605,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { ++indexItr; } } else if (partItr->_topology == graphics::Mesh::TRIANGLE_STRIP) { - // TODO: resurrect assert after we start sanitizing FBXMesh higher up + // TODO: resurrect assert after we start sanitizing HFMMesh higher up //assert(numIndices > 2); uint32_t approxNumIndices = TRIANGLE_STRIDE * numIndices; @@ -651,7 +651,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { std::set uniqueIndices; auto numIndices = partItr->_numIndices; if (partItr->_topology == graphics::Mesh::TRIANGLES) { - // TODO: assert rather than workaround after we start sanitizing FBXMesh higher up + // TODO: assert rather than workaround after we start sanitizing HFMMesh higher up //assert(numIndices% TRIANGLE_STRIDE == 0); numIndices -= numIndices % TRIANGLE_STRIDE; // WORKAROUND lack of sanity checking in FBXReader @@ -662,7 +662,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { ++indexItr; } } else if (partItr->_topology == graphics::Mesh::TRIANGLE_STRIP) { - // TODO: resurrect assert after we start sanitizing FBXMesh higher up + // TODO: resurrect assert after we start sanitizing HFMMesh higher up //assert(numIndices > TRIANGLE_STRIDE - 1); auto indexItr = indices.cbegin() + partItr->_startIndex; @@ -755,7 +755,7 @@ int RenderableModelEntityItem::avatarJointIndex(int modelJointIndex) { bool RenderableModelEntityItem::contains(const glm::vec3& point) const { auto model = getModel(); if (EntityItem::contains(point) && model && _compoundShapeResource && _compoundShapeResource->isLoaded()) { - return _compoundShapeResource->getFBXGeometry().convexHullContains(worldToEntity(point)); + return _compoundShapeResource->getHFMGeometry().convexHullContains(worldToEntity(point)); } return false; @@ -1135,7 +1135,7 @@ void ModelEntityRenderer::animate(const TypedEntityPointer& entity) { QVector jointsData; - const QVector& frames = _animation->getFramesReference(); // NOTE: getFrames() is too heavy + const QVector& frames = _animation->getFramesReference(); // NOTE: getFrames() is too heavy int frameCount = frames.size(); if (frameCount <= 0) { return; @@ -1160,10 +1160,10 @@ void ModelEntityRenderer::animate(const TypedEntityPointer& entity) { } QStringList animationJointNames = _animation->getGeometry().getJointNames(); - auto& fbxJoints = _animation->getGeometry().joints; + auto& hfmJoints = _animation->getGeometry().joints; - auto& originalFbxJoints = _model->getFBXGeometry().joints; - auto& originalFbxIndices = _model->getFBXGeometry().jointIndices; + auto& originalHFMJoints = _model->getHFMGeometry().joints; + auto& originalHFMIndices = _model->getHFMGeometry().jointIndices; bool allowTranslation = entity->getAnimationAllowTranslation(); @@ -1182,22 +1182,22 @@ void ModelEntityRenderer::animate(const TypedEntityPointer& entity) { translationMat = glm::translate(translations[index]); } } else if (index < animationJointNames.size()) { - QString jointName = fbxJoints[index].name; // Pushing this here so its not done on every entity, with the exceptions of those allowing for translation - if (originalFbxIndices.contains(jointName)) { + QString jointName = hfmJoints[index].name; // Pushing this here so its not done on every entity, with the exceptions of those allowing for translation + if (originalHFMIndices.contains(jointName)) { // Making sure the joint names exist in the original model the animation is trying to apply onto. If they do, then remap and get it's translation. - int remappedIndex = originalFbxIndices[jointName] - 1; // JointIndeces seem to always start from 1 and the found index is always 1 higher than actual. - translationMat = glm::translate(originalFbxJoints[remappedIndex].translation); + int remappedIndex = originalHFMIndices[jointName] - 1; // JointIndeces seem to always start from 1 and the found index is always 1 higher than actual. + translationMat = glm::translate(originalHFMJoints[remappedIndex].translation); } } glm::mat4 rotationMat; if (index < rotations.size()) { - rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * rotations[index] * fbxJoints[index].postRotation); + rotationMat = glm::mat4_cast(hfmJoints[index].preRotation * rotations[index] * hfmJoints[index].postRotation); } else { - rotationMat = glm::mat4_cast(fbxJoints[index].preRotation * fbxJoints[index].postRotation); + rotationMat = glm::mat4_cast(hfmJoints[index].preRotation * hfmJoints[index].postRotation); } - glm::mat4 finalMat = (translationMat * fbxJoints[index].preTransform * - rotationMat * fbxJoints[index].postTransform); + glm::mat4 finalMat = (translationMat * hfmJoints[index].preTransform * + rotationMat * hfmJoints[index].postTransform); auto& jointData = jointsData[j]; jointData.translation = extractTranslation(finalMat); jointData.translationSet = true; diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index fdebb16bc8..8051dbafea 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -70,8 +70,8 @@ public: }; -/// A single blendshape extracted from an FBX document. -class FBXBlendshape { +/// A single blendshape. +class HFMBlendshape { public: QVector indices; QVector vertices; @@ -79,19 +79,19 @@ public: QVector tangents; }; -struct FBXJointShapeInfo { - // same units and frame as FBXJoint.translation +struct HFMJointShapeInfo { + // same units and frame as HFMJoint.translation glm::vec3 avgPoint; std::vector dots; std::vector points; std::vector debugLines; }; -/// A single joint (transformation node) extracted from an FBX document. -class FBXJoint { +/// A single joint (transformation node). +class HFMJoint { public: - FBXJointShapeInfo shapeInfo; + HFMJointShapeInfo shapeInfo; QVector freeLineage; bool isFree; int parentIndex; @@ -126,8 +126,8 @@ public: }; -/// A single binding to a joint in an FBX document. -class FBXCluster { +/// A single binding to a joint. +class HFMCluster { public: int jointIndex; @@ -137,8 +137,8 @@ public: const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; -/// A texture map in an FBX document. -class FBXTexture { +/// A texture map. +class HFMTexture { public: QString id; QString name; @@ -156,7 +156,7 @@ public: }; /// A single part of a mesh (with the same material). -class FBXMeshPart { +class HFMMeshPart { public: QVector quadIndices; // original indices from the FBX mesh @@ -166,10 +166,10 @@ public: QString materialID; }; -class FBXMaterial { +class HFMMaterial { public: - FBXMaterial() {}; - FBXMaterial(const glm::vec3& diffuseColor, const glm::vec3& specularColor, const glm::vec3& emissiveColor, + HFMMaterial() {}; + HFMMaterial(const glm::vec3& diffuseColor, const glm::vec3& specularColor, const glm::vec3& emissiveColor, float shininess, float opacity) : diffuseColor(diffuseColor), specularColor(specularColor), @@ -203,17 +203,17 @@ public: QString shadingModel; graphics::MaterialPointer _material; - FBXTexture normalTexture; - FBXTexture albedoTexture; - FBXTexture opacityTexture; - FBXTexture glossTexture; - FBXTexture roughnessTexture; - FBXTexture specularTexture; - FBXTexture metallicTexture; - FBXTexture emissiveTexture; - FBXTexture occlusionTexture; - FBXTexture scatteringTexture; - FBXTexture lightmapTexture; + HFMTexture normalTexture; + HFMTexture albedoTexture; + HFMTexture opacityTexture; + HFMTexture glossTexture; + HFMTexture roughnessTexture; + HFMTexture specularTexture; + HFMTexture metallicTexture; + HFMTexture emissiveTexture; + HFMTexture occlusionTexture; + HFMTexture scatteringTexture; + HFMTexture lightmapTexture; glm::vec2 lightmapParams{ 0.0f, 1.0f }; @@ -232,10 +232,10 @@ public: }; /// A single mesh (with optional blendshapes) extracted from an FBX document. -class FBXMesh { +class HFMMesh { public: - QVector parts; + QVector parts; QVector vertices; QVector normals; @@ -247,12 +247,12 @@ public: QVector clusterWeights; QVector originalIndices; - QVector clusters; + QVector clusters; Extents meshExtents; glm::mat4 modelTransform; - QVector blendshapes; + QVector blendshapes; unsigned int meshIndex; // the order the meshes appeared in the object file @@ -265,7 +265,7 @@ public: class ExtractedMesh { public: - FBXMesh mesh; + HFMMesh mesh; QMultiHash newIndices; QVector > blendshapeIndexMaps; QVector > partMaterialTextures; @@ -278,14 +278,14 @@ public: * @property {Vec3[]} translations */ /// A single animation frame extracted from an FBX document. -class FBXAnimationFrame { +class HFMAnimationFrame { public: QVector rotations; QVector translations; }; -/// A light in an FBX document. -class FBXLight { +/// A light. +class HFMLight { public: QString name; Transform transform; @@ -293,7 +293,7 @@ public: float fogValue; glm::vec3 color; - FBXLight() : + HFMLight() : name(), transform(), intensity(1.0f), @@ -302,26 +302,26 @@ public: {} }; -Q_DECLARE_METATYPE(FBXAnimationFrame) -Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(HFMAnimationFrame) +Q_DECLARE_METATYPE(QVector) /// A set of meshes extracted from an FBX document. -class FBXGeometry { +class HFMGeometry { public: - using Pointer = std::shared_ptr; + using Pointer = std::shared_ptr; QString originalURL; QString author; QString applicationName; ///< the name of the application that generated the model - QVector joints; + QVector joints; QHash jointIndices; ///< 1-based, so as to more easily detect missing indices bool hasSkeletonJoints; - QVector meshes; + QVector meshes; QVector scripts; - QHash materials; + QHash materials; glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file @@ -348,7 +348,7 @@ public: Extents bindExtents; Extents meshExtents; - QVector animationFrames; + QVector animationFrames; int getJointIndex(const QString& name) const { return jointIndices.value(name) - 1; } QStringList getJointNames() const; @@ -368,7 +368,7 @@ public: QList blendshapeChannelNames; }; -Q_DECLARE_METATYPE(FBXGeometry) -Q_DECLARE_METATYPE(FBXGeometry::Pointer) +Q_DECLARE_METATYPE(HFMGeometry) +Q_DECLARE_METATYPE(HFMGeometry::Pointer) #endif // hifi_FBX_h_ diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index dd766f002c..df6abbfdf2 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -40,19 +40,19 @@ using namespace std; -int FBXGeometryPointerMetaTypeId = qRegisterMetaType(); +int HFMGeometryPointerMetaTypeId = qRegisterMetaType(); -QStringList FBXGeometry::getJointNames() const { +QStringList HFMGeometry::getJointNames() const { QStringList names; - foreach (const FBXJoint& joint, joints) { + foreach (const HFMJoint& joint, joints) { names.append(joint.name); } return names; } -bool FBXGeometry::hasBlendedMeshes() const { +bool HFMGeometry::hasBlendedMeshes() const { if (!meshes.isEmpty()) { - foreach (const FBXMesh& mesh, meshes) { + foreach (const HFMMesh& mesh, meshes) { if (!mesh.blendshapes.isEmpty()) { return true; } @@ -61,7 +61,7 @@ bool FBXGeometry::hasBlendedMeshes() const { return false; } -Extents FBXGeometry::getUnscaledMeshExtents() const { +Extents HFMGeometry::getUnscaledMeshExtents() const { const Extents& extents = meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which @@ -74,12 +74,12 @@ Extents FBXGeometry::getUnscaledMeshExtents() const { } // TODO: Move to graphics::Mesh when Sam's ready -bool FBXGeometry::convexHullContains(const glm::vec3& point) const { +bool HFMGeometry::convexHullContains(const glm::vec3& point) const { if (!getUnscaledMeshExtents().containsPoint(point)) { return false; } - auto checkEachPrimitive = [=](FBXMesh& mesh, QVector indices, int primitiveSize) -> bool { + auto checkEachPrimitive = [=](HFMMesh& mesh, QVector indices, int primitiveSize) -> bool { // Check whether the point is "behind" all the primitives. int verticesSize = mesh.vertices.size(); for (int j = 0; @@ -124,16 +124,16 @@ bool FBXGeometry::convexHullContains(const glm::vec3& point) const { return false; } -QString FBXGeometry::getModelNameOfMesh(int meshIndex) const { +QString HFMGeometry::getModelNameOfMesh(int meshIndex) const { if (meshIndicesToModelNames.contains(meshIndex)) { return meshIndicesToModelNames.value(meshIndex); } return QString(); } -int fbxGeometryMetaTypeId = qRegisterMetaType(); -int fbxAnimationFrameMetaTypeId = qRegisterMetaType(); -int fbxAnimationFrameVectorMetaTypeId = qRegisterMetaType >(); +int hfmGeometryMetaTypeId = qRegisterMetaType(); +int hfmAnimationFrameMetaTypeId = qRegisterMetaType(); +int hfmAnimationFrameVectorMetaTypeId = qRegisterMetaType >(); glm::vec3 parseVec3(const QString& string) { @@ -303,7 +303,7 @@ glm::mat4 getGlobalTransform(const QMultiMap& _connectionParen class ExtractedBlendshape { public: QString id; - FBXBlendshape blendshape; + HFMBlendshape blendshape; }; void printNode(const FBXNode& node, int indentLevel) { @@ -346,8 +346,8 @@ void appendModelIDs(const QString& parentID, const QMultiMap& } } -FBXBlendshape extractBlendshape(const FBXNode& object) { - FBXBlendshape blendshape; +HFMBlendshape extractBlendshape(const FBXNode& object) { + HFMBlendshape blendshape; foreach (const FBXNode& data, object.children) { if (data.name == "Indexes") { blendshape.indices = FBXReader::getIntVector(data); @@ -362,9 +362,9 @@ FBXBlendshape extractBlendshape(const FBXNode& object) { return blendshape; } -using IndexAccessor = std::function; +using IndexAccessor = std::function; -static void setTangents(const FBXMesh& mesh, const IndexAccessor& vertexAccessor, int firstIndex, int secondIndex, +static void setTangents(const HFMMesh& mesh, const IndexAccessor& vertexAccessor, int firstIndex, int secondIndex, const QVector& vertices, const QVector& normals, QVector& tangents) { glm::vec3 vertex[2]; glm::vec3 normal; @@ -381,14 +381,14 @@ static void setTangents(const FBXMesh& mesh, const IndexAccessor& vertexAccessor } } -static void createTangents(const FBXMesh& mesh, bool generateFromTexCoords, +static void createTangents(const HFMMesh& mesh, bool generateFromTexCoords, const QVector& vertices, const QVector& normals, QVector& tangents, IndexAccessor accessor) { // if we have a normal map (and texture coordinates), we must compute tangents if (generateFromTexCoords && !mesh.texCoords.isEmpty()) { tangents.resize(vertices.size()); - foreach(const FBXMeshPart& part, mesh.parts) { + foreach(const HFMMeshPart& part, mesh.parts) { for (int i = 0; i < part.quadIndices.size(); i += 4) { setTangents(mesh, accessor, part.quadIndices.at(i), part.quadIndices.at(i + 1), vertices, normals, tangents); setTangents(mesh, accessor, part.quadIndices.at(i + 1), part.quadIndices.at(i + 2), vertices, normals, tangents); @@ -403,27 +403,27 @@ static void createTangents(const FBXMesh& mesh, bool generateFromTexCoords, setTangents(mesh, accessor, part.triangleIndices.at(i + 2), part.triangleIndices.at(i), vertices, normals, tangents); } if ((part.triangleIndices.size() % 3) != 0) { - qCDebug(modelformat) << "Error in extractFBXGeometry part.triangleIndices.size() is not divisible by three "; + qCDebug(modelformat) << "Error in extractHFMGeometry part.triangleIndices.size() is not divisible by three "; } } } } -static void _createBlendShapeTangents(FBXMesh& mesh, bool generateFromTexCoords, FBXBlendshape& blendShape); +static void _createBlendShapeTangents(HFMMesh& mesh, bool generateFromTexCoords, HFMBlendshape& blendShape); -void FBXMesh::createBlendShapeTangents(bool generateTangents) { +void HFMMesh::createBlendShapeTangents(bool generateTangents) { for (auto& blendShape : blendshapes) { _createBlendShapeTangents(*this, generateTangents, blendShape); } } -void FBXMesh::createMeshTangents(bool generateFromTexCoords) { - FBXMesh& mesh = *this; +void HFMMesh::createMeshTangents(bool generateFromTexCoords) { + HFMMesh& mesh = *this; // This is the only workaround I've found to trick the compiler into understanding that mesh.tangents isn't // const in the lambda function. auto& tangents = mesh.tangents; createTangents(mesh, generateFromTexCoords, mesh.vertices, mesh.normals, mesh.tangents, - [&](const FBXMesh& mesh, int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec3& outNormal) { + [&](const HFMMesh& mesh, int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec3& outNormal) { outVertices[0] = mesh.vertices[firstIndex]; outVertices[1] = mesh.vertices[secondIndex]; outNormal = mesh.normals[firstIndex]; @@ -431,7 +431,7 @@ void FBXMesh::createMeshTangents(bool generateFromTexCoords) { }); } -static void _createBlendShapeTangents(FBXMesh& mesh, bool generateFromTexCoords, FBXBlendshape& blendShape) { +static void _createBlendShapeTangents(HFMMesh& mesh, bool generateFromTexCoords, HFMBlendshape& blendShape) { // Create lookup to get index in blend shape from vertex index in mesh std::vector reverseIndices; reverseIndices.resize(mesh.vertices.size()); @@ -443,7 +443,7 @@ static void _createBlendShapeTangents(FBXMesh& mesh, bool generateFromTexCoords, } createTangents(mesh, generateFromTexCoords, blendShape.vertices, blendShape.normals, blendShape.tangents, - [&](const FBXMesh& mesh, int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec3& outNormal) { + [&](const HFMMesh& mesh, int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec3& outNormal) { const auto index1 = reverseIndices[firstIndex]; const auto index2 = reverseIndices[secondIndex]; @@ -481,7 +481,7 @@ void addBlendshapes(const ExtractedBlendshape& extracted, const QList& blendshapeIndexMap = extractedMesh.blendshapeIndexMaps[index.first]; for (int i = 0; i < extracted.blendshape.indices.size(); i++) { int oldIndex = extracted.blendshape.indices.at(i); @@ -539,7 +539,7 @@ public: QVector values; }; -bool checkMaterialsHaveTextures(const QHash& materials, +bool checkMaterialsHaveTextures(const QHash& materials, const QHash& textureFilenames, const QMultiMap& _connectionChildMap) { foreach (const QString& materialID, materials.keys()) { foreach (const QString& childID, _connectionChildMap.values(materialID)) { @@ -569,8 +569,8 @@ int matchTextureUVSetToAttributeChannel(const QString& texUVSetName, const QHash } -FBXLight extractLight(const FBXNode& object) { - FBXLight light; +HFMLight extractLight(const FBXNode& object) { + HFMLight light; foreach (const FBXNode& subobject, object.children) { QString childname = QString(subobject.name); if (subobject.name == "Properties70") { @@ -615,7 +615,7 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { return filepath.mid(filepath.lastIndexOf('/') + 1); } -FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) { +HFMGeometry* FBXReader::extractHFMGeometry(const QVariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; QMap meshes; QHash modelIDsToNames; @@ -636,7 +636,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS QHash yComponents; QHash zComponents; - std::map lights; + std::map lights; QVariantHash joints = mapping.value("joint").toHash(); QString jointEyeLeftName = processID(getString(joints.value("jointEyeLeft", "jointEyeLeft"))); @@ -689,8 +689,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS #if defined(DEBUG_FBXREADER) int unknown = 0; #endif - FBXGeometry* geometryPtr = new FBXGeometry; - FBXGeometry& geometry = *geometryPtr; + HFMGeometry* geometryPtr = new HFMGeometry; + HFMGeometry& geometry = *geometryPtr; geometry.originalURL = url; @@ -944,7 +944,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS lightprop = vprop.toString(); } - FBXLight light = extractLight(object); + HFMLight light = extractLight(object); } } } else { @@ -1102,7 +1102,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS _textureContent.insert(filepath, content); } } else if (object.name == "Material") { - FBXMaterial material; + HFMMaterial material; material.name = (object.properties.at(1).toString()); foreach (const FBXNode& subobject, object.children) { bool properties = false; @@ -1255,7 +1255,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS #endif } material.materialID = getID(object.properties); - _fbxMaterials.insert(material.materialID, material); + _hfmMaterials.insert(material.materialID, material); } else if (object.name == "NodeAttribute") { @@ -1276,7 +1276,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS if (!attributetype.isEmpty()) { if (attributetype == "Light") { - FBXLight light = extractLight(object); + HFMLight light = extractLight(object); lights[attribID] = light; } } @@ -1345,7 +1345,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS QString parentID = getID(connection.properties, 2); ooChildToParent.insert(childID, parentID); if (!hifiGlobalNodeID.isEmpty() && (parentID == hifiGlobalNodeID)) { - std::map< QString, FBXLight >::iterator lightIt = lights.find(childID); + std::map< QString, HFMLight >::iterator lightIt = lights.find(childID); if (lightIt != lights.end()) { _lightmapLevel = (*lightIt).second.intensity; if (_lightmapLevel <= 0.0f) { @@ -1504,7 +1504,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS frameCount = qMax(frameCount, curve.values.size()); } for (int i = 0; i < frameCount; i++) { - FBXAnimationFrame frame; + HFMAnimationFrame frame; frame.rotations.resize(modelIDs.size()); frame.translations.resize(modelIDs.size()); geometry.animationFrames.append(frame); @@ -1515,7 +1515,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS geometry.hasSkeletonJoints = false; foreach (const QString& modelID, modelIDs) { const FBXModel& model = models[modelID]; - FBXJoint joint; + HFMJoint joint; joint.isFree = freeJoints.contains(model.name); joint.parentIndex = model.parentIndex; @@ -1553,7 +1553,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS joint.distanceToParent = 0.0f; } else { - const FBXJoint& parentJoint = geometry.joints.at(joint.parentIndex); + const HFMJoint& parentJoint = geometry.joints.at(joint.parentIndex); joint.transform = parentJoint.transform * glm::translate(joint.translation) * joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform; joint.inverseDefaultRotation = glm::inverse(combinedRotation) * parentJoint.inverseDefaultRotation; @@ -1631,7 +1631,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS geometry.meshExtents.reset(); // Create the Material Library - consolidateFBXMaterials(mapping); + consolidateHFMMaterials(mapping); // We can't allow the scaling of a given image to different sizes, because the hash used for the KTX cache is based on the original image // Allowing scaling of the same image to different sizes would cause different KTX files to target the same cache key @@ -1643,7 +1643,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // 33 - 128 textures --> 512 // etc... QSet uniqueTextures; - for (auto& material : _fbxMaterials) { + for (auto& material : _hfmMaterials) { material.getTextureNames(uniqueTextures); } int numTextures = uniqueTextures.size(); @@ -1659,15 +1659,15 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } while (numTextureThreshold < numTextures && maxWidth > MIN_MIP_TEXTURE_WIDTH); qCDebug(modelformat) << "Capped square texture width =" << maxWidth << "for model" << url << "with" << numTextures << "textures"; - for (auto& material : _fbxMaterials) { + for (auto& material : _hfmMaterials) { material.setMaxNumPixelsPerTexture(maxWidth * maxWidth); } } #endif - geometry.materials = _fbxMaterials; + geometry.materials = _hfmMaterials; // see if any materials have texture children - bool materialsHaveTextures = checkMaterialsHaveTextures(_fbxMaterials, _textureFilenames, _connectionChildMap); + bool materialsHaveTextures = checkMaterialsHaveTextures(_hfmMaterials, _textureFilenames, _connectionChildMap); for (QMap::iterator it = meshes.begin(); it != meshes.end(); it++) { ExtractedMesh& extracted = it.value(); @@ -1698,13 +1698,13 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS for (int i = children.size() - 1; i >= 0; i--) { const QString& childID = children.at(i); - if (_fbxMaterials.contains(childID)) { + if (_hfmMaterials.contains(childID)) { // the pure material associated with this part - FBXMaterial material = _fbxMaterials.value(childID); + HFMMaterial material = _hfmMaterials.value(childID); for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { if (extracted.partMaterialTextures.at(j).first == materialIndex) { - FBXMeshPart& part = extracted.mesh.parts[j]; + HFMMeshPart& part = extracted.mesh.parts[j]; part.materialID = material.materialID; generateTangents |= material.needTangentSpace(); } @@ -1713,7 +1713,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS materialIndex++; } else if (_textureFilenames.contains(childID)) { - FBXTexture texture = getTexture(childID); + HFMTexture texture = getTexture(childID); for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { int partTexture = extracted.partMaterialTextures.at(j).second; if (partTexture == textureIndex && !(partTexture == 0 && materialsHaveTextures)) { @@ -1736,34 +1736,34 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS if (!clusters.contains(clusterID)) { continue; } - FBXCluster fbxCluster; + HFMCluster hfmCluster; const Cluster& cluster = clusters[clusterID]; clusterIDs.append(clusterID); // see http://stackoverflow.com/questions/13566608/loading-skinning-information-from-fbx for a discussion // of skinning information in FBX QString jointID = _connectionChildMap.value(clusterID); - fbxCluster.jointIndex = modelIDs.indexOf(jointID); - if (fbxCluster.jointIndex == -1) { + hfmCluster.jointIndex = modelIDs.indexOf(jointID); + if (hfmCluster.jointIndex == -1) { qCDebug(modelformat) << "Joint not in model list: " << jointID; - fbxCluster.jointIndex = 0; + hfmCluster.jointIndex = 0; } - fbxCluster.inverseBindMatrix = glm::inverse(cluster.transformLink) * modelTransform; + hfmCluster.inverseBindMatrix = glm::inverse(cluster.transformLink) * modelTransform; // slam bottom row to (0, 0, 0, 1), we KNOW this is not a perspective matrix and // sometimes floating point fuzz can be introduced after the inverse. - fbxCluster.inverseBindMatrix[0][3] = 0.0f; - fbxCluster.inverseBindMatrix[1][3] = 0.0f; - fbxCluster.inverseBindMatrix[2][3] = 0.0f; - fbxCluster.inverseBindMatrix[3][3] = 1.0f; + hfmCluster.inverseBindMatrix[0][3] = 0.0f; + hfmCluster.inverseBindMatrix[1][3] = 0.0f; + hfmCluster.inverseBindMatrix[2][3] = 0.0f; + hfmCluster.inverseBindMatrix[3][3] = 1.0f; - fbxCluster.inverseBindTransform = Transform(fbxCluster.inverseBindMatrix); + hfmCluster.inverseBindTransform = Transform(hfmCluster.inverseBindMatrix); - extracted.mesh.clusters.append(fbxCluster); + extracted.mesh.clusters.append(hfmCluster); // override the bind rotation with the transform link - FBXJoint& joint = geometry.joints[fbxCluster.jointIndex]; + HFMJoint& joint = geometry.joints[hfmCluster.jointIndex]; joint.inverseBindRotation = glm::inverse(extractRotation(cluster.transformLink)); joint.bindTransform = cluster.transformLink; joint.bindTransformFoundInCluster = true; @@ -1776,7 +1776,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // if we don't have a skinned joint, parent to the model itself if (extracted.mesh.clusters.isEmpty()) { - FBXCluster cluster; + HFMCluster cluster; cluster.jointIndex = modelIDs.indexOf(modelID); if (cluster.jointIndex == -1) { qCDebug(modelformat) << "Model not in model list: " << modelID; @@ -1786,7 +1786,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } // whether we're skinned depends on how many clusters are attached - const FBXCluster& firstFBXCluster = extracted.mesh.clusters.at(0); + const HFMCluster& firstHFMCluster = extracted.mesh.clusters.at(0); glm::mat4 inverseModelTransform = glm::inverse(modelTransform); if (clusterIDs.size() > 1) { // this is a multi-mesh joint @@ -1799,9 +1799,9 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS for (int i = 0; i < clusterIDs.size(); i++) { QString clusterID = clusterIDs.at(i); const Cluster& cluster = clusters[clusterID]; - const FBXCluster& fbxCluster = extracted.mesh.clusters.at(i); - int jointIndex = fbxCluster.jointIndex; - FBXJoint& joint = geometry.joints[jointIndex]; + const HFMCluster& hfmCluster = extracted.mesh.clusters.at(i); + int jointIndex = hfmCluster.jointIndex; + HFMJoint& joint = geometry.joints[jointIndex]; glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform; glm::vec3 boneEnd = extractTranslation(transformJointToMesh); glm::vec3 boneBegin = boneEnd; @@ -1881,8 +1881,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } } else { // this is a single-mesh joint - int jointIndex = firstFBXCluster.jointIndex; - FBXJoint& joint = geometry.joints[jointIndex]; + int jointIndex = firstHFMCluster.jointIndex; + HFMJoint& joint = geometry.joints[jointIndex]; // transform cluster vertices to joint-frame and save for later glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; @@ -1924,7 +1924,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // now that all joints have been scanned compute a k-Dop bounding volume of mesh for (int i = 0; i < geometry.joints.size(); ++i) { - FBXJoint& joint = geometry.joints[i]; + HFMJoint& joint = geometry.joints[i]; // NOTE: points are in joint-frame ShapeVertices& points = shapeVertices.at(i); @@ -1994,13 +1994,13 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS return geometryPtr; } -FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { +HFMGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { QBuffer buffer(const_cast(&model)); buffer.open(QIODevice::ReadOnly); return readFBX(&buffer, mapping, url, loadLightmaps, lightmapLevel); } -FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { +HFMGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { FBXReader reader; reader._rootNode = FBXReader::parseFBX(device); reader._loadLightmaps = loadLightmaps; @@ -2008,5 +2008,5 @@ FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QStri qCDebug(modelformat) << "Reading FBX: " << url; - return reader.extractFBXGeometry(mapping, url); + return reader.extractHFMGeometry(mapping, url); } diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index c391ea6647..f95ba7fe73 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -36,11 +36,11 @@ class FBXNode; /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); +HFMGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); +HFMGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); class TextureParam { public: @@ -103,20 +103,20 @@ class ExtractedMesh; class FBXReader { public: - FBXGeometry* _fbxGeometry; + HFMGeometry* _hfmGeometry; FBXNode _rootNode; static FBXNode parseFBX(QIODevice* device); - FBXGeometry* extractFBXGeometry(const QVariantHash& mapping, const QString& url); + HFMGeometry* extractHFMGeometry(const QVariantHash& mapping, const QString& url); static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true); QHash meshes; - static void buildModelMesh(FBXMesh& extractedMesh, const QString& url); + static void buildModelMesh(HFMMesh& extractedMesh, const QString& url); static glm::vec3 normalizeDirForPacking(const glm::vec3& dir); - FBXTexture getTexture(const QString& textureID); + HFMTexture getTexture(const QString& textureID); QHash _textureNames; // Hashes the original RelativeFilename of textures @@ -142,9 +142,9 @@ public: QHash ambientFactorTextures; QHash occlusionTextures; - QHash _fbxMaterials; + QHash _hfmMaterials; - void consolidateFBXMaterials(const QVariantHash& mapping); + void consolidateHFMMaterials(const QVariantHash& mapping); bool _loadLightmaps = true; float _lightmapOffset = 0.0f; diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index d5902962e5..ff1de30b97 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -27,7 +27,7 @@ #include "ModelFormatLogging.h" -void FBXMaterial::getTextureNames(QSet& textureList) const { +void HFMMaterial::getTextureNames(QSet& textureList) const { if (!normalTexture.isNull()) { textureList.insert(normalTexture.name); } @@ -63,7 +63,7 @@ void FBXMaterial::getTextureNames(QSet& textureList) const { } } -void FBXMaterial::setMaxNumPixelsPerTexture(int maxNumPixels) { +void HFMMaterial::setMaxNumPixelsPerTexture(int maxNumPixels) { normalTexture.maxNumPixels = maxNumPixels; albedoTexture.maxNumPixels = maxNumPixels; opacityTexture.maxNumPixels = maxNumPixels; @@ -77,12 +77,12 @@ void FBXMaterial::setMaxNumPixelsPerTexture(int maxNumPixels) { lightmapTexture.maxNumPixels = maxNumPixels; } -bool FBXMaterial::needTangentSpace() const { +bool HFMMaterial::needTangentSpace() const { return !normalTexture.isNull(); } -FBXTexture FBXReader::getTexture(const QString& textureID) { - FBXTexture texture; +HFMTexture FBXReader::getTexture(const QString& textureID) { + HFMTexture texture; const QByteArray& filepath = _textureFilepaths.value(textureID); texture.content = _textureContent.value(filepath); @@ -123,7 +123,7 @@ FBXTexture FBXReader::getTexture(const QString& textureID) { return texture; } -void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { +void FBXReader::consolidateHFMMaterials(const QVariantHash& mapping) { QString materialMapString = mapping.value("materialMap").toString(); QJsonDocument materialMapDocument = QJsonDocument::fromJson(materialMapString.toUtf8()); @@ -133,16 +133,16 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { qCDebug(modelformat) << "fbx Material Map found but did not produce valid JSON:" << materialMapString; } } - for (QHash::iterator it = _fbxMaterials.begin(); it != _fbxMaterials.end(); it++) { - FBXMaterial& material = (*it); + for (QHash::iterator it = _hfmMaterials.begin(); it != _hfmMaterials.end(); it++) { + HFMMaterial& material = (*it); // Maya is the exporting the shading model and we are trying to use it bool isMaterialLambert = (material.shadingModel.toLower() == "lambert"); // the pure material associated with this part bool detectDifferentUVs = false; - FBXTexture diffuseTexture; - FBXTexture diffuseFactorTexture; + HFMTexture diffuseTexture; + HFMTexture diffuseFactorTexture; QString diffuseTextureID = diffuseTextures.value(material.materialID); QString diffuseFactorTextureID = diffuseFactorTextures.value(material.materialID); @@ -169,7 +169,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { detectDifferentUVs = (diffuseTexture.texcoordSet != 0) || (!diffuseTexture.transform.isIdentity()); } - FBXTexture transparentTexture; + HFMTexture transparentTexture; QString transparentTextureID = transparentTextures.value(material.materialID); // If PBS Material, systematically bind the albedo texture as transparency texture and check for the alpha channel if (material.isPBSMaterial) { @@ -181,7 +181,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { detectDifferentUVs |= (transparentTexture.texcoordSet != 0) || (!transparentTexture.transform.isIdentity()); } - FBXTexture normalTexture; + HFMTexture normalTexture; QString bumpTextureID = bumpTextures.value(material.materialID); QString normalTextureID = normalTextures.value(material.materialID); if (!normalTextureID.isNull()) { @@ -198,7 +198,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { detectDifferentUVs |= (normalTexture.texcoordSet != 0) || (!normalTexture.transform.isIdentity()); } - FBXTexture specularTexture; + HFMTexture specularTexture; QString specularTextureID = specularTextures.value(material.materialID); if (!specularTextureID.isNull()) { specularTexture = getTexture(specularTextureID); @@ -206,7 +206,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { material.specularTexture = specularTexture; } - FBXTexture metallicTexture; + HFMTexture metallicTexture; QString metallicTextureID = metallicTextures.value(material.materialID); if (!metallicTextureID.isNull()) { metallicTexture = getTexture(metallicTextureID); @@ -214,7 +214,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { material.metallicTexture = metallicTexture; } - FBXTexture roughnessTexture; + HFMTexture roughnessTexture; QString roughnessTextureID = roughnessTextures.value(material.materialID); if (!roughnessTextureID.isNull()) { roughnessTexture = getTexture(roughnessTextureID); @@ -222,7 +222,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { detectDifferentUVs |= (roughnessTexture.texcoordSet != 0) || (!roughnessTexture.transform.isIdentity()); } - FBXTexture shininessTexture; + HFMTexture shininessTexture; QString shininessTextureID = shininessTextures.value(material.materialID); if (!shininessTextureID.isNull()) { shininessTexture = getTexture(shininessTextureID); @@ -230,7 +230,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { detectDifferentUVs |= (shininessTexture.texcoordSet != 0) || (!shininessTexture.transform.isIdentity()); } - FBXTexture emissiveTexture; + HFMTexture emissiveTexture; QString emissiveTextureID = emissiveTextures.value(material.materialID); if (!emissiveTextureID.isNull()) { emissiveTexture = getTexture(emissiveTextureID); @@ -245,7 +245,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { } } - FBXTexture occlusionTexture; + HFMTexture occlusionTexture; QString occlusionTextureID = occlusionTextures.value(material.materialID); if (occlusionTextureID.isNull()) { // 2nd chance @@ -265,7 +265,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { lightmapParams.x = _lightmapOffset; lightmapParams.y = _lightmapLevel; - FBXTexture ambientTexture; + HFMTexture ambientTexture; QString ambientTextureID = ambientTextures.value(material.materialID); if (ambientTextureID.isNull()) { // 2nd chance @@ -326,7 +326,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { if (materialOptions.contains("scatteringMap")) { QByteArray scatteringMap = materialOptions.value("scatteringMap").toVariant().toByteArray(); - material.scatteringTexture = FBXTexture(); + material.scatteringTexture = HFMTexture(); material.scatteringTexture.name = material.name + ".scatteringMap"; material.scatteringTexture.filename = scatteringMap; } diff --git a/libraries/fbx/src/FBXReader_Mesh.cpp b/libraries/fbx/src/FBXReader_Mesh.cpp index c9b004c3a8..e098aff99a 100644 --- a/libraries/fbx/src/FBXReader_Mesh.cpp +++ b/libraries/fbx/src/FBXReader_Mesh.cpp @@ -42,9 +42,9 @@ using vec2h = glm::tvec2; -#define FBX_PACK_COLORS 1 +#define HFM_PACK_COLORS 1 -#if FBX_PACK_COLORS +#if HFM_PACK_COLORS using ColorType = glm::uint32; #define FBX_COLOR_ELEMENT gpu::Element::COLOR_RGBA_32 #else @@ -469,7 +469,7 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn QPair materialTexture(materialID, 0); - // grab or setup the FBXMeshPart for the part this face belongs to + // grab or setup the HFMMeshPart for the part this face belongs to int& partIndexPlusOne = materialTextureParts[materialTexture]; if (partIndexPlusOne == 0) { data.extracted.partMaterialTextures.append(materialTexture); @@ -478,7 +478,7 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn } // give the mesh part this index - FBXMeshPart& part = data.extracted.mesh.parts[partIndexPlusOne - 1]; + HFMMeshPart& part = data.extracted.mesh.parts[partIndexPlusOne - 1]; part.triangleIndices.append(firstCorner.value()); part.triangleIndices.append(dracoFace[1].value()); part.triangleIndices.append(dracoFace[2].value()); @@ -511,7 +511,7 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1); partIndex = data.extracted.mesh.parts.size(); } - FBXMeshPart& part = data.extracted.mesh.parts[partIndex - 1]; + HFMMeshPart& part = data.extracted.mesh.parts[partIndex - 1]; if (endIndex - beginIndex == 4) { appendIndex(data, part.quadIndices, beginIndex++, deduplicate); @@ -565,9 +565,9 @@ glm::vec3 FBXReader::normalizeDirForPacking(const glm::vec3& dir) { return dir; } -void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { +void FBXReader::buildModelMesh(HFMMesh& extractedMesh, const QString& url) { unsigned int totalSourceIndices = 0; - foreach(const FBXMeshPart& part, extractedMesh.parts) { + foreach(const HFMMeshPart& part, extractedMesh.parts) { totalSourceIndices += (part.quadTrianglesIndices.size() + part.triangleIndices.size()); } @@ -583,17 +583,17 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { return; } - FBXMesh& fbxMesh = extractedMesh; + HFMMesh& hfmMesh = extractedMesh; graphics::MeshPointer mesh(new graphics::Mesh()); int numVerts = extractedMesh.vertices.size(); - if (!fbxMesh.normals.empty() && fbxMesh.tangents.empty()) { + if (!hfmMesh.normals.empty() && hfmMesh.tangents.empty()) { // Fill with a dummy value to force tangents to be present if there are normals - fbxMesh.tangents.reserve(fbxMesh.normals.size()); - std::fill_n(std::back_inserter(fbxMesh.tangents), fbxMesh.normals.size(), Vectors::UNIT_X); + hfmMesh.tangents.reserve(hfmMesh.normals.size()); + std::fill_n(std::back_inserter(hfmMesh.tangents), hfmMesh.normals.size(), Vectors::UNIT_X); } // Same thing with blend shapes - for (auto& blendShape : fbxMesh.blendshapes) { + for (auto& blendShape : hfmMesh.blendshapes) { if (!blendShape.normals.empty() && blendShape.tangents.empty()) { // Fill with a dummy value to force tangents to be present if there are normals blendShape.tangents.reserve(blendShape.normals.size()); @@ -609,8 +609,8 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { // Normal and tangent are always there together packed in normalized xyz32bits word (times 2) const auto normalElement = FBX_NORMAL_ELEMENT; - const int normalsSize = fbxMesh.normals.size() * normalElement.getSize(); - const int tangentsSize = fbxMesh.tangents.size() * normalElement.getSize(); + const int normalsSize = hfmMesh.normals.size() * normalElement.getSize(); + const int tangentsSize = hfmMesh.tangents.size() * normalElement.getSize(); // If there are normals then there should be tangents assert(normalsSize <= tangentsSize); if (tangentsSize > normalsSize) { @@ -620,22 +620,22 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { // Color attrib const auto colorElement = FBX_COLOR_ELEMENT; - const int colorsSize = fbxMesh.colors.size() * colorElement.getSize(); + const int colorsSize = hfmMesh.colors.size() * colorElement.getSize(); // Texture coordinates are stored in 2 half floats const auto texCoordsElement = gpu::Element(gpu::VEC2, gpu::HALF, gpu::UV); - const int texCoordsSize = fbxMesh.texCoords.size() * texCoordsElement.getSize(); - const int texCoords1Size = fbxMesh.texCoords1.size() * texCoordsElement.getSize(); + const int texCoordsSize = hfmMesh.texCoords.size() * texCoordsElement.getSize(); + const int texCoords1Size = hfmMesh.texCoords1.size() * texCoordsElement.getSize(); // Support for 4 skinning clusters: // 4 Indices are uint8 ideally, uint16 if more than 256. - const auto clusterIndiceElement = (fbxMesh.clusters.size() < UINT8_MAX ? gpu::Element(gpu::VEC4, gpu::UINT8, gpu::XYZW) : gpu::Element(gpu::VEC4, gpu::UINT16, gpu::XYZW)); + const auto clusterIndiceElement = (hfmMesh.clusters.size() < UINT8_MAX ? gpu::Element(gpu::VEC4, gpu::UINT8, gpu::XYZW) : gpu::Element(gpu::VEC4, gpu::UINT16, gpu::XYZW)); // 4 Weights are normalized 16bits const auto clusterWeightElement = gpu::Element(gpu::VEC4, gpu::NUINT16, gpu::XYZW); // Cluster indices and weights must be the same sizes const int NUM_CLUSTERS_PER_VERT = 4; - const int numVertClusters = (fbxMesh.clusterIndices.size() == fbxMesh.clusterWeights.size() ? fbxMesh.clusterIndices.size() / NUM_CLUSTERS_PER_VERT : 0); + const int numVertClusters = (hfmMesh.clusterIndices.size() == hfmMesh.clusterWeights.size() ? hfmMesh.clusterIndices.size() / NUM_CLUSTERS_PER_VERT : 0); const int clusterIndicesSize = numVertClusters * clusterIndiceElement.getSize(); const int clusterWeightsSize = numVertClusters * clusterWeightElement.getSize(); @@ -660,9 +660,9 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { if (normalsSize > 0) { std::vector normalsAndTangents; - normalsAndTangents.reserve(fbxMesh.normals.size() + fbxMesh.tangents.size()); - for (auto normalIt = fbxMesh.normals.constBegin(), tangentIt = fbxMesh.tangents.constBegin(); - normalIt != fbxMesh.normals.constEnd(); + normalsAndTangents.reserve(hfmMesh.normals.size() + hfmMesh.tangents.size()); + for (auto normalIt = hfmMesh.normals.constBegin(), tangentIt = hfmMesh.tangents.constBegin(); + normalIt != hfmMesh.normals.constEnd(); ++normalIt, ++tangentIt) { #if FBX_PACK_NORMALS const auto normal = normalizeDirForPacking(*normalIt); @@ -681,24 +681,24 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { // Pack colors if (colorsSize > 0) { -#if FBX_PACK_COLORS +#if HFM_PACK_COLORS std::vector colors; - colors.reserve(fbxMesh.colors.size()); - for (const auto& color : fbxMesh.colors) { + colors.reserve(hfmMesh.colors.size()); + for (const auto& color : hfmMesh.colors) { colors.push_back(glm::packUnorm4x8(glm::vec4(color, 1.0f))); } vertBuffer->setSubData(colorsOffset, colorsSize, (const gpu::Byte*) colors.data()); #else - vertBuffer->setSubData(colorsOffset, colorsSize, (const gpu::Byte*) fbxMesh.colors.constData()); + vertBuffer->setSubData(colorsOffset, colorsSize, (const gpu::Byte*) hfmMesh.colors.constData()); #endif } // Pack Texcoords 0 and 1 (if exists) if (texCoordsSize > 0) { QVector texCoordData; - texCoordData.reserve(fbxMesh.texCoords.size()); - for (auto& texCoordVec2f : fbxMesh.texCoords) { + texCoordData.reserve(hfmMesh.texCoords.size()); + for (auto& texCoordVec2f : hfmMesh.texCoords) { vec2h texCoordVec2h; texCoordVec2h.x = glm::detail::toFloat16(texCoordVec2f.x); @@ -709,8 +709,8 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { } if (texCoords1Size > 0) { QVector texCoordData; - texCoordData.reserve(fbxMesh.texCoords1.size()); - for (auto& texCoordVec2f : fbxMesh.texCoords1) { + texCoordData.reserve(hfmMesh.texCoords1.size()); + for (auto& texCoordVec2f : hfmMesh.texCoords1) { vec2h texCoordVec2h; texCoordVec2h.x = glm::detail::toFloat16(texCoordVec2f.x); @@ -722,22 +722,22 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { // Clusters data if (clusterIndicesSize > 0) { - if (fbxMesh.clusters.size() < UINT8_MAX) { + if (hfmMesh.clusters.size() < UINT8_MAX) { // yay! we can fit the clusterIndices within 8-bits - int32_t numIndices = fbxMesh.clusterIndices.size(); + int32_t numIndices = hfmMesh.clusterIndices.size(); QVector clusterIndices; clusterIndices.resize(numIndices); for (int32_t i = 0; i < numIndices; ++i) { - assert(fbxMesh.clusterIndices[i] <= UINT8_MAX); - clusterIndices[i] = (uint8_t)(fbxMesh.clusterIndices[i]); + assert(hfmMesh.clusterIndices[i] <= UINT8_MAX); + clusterIndices[i] = (uint8_t)(hfmMesh.clusterIndices[i]); } vertBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (const gpu::Byte*) clusterIndices.constData()); } else { - vertBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (const gpu::Byte*) fbxMesh.clusterIndices.constData()); + vertBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (const gpu::Byte*) hfmMesh.clusterIndices.constData()); } } if (clusterWeightsSize > 0) { - vertBuffer->setSubData(clusterWeightsOffset, clusterWeightsSize, (const gpu::Byte*) fbxMesh.clusterWeights.constData()); + vertBuffer->setSubData(clusterWeightsOffset, clusterWeightsSize, (const gpu::Byte*) hfmMesh.clusterWeights.constData()); } @@ -856,7 +856,7 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { // Index and Part Buffers unsigned int totalIndices = 0; - foreach(const FBXMeshPart& part, extractedMesh.parts) { + foreach(const HFMMeshPart& part, extractedMesh.parts) { totalIndices += (part.quadTrianglesIndices.size() + part.triangleIndices.size()); } @@ -875,7 +875,7 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { if (extractedMesh.parts.size() > 1) { indexNum = 0; } - foreach(const FBXMeshPart& part, extractedMesh.parts) { + foreach(const HFMMeshPart& part, extractedMesh.parts) { graphics::Mesh::Part modelPart(indexNum, 0, 0, graphics::Mesh::TRIANGLES); if (part.quadTrianglesIndices.size()) { diff --git a/libraries/fbx/src/GLTFReader.cpp b/libraries/fbx/src/GLTFReader.cpp index b93dc3541b..7ee13c5cdf 100644 --- a/libraries/fbx/src/GLTFReader.cpp +++ b/libraries/fbx/src/GLTFReader.cpp @@ -697,7 +697,7 @@ glm::mat4 GLTFReader::getModelTransform(const GLTFNode& node) { return tmat; } -bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { +bool GLTFReader::buildGeometry(HFMGeometry& geometry, const QUrl& url) { //Build dependencies QVector> nodeDependencies(_file.nodes.size()); @@ -750,10 +750,10 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { for (int i = 0; i < materialIDs.size(); i++) { QString& matid = materialIDs[i]; - geometry.materials[matid] = FBXMaterial(); - FBXMaterial& fbxMaterial = geometry.materials[matid]; - fbxMaterial._material = std::make_shared(); - setFBXMaterial(fbxMaterial, _file.materials[i]); + geometry.materials[matid] = HFMMaterial(); + HFMMaterial& hfmMaterial = geometry.materials[matid]; + hfmMaterial._material = std::make_shared(); + setHFMMaterial(hfmMaterial, _file.materials[i]); } @@ -765,9 +765,9 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { if (node.defined["mesh"]) { qCDebug(modelformat) << "node_transforms" << node.transforms; foreach(auto &primitive, _file.meshes[node.mesh].primitives) { - geometry.meshes.append(FBXMesh()); - FBXMesh& mesh = geometry.meshes[geometry.meshes.size() - 1]; - FBXCluster cluster; + geometry.meshes.append(HFMMesh()); + HFMMesh& mesh = geometry.meshes[geometry.meshes.size() - 1]; + HFMCluster cluster; cluster.jointIndex = 0; cluster.inverseBindMatrix = glm::mat4(1, 0, 0, 0, 0, 1, 0, 0, @@ -775,7 +775,7 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { 0, 0, 0, 1); mesh.clusters.append(cluster); - FBXMeshPart part = FBXMeshPart(); + HFMMeshPart part = HFMMeshPart(); int indicesAccessorIdx = primitive.indices; @@ -910,7 +910,7 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { return true; } -FBXGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping, +HFMGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping, const QUrl& url, bool loadLightmaps, float lightmapLevel) { _url = url; @@ -924,12 +924,12 @@ FBXGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping parseGLTF(model); //_file.dump(); - FBXGeometry* geometryPtr = new FBXGeometry(); - FBXGeometry& geometry = *geometryPtr; + HFMGeometry* geometryPtr = new HFMGeometry(); + HFMGeometry& geometry = *geometryPtr; buildGeometry(geometry, url); - //fbxDebugDump(geometry); + //hfmDebugDump(geometry); return geometryPtr; } @@ -997,8 +997,8 @@ QNetworkReply* GLTFReader::request(QUrl& url, bool isTest) { return netReply; // trying to sync later on. } -FBXTexture GLTFReader::getFBXTexture(const GLTFTexture& texture) { - FBXTexture fbxtex = FBXTexture(); +HFMTexture GLTFReader::getHFMTexture(const GLTFTexture& texture) { + HFMTexture fbxtex = HFMTexture(); fbxtex.texcoordSet = 0; if (texture.defined["source"]) { @@ -1014,7 +1014,7 @@ FBXTexture GLTFReader::getFBXTexture(const GLTFTexture& texture) { return fbxtex; } -void GLTFReader::setFBXMaterial(FBXMaterial& fbxmat, const GLTFMaterial& material) { +void GLTFReader::setHFMMaterial(HFMMaterial& fbxmat, const GLTFMaterial& material) { if (material.defined["name"]) { @@ -1029,17 +1029,17 @@ void GLTFReader::setFBXMaterial(FBXMaterial& fbxmat, const GLTFMaterial& materia } if (material.defined["emissiveTexture"]) { - fbxmat.emissiveTexture = getFBXTexture(_file.textures[material.emissiveTexture]); + fbxmat.emissiveTexture = getHFMTexture(_file.textures[material.emissiveTexture]); fbxmat.useEmissiveMap = true; } if (material.defined["normalTexture"]) { - fbxmat.normalTexture = getFBXTexture(_file.textures[material.normalTexture]); + fbxmat.normalTexture = getHFMTexture(_file.textures[material.normalTexture]); fbxmat.useNormalMap = true; } if (material.defined["occlusionTexture"]) { - fbxmat.occlusionTexture = getFBXTexture(_file.textures[material.occlusionTexture]); + fbxmat.occlusionTexture = getHFMTexture(_file.textures[material.occlusionTexture]); fbxmat.useOcclusionMap = true; } @@ -1050,14 +1050,14 @@ void GLTFReader::setFBXMaterial(FBXMaterial& fbxmat, const GLTFMaterial& materia fbxmat.metallic = material.pbrMetallicRoughness.metallicFactor; } if (material.pbrMetallicRoughness.defined["baseColorTexture"]) { - fbxmat.opacityTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]); - fbxmat.albedoTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]); + fbxmat.opacityTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]); + fbxmat.albedoTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]); fbxmat.useAlbedoMap = true; } if (material.pbrMetallicRoughness.defined["metallicRoughnessTexture"]) { - fbxmat.roughnessTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]); + fbxmat.roughnessTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]); fbxmat.useRoughnessMap = true; - fbxmat.metallicTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]); + fbxmat.metallicTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]); fbxmat.useMetallicMap = true; } if (material.pbrMetallicRoughness.defined["roughnessFactor"]) { @@ -1181,37 +1181,37 @@ void GLTFReader::retriangulate(const QVector& inIndices, const QVector materialMeshIdMap; - QVector fbxMeshParts; + QVector hfmMeshParts; for (int i = 0, meshPartCount = 0; i < mesh.parts.count(); i++, meshPartCount++) { - FBXMeshPart& meshPart = mesh.parts[i]; + HFMMeshPart& meshPart = mesh.parts[i]; FaceGroup faceGroup = faceGroups[meshPartCount]; bool specifiesUV = false; foreach(OBJFace face, faceGroup) { // Go through all of the OBJ faces and determine the number of different materials necessary (each different material will be a unique mesh). // NOTE (trent/mittens 3/30/17): this seems hardcore wasteful and is slowed down a bit by iterating through the face group twice, but it's the best way I've thought of to hack multi-material support in an OBJ into this pipeline. if (!materialMeshIdMap.contains(face.materialName)) { - // Create a new FBXMesh for this material mapping. + // Create a new HFMMesh for this material mapping. materialMeshIdMap.insert(face.materialName, materialMeshIdMap.count()); - fbxMeshParts.append(FBXMeshPart()); - FBXMeshPart& meshPartNew = fbxMeshParts.last(); + hfmMeshParts.append(HFMMeshPart()); + HFMMeshPart& meshPartNew = hfmMeshParts.last(); meshPartNew.quadIndices = QVector(meshPart.quadIndices); // Copy over quad indices [NOTE (trent/mittens, 4/3/17): Likely unnecessary since they go unused anyway]. meshPartNew.quadTrianglesIndices = QVector(meshPart.quadTrianglesIndices); // Copy over quad triangulated indices [NOTE (trent/mittens, 4/3/17): Likely unnecessary since they go unused anyway]. meshPartNew.triangleIndices = QVector(meshPart.triangleIndices); // Copy over triangle indices. @@ -745,14 +745,14 @@ FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m // clean up old mesh parts. int unmodifiedMeshPartCount = mesh.parts.count(); mesh.parts.clear(); - mesh.parts = QVector(fbxMeshParts); + mesh.parts = QVector(hfmMeshParts); for (int i = 0, meshPartCount = 0; i < unmodifiedMeshPartCount; i++, meshPartCount++) { FaceGroup faceGroup = faceGroups[meshPartCount]; // Now that each mesh has been created with its own unique material mappings, fill them with data (vertex data is duplicated, face data is not). foreach(OBJFace face, faceGroup) { - FBXMeshPart& meshPart = mesh.parts[materialMeshIdMap[face.materialName]]; + HFMMeshPart& meshPart = mesh.parts[materialMeshIdMap[face.materialName]]; glm::vec3 v0 = checked_at(vertices, face.vertexIndices[0]); glm::vec3 v1 = checked_at(vertices, face.vertexIndices[1]); @@ -824,7 +824,7 @@ FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m // Build the single mesh. FBXReader::buildModelMesh(mesh, url.toString()); - // fbxDebugDump(geometry); + // hfmDebugDump(geometry); } catch(const std::exception& e) { qCDebug(modelformat) << "OBJ reader fail: " << e.what(); } @@ -885,38 +885,38 @@ FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m if (!objMaterial.used) { continue; } - geometry.materials[materialID] = FBXMaterial(objMaterial.diffuseColor, + geometry.materials[materialID] = HFMMaterial(objMaterial.diffuseColor, objMaterial.specularColor, objMaterial.emissiveColor, objMaterial.shininess, objMaterial.opacity); - FBXMaterial& fbxMaterial = geometry.materials[materialID]; - fbxMaterial.materialID = materialID; - fbxMaterial._material = std::make_shared(); - graphics::MaterialPointer modelMaterial = fbxMaterial._material; + HFMMaterial& hfmMaterial = geometry.materials[materialID]; + hfmMaterial.materialID = materialID; + hfmMaterial._material = std::make_shared(); + graphics::MaterialPointer modelMaterial = hfmMaterial._material; if (!objMaterial.diffuseTextureFilename.isEmpty()) { - fbxMaterial.albedoTexture.filename = objMaterial.diffuseTextureFilename; + hfmMaterial.albedoTexture.filename = objMaterial.diffuseTextureFilename; } if (!objMaterial.specularTextureFilename.isEmpty()) { - fbxMaterial.specularTexture.filename = objMaterial.specularTextureFilename; + hfmMaterial.specularTexture.filename = objMaterial.specularTextureFilename; } if (!objMaterial.emissiveTextureFilename.isEmpty()) { - fbxMaterial.emissiveTexture.filename = objMaterial.emissiveTextureFilename; + hfmMaterial.emissiveTexture.filename = objMaterial.emissiveTextureFilename; } if (!objMaterial.bumpTextureFilename.isEmpty()) { - fbxMaterial.normalTexture.filename = objMaterial.bumpTextureFilename; - fbxMaterial.normalTexture.isBumpmap = true; - fbxMaterial.bumpMultiplier = objMaterial.bumpTextureOptions.bumpMultiplier; + hfmMaterial.normalTexture.filename = objMaterial.bumpTextureFilename; + hfmMaterial.normalTexture.isBumpmap = true; + hfmMaterial.bumpMultiplier = objMaterial.bumpTextureOptions.bumpMultiplier; } if (!objMaterial.opacityTextureFilename.isEmpty()) { - fbxMaterial.opacityTexture.filename = objMaterial.opacityTextureFilename; + hfmMaterial.opacityTexture.filename = objMaterial.opacityTextureFilename; } - modelMaterial->setEmissive(fbxMaterial.emissiveColor); - modelMaterial->setAlbedo(fbxMaterial.diffuseColor); - modelMaterial->setMetallic(glm::length(fbxMaterial.specularColor)); - modelMaterial->setRoughness(graphics::Material::shininessToRoughness(fbxMaterial.shininess)); + modelMaterial->setEmissive(hfmMaterial.emissiveColor); + modelMaterial->setAlbedo(hfmMaterial.diffuseColor); + modelMaterial->setMetallic(glm::length(hfmMaterial.specularColor)); + modelMaterial->setRoughness(graphics::Material::shininessToRoughness(hfmMaterial.shininess)); bool applyTransparency = false; bool applyShininess = false; @@ -971,7 +971,7 @@ FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m } if (applyTransparency) { - fbxMaterial.opacity = std::max(fbxMaterial.opacity, ILLUMINATION_MODEL_MIN_OPACITY); + hfmMaterial.opacity = std::max(hfmMaterial.opacity, ILLUMINATION_MODEL_MIN_OPACITY); } if (applyShininess) { modelMaterial->setRoughness(ILLUMINATION_MODEL_APPLY_SHININESS); @@ -985,18 +985,18 @@ FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& m modelMaterial->setFresnel(glm::vec3(1.0f)); } - modelMaterial->setOpacity(fbxMaterial.opacity); + modelMaterial->setOpacity(hfmMaterial.opacity); } return geometryPtr; } -void fbxDebugDump(const FBXGeometry& fbxgeo) { - qCDebug(modelformat) << "---------------- fbxGeometry ----------------"; - qCDebug(modelformat) << " hasSkeletonJoints =" << fbxgeo.hasSkeletonJoints; - qCDebug(modelformat) << " offset =" << fbxgeo.offset; - qCDebug(modelformat) << " meshes.count() =" << fbxgeo.meshes.count(); - foreach (FBXMesh mesh, fbxgeo.meshes) { +void hfmDebugDump(const HFMGeometry& hfmgeo) { + qCDebug(modelformat) << "---------------- hfmGeometry ----------------"; + qCDebug(modelformat) << " hasSkeletonJoints =" << hfmgeo.hasSkeletonJoints; + qCDebug(modelformat) << " offset =" << hfmgeo.offset; + qCDebug(modelformat) << " meshes.count() =" << hfmgeo.meshes.count(); + foreach (HFMMesh mesh, hfmgeo.meshes) { qCDebug(modelformat) << " vertices.count() =" << mesh.vertices.count(); qCDebug(modelformat) << " colors.count() =" << mesh.colors.count(); qCDebug(modelformat) << " normals.count() =" << mesh.normals.count(); @@ -1014,7 +1014,7 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) { qCDebug(modelformat) << " meshExtents =" << mesh.meshExtents; qCDebug(modelformat) << " modelTransform =" << mesh.modelTransform; qCDebug(modelformat) << " parts.count() =" << mesh.parts.count(); - foreach (FBXMeshPart meshPart, mesh.parts) { + foreach (HFMMeshPart meshPart, mesh.parts) { qCDebug(modelformat) << " quadIndices.count() =" << meshPart.quadIndices.count(); qCDebug(modelformat) << " triangleIndices.count() =" << meshPart.triangleIndices.count(); /* @@ -1031,16 +1031,16 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) { */ } qCDebug(modelformat) << " clusters.count() =" << mesh.clusters.count(); - foreach (FBXCluster cluster, mesh.clusters) { + foreach (HFMCluster cluster, mesh.clusters) { qCDebug(modelformat) << " jointIndex =" << cluster.jointIndex; qCDebug(modelformat) << " inverseBindMatrix =" << cluster.inverseBindMatrix; } } - qCDebug(modelformat) << " jointIndices =" << fbxgeo.jointIndices; - qCDebug(modelformat) << " joints.count() =" << fbxgeo.joints.count(); + qCDebug(modelformat) << " jointIndices =" << hfmgeo.jointIndices; + qCDebug(modelformat) << " joints.count() =" << hfmgeo.joints.count(); - foreach (FBXJoint joint, fbxgeo.joints) { + foreach (HFMJoint joint, hfmgeo.joints) { qCDebug(modelformat) << " isFree =" << joint.isFree; qCDebug(modelformat) << " freeLineage" << joint.freeLineage; qCDebug(modelformat) << " parentIndex" << joint.parentIndex; diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index e432a3ea51..2eb039eba2 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -42,7 +42,7 @@ public: bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors); // Return a set of one or more OBJFaces from this one, in which each is just a triangle. - // Even though FBXMeshPart can handle quads, it would be messy to try to keep track of mixed-size faces, so we treat everything as triangles. + // Even though HFMMeshPart can handle quads, it would be messy to try to keep track of mixed-size faces, so we treat everything as triangles. QVector triangulate(); private: void addFrom(const OBJFace* face, int index); @@ -54,7 +54,7 @@ public: } ; // Materials and references to material names can come in any order, and different mesh parts can refer to the same material. -// Therefore it would get pretty hacky to try to use FBXMeshPart to store these as we traverse the files. +// Therefore it would get pretty hacky to try to use HFMMeshPart to store these as we traverse the files. class OBJMaterial { public: float shininess; @@ -87,13 +87,13 @@ public: QString currentMaterialName; QHash materials; - FBXGeometry::Pointer readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url = QUrl()); + HFMGeometry::Pointer readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url = QUrl()); private: QUrl _url; QHash librariesSeen; - bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, + bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMGeometry& geometry, float& scaleGuess, bool combineParts); void parseMaterialLibrary(QIODevice* device); void parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions); @@ -103,5 +103,5 @@ private: }; // What are these utilities doing here? One is used by fbx loading code in VHACD Utils, and the other a general debugging utility. -void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID); -void fbxDebugDump(const FBXGeometry& fbxgeo); +void setMeshPartDefaults(HFMMeshPart& meshPart, QString materialID); +void hfmDebugDump(const HFMGeometry& hfmgeo); diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index e96815d391..a950e1df3c 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -128,7 +128,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { void GeometryMappingResource::onGeometryMappingLoaded(bool success) { if (success && _geometryResource) { - _fbxGeometry = _geometryResource->_fbxGeometry; + _hfmGeometry = _geometryResource->_hfmGeometry; _meshParts = _geometryResource->_meshParts; _meshes = _geometryResource->_meshes; _materials = _geometryResource->_materials; @@ -193,38 +193,38 @@ void GeometryReader::run() { _url.path().toLower().endsWith(".obj.gz") || _url.path().toLower().endsWith(".gltf"))) { - FBXGeometry::Pointer fbxGeometry; + HFMGeometry::Pointer hfmGeometry; if (_url.path().toLower().endsWith(".fbx")) { - fbxGeometry.reset(readFBX(_data, _mapping, _url.path())); - if (fbxGeometry->meshes.size() == 0 && fbxGeometry->joints.size() == 0) { + hfmGeometry.reset(readFBX(_data, _mapping, _url.path())); + if (hfmGeometry->meshes.size() == 0 && hfmGeometry->joints.size() == 0) { throw QString("empty geometry, possibly due to an unsupported FBX version"); } } else if (_url.path().toLower().endsWith(".obj")) { - fbxGeometry = OBJReader().readOBJ(_data, _mapping, _combineParts, _url); + hfmGeometry = OBJReader().readOBJ(_data, _mapping, _combineParts, _url); } else if (_url.path().toLower().endsWith(".obj.gz")) { QByteArray uncompressedData; if (gunzip(_data, uncompressedData)){ - fbxGeometry = OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url); + hfmGeometry = OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url); } else { throw QString("failed to decompress .obj.gz"); } } else if (_url.path().toLower().endsWith(".gltf")) { std::shared_ptr glreader = std::make_shared(); - fbxGeometry.reset(glreader->readGLTF(_data, _mapping, _url)); - if (fbxGeometry->meshes.size() == 0 && fbxGeometry->joints.size() == 0) { + hfmGeometry.reset(glreader->readGLTF(_data, _mapping, _url)); + if (hfmGeometry->meshes.size() == 0 && hfmGeometry->joints.size() == 0) { throw QString("empty geometry, possibly due to an unsupported GLTF version"); } } else { throw QString("unsupported format"); } - // Add scripts to fbxgeometry + // Add scripts to hfmGeometry if (!_mapping.value(SCRIPT_FIELD).isNull()) { QVariantList scripts = _mapping.values(SCRIPT_FIELD); for (auto &script : scripts) { - fbxGeometry->scripts.push_back(script.toString()); + hfmGeometry->scripts.push_back(script.toString()); } } @@ -234,7 +234,7 @@ void GeometryReader::run() { qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; } else { QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition", - Q_ARG(FBXGeometry::Pointer, fbxGeometry)); + Q_ARG(HFMGeometry::Pointer, hfmGeometry)); } } else { throw QString("url is invalid"); @@ -262,7 +262,7 @@ public: virtual void downloadFinished(const QByteArray& data) override; protected: - Q_INVOKABLE void setGeometryDefinition(FBXGeometry::Pointer fbxGeometry); + Q_INVOKABLE void setGeometryDefinition(HFMGeometry::Pointer hfmGeometry); private: QVariantHash _mapping; @@ -277,13 +277,13 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { QThreadPool::globalInstance()->start(new GeometryReader(_self, _effectiveBaseURL, _mapping, data, _combineParts)); } -void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxGeometry) { +void GeometryDefinitionResource::setGeometryDefinition(HFMGeometry::Pointer hfmGeometry) { // Assume ownership of the geometry pointer - _fbxGeometry = fbxGeometry; + _hfmGeometry = hfmGeometry; // Copy materials QHash materialIDAtlas; - for (const FBXMaterial& material : _fbxGeometry->materials) { + for (const HFMMaterial& material : _hfmGeometry->materials) { materialIDAtlas[material.materialID] = _materials.size(); _materials.push_back(std::make_shared(material, _textureBaseUrl)); } @@ -291,11 +291,11 @@ void GeometryDefinitionResource::setGeometryDefinition(FBXGeometry::Pointer fbxG std::shared_ptr meshes = std::make_shared(); std::shared_ptr parts = std::make_shared(); int meshID = 0; - for (const FBXMesh& mesh : _fbxGeometry->meshes) { + for (const HFMMesh& mesh : _hfmGeometry->meshes) { // Copy mesh pointers meshes->emplace_back(mesh._mesh); int partID = 0; - for (const FBXMeshPart& part : mesh.parts) { + for (const HFMMeshPart& part : mesh.parts) { // Construct local parts parts->push_back(std::make_shared(meshID, partID, (int)materialIDAtlas[part.materialID])); partID++; @@ -371,7 +371,7 @@ const QVariantMap Geometry::getTextures() const { // FIXME: The materials should only be copied when modified, but the Model currently caches the original Geometry::Geometry(const Geometry& geometry) { - _fbxGeometry = geometry._fbxGeometry; + _hfmGeometry = geometry._hfmGeometry; _meshes = geometry._meshes; _meshParts = geometry._meshParts; @@ -444,8 +444,8 @@ void GeometryResource::deleter() { } void GeometryResource::setTextures() { - if (_fbxGeometry) { - for (const FBXMaterial& material : _fbxGeometry->materials) { + if (_hfmGeometry) { + for (const HFMMaterial& material : _hfmGeometry->materials) { _materials.push_back(std::make_shared(material, _textureBaseUrl)); } } @@ -512,7 +512,7 @@ const QString& NetworkMaterial::getTextureName(MapChannel channel) { return NO_TEXTURE; } -QUrl NetworkMaterial::getTextureUrl(const QUrl& baseUrl, const FBXTexture& texture) { +QUrl NetworkMaterial::getTextureUrl(const QUrl& baseUrl, const HFMTexture& texture) { if (texture.content.isEmpty()) { // External file: search relative to the baseUrl, in case filename is relative return baseUrl.resolved(QUrl(texture.filename)); @@ -529,22 +529,22 @@ QUrl NetworkMaterial::getTextureUrl(const QUrl& baseUrl, const FBXTexture& textu } } -graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, const FBXTexture& fbxTexture, +graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture, image::TextureUsage::Type type, MapChannel channel) { if (baseUrl.isEmpty()) { return nullptr; } - const auto url = getTextureUrl(baseUrl, fbxTexture); - const auto texture = DependencyManager::get()->getTexture(url, type, fbxTexture.content, fbxTexture.maxNumPixels); - _textures[channel] = Texture { fbxTexture.name, texture }; + const auto url = getTextureUrl(baseUrl, hfmTexture); + const auto texture = DependencyManager::get()->getTexture(url, type, hfmTexture.content, hfmTexture.maxNumPixels); + _textures[channel] = Texture { hfmTexture.name, texture }; auto map = std::make_shared(); if (texture) { map->setTextureSource(texture->_textureSource); } - map->setTextureTransform(fbxTexture.transform); + map->setTextureTransform(hfmTexture.transform); return map; } @@ -624,7 +624,7 @@ void NetworkMaterial::setLightmapMap(const QUrl& url) { } } -NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl) : +NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl) : graphics::Material(*material._material), _textures(MapChannel::NUM_MAP_CHANNELS) { diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 5cbe96ea03..2283c355d8 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -45,9 +45,9 @@ public: // Mutable, but must retain structure of vector using NetworkMaterials = std::vector>; - bool isGeometryLoaded() const { return (bool)_fbxGeometry; } + bool isGeometryLoaded() const { return (bool)_hfmGeometry; } - const FBXGeometry& getFBXGeometry() const { return *_fbxGeometry; } + const HFMGeometry& getHFMGeometry() const { return *_hfmGeometry; } const GeometryMeshes& getMeshes() const { return *_meshes; } const std::shared_ptr getShapeMaterial(int shapeID) const; @@ -62,7 +62,7 @@ protected: friend class GeometryMappingResource; // Shared across all geometries, constant throughout lifetime - std::shared_ptr _fbxGeometry; + std::shared_ptr _hfmGeometry; std::shared_ptr _meshes; std::shared_ptr _meshParts; @@ -94,7 +94,7 @@ protected: // Geometries may not hold onto textures while cached - that is for the texture cache // Instead, these methods clear and reset textures from the geometry when caching/loading - bool shouldSetTextures() const { return _fbxGeometry && _materials.empty(); } + bool shouldSetTextures() const { return _hfmGeometry && _materials.empty(); } void setTextures(); void resetTextures(); @@ -165,7 +165,7 @@ public: using MapChannel = graphics::Material::MapChannel; NetworkMaterial() : _textures(MapChannel::NUM_MAP_CHANNELS) {} - NetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl); + NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl); NetworkMaterial(const NetworkMaterial& material); void setAlbedoMap(const QUrl& url, bool useAlphaChannel); @@ -201,8 +201,8 @@ protected: private: // Helpers for the ctors - QUrl getTextureUrl(const QUrl& baseUrl, const FBXTexture& fbxTexture); - graphics::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const FBXTexture& fbxTexture, + QUrl getTextureUrl(const QUrl& baseUrl, const HFMTexture& hfmTexture); + graphics::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture, image::TextureUsage::Type type, MapChannel channel); graphics::TextureMapPointer fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel); diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 81a017a46d..31d6cef060 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -32,8 +32,8 @@ bool CauterizedModel::updateGeometry() { bool needsFullUpdate = Model::updateGeometry(); if (_isCauterized && needsFullUpdate) { assert(_cauterizeMeshStates.empty()); - const FBXGeometry& fbxGeometry = getFBXGeometry(); - foreach (const FBXMesh& mesh, fbxGeometry.meshes) { + const HFMGeometry& hfmGeometry = getHFMGeometry(); + foreach (const HFMMesh& mesh, hfmGeometry.meshes) { Model::MeshState state; if (_useDualQuaternionSkinning) { state.clusterDualQuaternions.resize(mesh.clusters.size()); @@ -76,7 +76,7 @@ void CauterizedModel::createRenderItemSet() { // Run through all of the meshes, and place them into their segregated, but unsorted buckets int shapeID = 0; uint32_t numMeshes = (uint32_t)meshes.size(); - const FBXGeometry& fbxGeometry = getFBXGeometry(); + const HFMGeometry& hfmGeometry = getHFMGeometry(); for (uint32_t i = 0; i < numMeshes; i++) { const auto& mesh = meshes.at(i); if (!mesh) { @@ -86,7 +86,7 @@ void CauterizedModel::createRenderItemSet() { // Create the render payloads int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { - initializeBlendshapes(fbxGeometry.meshes[i], i); + initializeBlendshapes(hfmGeometry.meshes[i], i); auto ptr = std::make_shared(shared_from_this(), i, partIndex, shapeID, transform, offset); _modelMeshRenderItems << std::static_pointer_cast(ptr); @@ -109,13 +109,13 @@ void CauterizedModel::updateClusterMatrices() { return; } _needsUpdateClusterMatrices = false; - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); for (int i = 0; i < (int)_meshStates.size(); i++) { Model::MeshState& state = _meshStates[i]; - const FBXMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { - const FBXCluster& cluster = mesh.clusters.at(j); + const HFMCluster& cluster = mesh.clusters.at(j); if (_useDualQuaternionSkinning) { auto jointPose = _rig.getJointPose(cluster.jointIndex); Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); @@ -145,10 +145,10 @@ void CauterizedModel::updateClusterMatrices() { for (int i = 0; i < _cauterizeMeshStates.size(); i++) { Model::MeshState& state = _cauterizeMeshStates[i]; - const FBXMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { - const FBXCluster& cluster = mesh.clusters.at(j); + const HFMCluster& cluster = mesh.clusters.at(j); if (_useDualQuaternionSkinning) { if (_cauterizeBoneSet.find(cluster.jointIndex) == _cauterizeBoneSet.end()) { diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 4ebd92bb05..8e2541fdda 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -260,8 +260,8 @@ void ModelMeshPartPayload::initCache(const ModelPointer& model) { _hasColorAttrib = vertexFormat->hasAttribute(gpu::Stream::COLOR); _isSkinned = vertexFormat->hasAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT) && vertexFormat->hasAttribute(gpu::Stream::SKIN_CLUSTER_INDEX); - const FBXGeometry& geometry = model->getFBXGeometry(); - const FBXMesh& mesh = geometry.meshes.at(_meshIndex); + const HFMGeometry& geometry = model->getHFMGeometry(); + const HFMMesh& mesh = geometry.meshes.at(_meshIndex); _isBlendShaped = !mesh.blendshapes.isEmpty(); _hasTangents = !mesh.tangents.isEmpty(); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 53009e8bfa..65b3fef7c0 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -183,7 +183,7 @@ bool Model::shouldInvalidatePayloadShapeKey(int meshIndex) { return true; } - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); const auto& networkMeshes = getGeometry()->getMeshes(); // if our index is ever out of range for either meshes or networkMeshes, then skip it, and set our _meshGroupsKnown // to false to rebuild out mesh groups. @@ -278,7 +278,7 @@ void Model::setRenderItemsNeedUpdate() { void Model::reset() { if (isLoaded()) { - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); _rig.reset(geometry); emit rigReset(); emit rigReady(); @@ -295,13 +295,13 @@ bool Model::updateGeometry() { _needsReload = false; // TODO: should all Models have a valid _rig? - if (_rig.jointStatesEmpty() && getFBXGeometry().joints.size() > 0) { + if (_rig.jointStatesEmpty() && getHFMGeometry().joints.size() > 0) { initJointStates(); assert(_meshStates.empty()); - const FBXGeometry& fbxGeometry = getFBXGeometry(); + const HFMGeometry& hfmGeometry = getHFMGeometry(); int i = 0; - foreach (const FBXMesh& mesh, fbxGeometry.meshes) { + foreach (const HFMMesh& mesh, hfmGeometry.meshes) { MeshState state; state.clusterDualQuaternions.resize(mesh.clusters.size()); state.clusterMatrices.resize(mesh.clusters.size()); @@ -319,7 +319,7 @@ bool Model::updateGeometry() { // virtual void Model::initJointStates() { - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); glm::mat4 modelOffset = glm::scale(_scale) * glm::translate(_offset); _rig.initJointStates(geometry, modelOffset); @@ -363,7 +363,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g int bestShapeID = 0; int bestSubMeshIndex = 0; - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); if (!_triangleSetsValid) { calculateTriangleSets(geometry); } @@ -506,7 +506,7 @@ bool Model::findParabolaIntersectionAgainstSubMeshes(const glm::vec3& origin, co int bestShapeID = 0; int bestSubMeshIndex = 0; - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); if (!_triangleSetsValid) { calculateTriangleSets(geometry); } @@ -641,7 +641,7 @@ bool Model::convexHullContains(glm::vec3 point) { QMutexLocker locker(&_mutex); if (!_triangleSetsValid) { - calculateTriangleSets(getFBXGeometry()); + calculateTriangleSets(getHFMGeometry()); } // If we are inside the models box, then consider the submeshes... @@ -753,14 +753,14 @@ bool Model::replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointe } // update triangles for picking { - FBXGeometry geometry; + HFMGeometry geometry; for (const auto& newMesh : meshes) { - FBXMesh mesh; + HFMMesh mesh; mesh._mesh = newMesh.getMeshPointer(); mesh.vertices = buffer_helpers::mesh::attributeToVector(mesh._mesh, gpu::Stream::POSITION); int numParts = (int)newMesh.getMeshPointer()->getNumParts(); for (int partID = 0; partID < numParts; partID++) { - FBXMeshPart part; + HFMMeshPart part; part.triangleIndices = buffer_helpers::bufferToVector(mesh._mesh->getIndexBuffer(), "part.triangleIndices"); mesh.parts << part; } @@ -789,12 +789,12 @@ scriptable::ScriptableModelBase Model::getScriptableModel() { return result; } - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); int numberOfMeshes = geometry.meshes.size(); int shapeID = 0; for (int i = 0; i < numberOfMeshes; i++) { - const FBXMesh& fbxMesh = geometry.meshes.at(i); - if (auto mesh = fbxMesh._mesh) { + const HFMMesh& hfmMesh = geometry.meshes.at(i); + if (auto mesh = hfmMesh._mesh) { result.append(mesh); int numParts = (int)mesh->getNumParts(); @@ -808,7 +808,7 @@ scriptable::ScriptableModelBase Model::getScriptableModel() { return result; } -void Model::calculateTriangleSets(const FBXGeometry& geometry) { +void Model::calculateTriangleSets(const HFMGeometry& geometry) { PROFILE_RANGE(render, __FUNCTION__); int numberOfMeshes = geometry.meshes.size(); @@ -818,14 +818,14 @@ void Model::calculateTriangleSets(const FBXGeometry& geometry) { _modelSpaceMeshTriangleSets.resize(numberOfMeshes); for (int i = 0; i < numberOfMeshes; i++) { - const FBXMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = geometry.meshes.at(i); const int numberOfParts = mesh.parts.size(); auto& meshTriangleSets = _modelSpaceMeshTriangleSets[i]; meshTriangleSets.resize(numberOfParts); for (int j = 0; j < numberOfParts; j++) { - const FBXMeshPart& part = mesh.parts.at(j); + const HFMMeshPart& part = mesh.parts.at(j); auto& partTriangleSet = meshTriangleSets[j]; @@ -1114,7 +1114,7 @@ Extents Model::getBindExtents() const { if (!isActive()) { return Extents(); } - const Extents& bindExtents = getFBXGeometry().bindExtents; + const Extents& bindExtents = getHFMGeometry().bindExtents; Extents scaledExtents = { bindExtents.minimum * _scale, bindExtents.maximum * _scale }; return scaledExtents; } @@ -1128,12 +1128,12 @@ Extents Model::getMeshExtents() const { if (!isActive()) { return Extents(); } - const Extents& extents = getFBXGeometry().meshExtents; + const Extents& extents = getHFMGeometry().meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which // is captured in the offset matrix - glm::vec3 minimum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f)); - glm::vec3 maximum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f)); + glm::vec3 minimum = glm::vec3(getHFMGeometry().offset * glm::vec4(extents.minimum, 1.0f)); + glm::vec3 maximum = glm::vec3(getHFMGeometry().offset * glm::vec4(extents.maximum, 1.0f)); Extents scaledExtents = { minimum * _scale, maximum * _scale }; return scaledExtents; } @@ -1143,12 +1143,12 @@ Extents Model::getUnscaledMeshExtents() const { return Extents(); } - const Extents& extents = getFBXGeometry().meshExtents; + const Extents& extents = getHFMGeometry().meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which // is captured in the offset matrix - glm::vec3 minimum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f)); - glm::vec3 maximum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f)); + glm::vec3 minimum = glm::vec3(getHFMGeometry().offset * glm::vec4(extents.minimum, 1.0f)); + glm::vec3 maximum = glm::vec3(getHFMGeometry().offset * glm::vec4(extents.maximum, 1.0f)); Extents scaledExtents = { minimum, maximum }; return scaledExtents; @@ -1171,11 +1171,11 @@ void Model::setJointTranslation(int index, bool valid, const glm::vec3& translat } int Model::getParentJointIndex(int jointIndex) const { - return (isActive() && jointIndex != -1) ? getFBXGeometry().joints.at(jointIndex).parentIndex : -1; + return (isActive() && jointIndex != -1) ? getHFMGeometry().joints.at(jointIndex).parentIndex : -1; } int Model::getLastFreeJointIndex(int jointIndex) const { - return (isActive() && jointIndex != -1) ? getFBXGeometry().joints.at(jointIndex).freeLineage.last() : -1; + return (isActive() && jointIndex != -1) ? getHFMGeometry().joints.at(jointIndex).freeLineage.last() : -1; } void Model::setTextures(const QVariantMap& textures) { @@ -1275,7 +1275,7 @@ QStringList Model::getJointNames() const { Q_RETURN_ARG(QStringList, result)); return result; } - return isActive() ? getFBXGeometry().getJointNames() : QStringList(); + return isActive() ? getHFMGeometry().getJointNames() : QStringList(); } void Model::setScaleToFit(bool scaleToFit, const glm::vec3& dimensions, bool forceRescale) { @@ -1415,12 +1415,12 @@ void Model::updateClusterMatrices() { } _needsUpdateClusterMatrices = false; - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); for (int i = 0; i < (int) _meshStates.size(); i++) { MeshState& state = _meshStates[i]; - const FBXMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { - const FBXCluster& cluster = mesh.clusters.at(j); + const HFMCluster& cluster = mesh.clusters.at(j); if (_useDualQuaternionSkinning) { auto jointPose = _rig.getJointPose(cluster.jointIndex); Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); @@ -1505,7 +1505,7 @@ void Model::createRenderItemSet() { // Run through all of the meshes, and place them into their segregated, but unsorted buckets int shapeID = 0; uint32_t numMeshes = (uint32_t)meshes.size(); - auto& fbxGeometry = getFBXGeometry(); + auto& hfmGeometry = getHFMGeometry(); for (uint32_t i = 0; i < numMeshes; i++) { const auto& mesh = meshes.at(i); if (!mesh) { @@ -1515,7 +1515,7 @@ void Model::createRenderItemSet() { // Create the render payloads int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { - initializeBlendshapes(fbxGeometry.meshes[i], i); + initializeBlendshapes(hfmGeometry.meshes[i], i); _modelMeshRenderItems << std::make_shared(shared_from_this(), i, partIndex, shapeID, transform, offset); auto material = getGeometry()->getShapeMaterial(shapeID); _modelMeshMaterialNames.push_back(material ? material->getName() : ""); @@ -1600,7 +1600,7 @@ void Model::removeMaterial(graphics::MaterialPointer material, const std::string class CollisionRenderGeometry : public Geometry { public: CollisionRenderGeometry(graphics::MeshPointer mesh) { - _fbxGeometry = std::make_shared(); + _hfmGeometry = std::make_shared(); std::shared_ptr meshes = std::make_shared(); meshes->push_back(mesh); _meshes = meshes; @@ -1656,9 +1656,9 @@ void Blender::run() { if (_model && _model->isLoaded()) { DETAILED_PROFILE_RANGE_EX(simulation_animation, __FUNCTION__, 0xFFFF0000, 0, { { "url", _model->getURL().toString() } }); int offset = 0; - auto meshes = _model->getFBXGeometry().meshes; + auto meshes = _model->getHFMGeometry().meshes; int meshIndex = 0; - foreach(const FBXMesh& mesh, meshes) { + foreach(const HFMMesh& mesh, meshes) { auto modelMeshBlendshapeOffsets = _model->_blendshapeOffsets.find(meshIndex++); if (mesh.blendshapes.isEmpty() || modelMeshBlendshapeOffsets == _model->_blendshapeOffsets.end()) { // Not blendshaped or not initialized @@ -1688,7 +1688,7 @@ void Blender::run() { } float normalCoefficient = vertexCoefficient * NORMAL_COEFFICIENT_SCALE; - const FBXBlendshape& blendshape = mesh.blendshapes.at(i); + const HFMBlendshape& blendshape = mesh.blendshapes.at(i); tbb::parallel_for(tbb::blocked_range(0, blendshape.indices.size()), [&](const tbb::blocked_range& range) { for (auto j = range.begin(); j < range.end(); j++) { @@ -1731,7 +1731,7 @@ bool Model::maybeStartBlender() { return false; } -void Model::initializeBlendshapes(const FBXMesh& mesh, int index) { +void Model::initializeBlendshapes(const HFMMesh& mesh, int index) { if (mesh.blendshapes.empty()) { // mesh doesn't have blendshape, did we allocate one though ? if (_blendshapeOffsets.find(index) != _blendshapeOffsets.end()) { diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 71809821eb..db5625b3d8 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -185,7 +185,7 @@ public: /// Provided as a convenience, will crash if !isLoaded() // And so that getGeometry() isn't chained everywhere - const FBXGeometry& getFBXGeometry() const { assert(isLoaded()); return _renderGeometry->getFBXGeometry(); } + const HFMGeometry& getHFMGeometry() const { assert(isLoaded()); return _renderGeometry->getHFMGeometry(); } bool isActive() const { return isLoaded(); } @@ -450,7 +450,7 @@ protected: bool _overrideModelTransform { false }; bool _triangleSetsValid { false }; - void calculateTriangleSets(const FBXGeometry& geometry); + void calculateTriangleSets(const HFMGeometry& geometry); std::vector> _modelSpaceMeshTriangleSets; // model space triangles for all sub meshes virtual void createRenderItemSet(); @@ -506,7 +506,7 @@ protected: bool shouldInvalidatePayloadShapeKey(int meshIndex); - void initializeBlendshapes(const FBXMesh& mesh, int index); + void initializeBlendshapes(const HFMMesh& mesh, int index); private: float _loadingPriority { 0.0f }; diff --git a/libraries/render-utils/src/SoftAttachmentModel.cpp b/libraries/render-utils/src/SoftAttachmentModel.cpp index 90015768d0..77b09caa1d 100644 --- a/libraries/render-utils/src/SoftAttachmentModel.cpp +++ b/libraries/render-utils/src/SoftAttachmentModel.cpp @@ -41,14 +41,14 @@ void SoftAttachmentModel::updateClusterMatrices() { _needsUpdateClusterMatrices = false; - const FBXGeometry& geometry = getFBXGeometry(); + const HFMGeometry& geometry = getHFMGeometry(); for (int i = 0; i < (int) _meshStates.size(); i++) { MeshState& state = _meshStates[i]; - const FBXMesh& mesh = geometry.meshes.at(i); + const HFMMesh& mesh = geometry.meshes.at(i); for (int j = 0; j < mesh.clusters.size(); j++) { - const FBXCluster& cluster = mesh.clusters.at(j); + const HFMCluster& cluster = mesh.clusters.at(j); // TODO: cache these look-ups as an optimization int jointIndexOverride = getJointIndexOverride(cluster.jointIndex); diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 4fb7ddd40b..f9857992bb 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -18,7 +18,7 @@ Script.include("/~/system/libraries/accountUtils.js"); Script.include("/~/system/libraries/connectionUtils.js"); var AppUi = Script.require('appUi'); - var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace";; +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; // BEGIN AVATAR SELECTOR LOGIC var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 }; @@ -607,13 +607,13 @@ function notificationPollCallbackHistory(historyArray) { if (notificationCount > 0) { var message; if (!ui.notificationInitialCallbackMade[1]) { - message = "You have " + notificationCount + " unread wallet " + - "transaction" + (notificationCount === 1 ? "" : "s") + ". Open WALLET to see all activity."; + message = "You have " + notificationCount + " unread recent " + + "transaction" + (notificationCount === 1 ? "" : "s") + ". Open ASSETS to see all activity."; ui.notificationDisplayBanner(message); } else { for (var i = 0; i < notificationCount; i++) { message = '"' + (historyArray[i].message) + '" ' + - "Open WALLET to see all activity."; + "Open ASSETS to see all activity."; ui.notificationDisplayBanner(message); } } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index ed8ed9f288..8bfd776971 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -527,7 +527,13 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { Window.location = "hifi://BankOfHighFidelity"; } break; - case 'checkout_openWallet': + case 'checkout_openRecentActivity': + ui.open(MARKETPLACE_WALLET_QML_PATH); + wireQmlEventBridge(true); + ui.tablet.sendToQml({ + method: 'checkout_openRecentActivity' + }); + break; case 'checkout_setUpClicked': openWallet(); break; @@ -589,9 +595,6 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { break; case 'updateItemClicked': openMarketplace(message.upgradeUrl + "?edition=" + message.itemEdition); - break; - case 'giftAsset': - break; case 'passphrasePopup_cancelClicked': case 'needsLogIn_cancelClicked': @@ -635,6 +638,20 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'http.request': // Handled elsewhere, don't log. break; + // All of these are handled by wallet.js + case 'purchases_updateWearables': + case 'enable_ChooseRecipientNearbyMode': + case 'disable_ChooseRecipientNearbyMode': + case 'sendAsset_sendPublicly': + case 'refreshConnections': + case 'transactionHistory_goToBank': + case 'purchases_walletNotSetUp': + case 'purchases_openGoTo': + case 'purchases_itemInfoClicked': + case 'purchases_itemCertificateClicked': + case 'clearShouldShowDotHistory': + case 'giftAsset': + break; default: print('Unrecognized message from Checkout.qml: ' + JSON.stringify(message)); } @@ -705,6 +722,8 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { referrerURL: referrerURL, filterText: filterText }); + referrerURL = ""; + filterText = ""; } ui.isOpen = (onMarketplaceScreen || onCommerceScreen) && !onWalletScreen; diff --git a/tests-manual/gpu/src/TestFbx.cpp b/tests-manual/gpu/src/TestFbx.cpp index 538bb0a973..9890e4fbe7 100644 --- a/tests-manual/gpu/src/TestFbx.cpp +++ b/tests-manual/gpu/src/TestFbx.cpp @@ -100,12 +100,12 @@ bool TestFbx::isReady() const { void TestFbx::parseFbx(const QByteArray& fbxData) { QVariantHash mapping; - FBXGeometry* fbx = readFBX(fbxData, mapping); + HFMGeometry* geometry = readFBX(fbxData, mapping); size_t totalVertexCount = 0; size_t totalIndexCount = 0; size_t totalPartCount = 0; size_t highestIndex = 0; - for (const auto& mesh : fbx->meshes) { + for (const auto& mesh : geometry->meshes) { size_t vertexCount = mesh.vertices.size(); totalVertexCount += mesh.vertices.size(); highestIndex = std::max(highestIndex, vertexCount); @@ -123,7 +123,7 @@ void TestFbx::parseFbx(const QByteArray& fbxData) { std::vector parts; parts.reserve(totalPartCount); _partCount = totalPartCount; - for (const auto& mesh : fbx->meshes) { + for (const auto& mesh : geometry->meshes) { baseVertex = vertices.size(); vec3 color; @@ -133,7 +133,7 @@ void TestFbx::parseFbx(const QByteArray& fbxData) { partIndirect.firstIndex = (uint)indices.size(); partIndirect.baseInstance = (uint)parts.size(); _partTransforms.push_back(mesh.modelTransform); - auto material = fbx->materials[part.materialID]; + auto material = geometry->materials[part.materialID]; color = material.diffuseColor; for (auto index : part.quadTrianglesIndices) { indices.push_back(index); @@ -163,7 +163,7 @@ void TestFbx::parseFbx(const QByteArray& fbxData) { _vertexBuffer->append(vertices); _indexBuffer->append(indices); _indirectBuffer->append(parts); - delete fbx; + delete geometry; } void TestFbx::renderTest(size_t testId, RenderArgs* args) { diff --git a/tests-manual/gpu/src/TestFbx.h b/tests-manual/gpu/src/TestFbx.h index 391fff1091..4e22928460 100644 --- a/tests-manual/gpu/src/TestFbx.h +++ b/tests-manual/gpu/src/TestFbx.h @@ -11,7 +11,7 @@ #include -class FBXGeometry; +class HFMGeometry; class TestFbx : public GpuTestBase { size_t _partCount { 0 }; diff --git a/tests/animation/src/AnimInverseKinematicsTests.cpp b/tests/animation/src/AnimInverseKinematicsTests.cpp index f5d3597f56..f51fe12ecb 100644 --- a/tests/animation/src/AnimInverseKinematicsTests.cpp +++ b/tests/animation/src/AnimInverseKinematicsTests.cpp @@ -28,8 +28,8 @@ const glm::quat identity = glm::quat(); const glm::quat quaterTurnAroundZ = glm::angleAxis(0.5f * PI, zAxis); -void makeTestFBXJoints(FBXGeometry& geometry) { - FBXJoint joint; +void makeTestFBXJoints(HFMGeometry& geometry) { + HFMJoint joint; joint.isFree = false; joint.freeLineage.clear(); joint.parentIndex = -1; @@ -79,7 +79,7 @@ void makeTestFBXJoints(FBXGeometry& geometry) { // compute each joint's transform for (int i = 1; i < (int)geometry.joints.size(); ++i) { - FBXJoint& j = geometry.joints[i]; + HFMJoint& j = geometry.joints[i]; int parentIndex = j.parentIndex; // World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1) j.transform = geometry.joints[parentIndex].transform * @@ -96,7 +96,7 @@ void AnimInverseKinematicsTests::testSingleChain() { AnimContext context(false, false, false, glm::mat4(), glm::mat4()); - FBXGeometry geometry; + HFMGeometry geometry; makeTestFBXJoints(geometry); // create a skeleton and doll diff --git a/tools/skeleton-dump/src/SkeletonDumpApp.cpp b/tools/skeleton-dump/src/SkeletonDumpApp.cpp index e9d8243e38..5107931da1 100644 --- a/tools/skeleton-dump/src/SkeletonDumpApp.cpp +++ b/tools/skeleton-dump/src/SkeletonDumpApp.cpp @@ -54,8 +54,8 @@ SkeletonDumpApp::SkeletonDumpApp(int argc, char* argv[]) : QCoreApplication(argc return; } QByteArray blob = file.readAll(); - std::unique_ptr fbxGeometry(readFBX(blob, QVariantHash())); - std::unique_ptr skeleton(new AnimSkeleton(*fbxGeometry)); + std::unique_ptr geometry(readFBX(blob, QVariantHash())); + std::unique_ptr skeleton(new AnimSkeleton(*geometry)); skeleton->dump(verbose); } diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index a52e948f01..bb2958e11d 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -19,16 +19,16 @@ // FBXReader jumbles the order of the meshes by reading them back out of a hashtable. This will put // them back in the order in which they appeared in the file. -bool FBXGeometryLessThan(const FBXMesh& e1, const FBXMesh& e2) { +bool HFMGeometryLessThan(const HFMMesh& e1, const HFMMesh& e2) { return e1.meshIndex < e2.meshIndex; } -void reSortFBXGeometryMeshes(FBXGeometry& geometry) { - qSort(geometry.meshes.begin(), geometry.meshes.end(), FBXGeometryLessThan); +void reSortHFMGeometryMeshes(HFMGeometry& geometry) { + qSort(geometry.meshes.begin(), geometry.meshes.end(), HFMGeometryLessThan); } // Read all the meshes from provided FBX file -bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { +bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMGeometry& result) { if (_verbose) { qDebug() << "reading FBX file =" << filename << "..."; } @@ -41,7 +41,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } try { QByteArray fbxContents = fbx.readAll(); - FBXGeometry::Pointer geom; + HFMGeometry::Pointer geom; if (filename.toLower().endsWith(".obj")) { bool combineParts = false; geom = OBJReader().readOBJ(fbxContents, QVariantHash(), combineParts); @@ -53,7 +53,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } result = *geom; - reSortFBXGeometryMeshes(result); + reSortHFMGeometryMeshes(result); } catch (const QString& error) { qWarning() << "error reading" << filename << ":" << error; return false; @@ -63,7 +63,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } -void getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector& triangleIndices) { +void getTrianglesInMeshPart(const HFMMeshPart &meshPart, std::vector& triangleIndices) { // append triangle indices triangleIndices.reserve(triangleIndices.size() + (size_t)meshPart.triangleIndices.size()); for (auto index : meshPart.triangleIndices) { @@ -88,12 +88,12 @@ void getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector& trian } } -void vhacd::VHACDUtil::fattenMesh(const FBXMesh& mesh, const glm::mat4& geometryOffset, FBXMesh& result) const { +void vhacd::VHACDUtil::fattenMesh(const HFMMesh& mesh, const glm::mat4& geometryOffset, HFMMesh& result) const { // this is used to make meshes generated from a highfield collidable. each triangle // is converted into a tetrahedron and made into its own mesh-part. std::vector triangleIndices; - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { getTrianglesInMeshPart(meshPart, triangleIndices); } @@ -145,7 +145,7 @@ void vhacd::VHACDUtil::fattenMesh(const FBXMesh& mesh, const glm::mat4& geometry int index3 = result.vertices.size(); result.vertices << p3; // add the new point to the result mesh - FBXMeshPart newMeshPart; + HFMMeshPart newMeshPart; setMeshPartDefaults(newMeshPart, "unknown"); newMeshPart.triangleIndices << index0 << index1 << index2; newMeshPart.triangleIndices << index0 << index3 << index1; @@ -155,7 +155,7 @@ void vhacd::VHACDUtil::fattenMesh(const FBXMesh& mesh, const glm::mat4& geometry } } -AABox getAABoxForMeshPart(const FBXMesh& mesh, const FBXMeshPart &meshPart) { +AABox getAABoxForMeshPart(const HFMMesh& mesh, const HFMMeshPart &meshPart) { AABox aaBox; const int TRIANGLE_STRIDE = 3; for (int i = 0; i < meshPart.triangleIndices.size(); i += TRIANGLE_STRIDE) { @@ -242,7 +242,7 @@ bool isClosedManifold(const std::vector& triangleIndices) { return true; } -void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& resultMesh) const { +void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, HFMMesh& resultMesh) const { // Number of hulls for this input meshPart uint32_t numHulls = convexifier->GetNConvexHulls(); if (_verbose) { @@ -256,8 +256,8 @@ void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& res VHACD::IVHACD::ConvexHull hull; convexifier->GetConvexHull(j, hull); - resultMesh.parts.append(FBXMeshPart()); - FBXMeshPart& resultMeshPart = resultMesh.parts.last(); + resultMesh.parts.append(HFMMeshPart()); + HFMMeshPart& resultMeshPart = resultMesh.parts.last(); int hullIndexStart = resultMesh.vertices.size(); resultMesh.vertices.reserve(hullIndexStart + hull.m_nPoints); @@ -288,9 +288,9 @@ float computeDt(uint64_t start) { return (float)(usecTimestampNow() - start) / (float)USECS_PER_SECOND; } -bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, +bool vhacd::VHACDUtil::computeVHACD(HFMGeometry& geometry, VHACD::IVHACD::Parameters params, - FBXGeometry& result, + HFMGeometry& result, float minimumMeshSize, float maximumMeshSize) { if (_verbose) { qDebug() << "meshes =" << geometry.meshes.size(); @@ -298,7 +298,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, // count the mesh-parts int numParts = 0; - foreach (const FBXMesh& mesh, geometry.meshes) { + foreach (const HFMMesh& mesh, geometry.meshes) { numParts += mesh.parts.size(); } if (_verbose) { @@ -308,15 +308,15 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, VHACD::IVHACD * convexifier = VHACD::CreateVHACD(); result.meshExtents.reset(); - result.meshes.append(FBXMesh()); - FBXMesh &resultMesh = result.meshes.last(); + result.meshes.append(HFMMesh()); + HFMMesh &resultMesh = result.meshes.last(); const uint32_t POINT_STRIDE = 3; const uint32_t TRIANGLE_STRIDE = 3; int meshIndex = 0; int validPartsFound = 0; - foreach (const FBXMesh& mesh, geometry.meshes) { + foreach (const HFMMesh& mesh, geometry.meshes) { // find duplicate points int numDupes = 0; @@ -354,7 +354,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, int partIndex = 0; std::vector triangleIndices; - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { triangleIndices.clear(); getTrianglesInMeshPart(meshPart, triangleIndices); @@ -421,7 +421,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, triangleIndices.clear(); for (auto index : openParts) { - const FBXMeshPart &meshPart = mesh.parts[index]; + const HFMMeshPart &meshPart = mesh.parts[index]; getTrianglesInMeshPart(meshPart, triangleIndices); } diff --git a/tools/vhacd-util/src/VHACDUtil.h b/tools/vhacd-util/src/VHACDUtil.h index 35ec3ef56b..64e86ed7df 100644 --- a/tools/vhacd-util/src/VHACDUtil.h +++ b/tools/vhacd-util/src/VHACDUtil.h @@ -27,16 +27,16 @@ namespace vhacd { public: void setVerbose(bool verbose) { _verbose = verbose; } - bool loadFBX(const QString filename, FBXGeometry& result); + bool loadFBX(const QString filename, HFMGeometry& result); - void fattenMesh(const FBXMesh& mesh, const glm::mat4& gometryOffset, FBXMesh& result) const; + void fattenMesh(const HFMMesh& mesh, const glm::mat4& gometryOffset, HFMMesh& result) const; - bool computeVHACD(FBXGeometry& geometry, + bool computeVHACD(HFMGeometry& geometry, VHACD::IVHACD::Parameters params, - FBXGeometry& result, + HFMGeometry& result, float minimumMeshSize, float maximumMeshSize); - void getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& resultMesh) const; + void getConvexResults(VHACD::IVHACD* convexifier, HFMMesh& resultMesh) const; ~VHACDUtil(); @@ -55,6 +55,6 @@ namespace vhacd { }; } -AABox getAABoxForMeshPart(const FBXMeshPart &meshPart); +AABox getAABoxForMeshPart(const HFMMeshPart &meshPart); #endif //hifi_VHACDUtil_h diff --git a/tools/vhacd-util/src/VHACDUtilApp.cpp b/tools/vhacd-util/src/VHACDUtilApp.cpp index c263dce609..0941198234 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.cpp +++ b/tools/vhacd-util/src/VHACDUtilApp.cpp @@ -36,7 +36,7 @@ QString formatFloat(double n) { } -bool VHACDUtilApp::writeOBJ(QString outFileName, FBXGeometry& geometry, bool outputCentimeters, int whichMeshPart) { +bool VHACDUtilApp::writeOBJ(QString outFileName, HFMGeometry& geometry, bool outputCentimeters, int whichMeshPart) { QFile file(outFileName); if (!file.open(QIODevice::WriteOnly)) { qWarning() << "unable to write to" << outFileName; @@ -56,9 +56,9 @@ bool VHACDUtilApp::writeOBJ(QString outFileName, FBXGeometry& geometry, bool out int vertexIndexOffset = 0; - foreach (const FBXMesh& mesh, geometry.meshes) { + foreach (const HFMMesh& mesh, geometry.meshes) { bool verticesHaveBeenOutput = false; - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { if (whichMeshPart >= 0 && nth != (unsigned int) whichMeshPart) { nth++; continue; @@ -297,7 +297,7 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } // load the mesh - FBXGeometry fbx; + HFMGeometry fbx; auto begin = std::chrono::high_resolution_clock::now(); if (!vUtil.loadFBX(inputFilename, fbx)){ _returnCode = VHACD_RETURN_CODE_FAILURE_TO_READ; @@ -315,8 +315,8 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : QVector infileExtensions = {"fbx", "obj"}; QString baseFileName = fileNameWithoutExtension(outputFilename, infileExtensions); int count = 0; - foreach (const FBXMesh& mesh, fbx.meshes) { - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMesh& mesh, fbx.meshes) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { QString outputFileName = baseFileName + "-" + QString::number(count) + ".obj"; writeOBJ(outputFileName, fbx, outputCentimeters, count); count++; @@ -358,7 +358,7 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } begin = std::chrono::high_resolution_clock::now(); - FBXGeometry result; + HFMGeometry result; bool success = vUtil.computeVHACD(fbx, params, result, minimumMeshSize, maximumMeshSize); end = std::chrono::high_resolution_clock::now(); @@ -377,9 +377,9 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : int totalVertices = 0; int totalTriangles = 0; - foreach (const FBXMesh& mesh, result.meshes) { + foreach (const HFMMesh& mesh, result.meshes) { totalVertices += mesh.vertices.size(); - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { totalTriangles += meshPart.triangleIndices.size() / 3; // each quad was made into two triangles totalTriangles += 2 * meshPart.quadIndices.size() / 4; @@ -398,17 +398,17 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } if (fattenFaces) { - FBXGeometry newFbx; - FBXMesh result; + HFMGeometry newFbx; + HFMMesh result; // count the mesh-parts unsigned int meshCount = 0; - foreach (const FBXMesh& mesh, fbx.meshes) { + foreach (const HFMMesh& mesh, fbx.meshes) { meshCount += mesh.parts.size(); } result.modelTransform = glm::mat4(); // Identity matrix - foreach (const FBXMesh& mesh, fbx.meshes) { + foreach (const HFMMesh& mesh, fbx.meshes) { vUtil.fattenMesh(mesh, fbx.offset, result); } diff --git a/tools/vhacd-util/src/VHACDUtilApp.h b/tools/vhacd-util/src/VHACDUtilApp.h index 0d75275802..3db49456a0 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.h +++ b/tools/vhacd-util/src/VHACDUtilApp.h @@ -28,7 +28,7 @@ public: VHACDUtilApp(int argc, char* argv[]); ~VHACDUtilApp(); - bool writeOBJ(QString outFileName, FBXGeometry& geometry, bool outputCentimeters, int whichMeshPart = -1); + bool writeOBJ(QString outFileName, HFMGeometry& geometry, bool outputCentimeters, int whichMeshPart = -1); int getReturnCode() const { return _returnCode; } From 3631e304badd027966f363c54089f2eadbd80e9b Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 5 Nov 2018 14:58:12 -0800 Subject: [PATCH 335/362] Acquisition --- .../InspectionCertificate.qml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml index 734292d774..885838a26e 100644 --- a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -103,8 +103,8 @@ Rectangle { showInMarketplaceButton.visible = false; // "Edition" text previously set above in this function // "Owner" text previously set above in this function - // "Date Acquired" text previously set above in this function - // "Original Price" text previously set above in this function + // "Acquisition Date" text previously set above in this function + // "Acquisition Price" text previously set above in this function if (result.data.invalid_reason) { errorText.text = result.data.invalid_reason; } @@ -118,8 +118,8 @@ Rectangle { showInMarketplaceButton.visible = true; // "Edition" text previously set above in this function // "Owner" text previously set above in this function - // "Date Acquired" text previously set above in this function - // "Original Price" text previously set above in this function + // "Acquisition Date" text previously set above in this function + // "Acquisition Price" text previously set above in this function errorText.text = "The status of this item is still pending confirmation. If the purchase is not confirmed, " + "this entity will be cleaned up by the domain."; } @@ -146,8 +146,8 @@ Rectangle { // "Item Name" text will be set in "onCertificateInfoResult()" // "Edition" text will be set in "onCertificateInfoResult()" // "Owner" text will be set in "onCertificateInfoResult()" - // "Date Acquired" text will be set in "onCertificateInfoResult()" - // "Original Price" text will be set in "onCertificateInfoResult()" + // "Acquisition Date" text will be set in "onCertificateInfoResult()" + // "Acquisition Price" text will be set in "onCertificateInfoResult()" errorText.text = ""; } else if (root.certificateStatus === 2) { // CERTIFICATE_STATUS_VERIFICATION_TIMEOUT root.useGoldCert = false; @@ -176,8 +176,8 @@ Rectangle { // "Item Name" text will be set in "onCertificateInfoResult()" // "Edition" text will be set in "onCertificateInfoResult()" // "Owner" text will be set in "onCertificateInfoResult()" - // "Date Acquired" text will be set in "onCertificateInfoResult()" - // "Original Price" text will be set in "onCertificateInfoResult()" + // "Acquisition Date" text will be set in "onCertificateInfoResult()" + // "Acquisition Price" text will be set in "onCertificateInfoResult()" errorText.text = "The information associated with this item has been modified and it no longer matches the original certified item."; } else if (root.certificateStatus === 4) { // CERTIFICATE_STATUS_OWNER_VERIFICATION_FAILED root.useGoldCert = false; @@ -191,8 +191,8 @@ Rectangle { // "Item Name" text will be set in "onCertificateInfoResult()" root.itemEdition = "Uncertified Copy" // "Owner" text will be set in "onCertificateInfoResult()" - // "Date Acquired" text will be set in "onCertificateInfoResult()" - // "Original Price" text will be set in "onCertificateInfoResult()" + // "Acquisition Date" text will be set in "onCertificateInfoResult()" + // "Acquisition Price" text will be set in "onCertificateInfoResult()" // "Error Text" text will be set in "onCertificateInfoResult()" } else { console.log("Unknown certificate status received from ledger signal!"); @@ -487,7 +487,7 @@ Rectangle { RalewayRegular { id: dateAcquiredHeader; - text: "DATE ACQUIRED"; + text: "ACQUISITION DATE"; // Text size size: 16; // Anchors @@ -517,7 +517,7 @@ Rectangle { RalewayRegular { id: priceHeader; - text: "ORIGINAL PRICE"; + text: "ACQUISITION PRICE"; // Text size size: 16; // Anchors From 5b042ec3dfbd846f8a26d09ec4284ffe1e7bdf83 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 5 Nov 2018 15:23:25 -0800 Subject: [PATCH 336/362] My items to my submissions --- .../qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml | 4 ++-- interface/resources/qml/hifi/commerce/purchases/Purchases.qml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index 4b0166425a..99c2d89da8 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -213,7 +213,7 @@ Item { anchors.topMargin: -buttonAndUsernameContainer.anchors.bottomMargin; anchors.right: buttonAndUsernameContainer.right; height: childrenRect.height; - width: 100; + width: 150; Rectangle { id: myItemsButton; @@ -225,7 +225,7 @@ Item { RalewaySemiBold { anchors.fill: parent; - text: "My Items" + text: "My Submissions" color: hifi.colors.baseGray; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index ced5450290..df4d7f6175 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -882,7 +882,7 @@ Rectangle { // Explanitory text RalewayRegular { id: noItemsYet; - text: "You haven't submitted anything to the Marketplace yet!

Submit an item to the Marketplace to add it to My Items."; + text: "You haven't submitted anything to the Marketplace yet!

Submit an item to the Marketplace to add it to My Submissions."; // Text size size: 22; // Anchors From 89656ea3f6e44835bfca84fb1f63e727cab364b3 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 5 Nov 2018 16:43:06 -0800 Subject: [PATCH 337/362] activity notifications also change ASSETS to INVENTORY --- scripts/system/commerce/wallet.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index f9857992bb..9fb336f79c 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -608,12 +608,12 @@ function notificationPollCallbackHistory(historyArray) { var message; if (!ui.notificationInitialCallbackMade[1]) { message = "You have " + notificationCount + " unread recent " + - "transaction" + (notificationCount === 1 ? "" : "s") + ". Open ASSETS to see all activity."; + "transaction" + (notificationCount === 1 ? "" : "s") + ". Open INVENTORY to see all activity."; ui.notificationDisplayBanner(message); } else { for (var i = 0; i < notificationCount; i++) { message = '"' + (historyArray[i].message) + '" ' + - "Open ASSETS to see all activity."; + "Open INVENTORY to see all activity."; ui.notificationDisplayBanner(message); } } From 27bb8a0de7cc460576125c6a2d529c8fa40836d6 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 15 Jun 2018 21:44:12 +0300 Subject: [PATCH 338/362] rename stylues-uit => stylesUit & controls-uit => controlsUit note: the idea is to make imports like 'import controlsUit 1.0' to work with 'styles-uit'/'controls-uit' it is not possible because of two reasons: 1. import controls-uit 1.0 is invalid syntax 2. qmldir inside controls-uit is 'module controlsUit' --- interface/resources/qml/AudioScopeUI.qml | 4 ++-- interface/resources/qml/Browser.qml | 4 ++-- interface/resources/qml/CurrentAPI.qml | 4 ++-- interface/resources/qml/InfoView.qml | 2 +- interface/resources/qml/LoginDialog.qml | 4 ++-- .../qml/LoginDialog/+android/LinkAccountBody.qml | 4 ++-- .../resources/qml/LoginDialog/+android/SignUpBody.qml | 4 ++-- .../resources/qml/LoginDialog/CompleteProfileBody.qml | 4 ++-- .../resources/qml/LoginDialog/LinkAccountBody.qml | 5 +++-- interface/resources/qml/LoginDialog/SignInBody.qml | 4 ++-- interface/resources/qml/LoginDialog/SignUpBody.qml | 4 ++-- .../qml/LoginDialog/UsernameCollisionBody.qml | 4 ++-- interface/resources/qml/LoginDialog/WelcomeBody.qml | 4 ++-- interface/resources/qml/QmlWebWindow.qml | 4 ++-- interface/resources/qml/QmlWindow.qml | 4 ++-- interface/resources/qml/TabletBrowser.qml | 4 ++-- interface/resources/qml/UpdateDialog.qml | 4 ++-- interface/resources/qml/controls/Button.qml | 1 - .../resources/qml/controls/FlickableWebViewCore.qml | 2 +- interface/resources/qml/controls/TabletWebButton.qml | 2 +- interface/resources/qml/controls/TabletWebScreen.qml | 2 +- interface/resources/qml/controls/TabletWebView.qml | 4 ++-- interface/resources/qml/controls/WebView.qml | 2 +- .../+android/ImageButton.qml | 6 +++--- .../AttachmentsTable.qml | 4 ++-- .../qml/{controls-uit => controlsUit}/BaseWebView.qml | 0 .../qml/{controls-uit => controlsUit}/Button.qml | 2 +- .../qml/{controls-uit => controlsUit}/CheckBox.qml | 2 +- .../{controls-uit => controlsUit}/CheckBoxQQC2.qml | 4 ++-- .../qml/{controls-uit => controlsUit}/ComboBox.qml | 4 ++-- .../{controls-uit => controlsUit}/ContentSection.qml | 2 +- .../qml/{controls-uit => controlsUit}/FilterBar.qml | 4 ++-- .../qml/{controls-uit => controlsUit}/GlyphButton.qml | 2 +- .../{controls-uit => controlsUit}/HorizontalRule.qml | 0 .../HorizontalSpacer.qml | 2 +- .../{controls-uit => controlsUit}/ImageMessageBox.qml | 2 +- .../qml/{controls-uit => controlsUit}/Key.qml | 0 .../qml/{controls-uit => controlsUit}/Keyboard.qml | 0 .../qml/{controls-uit => controlsUit}/Label.qml | 2 +- .../{controls-uit => controlsUit}/QueuedButton.qml | 2 +- .../qml/{controls-uit => controlsUit}/RadioButton.qml | 4 ++-- .../qml/{controls-uit => controlsUit}/ScrollBar.qml | 2 +- .../qml/{controls-uit => controlsUit}/Separator.qml | 2 +- .../qml/{controls-uit => controlsUit}/Slider.qml | 4 ++-- .../qml/{controls-uit => controlsUit}/SpinBox.qml | 4 ++-- .../qml/{controls-uit => controlsUit}/Switch.qml | 2 +- .../qml/{controls-uit => controlsUit}/Table.qml | 2 +- .../TabletContentSection.qml | 2 +- .../{controls-uit => controlsUit}/TabletHeader.qml | 2 +- .../qml/{controls-uit => controlsUit}/TextAction.qml | 4 ++-- .../qml/{controls-uit => controlsUit}/TextEdit.qml | 2 +- .../qml/{controls-uit => controlsUit}/TextField.qml | 4 ++-- .../qml/{controls-uit => controlsUit}/ToolTip.qml | 0 .../qml/{controls-uit => controlsUit}/Tree.qml | 2 +- .../{controls-uit => controlsUit}/VerticalSpacer.qml | 2 +- .../{controls-uit => controlsUit}/WebGlyphButton.qml | 2 +- .../qml/{controls-uit => controlsUit}/WebSpinner.qml | 0 .../qml/{controls-uit => controlsUit}/WebView.qml | 0 .../qml/{controls-uit => controlsUit}/qmldir | 0 interface/resources/qml/dialogs/AssetDialog.qml | 2 +- interface/resources/qml/dialogs/CustomQueryDialog.qml | 4 ++-- interface/resources/qml/dialogs/FileDialog.qml | 4 ++-- interface/resources/qml/dialogs/MessageDialog.qml | 4 ++-- interface/resources/qml/dialogs/PreferencesDialog.qml | 4 ++-- interface/resources/qml/dialogs/QueryDialog.qml | 4 ++-- interface/resources/qml/dialogs/TabletAssetDialog.qml | 2 +- .../resources/qml/dialogs/TabletCustomQueryDialog.qml | 4 ++-- interface/resources/qml/dialogs/TabletFileDialog.qml | 4 ++-- interface/resources/qml/dialogs/TabletLoginDialog.qml | 4 ++-- interface/resources/qml/dialogs/TabletMessageBox.qml | 4 ++-- interface/resources/qml/dialogs/TabletQueryDialog.qml | 4 ++-- .../qml/dialogs/assetDialog/AssetDialogContent.qml | 4 ++-- .../qml/dialogs/fileDialog/FileTypeSelection.qml | 2 +- .../qml/dialogs/messageDialog/MessageDialogButton.qml | 2 +- .../qml/dialogs/preferences/AvatarPreference.qml | 2 +- .../qml/dialogs/preferences/BrowsablePreference.qml | 2 +- .../qml/dialogs/preferences/ButtonPreference.qml | 2 +- .../qml/dialogs/preferences/CheckBoxPreference.qml | 2 +- .../qml/dialogs/preferences/ComboBoxPreference.qml | 4 ++-- .../qml/dialogs/preferences/EditablePreference.qml | 2 +- .../qml/dialogs/preferences/PrimaryHandPreference.qml | 2 +- .../dialogs/preferences/RadioButtonsPreference.qml | 4 ++-- .../resources/qml/dialogs/preferences/Section.qml | 4 ++-- .../qml/dialogs/preferences/SliderPreference.qml | 2 +- .../qml/dialogs/preferences/SpinBoxPreference.qml | 2 +- .../dialogs/preferences/SpinnerSliderPreference.qml | 2 +- interface/resources/qml/hifi/+android/ActionBar.qml | 4 ++-- interface/resources/qml/hifi/+android/AudioBar.qml | 4 ++-- .../resources/qml/hifi/+android/AvatarOption.qml | 2 +- interface/resources/qml/hifi/+android/StatsBar.qml | 4 ++-- .../resources/qml/hifi/+android/WindowHeader.qml | 4 ++-- .../resources/qml/hifi/+android/bottomHudOptions.qml | 4 ++-- interface/resources/qml/hifi/+android/modesbar.qml | 4 ++-- interface/resources/qml/hifi/AssetServer.qml | 4 ++-- interface/resources/qml/hifi/Card.qml | 2 +- interface/resources/qml/hifi/ComboDialog.qml | 4 ++-- interface/resources/qml/hifi/Desktop.qml | 2 +- .../resources/qml/hifi/DesktopLetterboxMessage.qml | 2 +- interface/resources/qml/hifi/Feed.qml | 2 +- interface/resources/qml/hifi/LetterboxMessage.qml | 2 +- interface/resources/qml/hifi/NameCard.qml | 4 ++-- interface/resources/qml/hifi/Pal.qml | 4 ++-- interface/resources/qml/hifi/SkyboxChanger.qml | 4 ++-- interface/resources/qml/hifi/SpectatorCamera.qml | 4 ++-- interface/resources/qml/hifi/TabletTextButton.qml | 2 +- interface/resources/qml/hifi/TextButton.qml | 2 +- interface/resources/qml/hifi/WebBrowser.qml | 4 ++-- interface/resources/qml/hifi/audio/Audio.qml | 4 ++-- interface/resources/qml/hifi/audio/AudioTabButton.qml | 4 ++-- interface/resources/qml/hifi/audio/CheckBox.qml | 2 +- .../resources/qml/hifi/audio/PlaySampleSound.qml | 4 ++-- .../resources/qml/hifi/commerce/checkout/Checkout.qml | 4 ++-- .../qml/hifi/commerce/common/CommerceLightbox.qml | 4 ++-- .../commerce/common/EmulatedMarketplaceHeader.qml | 4 ++-- .../qml/hifi/commerce/common/FirstUseTutorial.qml | 4 ++-- .../hifi/commerce/common/sendAsset/ConnectionItem.qml | 4 ++-- .../commerce/common/sendAsset/RecipientDisplay.qml | 4 ++-- .../qml/hifi/commerce/common/sendAsset/SendAsset.qml | 4 ++-- .../inspectionCertificate/InspectionCertificate.qml | 4 ++-- .../qml/hifi/commerce/purchases/PurchasedItem.qml | 4 ++-- .../qml/hifi/commerce/purchases/Purchases.qml | 4 ++-- interface/resources/qml/hifi/commerce/wallet/Help.qml | 4 ++-- .../resources/qml/hifi/commerce/wallet/NeedsLogIn.qml | 4 ++-- .../qml/hifi/commerce/wallet/PassphraseChange.qml | 4 ++-- .../qml/hifi/commerce/wallet/PassphraseModal.qml | 4 ++-- .../qml/hifi/commerce/wallet/PassphraseSelection.qml | 4 ++-- .../resources/qml/hifi/commerce/wallet/Wallet.qml | 4 ++-- .../qml/hifi/commerce/wallet/WalletChoice.qml | 4 ++-- .../resources/qml/hifi/commerce/wallet/WalletHome.qml | 4 ++-- .../qml/hifi/commerce/wallet/WalletSetup.qml | 4 ++-- interface/resources/qml/hifi/dialogs/AboutDialog.qml | 2 +- .../resources/qml/hifi/dialogs/RunningScripts.qml | 4 ++-- .../resources/qml/hifi/dialogs/TabletAboutDialog.qml | 2 +- .../resources/qml/hifi/dialogs/TabletAssetServer.qml | 4 ++-- .../resources/qml/hifi/dialogs/TabletDCDialog.qml | 4 ++-- .../resources/qml/hifi/dialogs/TabletDebugWindow.qml | 4 ++-- .../qml/hifi/dialogs/TabletEntityStatistics.qml | 4 ++-- .../qml/hifi/dialogs/TabletEntityStatisticsItem.qml | 4 ++-- .../resources/qml/hifi/dialogs/TabletLODTools.qml | 4 ++-- .../qml/hifi/dialogs/TabletRunningScripts.qml | 4 ++-- .../qml/hifi/dialogs/content/ModelBrowserContent.qml | 2 +- .../qml/hifi/dialogs/security/SecurityImageChange.qml | 4 ++-- .../hifi/dialogs/security/SecurityImageSelection.qml | 4 ++-- .../resources/qml/hifi/tablet/CalibratingScreen.qml | 4 ++-- .../resources/qml/hifi/tablet/ControllerSettings.qml | 4 ++-- interface/resources/qml/hifi/tablet/EditTabButton.qml | 4 ++-- interface/resources/qml/hifi/tablet/EditTabView.qml | 4 ++-- interface/resources/qml/hifi/tablet/InputRecorder.qml | 4 ++-- .../resources/qml/hifi/tablet/NewMaterialDialog.qml | 4 ++-- .../resources/qml/hifi/tablet/NewModelDialog.qml | 4 ++-- .../resources/qml/hifi/tablet/OpenVrConfiguration.qml | 4 ++-- .../resources/qml/hifi/tablet/TabletAddressDialog.qml | 4 ++-- interface/resources/qml/hifi/tablet/TabletHome.qml | 2 +- interface/resources/qml/hifi/tablet/TabletMenu.qml | 2 +- .../resources/qml/hifi/tablet/TabletMenuItem.qml | 4 ++-- .../resources/qml/hifi/tablet/TabletMenuView.qml | 2 +- .../qml/hifi/tablet/TabletModelBrowserDialog.qml | 4 ++-- .../hifi/tablet/tabletWindows/TabletFileDialog.qml | 4 ++-- .../tablet/tabletWindows/TabletPreferencesDialog.qml | 4 ++-- .../hifi/tablet/tabletWindows/preferences/Section.qml | 4 ++-- .../preferences/TabletBrowsablePreference.qml | 2 +- .../+android/HifiConstants.qml | 0 .../{styles-uit => stylesUit}/AnonymousProRegular.qml | 0 .../qml/{styles-uit => stylesUit}/ButtonLabel.qml | 0 .../qml/{styles-uit => stylesUit}/FiraSansRegular.qml | 0 .../{styles-uit => stylesUit}/FiraSansSemiBold.qml | 0 .../qml/{styles-uit => stylesUit}/HiFiGlyphs.qml | 0 .../qml/{styles-uit => stylesUit}/HifiConstants.qml | 0 .../qml/{styles-uit => stylesUit}/IconButton.qml | 0 .../qml/{styles-uit => stylesUit}/InfoItem.qml | 0 .../qml/{styles-uit => stylesUit}/InputLabel.qml | 0 .../qml/{styles-uit => stylesUit}/ListItem.qml | 0 .../resources/qml/{styles-uit => stylesUit}/Logs.qml | 0 .../qml/{styles-uit => stylesUit}/OverlayTitle.qml | 0 .../qml/{styles-uit => stylesUit}/RalewayBold.qml | 0 .../qml/{styles-uit => stylesUit}/RalewayLight.qml | 0 .../qml/{styles-uit => stylesUit}/RalewayRegular.qml | 0 .../qml/{styles-uit => stylesUit}/RalewaySemiBold.qml | 0 .../qml/{styles-uit => stylesUit}/SectionName.qml | 0 .../qml/{styles-uit => stylesUit}/Separator.qml | 2 +- .../qml/{styles-uit => stylesUit}/ShortcutText.qml | 0 .../qml/{styles-uit => stylesUit}/TabName.qml | 0 .../qml/{styles-uit => stylesUit}/TextFieldInput.qml | 0 .../resources/qml/{styles-uit => stylesUit}/qmldir | 0 interface/resources/qml/windows/Decoration.qml | 2 +- interface/resources/qml/windows/DefaultFrame.qml | 2 +- .../resources/qml/windows/DefaultFrameDecoration.qml | 2 +- interface/resources/qml/windows/Fadable.qml | 2 +- interface/resources/qml/windows/Frame.qml | 2 +- interface/resources/qml/windows/ModalFrame.qml | 4 ++-- interface/resources/qml/windows/ScrollingWindow.qml | 4 ++-- interface/resources/qml/windows/TabletModalFrame.qml | 4 ++-- interface/resources/qml/windows/ToolFrame.qml | 2 +- .../resources/qml/windows/ToolFrameDecoration.qml | 2 +- interface/resources/qml/windows/Window.qml | 2 +- libraries/ui/src/ui/OffscreenQmlSurface.cpp | 1 + scripts/developer/tests/ControlsGallery.qml | 11 ++--------- 197 files changed, 272 insertions(+), 278 deletions(-) rename interface/resources/qml/{controls-uit => controlsUit}/+android/ImageButton.qml (96%) rename interface/resources/qml/{controls-uit => controlsUit}/AttachmentsTable.qml (98%) rename interface/resources/qml/{controls-uit => controlsUit}/BaseWebView.qml (100%) rename interface/resources/qml/{controls-uit => controlsUit}/Button.qml (99%) rename interface/resources/qml/{controls-uit => controlsUit}/CheckBox.qml (99%) rename interface/resources/qml/{controls-uit => controlsUit}/CheckBoxQQC2.qml (98%) rename interface/resources/qml/{controls-uit => controlsUit}/ComboBox.qml (99%) rename interface/resources/qml/{controls-uit => controlsUit}/ContentSection.qml (99%) rename interface/resources/qml/{controls-uit => controlsUit}/FilterBar.qml (99%) rename interface/resources/qml/{controls-uit => controlsUit}/GlyphButton.qml (99%) rename interface/resources/qml/{controls-uit => controlsUit}/HorizontalRule.qml (100%) rename interface/resources/qml/{controls-uit => controlsUit}/HorizontalSpacer.qml (94%) rename interface/resources/qml/{controls-uit => controlsUit}/ImageMessageBox.qml (98%) rename interface/resources/qml/{controls-uit => controlsUit}/Key.qml (100%) rename interface/resources/qml/{controls-uit => controlsUit}/Keyboard.qml (100%) rename interface/resources/qml/{controls-uit => controlsUit}/Label.qml (97%) rename interface/resources/qml/{controls-uit => controlsUit}/QueuedButton.qml (98%) rename interface/resources/qml/{controls-uit => controlsUit}/RadioButton.qml (97%) rename interface/resources/qml/{controls-uit => controlsUit}/ScrollBar.qml (98%) rename interface/resources/qml/{controls-uit => controlsUit}/Separator.qml (98%) rename interface/resources/qml/{controls-uit => controlsUit}/Slider.qml (98%) rename interface/resources/qml/{controls-uit => controlsUit}/SpinBox.qml (98%) rename interface/resources/qml/{controls-uit => controlsUit}/Switch.qml (99%) rename interface/resources/qml/{controls-uit => controlsUit}/Table.qml (99%) rename interface/resources/qml/{controls-uit => controlsUit}/TabletContentSection.qml (99%) rename interface/resources/qml/{controls-uit => controlsUit}/TabletHeader.qml (96%) rename interface/resources/qml/{controls-uit => controlsUit}/TextAction.qml (96%) rename interface/resources/qml/{controls-uit => controlsUit}/TextEdit.qml (95%) rename interface/resources/qml/{controls-uit => controlsUit}/TextField.qml (99%) rename interface/resources/qml/{controls-uit => controlsUit}/ToolTip.qml (100%) rename interface/resources/qml/{controls-uit => controlsUit}/Tree.qml (99%) rename interface/resources/qml/{controls-uit => controlsUit}/VerticalSpacer.qml (94%) rename interface/resources/qml/{controls-uit => controlsUit}/WebGlyphButton.qml (98%) rename interface/resources/qml/{controls-uit => controlsUit}/WebSpinner.qml (100%) rename interface/resources/qml/{controls-uit => controlsUit}/WebView.qml (100%) rename interface/resources/qml/{controls-uit => controlsUit}/qmldir (100%) rename interface/resources/qml/{styles-uit => stylesUit}/+android/HifiConstants.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/AnonymousProRegular.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/ButtonLabel.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/FiraSansRegular.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/FiraSansSemiBold.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/HiFiGlyphs.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/HifiConstants.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/IconButton.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/InfoItem.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/InputLabel.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/ListItem.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/Logs.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/OverlayTitle.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/RalewayBold.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/RalewayLight.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/RalewayRegular.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/RalewaySemiBold.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/SectionName.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/Separator.qml (97%) rename interface/resources/qml/{styles-uit => stylesUit}/ShortcutText.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/TabName.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/TextFieldInput.qml (100%) rename interface/resources/qml/{styles-uit => stylesUit}/qmldir (100%) diff --git a/interface/resources/qml/AudioScopeUI.qml b/interface/resources/qml/AudioScopeUI.qml index aa181dbf8d..91908807e2 100644 --- a/interface/resources/qml/AudioScopeUI.qml +++ b/interface/resources/qml/AudioScopeUI.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 -import "styles-uit" -import "controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit Item { id: root diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 4474cfb2cd..01de7a36f9 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -2,9 +2,9 @@ import QtQuick 2.5 import QtWebChannel 1.0 import QtWebEngine 1.5 -import "controls-uit" +import controlsUit 1.0 import "styles" as HifiStyles -import "styles-uit" +import stylesUit 1.0 import "windows" ScrollingWindow { diff --git a/interface/resources/qml/CurrentAPI.qml b/interface/resources/qml/CurrentAPI.qml index 96bfb5c36b..4ea45041c3 100644 --- a/interface/resources/qml/CurrentAPI.qml +++ b/interface/resources/qml/CurrentAPI.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 -import "styles-uit" -import "controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls Item { id: root diff --git a/interface/resources/qml/InfoView.qml b/interface/resources/qml/InfoView.qml index f18969fb2f..8c5900b4c3 100644 --- a/interface/resources/qml/InfoView.qml +++ b/interface/resources/qml/InfoView.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import Hifi 1.0 as Hifi -import "controls-uit" +import controlsUit 1.0 import "windows" as Windows Windows.ScrollingWindow { diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index 336858502d..12117aaba4 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -11,8 +11,8 @@ import Hifi 1.0 import QtQuick 2.4 -import "controls-uit" -import "styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 import "windows" import "LoginDialog" diff --git a/interface/resources/qml/LoginDialog/+android/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/+android/LinkAccountBody.qml index 96b638c911..a40110b1e9 100644 --- a/interface/resources/qml/LoginDialog/+android/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/+android/LinkAccountBody.qml @@ -13,8 +13,8 @@ import QtQuick 2.4 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 as OriginalStyles -import "../../controls-uit" -import "../../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Item { id: linkAccountBody diff --git a/interface/resources/qml/LoginDialog/+android/SignUpBody.qml b/interface/resources/qml/LoginDialog/+android/SignUpBody.qml index 3a44a8d741..10909e4c85 100644 --- a/interface/resources/qml/LoginDialog/+android/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/+android/SignUpBody.qml @@ -13,8 +13,8 @@ import QtQuick 2.4 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 as OriginalStyles -import "../../controls-uit" -import "../../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Item { id: signupBody diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index fe4c511f1d..3a57061de4 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -12,8 +12,8 @@ import Hifi 1.0 import QtQuick 2.4 import QtQuick.Controls.Styles 1.4 as OriginalStyles -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Item { id: completeProfileBody diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index dbe4235cdd..103761236d 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -13,8 +13,9 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 as OriginalStyles -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 + Item { id: linkAccountBody clip: true diff --git a/interface/resources/qml/LoginDialog/SignInBody.qml b/interface/resources/qml/LoginDialog/SignInBody.qml index 9cb1add704..7fe29e13f6 100644 --- a/interface/resources/qml/LoginDialog/SignInBody.qml +++ b/interface/resources/qml/LoginDialog/SignInBody.qml @@ -12,8 +12,8 @@ import Hifi 1.0 import QtQuick 2.7 import QtQuick.Controls.Styles 1.4 as OriginalStyles -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Item { id: signInBody diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index bb30696e4c..d3c898d76f 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -12,8 +12,8 @@ import Hifi 1.0 import QtQuick 2.7 import QtQuick.Controls 1.4 -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Item { id: signupBody diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index bf05a36ce1..2a41353534 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -12,8 +12,8 @@ import Hifi 1.0 import QtQuick 2.4 import QtQuick.Controls 1.4 -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Item { id: usernameCollisionBody diff --git a/interface/resources/qml/LoginDialog/WelcomeBody.qml b/interface/resources/qml/LoginDialog/WelcomeBody.qml index 551ec263b7..020e6db002 100644 --- a/interface/resources/qml/LoginDialog/WelcomeBody.qml +++ b/interface/resources/qml/LoginDialog/WelcomeBody.qml @@ -11,8 +11,8 @@ import Hifi 1.0 import QtQuick 2.4 -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Item { id: welcomeBody diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 8c4d6145ec..322535641d 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -13,8 +13,8 @@ import QtWebEngine 1.1 import QtWebChannel 1.0 import "windows" as Windows -import "controls-uit" as Controls -import "styles-uit" +import controlsUit 1.0 as Controls +import stylesUit 1.0 Windows.ScrollingWindow { id: root diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index bef6423e25..53e6bcc37d 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -2,9 +2,9 @@ import QtQuick 2.3 import "windows" as Windows import "controls" -import "controls-uit" as Controls +import controlsUit 1.0 as Controls import "styles" -import "styles-uit" +import stylesUit 1.0 Windows.Window { id: root diff --git a/interface/resources/qml/TabletBrowser.qml b/interface/resources/qml/TabletBrowser.qml index 141c1f25a7..720a904231 100644 --- a/interface/resources/qml/TabletBrowser.qml +++ b/interface/resources/qml/TabletBrowser.qml @@ -3,9 +3,9 @@ import QtWebChannel 1.0 import QtWebEngine 1.5 import "controls" -import "controls-uit" as HifiControls +import controlsUit 1.0 as HifiControls import "styles" as HifiStyles -import "styles-uit" +import stylesUit 1.0 import "windows" Item { diff --git a/interface/resources/qml/UpdateDialog.qml b/interface/resources/qml/UpdateDialog.qml index 5e05601ce4..9c22d0b65b 100644 --- a/interface/resources/qml/UpdateDialog.qml +++ b/interface/resources/qml/UpdateDialog.qml @@ -4,9 +4,9 @@ import QtQuick.Controls 1.3 import QtQuick.Controls.Styles 1.3 import QtGraphicalEffects 1.0 -import "controls-uit" +import controlsUit 1.0 import "styles" as HifiStyles -import "styles-uit" +import stylesUit 1.0 import "windows" ScrollingWindow { diff --git a/interface/resources/qml/controls/Button.qml b/interface/resources/qml/controls/Button.qml index 6cbdec5644..b677822c0e 100644 --- a/interface/resources/qml/controls/Button.qml +++ b/interface/resources/qml/controls/Button.qml @@ -3,7 +3,6 @@ import QtQuick.Controls 2.2 as Original import "." import "../styles" -import "../controls-uit" Original.Button { id: control diff --git a/interface/resources/qml/controls/FlickableWebViewCore.qml b/interface/resources/qml/controls/FlickableWebViewCore.qml index 943f15e1de..cce32c137a 100644 --- a/interface/resources/qml/controls/FlickableWebViewCore.qml +++ b/interface/resources/qml/controls/FlickableWebViewCore.qml @@ -4,7 +4,7 @@ import QtWebChannel 1.0 import QtQuick.Controls 2.2 -import "../styles-uit" as StylesUIt +import stylesUit 1.0 as StylesUIt Item { id: flick diff --git a/interface/resources/qml/controls/TabletWebButton.qml b/interface/resources/qml/controls/TabletWebButton.qml index d016f71f2d..140461d817 100644 --- a/interface/resources/qml/controls/TabletWebButton.qml +++ b/interface/resources/qml/controls/TabletWebButton.qml @@ -10,7 +10,7 @@ import Hifi 1.0 import QtQuick 2.4 -import "../styles-uit" +import stylesUit 1.0 Rectangle { property alias text: label.text diff --git a/interface/resources/qml/controls/TabletWebScreen.qml b/interface/resources/qml/controls/TabletWebScreen.qml index bb037ad478..be11f16498 100644 --- a/interface/resources/qml/controls/TabletWebScreen.qml +++ b/interface/resources/qml/controls/TabletWebScreen.qml @@ -1,5 +1,5 @@ import QtQuick 2.7 -import "../controls-uit" as HiFiControls +import controlsUit 1.0 as HiFiControls Item { id: root diff --git a/interface/resources/qml/controls/TabletWebView.qml b/interface/resources/qml/controls/TabletWebView.qml index db695dbfb2..0c5ca37e00 100644 --- a/interface/resources/qml/controls/TabletWebView.qml +++ b/interface/resources/qml/controls/TabletWebView.qml @@ -1,8 +1,8 @@ import QtQuick 2.7 import QtWebEngine 1.5 -import "../controls-uit" as HiFiControls +import controlsUit 1.0 as HiFiControls import "../styles" as HifiStyles -import "../styles-uit" +import stylesUit 1.0 Item { id: root diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index 71bf69fdc8..375bcd50e0 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -1,5 +1,5 @@ import QtQuick 2.7 -import "../controls-uit" as HiFiControls +import controlsUit 1.0 as HiFiControls Item { width: parent !== null ? parent.width : undefined diff --git a/interface/resources/qml/controls-uit/+android/ImageButton.qml b/interface/resources/qml/controlsUit/+android/ImageButton.qml similarity index 96% rename from interface/resources/qml/controls-uit/+android/ImageButton.qml rename to interface/resources/qml/controlsUit/+android/ImageButton.qml index 5ebf7cd3e9..88eaf95d76 100644 --- a/interface/resources/qml/controls-uit/+android/ImageButton.qml +++ b/interface/resources/qml/controlsUit/+android/ImageButton.qml @@ -1,6 +1,6 @@ // // ImageButton.qml -// interface/resources/qml/controls-uit +// interface/resources/qml/controlsUit // // Created by Gabriel Calero & Cristian Duarte on 12 Oct 2017 // Copyright 2017 High Fidelity, Inc. @@ -11,7 +11,7 @@ import QtQuick 2.5 import QtQuick.Layouts 1.3 -import "../styles-uit" as HifiStyles +import "../stylesUit" as HifiStyles Item { id: button @@ -79,4 +79,4 @@ Item { } } ] -} \ No newline at end of file +} diff --git a/interface/resources/qml/controls-uit/AttachmentsTable.qml b/interface/resources/qml/controlsUit/AttachmentsTable.qml similarity index 98% rename from interface/resources/qml/controls-uit/AttachmentsTable.qml rename to interface/resources/qml/controlsUit/AttachmentsTable.qml index 8ee9909ab8..a2677962da 100644 --- a/interface/resources/qml/controls-uit/AttachmentsTable.qml +++ b/interface/resources/qml/controlsUit/AttachmentsTable.qml @@ -13,8 +13,8 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQuick.XmlListModel 2.0 -import "../styles-uit" -import "../controls-uit" as HifiControls +import "../stylesUit" +import "." as HifiControls import "../windows" import "../hifi/models" diff --git a/interface/resources/qml/controls-uit/BaseWebView.qml b/interface/resources/qml/controlsUit/BaseWebView.qml similarity index 100% rename from interface/resources/qml/controls-uit/BaseWebView.qml rename to interface/resources/qml/controlsUit/BaseWebView.qml diff --git a/interface/resources/qml/controls-uit/Button.qml b/interface/resources/qml/controlsUit/Button.qml similarity index 99% rename from interface/resources/qml/controls-uit/Button.qml rename to interface/resources/qml/controlsUit/Button.qml index f1a6e4bb4a..6ea7ce4b4c 100644 --- a/interface/resources/qml/controls-uit/Button.qml +++ b/interface/resources/qml/controlsUit/Button.qml @@ -12,7 +12,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 as Original import TabletScriptingInterface 1.0 -import "../styles-uit" +import "../stylesUit" Original.Button { id: control; diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controlsUit/CheckBox.qml similarity index 99% rename from interface/resources/qml/controls-uit/CheckBox.qml rename to interface/resources/qml/controlsUit/CheckBox.qml index 6e4a3df010..abf08908fb 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controlsUit/CheckBox.qml @@ -11,7 +11,7 @@ import QtQuick 2.2 import QtQuick.Controls 2.2 as Original -import "../styles-uit" +import "../stylesUit" import TabletScriptingInterface 1.0 diff --git a/interface/resources/qml/controls-uit/CheckBoxQQC2.qml b/interface/resources/qml/controlsUit/CheckBoxQQC2.qml similarity index 98% rename from interface/resources/qml/controls-uit/CheckBoxQQC2.qml rename to interface/resources/qml/controlsUit/CheckBoxQQC2.qml index 8a9686ff5e..91d35ecd58 100644 --- a/interface/resources/qml/controls-uit/CheckBoxQQC2.qml +++ b/interface/resources/qml/controlsUit/CheckBoxQQC2.qml @@ -11,8 +11,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 -import "../styles-uit" -import "../controls-uit" as HiFiControls +import "../stylesUit" +import "." as HiFiControls import TabletScriptingInterface 1.0 CheckBox { diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controlsUit/ComboBox.qml similarity index 99% rename from interface/resources/qml/controls-uit/ComboBox.qml rename to interface/resources/qml/controlsUit/ComboBox.qml index 245b565a62..8d1d7a5262 100644 --- a/interface/resources/qml/controls-uit/ComboBox.qml +++ b/interface/resources/qml/controlsUit/ComboBox.qml @@ -11,8 +11,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 -import "../styles-uit" -import "../controls-uit" as HifiControls +import "../stylesUit" +import "." as HifiControls FocusScope { id: root diff --git a/interface/resources/qml/controls-uit/ContentSection.qml b/interface/resources/qml/controlsUit/ContentSection.qml similarity index 99% rename from interface/resources/qml/controls-uit/ContentSection.qml rename to interface/resources/qml/controlsUit/ContentSection.qml index 47a13e9262..262c29220f 100644 --- a/interface/resources/qml/controls-uit/ContentSection.qml +++ b/interface/resources/qml/controlsUit/ContentSection.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 -import "../styles-uit" +import "../stylesUit" Column { property string name: "Content Section" diff --git a/interface/resources/qml/controls-uit/FilterBar.qml b/interface/resources/qml/controlsUit/FilterBar.qml similarity index 99% rename from interface/resources/qml/controls-uit/FilterBar.qml rename to interface/resources/qml/controlsUit/FilterBar.qml index 3d4e18ed48..0892018913 100644 --- a/interface/resources/qml/controls-uit/FilterBar.qml +++ b/interface/resources/qml/controlsUit/FilterBar.qml @@ -12,8 +12,8 @@ import QtQuick 2.9 import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 -import "../styles-uit" -import "../controls-uit" as HifiControls +import "../stylesUit" +import "." as HifiControls Item { id: root; diff --git a/interface/resources/qml/controls-uit/GlyphButton.qml b/interface/resources/qml/controlsUit/GlyphButton.qml similarity index 99% rename from interface/resources/qml/controls-uit/GlyphButton.qml rename to interface/resources/qml/controlsUit/GlyphButton.qml index 9129486720..17f7fba2d6 100644 --- a/interface/resources/qml/controls-uit/GlyphButton.qml +++ b/interface/resources/qml/controlsUit/GlyphButton.qml @@ -12,7 +12,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 as Original import TabletScriptingInterface 1.0 -import "../styles-uit" +import "../stylesUit" Original.Button { id: control diff --git a/interface/resources/qml/controls-uit/HorizontalRule.qml b/interface/resources/qml/controlsUit/HorizontalRule.qml similarity index 100% rename from interface/resources/qml/controls-uit/HorizontalRule.qml rename to interface/resources/qml/controlsUit/HorizontalRule.qml diff --git a/interface/resources/qml/controls-uit/HorizontalSpacer.qml b/interface/resources/qml/controlsUit/HorizontalSpacer.qml similarity index 94% rename from interface/resources/qml/controls-uit/HorizontalSpacer.qml rename to interface/resources/qml/controlsUit/HorizontalSpacer.qml index 545154ab44..efcabf2699 100644 --- a/interface/resources/qml/controls-uit/HorizontalSpacer.qml +++ b/interface/resources/qml/controlsUit/HorizontalSpacer.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 -import "../styles-uit" +import "../stylesUit" Item { id: root diff --git a/interface/resources/qml/controls-uit/ImageMessageBox.qml b/interface/resources/qml/controlsUit/ImageMessageBox.qml similarity index 98% rename from interface/resources/qml/controls-uit/ImageMessageBox.qml rename to interface/resources/qml/controlsUit/ImageMessageBox.qml index 74313f7ffe..46d93383a4 100644 --- a/interface/resources/qml/controls-uit/ImageMessageBox.qml +++ b/interface/resources/qml/controlsUit/ImageMessageBox.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import "../styles-uit" +import "../stylesUit" Item { id: imageBox diff --git a/interface/resources/qml/controls-uit/Key.qml b/interface/resources/qml/controlsUit/Key.qml similarity index 100% rename from interface/resources/qml/controls-uit/Key.qml rename to interface/resources/qml/controlsUit/Key.qml diff --git a/interface/resources/qml/controls-uit/Keyboard.qml b/interface/resources/qml/controlsUit/Keyboard.qml similarity index 100% rename from interface/resources/qml/controls-uit/Keyboard.qml rename to interface/resources/qml/controlsUit/Keyboard.qml diff --git a/interface/resources/qml/controls-uit/Label.qml b/interface/resources/qml/controlsUit/Label.qml similarity index 97% rename from interface/resources/qml/controls-uit/Label.qml rename to interface/resources/qml/controlsUit/Label.qml index 4c7051b495..7f208cde88 100644 --- a/interface/resources/qml/controls-uit/Label.qml +++ b/interface/resources/qml/controlsUit/Label.qml @@ -10,7 +10,7 @@ import QtQuick 2.7 -import "../styles-uit" +import "../stylesUit" RalewaySemiBold { HifiConstants { id: hifi } diff --git a/interface/resources/qml/controls-uit/QueuedButton.qml b/interface/resources/qml/controlsUit/QueuedButton.qml similarity index 98% rename from interface/resources/qml/controls-uit/QueuedButton.qml rename to interface/resources/qml/controlsUit/QueuedButton.qml index 6612d582df..70ad9eb112 100644 --- a/interface/resources/qml/controls-uit/QueuedButton.qml +++ b/interface/resources/qml/controlsUit/QueuedButton.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 -import "../styles-uit" +import "../stylesUit" import "." as HifiControls HifiControls.Button { diff --git a/interface/resources/qml/controls-uit/RadioButton.qml b/interface/resources/qml/controlsUit/RadioButton.qml similarity index 97% rename from interface/resources/qml/controls-uit/RadioButton.qml rename to interface/resources/qml/controlsUit/RadioButton.qml index 56324c55d7..ad62a77aa7 100644 --- a/interface/resources/qml/controls-uit/RadioButton.qml +++ b/interface/resources/qml/controlsUit/RadioButton.qml @@ -11,8 +11,8 @@ import QtQuick 2.5 import QtQuick.Controls 2.2 as Original -import "../styles-uit" -import "../controls-uit" as HifiControls +import "../stylesUit" +import "." as HifiControls import TabletScriptingInterface 1.0 diff --git a/interface/resources/qml/controls-uit/ScrollBar.qml b/interface/resources/qml/controlsUit/ScrollBar.qml similarity index 98% rename from interface/resources/qml/controls-uit/ScrollBar.qml rename to interface/resources/qml/controlsUit/ScrollBar.qml index 125e84e585..bcb1f62429 100644 --- a/interface/resources/qml/controls-uit/ScrollBar.qml +++ b/interface/resources/qml/controlsUit/ScrollBar.qml @@ -11,7 +11,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 -import "../styles-uit" +import "../stylesUit" ScrollBar { visible: size < 1.0 diff --git a/interface/resources/qml/controls-uit/Separator.qml b/interface/resources/qml/controlsUit/Separator.qml similarity index 98% rename from interface/resources/qml/controls-uit/Separator.qml rename to interface/resources/qml/controlsUit/Separator.qml index 3350764ae9..da6b9adf57 100644 --- a/interface/resources/qml/controls-uit/Separator.qml +++ b/interface/resources/qml/controlsUit/Separator.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import "../styles-uit" +import "../stylesUit" Item { property int colorScheme: 0; diff --git a/interface/resources/qml/controls-uit/Slider.qml b/interface/resources/qml/controlsUit/Slider.qml similarity index 98% rename from interface/resources/qml/controls-uit/Slider.qml rename to interface/resources/qml/controlsUit/Slider.qml index 2a5d4c137d..8cb08b69e2 100644 --- a/interface/resources/qml/controls-uit/Slider.qml +++ b/interface/resources/qml/controlsUit/Slider.qml @@ -11,8 +11,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 -import "../styles-uit" -import "../controls-uit" as HifiControls +import "../stylesUit" +import "." as HifiControls Slider { id: slider diff --git a/interface/resources/qml/controls-uit/SpinBox.qml b/interface/resources/qml/controlsUit/SpinBox.qml similarity index 98% rename from interface/resources/qml/controls-uit/SpinBox.qml rename to interface/resources/qml/controlsUit/SpinBox.qml index 3d3ea7a75e..d24c7c5e8c 100644 --- a/interface/resources/qml/controls-uit/SpinBox.qml +++ b/interface/resources/qml/controlsUit/SpinBox.qml @@ -11,8 +11,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 -import "../styles-uit" -import "../controls-uit" as HifiControls +import "../stylesUit" +import "." as HifiControls SpinBox { id: spinBox diff --git a/interface/resources/qml/controls-uit/Switch.qml b/interface/resources/qml/controlsUit/Switch.qml similarity index 99% rename from interface/resources/qml/controls-uit/Switch.qml rename to interface/resources/qml/controlsUit/Switch.qml index bfe86b1420..0961ef2500 100644 --- a/interface/resources/qml/controls-uit/Switch.qml +++ b/interface/resources/qml/controlsUit/Switch.qml @@ -11,7 +11,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 as Original -import "../styles-uit" +import "../stylesUit" Item { id: rootSwitch; diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controlsUit/Table.qml similarity index 99% rename from interface/resources/qml/controls-uit/Table.qml rename to interface/resources/qml/controlsUit/Table.qml index ce4e1c376a..ab74361046 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controlsUit/Table.qml @@ -13,7 +13,7 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQuick.Controls 2.3 as QQC2 -import "../styles-uit" +import "../stylesUit" TableView { id: tableView diff --git a/interface/resources/qml/controls-uit/TabletContentSection.qml b/interface/resources/qml/controlsUit/TabletContentSection.qml similarity index 99% rename from interface/resources/qml/controls-uit/TabletContentSection.qml rename to interface/resources/qml/controlsUit/TabletContentSection.qml index c34f4afdd6..dccaf31bbe 100644 --- a/interface/resources/qml/controls-uit/TabletContentSection.qml +++ b/interface/resources/qml/controlsUit/TabletContentSection.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 -import "../styles-uit" +import "../stylesUit" Column { property string name: "Content Section" diff --git a/interface/resources/qml/controls-uit/TabletHeader.qml b/interface/resources/qml/controlsUit/TabletHeader.qml similarity index 96% rename from interface/resources/qml/controls-uit/TabletHeader.qml rename to interface/resources/qml/controlsUit/TabletHeader.qml index 56203de286..f626700742 100644 --- a/interface/resources/qml/controls-uit/TabletHeader.qml +++ b/interface/resources/qml/controlsUit/TabletHeader.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 -import "../styles-uit" +import "../stylesUit" Rectangle { diff --git a/interface/resources/qml/controls-uit/TextAction.qml b/interface/resources/qml/controlsUit/TextAction.qml similarity index 96% rename from interface/resources/qml/controls-uit/TextAction.qml rename to interface/resources/qml/controlsUit/TextAction.qml index 1745a6c273..a0a1bb7d07 100644 --- a/interface/resources/qml/controls-uit/TextAction.qml +++ b/interface/resources/qml/controlsUit/TextAction.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 -import "../styles-uit" -import "../controls-uit" as HifiControls +import "../stylesUit" +import "." as HifiControls Item { property string icon: "" diff --git a/interface/resources/qml/controls-uit/TextEdit.qml b/interface/resources/qml/controlsUit/TextEdit.qml similarity index 95% rename from interface/resources/qml/controls-uit/TextEdit.qml rename to interface/resources/qml/controlsUit/TextEdit.qml index a72a3b13d8..7446c5040f 100644 --- a/interface/resources/qml/controls-uit/TextEdit.qml +++ b/interface/resources/qml/controlsUit/TextEdit.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import "../styles-uit" +import "../stylesUit" TextEdit { diff --git a/interface/resources/qml/controls-uit/TextField.qml b/interface/resources/qml/controlsUit/TextField.qml similarity index 99% rename from interface/resources/qml/controls-uit/TextField.qml rename to interface/resources/qml/controlsUit/TextField.qml index 917068ac01..d78f3a1340 100644 --- a/interface/resources/qml/controls-uit/TextField.qml +++ b/interface/resources/qml/controlsUit/TextField.qml @@ -12,8 +12,8 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 -import "../styles-uit" -import "../controls-uit" as HifiControls +import "../stylesUit" +import "." as HifiControls TextField { id: textField diff --git a/interface/resources/qml/controls-uit/ToolTip.qml b/interface/resources/qml/controlsUit/ToolTip.qml similarity index 100% rename from interface/resources/qml/controls-uit/ToolTip.qml rename to interface/resources/qml/controlsUit/ToolTip.qml diff --git a/interface/resources/qml/controls-uit/Tree.qml b/interface/resources/qml/controlsUit/Tree.qml similarity index 99% rename from interface/resources/qml/controls-uit/Tree.qml rename to interface/resources/qml/controlsUit/Tree.qml index 5199a10a27..f2c49095b1 100644 --- a/interface/resources/qml/controls-uit/Tree.qml +++ b/interface/resources/qml/controlsUit/Tree.qml @@ -15,7 +15,7 @@ import QtQuick.Controls.Styles 1.4 import QtQuick.Controls 2.2 as QQC2 -import "../styles-uit" +import "../stylesUit" TreeView { id: treeView diff --git a/interface/resources/qml/controls-uit/VerticalSpacer.qml b/interface/resources/qml/controlsUit/VerticalSpacer.qml similarity index 94% rename from interface/resources/qml/controls-uit/VerticalSpacer.qml rename to interface/resources/qml/controlsUit/VerticalSpacer.qml index 2df65f1002..4c93aa1801 100644 --- a/interface/resources/qml/controls-uit/VerticalSpacer.qml +++ b/interface/resources/qml/controlsUit/VerticalSpacer.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 -import "../styles-uit" +import "../stylesUit" Item { id: root diff --git a/interface/resources/qml/controls-uit/WebGlyphButton.qml b/interface/resources/qml/controlsUit/WebGlyphButton.qml similarity index 98% rename from interface/resources/qml/controls-uit/WebGlyphButton.qml rename to interface/resources/qml/controlsUit/WebGlyphButton.qml index fd7cd001b2..7739ecd5e7 100644 --- a/interface/resources/qml/controls-uit/WebGlyphButton.qml +++ b/interface/resources/qml/controlsUit/WebGlyphButton.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import QtQuick.Controls 2.2 as Original -import "../styles-uit" +import "../stylesUit" Original.Button { id: control diff --git a/interface/resources/qml/controls-uit/WebSpinner.qml b/interface/resources/qml/controlsUit/WebSpinner.qml similarity index 100% rename from interface/resources/qml/controls-uit/WebSpinner.qml rename to interface/resources/qml/controlsUit/WebSpinner.qml diff --git a/interface/resources/qml/controls-uit/WebView.qml b/interface/resources/qml/controlsUit/WebView.qml similarity index 100% rename from interface/resources/qml/controls-uit/WebView.qml rename to interface/resources/qml/controlsUit/WebView.qml diff --git a/interface/resources/qml/controls-uit/qmldir b/interface/resources/qml/controlsUit/qmldir similarity index 100% rename from interface/resources/qml/controls-uit/qmldir rename to interface/resources/qml/controlsUit/qmldir diff --git a/interface/resources/qml/dialogs/AssetDialog.qml b/interface/resources/qml/dialogs/AssetDialog.qml index e8d28e9b37..b8eaab0b8d 100644 --- a/interface/resources/qml/dialogs/AssetDialog.qml +++ b/interface/resources/qml/dialogs/AssetDialog.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import Qt.labs.settings 1.0 -import "../styles-uit" +import stylesUit 1.0 import "../windows" import "assetDialog" diff --git a/interface/resources/qml/dialogs/CustomQueryDialog.qml b/interface/resources/qml/dialogs/CustomQueryDialog.qml index 0c86b93c4b..026068eee1 100644 --- a/interface/resources/qml/dialogs/CustomQueryDialog.qml +++ b/interface/resources/qml/dialogs/CustomQueryDialog.qml @@ -12,8 +12,8 @@ import QtQuick 2.7; import QtQuick.Dialogs 1.2 as OriginalDialogs; import QtQuick.Controls 2.3 -import "../controls-uit"; -import "../styles-uit"; +import controlsUit 1.0 +import stylesUit 1.0 import "../windows"; ModalWindow { diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 6651af0db3..b7340575dd 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -16,8 +16,8 @@ import QtQuick.Controls 1.4 as QQC1 import QtQuick.Controls 2.3 import ".." -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 import "../windows" import "fileDialog" diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index b5ac6cab72..9428e3ab6e 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -11,8 +11,8 @@ import QtQuick 2.5 import QtQuick.Dialogs 1.2 as OriginalDialogs -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 import "../windows" import "messageDialog" diff --git a/interface/resources/qml/dialogs/PreferencesDialog.qml b/interface/resources/qml/dialogs/PreferencesDialog.qml index fffd0e2ed9..9df1d0b963 100644 --- a/interface/resources/qml/dialogs/PreferencesDialog.qml +++ b/interface/resources/qml/dialogs/PreferencesDialog.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 -import "../controls-uit" as HifiControls -import "../styles-uit" +import controlsUit 1.0 as HifiControls +import stylesUit 1.0 import "../windows" import "preferences" diff --git a/interface/resources/qml/dialogs/QueryDialog.qml b/interface/resources/qml/dialogs/QueryDialog.qml index 41ee30e6d5..9cfb3011bd 100644 --- a/interface/resources/qml/dialogs/QueryDialog.qml +++ b/interface/resources/qml/dialogs/QueryDialog.qml @@ -11,8 +11,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 import "../windows" ModalWindow { diff --git a/interface/resources/qml/dialogs/TabletAssetDialog.qml b/interface/resources/qml/dialogs/TabletAssetDialog.qml index 897378e40c..b3bd45f972 100644 --- a/interface/resources/qml/dialogs/TabletAssetDialog.qml +++ b/interface/resources/qml/dialogs/TabletAssetDialog.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 -import "../styles-uit" +import stylesUit 1.0 import "../windows" import "assetDialog" diff --git a/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml b/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml index 81a2c5c1e0..c7772984ab 100644 --- a/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml +++ b/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml @@ -12,8 +12,8 @@ import QtQuick 2.7 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 2.3 -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 import "../windows" TabletModalWindow { diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml index 6848c230e3..3be6e30dd0 100644 --- a/interface/resources/qml/dialogs/TabletFileDialog.qml +++ b/interface/resources/qml/dialogs/TabletFileDialog.qml @@ -16,8 +16,8 @@ import QtQuick.Controls 1.4 as QQC1 import QtQuick.Controls 2.3 import ".." -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 import "../windows" import "fileDialog" diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index c85b2b2ba0..6314921286 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -11,8 +11,8 @@ import Hifi 1.0 import QtQuick 2.5 -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 import "../windows" import "../LoginDialog" diff --git a/interface/resources/qml/dialogs/TabletMessageBox.qml b/interface/resources/qml/dialogs/TabletMessageBox.qml index fabe0dd247..1e6f0734ad 100644 --- a/interface/resources/qml/dialogs/TabletMessageBox.qml +++ b/interface/resources/qml/dialogs/TabletMessageBox.qml @@ -11,8 +11,8 @@ import QtQuick 2.5 import QtQuick.Dialogs 1.2 as OriginalDialogs -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 import "../windows" import "messageDialog" diff --git a/interface/resources/qml/dialogs/TabletQueryDialog.qml b/interface/resources/qml/dialogs/TabletQueryDialog.qml index 5746a3d67c..8f63730b8e 100644 --- a/interface/resources/qml/dialogs/TabletQueryDialog.qml +++ b/interface/resources/qml/dialogs/TabletQueryDialog.qml @@ -12,8 +12,8 @@ import QtQuick 2.7 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 2.3 -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 import "../windows" TabletModalWindow { diff --git a/interface/resources/qml/dialogs/assetDialog/AssetDialogContent.qml b/interface/resources/qml/dialogs/assetDialog/AssetDialogContent.qml index c3e842bc2f..da976ef3e1 100644 --- a/interface/resources/qml/dialogs/assetDialog/AssetDialogContent.qml +++ b/interface/resources/qml/dialogs/assetDialog/AssetDialogContent.qml @@ -12,8 +12,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 import QtQuick.Controls 1.5 as QQC1 -import "../../controls-uit" -import "../../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 import "../fileDialog" diff --git a/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml b/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml index 50a10974b5..6c042b5598 100644 --- a/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml +++ b/interface/resources/qml/dialogs/fileDialog/FileTypeSelection.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 -import "../../controls-uit" +import controlsUit 1.0 ComboBox { id: root diff --git a/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml b/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml index 8411980db7..f5715fa2c2 100644 --- a/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml +++ b/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import QtQuick.Dialogs 1.2 -import "../../controls-uit" +import controlsUit 1.0 Button { property var dialog; diff --git a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml index 0efc3776b3..9505e70530 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 -import "../../controls-uit" +import controlsUit 1.0 import "../../hifi/tablet/tabletWindows/preferences" Preference { diff --git a/interface/resources/qml/dialogs/preferences/BrowsablePreference.qml b/interface/resources/qml/dialogs/preferences/BrowsablePreference.qml index 2cf50891c9..6059f8ff1c 100644 --- a/interface/resources/qml/dialogs/preferences/BrowsablePreference.qml +++ b/interface/resources/qml/dialogs/preferences/BrowsablePreference.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import "../../dialogs" -import "../../controls-uit" +import controlsUit 1.0 Preference { id: root diff --git a/interface/resources/qml/dialogs/preferences/ButtonPreference.qml b/interface/resources/qml/dialogs/preferences/ButtonPreference.qml index 454a9124ae..09c5b4329d 100644 --- a/interface/resources/qml/dialogs/preferences/ButtonPreference.qml +++ b/interface/resources/qml/dialogs/preferences/ButtonPreference.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import TabletScriptingInterface 1.0 -import "../../controls-uit" +import controlsUit 1.0 Preference { id: root diff --git a/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml b/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml index e2172d8eda..f6f840bbe8 100644 --- a/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml +++ b/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import TabletScriptingInterface 1.0 -import "../../controls-uit" +import controlsUit 1.0 Preference { id: root diff --git a/interface/resources/qml/dialogs/preferences/ComboBoxPreference.qml b/interface/resources/qml/dialogs/preferences/ComboBoxPreference.qml index 3b3efaf520..98cb397976 100644 --- a/interface/resources/qml/dialogs/preferences/ComboBoxPreference.qml +++ b/interface/resources/qml/dialogs/preferences/ComboBoxPreference.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 -import "../../controls-uit" as HiFiControls -import "../../styles-uit" +import controlsUit 1.0 as HiFiControls +import stylesUit 1.0 Preference { id: root diff --git a/interface/resources/qml/dialogs/preferences/EditablePreference.qml b/interface/resources/qml/dialogs/preferences/EditablePreference.qml index 8acf8e1f76..e0c79ebba0 100644 --- a/interface/resources/qml/dialogs/preferences/EditablePreference.qml +++ b/interface/resources/qml/dialogs/preferences/EditablePreference.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import "../../dialogs" -import "../../controls-uit" +import controlsUit 1.0 Preference { id: root diff --git a/interface/resources/qml/dialogs/preferences/PrimaryHandPreference.qml b/interface/resources/qml/dialogs/preferences/PrimaryHandPreference.qml index cfc2e94ed9..f963003c59 100644 --- a/interface/resources/qml/dialogs/preferences/PrimaryHandPreference.qml +++ b/interface/resources/qml/dialogs/preferences/PrimaryHandPreference.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 -import "../../controls-uit" +import controlsUit 1.0 Preference { id: root diff --git a/interface/resources/qml/dialogs/preferences/RadioButtonsPreference.qml b/interface/resources/qml/dialogs/preferences/RadioButtonsPreference.qml index 103904a666..0a09d8d609 100644 --- a/interface/resources/qml/dialogs/preferences/RadioButtonsPreference.qml +++ b/interface/resources/qml/dialogs/preferences/RadioButtonsPreference.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 -import "../../controls-uit" -import "../../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Preference { id: root diff --git a/interface/resources/qml/dialogs/preferences/Section.qml b/interface/resources/qml/dialogs/preferences/Section.qml index c2c6583b7e..a9b755ad83 100644 --- a/interface/resources/qml/dialogs/preferences/Section.qml +++ b/interface/resources/qml/dialogs/preferences/Section.qml @@ -12,8 +12,8 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import Hifi 1.0 -import "../../controls-uit" as HiFiControls -import "../../styles-uit" +import controlsUit 1.0 as HiFiControls +import stylesUit 1.0 import "." Preference { diff --git a/interface/resources/qml/dialogs/preferences/SliderPreference.qml b/interface/resources/qml/dialogs/preferences/SliderPreference.qml index 2bdda09fc3..c8a2aae158 100644 --- a/interface/resources/qml/dialogs/preferences/SliderPreference.qml +++ b/interface/resources/qml/dialogs/preferences/SliderPreference.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import "../../dialogs" -import "../../controls-uit" +import controlsUit 1.0 Preference { id: root diff --git a/interface/resources/qml/dialogs/preferences/SpinBoxPreference.qml b/interface/resources/qml/dialogs/preferences/SpinBoxPreference.qml index b2c334b674..1b080c2759 100644 --- a/interface/resources/qml/dialogs/preferences/SpinBoxPreference.qml +++ b/interface/resources/qml/dialogs/preferences/SpinBoxPreference.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 -import "../../controls-uit" +import controlsUit 1.0 Preference { id: root diff --git a/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml b/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml index 126e62fc30..cbc804d9d7 100644 --- a/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml +++ b/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import "../../dialogs" -import "../../controls-uit" +import controlsUit 1.0 Preference { id: root diff --git a/interface/resources/qml/hifi/+android/ActionBar.qml b/interface/resources/qml/hifi/+android/ActionBar.qml index d487901d6f..3c58156f30 100644 --- a/interface/resources/qml/hifi/+android/ActionBar.qml +++ b/interface/resources/qml/hifi/+android/ActionBar.qml @@ -3,8 +3,8 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../controls" as HifiControls import ".." diff --git a/interface/resources/qml/hifi/+android/AudioBar.qml b/interface/resources/qml/hifi/+android/AudioBar.qml index 6cc17fccf7..912572fdf8 100644 --- a/interface/resources/qml/hifi/+android/AudioBar.qml +++ b/interface/resources/qml/hifi/+android/AudioBar.qml @@ -3,8 +3,8 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../controls" as HifiControls import ".." diff --git a/interface/resources/qml/hifi/+android/AvatarOption.qml b/interface/resources/qml/hifi/+android/AvatarOption.qml index 85d7e52eb2..7eba3c2a67 100644 --- a/interface/resources/qml/hifi/+android/AvatarOption.qml +++ b/interface/resources/qml/hifi/+android/AvatarOption.qml @@ -11,7 +11,7 @@ import QtQuick.Layouts 1.3 import QtQuick 2.5 -import "../controls-uit" as HifiControlsUit +import controlsUit 1.0 as HifiControlsUit ColumnLayout { id: itemRoot diff --git a/interface/resources/qml/hifi/+android/StatsBar.qml b/interface/resources/qml/hifi/+android/StatsBar.qml index aee438b44f..64e93b4a08 100644 --- a/interface/resources/qml/hifi/+android/StatsBar.qml +++ b/interface/resources/qml/hifi/+android/StatsBar.qml @@ -3,8 +3,8 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../controls" as HifiControls import ".." diff --git a/interface/resources/qml/hifi/+android/WindowHeader.qml b/interface/resources/qml/hifi/+android/WindowHeader.qml index 4ec0a0c6e6..5316fc4786 100644 --- a/interface/resources/qml/hifi/+android/WindowHeader.qml +++ b/interface/resources/qml/hifi/+android/WindowHeader.qml @@ -16,8 +16,8 @@ import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 import "." import "../styles" as HifiStyles -import "../styles-uit" -import "../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../controls" as HifiControls import ".." diff --git a/interface/resources/qml/hifi/+android/bottomHudOptions.qml b/interface/resources/qml/hifi/+android/bottomHudOptions.qml index 22beccf531..6b830d94c2 100644 --- a/interface/resources/qml/hifi/+android/bottomHudOptions.qml +++ b/interface/resources/qml/hifi/+android/bottomHudOptions.qml @@ -16,8 +16,8 @@ import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 import "../../styles" as HifiStyles -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../controls" as HifiControls import ".." import "." diff --git a/interface/resources/qml/hifi/+android/modesbar.qml b/interface/resources/qml/hifi/+android/modesbar.qml index 994bf1efe4..1bf04fb8d9 100644 --- a/interface/resources/qml/hifi/+android/modesbar.qml +++ b/interface/resources/qml/hifi/+android/modesbar.qml @@ -3,8 +3,8 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../controls" as HifiControls import ".." diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index 1a7f5bac40..ad337a6361 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -14,8 +14,8 @@ import QtQuick.Controls.Styles 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 -import "../styles-uit" -import "../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../windows" as Windows import "../dialogs" diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 83bf1e2c54..7f29324416 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -17,7 +17,7 @@ import QtGraphicalEffects 1.0 import TabletScriptingInterface 1.0 import "toolbars" -import "../styles-uit" +import stylesUit 1.0 Item { id: root; diff --git a/interface/resources/qml/hifi/ComboDialog.qml b/interface/resources/qml/hifi/ComboDialog.qml index e5dc8a9c1a..74d9c1019b 100644 --- a/interface/resources/qml/hifi/ComboDialog.qml +++ b/interface/resources/qml/hifi/ComboDialog.qml @@ -10,8 +10,8 @@ // import QtQuick 2.5 -import "../styles-uit" -import "../controls-uit" +import stylesUit 1.0 +import controlsUit 1.0 Item { property var dialogTitleText : ""; diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 4d342fe775..511d9377e5 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -8,7 +8,7 @@ import "../desktop" as OriginalDesktop import ".." import "." import "./toolbars" -import "../controls-uit" +import controlsUit 1.0 OriginalDesktop.Desktop { id: desktop diff --git a/interface/resources/qml/hifi/DesktopLetterboxMessage.qml b/interface/resources/qml/hifi/DesktopLetterboxMessage.qml index 9e9dcc75b2..048add24e5 100644 --- a/interface/resources/qml/hifi/DesktopLetterboxMessage.qml +++ b/interface/resources/qml/hifi/DesktopLetterboxMessage.qml @@ -10,7 +10,7 @@ // import QtQuick 2.5 -import "../styles-uit" +import stylesUit 1.0 Item { property alias text: popupText.text diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 346481fe1f..4cfd4804b3 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -15,7 +15,7 @@ import Hifi 1.0 import QtQuick 2.5 import QtGraphicalEffects 1.0 import "toolbars" -import "../styles-uit" +import stylesUit 1.0 import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. Column { diff --git a/interface/resources/qml/hifi/LetterboxMessage.qml b/interface/resources/qml/hifi/LetterboxMessage.qml index 8a18d88842..68bebdd041 100644 --- a/interface/resources/qml/hifi/LetterboxMessage.qml +++ b/interface/resources/qml/hifi/LetterboxMessage.qml @@ -10,7 +10,7 @@ // import QtQuick 2.5 -import "../styles-uit" +import stylesUit 1.0 Item { property alias text: popupText.text diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index dfa6555150..242ca5ab57 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -13,8 +13,8 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0 -import "../styles-uit" -import "../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "toolbars" // references Users, UserActivityLogger, MyAvatar, Vec3, Quat, AddressManager, Account from root context diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 1384cb8711..368beaab47 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -15,8 +15,8 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtGraphicalEffects 1.0 import Qt.labs.settings 1.0 -import "../styles-uit" -import "../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../controls" as HifiControls import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. diff --git a/interface/resources/qml/hifi/SkyboxChanger.qml b/interface/resources/qml/hifi/SkyboxChanger.qml index f0c97a11a3..a66fc38415 100644 --- a/interface/resources/qml/hifi/SkyboxChanger.qml +++ b/interface/resources/qml/hifi/SkyboxChanger.qml @@ -10,8 +10,8 @@ // import QtQuick 2.5 -import "../styles-uit" -import "../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import QtQuick.Controls 2.2 Item { diff --git a/interface/resources/qml/hifi/SpectatorCamera.qml b/interface/resources/qml/hifi/SpectatorCamera.qml index 4bf80e410b..09b722b906 100644 --- a/interface/resources/qml/hifi/SpectatorCamera.qml +++ b/interface/resources/qml/hifi/SpectatorCamera.qml @@ -13,8 +13,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.7 -import "../styles-uit" -import "../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../controls" as HifiControls // references HMD, XXX from root context diff --git a/interface/resources/qml/hifi/TabletTextButton.qml b/interface/resources/qml/hifi/TabletTextButton.qml index e5ff1d381d..6c9e0331df 100644 --- a/interface/resources/qml/hifi/TabletTextButton.qml +++ b/interface/resources/qml/hifi/TabletTextButton.qml @@ -10,7 +10,7 @@ import Hifi 1.0 import QtQuick 2.4 -import "../styles-uit" +import stylesUit 1.0 Rectangle { property alias text: label.text diff --git a/interface/resources/qml/hifi/TextButton.qml b/interface/resources/qml/hifi/TextButton.qml index 02e49d86e4..61588a9603 100644 --- a/interface/resources/qml/hifi/TextButton.qml +++ b/interface/resources/qml/hifi/TextButton.qml @@ -9,7 +9,7 @@ // import Hifi 1.0 import QtQuick 2.4 -import "../styles-uit" +import stylesUit 1.0 Rectangle { property alias text: label.text; diff --git a/interface/resources/qml/hifi/WebBrowser.qml b/interface/resources/qml/hifi/WebBrowser.qml index ab93752d92..c05de26471 100644 --- a/interface/resources/qml/hifi/WebBrowser.qml +++ b/interface/resources/qml/hifi/WebBrowser.qml @@ -18,8 +18,8 @@ import QtGraphicalEffects 1.0 import QtWebEngine 1.5 import QtWebChannel 1.0 -import "../styles-uit" -import "../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../windows" import "../controls" diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index f4a708567a..c8dd83cd62 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -15,8 +15,8 @@ import QtQuick 2.5 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 -import "../../styles-uit" -import "../../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../../windows" import "./" as AudioControls diff --git a/interface/resources/qml/hifi/audio/AudioTabButton.qml b/interface/resources/qml/hifi/audio/AudioTabButton.qml index 3a3ed90f5e..32331ccb6e 100644 --- a/interface/resources/qml/hifi/audio/AudioTabButton.qml +++ b/interface/resources/qml/hifi/audio/AudioTabButton.qml @@ -11,8 +11,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 -import "../../controls-uit" as HifiControls -import "../../styles-uit" +import controlsUit 1.0 as HifiControls +import stylesUit 1.0 TabButton { id: control diff --git a/interface/resources/qml/hifi/audio/CheckBox.qml b/interface/resources/qml/hifi/audio/CheckBox.qml index 3a954d4004..5ab62a5091 100644 --- a/interface/resources/qml/hifi/audio/CheckBox.qml +++ b/interface/resources/qml/hifi/audio/CheckBox.qml @@ -11,7 +11,7 @@ import QtQuick 2.7 -import "../../controls-uit" as HifiControls +import controlsUit 1.0 as HifiControls HifiControls.CheckBoxQQC2 { color: "white" diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index 2b9599a3cc..cfe55af9c4 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -13,8 +13,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 -import "../../styles-uit" -import "../../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls RowLayout { property var sound: null; diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 271aab87d1..2cf176c1e5 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -14,8 +14,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 import QtQuick.Controls 1.4 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls import "../wallet" as HifiWallet import "../common" as HifiCommerceCommon diff --git a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml index 5901adc484..b7215500d2 100644 --- a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml +++ b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml @@ -14,8 +14,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 import QtGraphicalEffects 1.0 -import "qrc:////qml//styles-uit" -import "qrc:////qml//controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "qrc:////qml//controls" as HifiControls // references XXX from root context diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index 99c2d89da8..0d0af875d1 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -14,8 +14,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.7 import QtGraphicalEffects 1.0 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls // references XXX from root context diff --git a/interface/resources/qml/hifi/commerce/common/FirstUseTutorial.qml b/interface/resources/qml/hifi/commerce/common/FirstUseTutorial.qml index 0b982893f1..c2d85b68b4 100644 --- a/interface/resources/qml/hifi/commerce/common/FirstUseTutorial.qml +++ b/interface/resources/qml/hifi/commerce/common/FirstUseTutorial.qml @@ -14,8 +14,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 import QtGraphicalEffects 1.0 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls // references XXX from root context diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml index 41eacd68d5..1eb8af31e6 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml @@ -16,8 +16,8 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 -import "../../../../styles-uit" -import "../../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../../controls" as HifiControls import "../../wallet" as HifiWallet diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/RecipientDisplay.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/RecipientDisplay.qml index 9293dc83ab..9e1a967d50 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/RecipientDisplay.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/RecipientDisplay.qml @@ -15,8 +15,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.6 import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 -import "../../../../styles-uit" -import "../../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../../controls" as HifiControls import "../" as HifiCommerceCommon diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index bb4bb624bc..ec7146f33e 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -15,8 +15,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.6 import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 -import "../../../../styles-uit" -import "../../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../../controls" as HifiControls import "../" as HifiCommerceCommon import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml index 885838a26e..8d0b93d11a 100644 --- a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -13,8 +13,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls import "../wallet" as HifiWallet diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 0828d86eff..c8ec7238d6 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -15,8 +15,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls import "../wallet" as HifiWallet import TabletScriptingInterface 1.0 diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index e7b541b350..932a3ab6c4 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -13,8 +13,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. import "../wallet" as HifiWallet diff --git a/interface/resources/qml/hifi/commerce/wallet/Help.qml b/interface/resources/qml/hifi/commerce/wallet/Help.qml index 575edfc34d..1598aec41c 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Help.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Help.qml @@ -14,8 +14,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.7 import QtQuick.Controls 2.2 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls // references XXX from root context diff --git a/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml b/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml index 03af964830..ab05f55deb 100644 --- a/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml +++ b/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml @@ -13,8 +13,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls // references XXX from root context diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml index 8451c90836..6ddfe0da1c 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml @@ -13,8 +13,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls // references XXX from root context diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml index c4abd40d2a..86d50e87ec 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml @@ -13,8 +13,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls import "../common" as HifiCommerceCommon diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index e052b78876..179ffcf707 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -13,8 +13,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls // references XXX from root context diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index f44e715703..ed4ba66b2b 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -14,8 +14,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 import QtGraphicalEffects 1.0 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls import "../common" as HifiCommerceCommon import "../common/sendAsset" diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletChoice.qml b/interface/resources/qml/hifi/commerce/wallet/WalletChoice.qml index 19065ee542..e7163a3641 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletChoice.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletChoice.qml @@ -14,8 +14,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 import "../common" as HifiCommerceCommon -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit Item { diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index d32017189e..f18d4c7b44 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -15,8 +15,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 import QtGraphicalEffects 1.0 import QtQuick.Controls 2.2 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml index ecd7234400..1cecebc41b 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -14,8 +14,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 import QtGraphicalEffects 1.0 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls import "../common" as HifiCommerceCommon diff --git a/interface/resources/qml/hifi/dialogs/AboutDialog.qml b/interface/resources/qml/hifi/dialogs/AboutDialog.qml index b8e6e89aec..3d5d1a94a3 100644 --- a/interface/resources/qml/hifi/dialogs/AboutDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AboutDialog.qml @@ -10,7 +10,7 @@ import QtQuick 2.8 -import "../../styles-uit" +import stylesUit 1.0 import "../../windows" ScrollingWindow { diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index 9a180a66f6..be17e65ab3 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -13,8 +13,8 @@ import QtQuick.Controls 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 -import "../../styles-uit" -import "../../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../../windows" import "../" diff --git a/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml b/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml index 579aa1cb1e..d26bf81e57 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import "../../styles-uit" +import stylesUit 1.0 Rectangle { width: 480 diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 0eeb252049..f665032b01 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -14,8 +14,8 @@ import QtQuick.Controls.Styles 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 -import "../../styles-uit" -import "../../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../../windows" import ".." diff --git a/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml b/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml index afe06897df..763f56b92b 100644 --- a/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml +++ b/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml @@ -11,8 +11,8 @@ import QtQuick 2.5 import Qt.labs.settings 1.0 -import "../../styles-uit" -import "../../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../../windows" Rectangle { diff --git a/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml b/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml index 50df4dedbc..213dca8b48 100644 --- a/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml +++ b/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml @@ -12,8 +12,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 import Hifi 1.0 as Hifi -import "../../styles-uit" -import "../../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls Rectangle { id: root diff --git a/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml b/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml index 24798af21a..4cfc99e0eb 100644 --- a/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml +++ b/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml @@ -11,8 +11,8 @@ import QtQuick 2.5 import Qt.labs.settings 1.0 -import "../../styles-uit" -import "../../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../../windows" Rectangle { diff --git a/interface/resources/qml/hifi/dialogs/TabletEntityStatisticsItem.qml b/interface/resources/qml/hifi/dialogs/TabletEntityStatisticsItem.qml index d5c5a5ee02..e86dfd7554 100644 --- a/interface/resources/qml/hifi/dialogs/TabletEntityStatisticsItem.qml +++ b/interface/resources/qml/hifi/dialogs/TabletEntityStatisticsItem.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 -import "../../styles-uit" -import "../../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls Column { id: root diff --git a/interface/resources/qml/hifi/dialogs/TabletLODTools.qml b/interface/resources/qml/hifi/dialogs/TabletLODTools.qml index ab53f03477..bb3d668850 100644 --- a/interface/resources/qml/hifi/dialogs/TabletLODTools.qml +++ b/interface/resources/qml/hifi/dialogs/TabletLODTools.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 -import "../../styles-uit" -import "../../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../../windows" Rectangle { diff --git a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml index 018c8f5737..6cd220307d 100644 --- a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml @@ -13,8 +13,8 @@ import QtQuick.Controls 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 -import "../../styles-uit" -import "../../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../../windows" import "../" diff --git a/interface/resources/qml/hifi/dialogs/content/ModelBrowserContent.qml b/interface/resources/qml/hifi/dialogs/content/ModelBrowserContent.qml index ce1abc6154..b1aa8e5c45 100644 --- a/interface/resources/qml/hifi/dialogs/content/ModelBrowserContent.qml +++ b/interface/resources/qml/hifi/dialogs/content/ModelBrowserContent.qml @@ -1,7 +1,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 -import "../../../controls-uit" as HifiControls +import controlsUit 1.0 as HifiControls Column { width: pane.contentWidth diff --git a/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml b/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml index 96d75e340b..82d094144d 100644 --- a/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml +++ b/interface/resources/qml/hifi/dialogs/security/SecurityImageChange.qml @@ -13,8 +13,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "qrc:////qml//styles-uit" as HifiStylesUit -import "qrc:////qml//controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "qrc:////qml//controls" as HifiControls // references XXX from root context diff --git a/interface/resources/qml/hifi/dialogs/security/SecurityImageSelection.qml b/interface/resources/qml/hifi/dialogs/security/SecurityImageSelection.qml index f058aad40a..366372622c 100644 --- a/interface/resources/qml/hifi/dialogs/security/SecurityImageSelection.qml +++ b/interface/resources/qml/hifi/dialogs/security/SecurityImageSelection.qml @@ -13,8 +13,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "qrc:////qml//styles-uit" as HifiStylesUit -import "qrc:////qml//controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "qrc:////qml//controls" as HifiControls // references XXX from root context diff --git a/interface/resources/qml/hifi/tablet/CalibratingScreen.qml b/interface/resources/qml/hifi/tablet/CalibratingScreen.qml index e3115a5738..6b2aa331e8 100644 --- a/interface/resources/qml/hifi/tablet/CalibratingScreen.qml +++ b/interface/resources/qml/hifi/tablet/CalibratingScreen.qml @@ -10,9 +10,9 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 -import "../../styles-uit" +import stylesUit 1.0 import "../../controls" -import "../../controls-uit" as HifiControls +import controlsUit 1.0 as HifiControls Rectangle { diff --git a/interface/resources/qml/hifi/tablet/ControllerSettings.qml b/interface/resources/qml/hifi/tablet/ControllerSettings.qml index 6706830537..b8bbd71f33 100644 --- a/interface/resources/qml/hifi/tablet/ControllerSettings.qml +++ b/interface/resources/qml/hifi/tablet/ControllerSettings.qml @@ -11,9 +11,9 @@ import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtGraphicalEffects 1.0 import Qt.labs.settings 1.0 -import "../../styles-uit" +import stylesUit 1.0 import "../../controls" -import "../../controls-uit" as HifiControls +import controlsUit 1.0 as HifiControls import "../../dialogs" import "../../dialogs/preferences" import "tabletWindows" diff --git a/interface/resources/qml/hifi/tablet/EditTabButton.qml b/interface/resources/qml/hifi/tablet/EditTabButton.qml index 13894f4d15..5fc4341eb8 100644 --- a/interface/resources/qml/hifi/tablet/EditTabButton.qml +++ b/interface/resources/qml/hifi/tablet/EditTabButton.qml @@ -11,8 +11,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 -import "../../controls-uit" as HifiControls -import "../../styles-uit" +import controlsUit 1.0 as HifiControls +import stylesUit 1.0 TabButton { id: control diff --git a/interface/resources/qml/hifi/tablet/EditTabView.qml b/interface/resources/qml/hifi/tablet/EditTabView.qml index bf7dd3e66b..d9549feeb0 100644 --- a/interface/resources/qml/hifi/tablet/EditTabView.qml +++ b/interface/resources/qml/hifi/tablet/EditTabView.qml @@ -4,8 +4,8 @@ import QtWebChannel 1.0 import "../../controls" import "../toolbars" import QtGraphicalEffects 1.0 -import "../../controls-uit" as HifiControls -import "../../styles-uit" +import controlsUit 1.0 as HifiControls +import stylesUit 1.0 TabBar { id: editTabView diff --git a/interface/resources/qml/hifi/tablet/InputRecorder.qml b/interface/resources/qml/hifi/tablet/InputRecorder.qml index 527a6cacb4..9b63a612a8 100644 --- a/interface/resources/qml/hifi/tablet/InputRecorder.qml +++ b/interface/resources/qml/hifi/tablet/InputRecorder.qml @@ -9,8 +9,8 @@ import QtQuick 2.5 import Hifi 1.0 -import "../../styles-uit" -import "../../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../../windows" import "../../dialogs" diff --git a/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml b/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml index 526a42f8e2..dde372648b 100644 --- a/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml @@ -13,8 +13,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQuick.Dialogs 1.2 as OriginalDialogs -import "../../styles-uit" -import "../../controls-uit" +import stylesUit 1.0 +import controlsUit 1.0 import "../dialogs" Rectangle { diff --git a/interface/resources/qml/hifi/tablet/NewModelDialog.qml b/interface/resources/qml/hifi/tablet/NewModelDialog.qml index 553a4fd59f..9540979479 100644 --- a/interface/resources/qml/hifi/tablet/NewModelDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewModelDialog.qml @@ -12,8 +12,8 @@ import QtQuick 2.5 import QtQuick.Dialogs 1.2 as OriginalDialogs -import "../../styles-uit" -import "../../controls-uit" +import stylesUit 1.0 +import controlsUit 1.0 import "../dialogs" Rectangle { diff --git a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml index f91642105f..2fc5cc4196 100644 --- a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml +++ b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml @@ -9,9 +9,9 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 -import "../../styles-uit" +import stylesUit 1.0 import "../../controls" -import "../../controls-uit" as HifiControls +import controlsUit 1.0 as HifiControls import "." diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 3d518289fb..0f26ba20aa 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -18,8 +18,8 @@ import "../../styles" import "../../windows" import "../" import "../toolbars" -import "../../styles-uit" as HifiStyles -import "../../controls-uit" as HifiControls +import stylesUit 1.0 as HifiStyles +import controlsUit 1.0 as HifiControls import QtQuick.Controls 2.2 as QQC2 import QtQuick.Templates 2.2 as T diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml index 1922b02f93..934ed91995 100644 --- a/interface/resources/qml/hifi/tablet/TabletHome.qml +++ b/interface/resources/qml/hifi/tablet/TabletHome.qml @@ -6,7 +6,7 @@ import QtQuick.Layouts 1.3 import TabletScriptingInterface 1.0 import "." -import "../../styles-uit" +import stylesUit 1.0 import "../audio" as HifiAudio Item { diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml index 6540d53fca..267fb9f0cf 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenu.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -7,7 +7,7 @@ import QtWebEngine 1.1 import "." -import "../../styles-uit" +import stylesUit 1.0 import "../../controls" FocusScope { diff --git a/interface/resources/qml/hifi/tablet/TabletMenuItem.qml b/interface/resources/qml/hifi/tablet/TabletMenuItem.qml index 74f175e049..25db90c771 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuItem.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuItem.qml @@ -11,8 +11,8 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 -import "../../controls-uit" -import "../../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Item { id: root diff --git a/interface/resources/qml/hifi/tablet/TabletMenuView.qml b/interface/resources/qml/hifi/tablet/TabletMenuView.qml index b632a17e57..73b0405984 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuView.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuView.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import TabletScriptingInterface 1.0 -import "../../styles-uit" +import stylesUit 1.0 import "." FocusScope { diff --git a/interface/resources/qml/hifi/tablet/TabletModelBrowserDialog.qml b/interface/resources/qml/hifi/tablet/TabletModelBrowserDialog.qml index d69d760b95..ce4e641476 100644 --- a/interface/resources/qml/hifi/tablet/TabletModelBrowserDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletModelBrowserDialog.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 -import "../../controls-uit" as HifiControls -import "../../styles-uit" +import controlsUit 1.0 as HifiControls +import stylesUit 1.0 import "../dialogs/content" Item { diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml index 871d1c92a9..8e91655dda 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -16,8 +16,8 @@ import QtQuick.Controls 1.4 as QQC1 import QtQuick.Controls 2.3 import ".." -import "../../../controls-uit" -import "../../../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 import "../../../windows" import "../../../dialogs/fileDialog" diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml index 3708f75114..57ca705352 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -12,8 +12,8 @@ import QtQuick 2.5 import "." import "./preferences" -import "../../../styles-uit" -import "../../../controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls Item { id: dialog diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index 6ac3f706e4..57fdeb482b 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -12,8 +12,8 @@ import QtQuick 2.5 import Hifi 1.0 import "../../../../dialogs/preferences" -import "../../../../controls-uit" as HiFiControls -import "../../../../styles-uit" +import controlsUit 1.0 as HiFiControls +import stylesUit 1.0 import "." Preference { diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml index 8c0e934971..36b927f5f9 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import "../../../../dialogs" -import "../../../../controls-uit" +import controlsUit 1.0 import "../" Preference { diff --git a/interface/resources/qml/styles-uit/+android/HifiConstants.qml b/interface/resources/qml/stylesUit/+android/HifiConstants.qml similarity index 100% rename from interface/resources/qml/styles-uit/+android/HifiConstants.qml rename to interface/resources/qml/stylesUit/+android/HifiConstants.qml diff --git a/interface/resources/qml/styles-uit/AnonymousProRegular.qml b/interface/resources/qml/stylesUit/AnonymousProRegular.qml similarity index 100% rename from interface/resources/qml/styles-uit/AnonymousProRegular.qml rename to interface/resources/qml/stylesUit/AnonymousProRegular.qml diff --git a/interface/resources/qml/styles-uit/ButtonLabel.qml b/interface/resources/qml/stylesUit/ButtonLabel.qml similarity index 100% rename from interface/resources/qml/styles-uit/ButtonLabel.qml rename to interface/resources/qml/stylesUit/ButtonLabel.qml diff --git a/interface/resources/qml/styles-uit/FiraSansRegular.qml b/interface/resources/qml/stylesUit/FiraSansRegular.qml similarity index 100% rename from interface/resources/qml/styles-uit/FiraSansRegular.qml rename to interface/resources/qml/stylesUit/FiraSansRegular.qml diff --git a/interface/resources/qml/styles-uit/FiraSansSemiBold.qml b/interface/resources/qml/stylesUit/FiraSansSemiBold.qml similarity index 100% rename from interface/resources/qml/styles-uit/FiraSansSemiBold.qml rename to interface/resources/qml/stylesUit/FiraSansSemiBold.qml diff --git a/interface/resources/qml/styles-uit/HiFiGlyphs.qml b/interface/resources/qml/stylesUit/HiFiGlyphs.qml similarity index 100% rename from interface/resources/qml/styles-uit/HiFiGlyphs.qml rename to interface/resources/qml/stylesUit/HiFiGlyphs.qml diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/stylesUit/HifiConstants.qml similarity index 100% rename from interface/resources/qml/styles-uit/HifiConstants.qml rename to interface/resources/qml/stylesUit/HifiConstants.qml diff --git a/interface/resources/qml/styles-uit/IconButton.qml b/interface/resources/qml/stylesUit/IconButton.qml similarity index 100% rename from interface/resources/qml/styles-uit/IconButton.qml rename to interface/resources/qml/stylesUit/IconButton.qml diff --git a/interface/resources/qml/styles-uit/InfoItem.qml b/interface/resources/qml/stylesUit/InfoItem.qml similarity index 100% rename from interface/resources/qml/styles-uit/InfoItem.qml rename to interface/resources/qml/stylesUit/InfoItem.qml diff --git a/interface/resources/qml/styles-uit/InputLabel.qml b/interface/resources/qml/stylesUit/InputLabel.qml similarity index 100% rename from interface/resources/qml/styles-uit/InputLabel.qml rename to interface/resources/qml/stylesUit/InputLabel.qml diff --git a/interface/resources/qml/styles-uit/ListItem.qml b/interface/resources/qml/stylesUit/ListItem.qml similarity index 100% rename from interface/resources/qml/styles-uit/ListItem.qml rename to interface/resources/qml/stylesUit/ListItem.qml diff --git a/interface/resources/qml/styles-uit/Logs.qml b/interface/resources/qml/stylesUit/Logs.qml similarity index 100% rename from interface/resources/qml/styles-uit/Logs.qml rename to interface/resources/qml/stylesUit/Logs.qml diff --git a/interface/resources/qml/styles-uit/OverlayTitle.qml b/interface/resources/qml/stylesUit/OverlayTitle.qml similarity index 100% rename from interface/resources/qml/styles-uit/OverlayTitle.qml rename to interface/resources/qml/stylesUit/OverlayTitle.qml diff --git a/interface/resources/qml/styles-uit/RalewayBold.qml b/interface/resources/qml/stylesUit/RalewayBold.qml similarity index 100% rename from interface/resources/qml/styles-uit/RalewayBold.qml rename to interface/resources/qml/stylesUit/RalewayBold.qml diff --git a/interface/resources/qml/styles-uit/RalewayLight.qml b/interface/resources/qml/stylesUit/RalewayLight.qml similarity index 100% rename from interface/resources/qml/styles-uit/RalewayLight.qml rename to interface/resources/qml/stylesUit/RalewayLight.qml diff --git a/interface/resources/qml/styles-uit/RalewayRegular.qml b/interface/resources/qml/stylesUit/RalewayRegular.qml similarity index 100% rename from interface/resources/qml/styles-uit/RalewayRegular.qml rename to interface/resources/qml/stylesUit/RalewayRegular.qml diff --git a/interface/resources/qml/styles-uit/RalewaySemiBold.qml b/interface/resources/qml/stylesUit/RalewaySemiBold.qml similarity index 100% rename from interface/resources/qml/styles-uit/RalewaySemiBold.qml rename to interface/resources/qml/stylesUit/RalewaySemiBold.qml diff --git a/interface/resources/qml/styles-uit/SectionName.qml b/interface/resources/qml/stylesUit/SectionName.qml similarity index 100% rename from interface/resources/qml/styles-uit/SectionName.qml rename to interface/resources/qml/stylesUit/SectionName.qml diff --git a/interface/resources/qml/styles-uit/Separator.qml b/interface/resources/qml/stylesUit/Separator.qml similarity index 97% rename from interface/resources/qml/styles-uit/Separator.qml rename to interface/resources/qml/stylesUit/Separator.qml index 4134b928a7..d9f11e192c 100644 --- a/interface/resources/qml/styles-uit/Separator.qml +++ b/interface/resources/qml/stylesUit/Separator.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import "../styles-uit" +import "." Item { // Size diff --git a/interface/resources/qml/styles-uit/ShortcutText.qml b/interface/resources/qml/stylesUit/ShortcutText.qml similarity index 100% rename from interface/resources/qml/styles-uit/ShortcutText.qml rename to interface/resources/qml/stylesUit/ShortcutText.qml diff --git a/interface/resources/qml/styles-uit/TabName.qml b/interface/resources/qml/stylesUit/TabName.qml similarity index 100% rename from interface/resources/qml/styles-uit/TabName.qml rename to interface/resources/qml/stylesUit/TabName.qml diff --git a/interface/resources/qml/styles-uit/TextFieldInput.qml b/interface/resources/qml/stylesUit/TextFieldInput.qml similarity index 100% rename from interface/resources/qml/styles-uit/TextFieldInput.qml rename to interface/resources/qml/stylesUit/TextFieldInput.qml diff --git a/interface/resources/qml/styles-uit/qmldir b/interface/resources/qml/stylesUit/qmldir similarity index 100% rename from interface/resources/qml/styles-uit/qmldir rename to interface/resources/qml/stylesUit/qmldir diff --git a/interface/resources/qml/windows/Decoration.qml b/interface/resources/qml/windows/Decoration.qml index f8fd9f4e6c..efaea6be8a 100644 --- a/interface/resources/qml/windows/Decoration.qml +++ b/interface/resources/qml/windows/Decoration.qml @@ -12,7 +12,7 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import "." -import "../styles-uit" +import stylesUit 1.0 Rectangle { HifiConstants { id: hifi } diff --git a/interface/resources/qml/windows/DefaultFrame.qml b/interface/resources/qml/windows/DefaultFrame.qml index 60e744bec3..5a366e367b 100644 --- a/interface/resources/qml/windows/DefaultFrame.qml +++ b/interface/resources/qml/windows/DefaultFrame.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import "." -import "../styles-uit" +import stylesUit 1.0 Frame { HifiConstants { id: hifi } diff --git a/interface/resources/qml/windows/DefaultFrameDecoration.qml b/interface/resources/qml/windows/DefaultFrameDecoration.qml index 1ddd83976e..fb0dd55985 100644 --- a/interface/resources/qml/windows/DefaultFrameDecoration.qml +++ b/interface/resources/qml/windows/DefaultFrameDecoration.qml @@ -12,7 +12,7 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import "." -import "../styles-uit" +import stylesUit 1.0 Decoration { HifiConstants { id: hifi } diff --git a/interface/resources/qml/windows/Fadable.qml b/interface/resources/qml/windows/Fadable.qml index 406c6be556..6d88fb067a 100644 --- a/interface/resources/qml/windows/Fadable.qml +++ b/interface/resources/qml/windows/Fadable.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 -import "../styles-uit" +import stylesUit 1.0 // Enable window visibility transitions FocusScope { diff --git a/interface/resources/qml/windows/Frame.qml b/interface/resources/qml/windows/Frame.qml index 271d4f2e07..7b0fbf8d8c 100644 --- a/interface/resources/qml/windows/Frame.qml +++ b/interface/resources/qml/windows/Frame.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 -import "../styles-uit" +import stylesUit 1.0 import "../js/Utils.js" as Utils Item { diff --git a/interface/resources/qml/windows/ModalFrame.qml b/interface/resources/qml/windows/ModalFrame.qml index cb23ccd5ad..ae149224e3 100644 --- a/interface/resources/qml/windows/ModalFrame.qml +++ b/interface/resources/qml/windows/ModalFrame.qml @@ -11,8 +11,8 @@ import QtQuick 2.5 import "." -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Frame { HifiConstants { id: hifi } diff --git a/interface/resources/qml/windows/ScrollingWindow.qml b/interface/resources/qml/windows/ScrollingWindow.qml index c156b80388..4cab96701e 100644 --- a/interface/resources/qml/windows/ScrollingWindow.qml +++ b/interface/resources/qml/windows/ScrollingWindow.qml @@ -14,8 +14,8 @@ import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 import "." -import "../styles-uit" -import "../controls-uit" as HiFiControls +import stylesUit 1.0 +import controlsUit 1.0 as HiFiControls // FIXME how do I set the initial position of a window without // overriding places where the a individual client of the window diff --git a/interface/resources/qml/windows/TabletModalFrame.qml b/interface/resources/qml/windows/TabletModalFrame.qml index 550eec8357..1e9310eb5a 100644 --- a/interface/resources/qml/windows/TabletModalFrame.qml +++ b/interface/resources/qml/windows/TabletModalFrame.qml @@ -11,8 +11,8 @@ import QtQuick 2.5 import "." -import "../controls-uit" -import "../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Rectangle { diff --git a/interface/resources/qml/windows/ToolFrame.qml b/interface/resources/qml/windows/ToolFrame.qml index 20c86afb5e..bb2bada498 100644 --- a/interface/resources/qml/windows/ToolFrame.qml +++ b/interface/resources/qml/windows/ToolFrame.qml @@ -12,7 +12,7 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import "." -import "../styles-uit" +import stylesUit 1.0 Frame { HifiConstants { id: hifi } diff --git a/interface/resources/qml/windows/ToolFrameDecoration.qml b/interface/resources/qml/windows/ToolFrameDecoration.qml index ba36a2a38c..4f149037b3 100644 --- a/interface/resources/qml/windows/ToolFrameDecoration.qml +++ b/interface/resources/qml/windows/ToolFrameDecoration.qml @@ -12,7 +12,7 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import "." -import "../styles-uit" +import stylesUit 1.0 Decoration { id: root diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 835967c628..9f180af55d 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -12,7 +12,7 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import "." -import "../styles-uit" +import stylesUit 1.0 // FIXME how do I set the initial position of a window without // overriding places where the a individual client of the window diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 74098f69c7..f67a356078 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -250,6 +250,7 @@ void OffscreenQmlSurface::initializeEngine(QQmlEngine* engine) { engine->setNetworkAccessManagerFactory(new QmlNetworkAccessManagerFactory); auto importList = engine->importPathList(); + importList.insert(importList.begin(), PathUtils::resourcesPath() + "qml/"); importList.insert(importList.begin(), PathUtils::resourcesPath()); engine->setImportPathList(importList); for (const auto& path : importList) { diff --git a/scripts/developer/tests/ControlsGallery.qml b/scripts/developer/tests/ControlsGallery.qml index ceb8a26dc9..9685fa6fe8 100644 --- a/scripts/developer/tests/ControlsGallery.qml +++ b/scripts/developer/tests/ControlsGallery.qml @@ -2,16 +2,9 @@ import QtQuick 2.10 import QtQuick.Window 2.10 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 -import "qrc:////qml//styles-uit" as HifiStylesUit -import "qrc:////qml//controls-uit" as HifiControlsUit -//uncomment to use from qmlscratch tool -//import '../../../interface/resources/qml/controls-uit' as HifiControlsUit -//import '../../../interface/resources/qml/styles-uit' - -//uncomment to use with HIFI_USE_SOURCE_TREE_RESOURCES=1 -//import '../../../resources/qml/controls-uit' as HifiControlsUit -//import '../../../resources/qml/styles-uit' +import stylesUit 1.0 as HifiStylesUit +import controlsUit 1.0 as HifiControlsUit Item { visible: true From d0ac5128b02316817a52d8da76276757f99532e3 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Sun, 17 Jun 2018 14:53:52 +0300 Subject: [PATCH 339/362] repurpose qmlscratch into 'controls gallery' launcher --- tests-manual/ui/qml/ControlsGalleryWindow.qml | 14 + tests-manual/ui/qml/Palettes.qml | 150 ---- tests-manual/ui/qml/ScrollingGraph.qml | 111 --- tests-manual/ui/qml/StubMenu.qml | 730 ------------------ tests-manual/ui/qml/Stubs.qml | 346 --------- tests-manual/ui/qml/TestControllers.qml | 160 ---- tests-manual/ui/qml/TestDialog.qml | 94 --- tests-manual/ui/qml/TestMenu.qml | 10 - tests-manual/ui/qml/TestRoot.qml | 43 -- .../ui/qml/controlDemo/ButtonPage.qml | 128 --- tests-manual/ui/qml/controlDemo/InputPage.qml | 114 --- .../ui/qml/controlDemo/ProgressPage.qml | 90 --- tests-manual/ui/qml/controlDemo/main.qml | 161 ---- tests-manual/ui/qml/main.qml | 461 ----------- tests-manual/ui/qmlscratch.pro | 28 +- tests-manual/ui/src/main.cpp | 125 +-- 16 files changed, 40 insertions(+), 2725 deletions(-) create mode 100644 tests-manual/ui/qml/ControlsGalleryWindow.qml delete mode 100644 tests-manual/ui/qml/Palettes.qml delete mode 100644 tests-manual/ui/qml/ScrollingGraph.qml delete mode 100644 tests-manual/ui/qml/StubMenu.qml delete mode 100644 tests-manual/ui/qml/Stubs.qml delete mode 100644 tests-manual/ui/qml/TestControllers.qml delete mode 100644 tests-manual/ui/qml/TestDialog.qml delete mode 100644 tests-manual/ui/qml/TestMenu.qml delete mode 100644 tests-manual/ui/qml/TestRoot.qml delete mode 100644 tests-manual/ui/qml/controlDemo/ButtonPage.qml delete mode 100644 tests-manual/ui/qml/controlDemo/InputPage.qml delete mode 100644 tests-manual/ui/qml/controlDemo/ProgressPage.qml delete mode 100644 tests-manual/ui/qml/controlDemo/main.qml delete mode 100644 tests-manual/ui/qml/main.qml diff --git a/tests-manual/ui/qml/ControlsGalleryWindow.qml b/tests-manual/ui/qml/ControlsGalleryWindow.qml new file mode 100644 index 0000000000..32fd62da36 --- /dev/null +++ b/tests-manual/ui/qml/ControlsGalleryWindow.qml @@ -0,0 +1,14 @@ +import QtQuick 2.0 +import QtQuick.Window 2.3 +import QtQuick.Controls 1.4 +import '../../../scripts/developer/tests' as Tests + +ApplicationWindow { + width: 640 + height: 480 + visible: true + + Tests.ControlsGallery { + anchors.fill: parent + } +} diff --git a/tests-manual/ui/qml/Palettes.qml b/tests-manual/ui/qml/Palettes.qml deleted file mode 100644 index 2bdf6eba8b..0000000000 --- a/tests-manual/ui/qml/Palettes.qml +++ /dev/null @@ -1,150 +0,0 @@ -import QtQuick 2.3 -import QtQuick.Controls 1.2 - -Rectangle { - color: "teal" - height: 512 - width: 192 - SystemPalette { id: sp; colorGroup: SystemPalette.Active } - SystemPalette { id: spi; colorGroup: SystemPalette.Inactive } - SystemPalette { id: spd; colorGroup: SystemPalette.Disabled } - - Column { - anchors.margins: 8 - anchors.fill: parent - spacing: 8 - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "base" } - Rectangle { height: parent.height; width: 16; color: sp.base } - Rectangle { height: parent.height; width: 16; color: spi.base } - Rectangle { height: parent.height; width: 16; color: spd.base } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "alternateBase" } - Rectangle { height: parent.height; width: 16; color: sp.alternateBase } - Rectangle { height: parent.height; width: 16; color: spi.alternateBase } - Rectangle { height: parent.height; width: 16; color: spd.alternateBase } - } - Item { - height: 16 - width:parent.width - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "dark" } - Rectangle { height: parent.height; width: 16; color: sp.dark } - Rectangle { height: parent.height; width: 16; color: spi.dark } - Rectangle { height: parent.height; width: 16; color: spd.dark } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "mid" } - Rectangle { height: parent.height; width: 16; color: sp.mid } - Rectangle { height: parent.height; width: 16; color: spi.mid } - Rectangle { height: parent.height; width: 16; color: spd.mid } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "mid light" } - Rectangle { height: parent.height; width: 16; color: sp.midlight } - Rectangle { height: parent.height; width: 16; color: spi.midlight } - Rectangle { height: parent.height; width: 16; color: spd.midlight } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "light" } - Rectangle { height: parent.height; width: 16; color: sp.light} - Rectangle { height: parent.height; width: 16; color: spi.light} - Rectangle { height: parent.height; width: 16; color: spd.light} - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "shadow" } - Rectangle { height: parent.height; width: 16; color: sp.shadow} - Rectangle { height: parent.height; width: 16; color: spi.shadow} - Rectangle { height: parent.height; width: 16; color: spd.shadow} - } - Item { - height: 16 - width:parent.width - } - - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "text" } - Rectangle { height: parent.height; width: 16; color: sp.text } - Rectangle { height: parent.height; width: 16; color: spi.text } - Rectangle { height: parent.height; width: 16; color: spd.text } - } - Item { - height: 16 - width:parent.width - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "window" } - Rectangle { height: parent.height; width: 16; color: sp.window } - Rectangle { height: parent.height; width: 16; color: spi.window } - Rectangle { height: parent.height; width: 16; color: spd.window } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "window text" } - Rectangle { height: parent.height; width: 16; color: sp.windowText } - Rectangle { height: parent.height; width: 16; color: spi.windowText } - Rectangle { height: parent.height; width: 16; color: spd.windowText } - } - Item { - height: 16 - width:parent.width - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "button" } - Rectangle { height: parent.height; width: 16; color: sp.button } - Rectangle { height: parent.height; width: 16; color: spi.button } - Rectangle { height: parent.height; width: 16; color: spd.button } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "buttonText" } - Rectangle { height: parent.height; width: 16; color: sp.buttonText } - Rectangle { height: parent.height; width: 16; color: spi.buttonText } - Rectangle { height: parent.height; width: 16; color: spd.buttonText } - } - Item { - height: 16 - width:parent.width - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "highlight" } - Rectangle { height: parent.height; width: 16; color: sp.highlight } - Rectangle { height: parent.height; width: 16; color: spi.highlight } - Rectangle { height: parent.height; width: 16; color: spd.highlight } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "highlighted text" } - Rectangle { height: parent.height; width: 16; color: sp.highlightedText} - Rectangle { height: parent.height; width: 16; color: spi.highlightedText} - Rectangle { height: parent.height; width: 16; color: spd.highlightedText} - } - } -} diff --git a/tests-manual/ui/qml/ScrollingGraph.qml b/tests-manual/ui/qml/ScrollingGraph.qml deleted file mode 100644 index 55523a23f4..0000000000 --- a/tests-manual/ui/qml/ScrollingGraph.qml +++ /dev/null @@ -1,111 +0,0 @@ -import QtQuick 2.1 -import QtQuick.Controls 1.0 -import QtQuick.Layouts 1.0 -import QtQuick.Dialogs 1.0 - -Rectangle { - id: root - property int size: 64 - width: size - height: size - color: 'black' - property int controlId: 0 - property real value: 0.5 - property int scrollWidth: 1 - property real min: 0.0 - property real max: 1.0 - property bool log: false - property real range: max - min - property color lineColor: 'yellow' - property bool bar: false - property real lastHeight: -1 - property string label: "" - - function update() { - value = Controller.getValue(controlId); - if (log) { - var log = Math.log(10) / Math.log(Math.abs(value)); - var sign = Math.sign(value); - value = log * sign; - } - canvas.requestPaint(); - } - - function drawHeight() { - if (value < min) { - return 0; - } - if (value > max) { - return height; - } - return ((value - min) / range) * height; - } - - Timer { - interval: 50; running: true; repeat: true - onTriggered: root.update() - } - - Canvas { - id: canvas - anchors.fill: parent - antialiasing: false - - Text { - anchors.top: parent.top - text: root.label - color: 'white' - } - - Text { - anchors.right: parent.right - anchors.top: parent.top - text: root.max - color: 'white' - } - - Text { - anchors.right: parent.right - anchors.bottom: parent.bottom - text: root.min - color: 'white' - } - - function scroll() { - var ctx = canvas.getContext('2d'); - var image = ctx.getImageData(0, 0, canvas.width, canvas.height); - ctx.beginPath(); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(image, -root.scrollWidth, 0, canvas.width, canvas.height) - ctx.restore() - } - - onPaint: { - scroll(); - var ctx = canvas.getContext('2d'); - ctx.save(); - var currentHeight = root.drawHeight(); - if (root.lastHeight == -1) { - root.lastHeight = currentHeight - } - -// var x = canvas.width - root.drawWidth; -// var y = canvas.height - drawHeight; -// ctx.fillStyle = root.color -// ctx.fillRect(x, y, root.drawWidth, root.bar ? drawHeight : 1) -// ctx.fill(); -// ctx.restore() - - - ctx.beginPath(); - ctx.lineWidth = 1 - ctx.strokeStyle = root.lineColor - ctx.moveTo(canvas.width - root.scrollWidth, root.lastHeight).lineTo(canvas.width, currentHeight) - ctx.stroke() - ctx.restore() - root.lastHeight = currentHeight - } - } -} - - diff --git a/tests-manual/ui/qml/StubMenu.qml b/tests-manual/ui/qml/StubMenu.qml deleted file mode 100644 index fd0298988a..0000000000 --- a/tests-manual/ui/qml/StubMenu.qml +++ /dev/null @@ -1,730 +0,0 @@ -import QtQuick 2.5 -import QtQuick.Controls 1.4 - -import "../../../interface/resources/qml/hifi" - -Menu { - property var menuOption: MenuOption {} - Item { - Action { - id: login; - text: menuOption.login - } - Action { - id: update; - text: "Update"; - enabled: false - } - Action { - id: crashReporter; - text: "Crash Reporter..."; - enabled: false - } - Action { - id: help; - text: menuOption.help - onTriggered: Application.showHelp() - } - Action { - id: aboutApp; - text: menuOption.aboutApp - } - Action { - id: quit; - text: menuOption.quit - } - - ExclusiveGroup { id: renderResolutionGroup } - Action { - id: renderResolutionOne; - exclusiveGroup: renderResolutionGroup; - text: menuOption.renderResolutionOne; - checkable: true; - checked: true - } - Action { - id: renderResolutionTwoThird; - exclusiveGroup: renderResolutionGroup; - text: menuOption.renderResolutionTwoThird; - checkable: true - } - Action { - id: renderResolutionHalf; - exclusiveGroup: renderResolutionGroup; - text: menuOption.renderResolutionHalf; - checkable: true - } - Action { - id: renderResolutionThird; - exclusiveGroup: renderResolutionGroup; - text: menuOption.renderResolutionThird; - checkable: true - } - Action { - id: renderResolutionQuarter; - exclusiveGroup: renderResolutionGroup; - text: menuOption.renderResolutionQuarter; - checkable: true - } - - ExclusiveGroup { id: ambientLightGroup } - Action { - id: renderAmbientLightGlobal; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLightGlobal; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight0; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight0; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight1; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight1; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight2; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight2; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight3; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight3; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight4; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight4; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight5; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight5; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight6; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight6; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight7; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight7; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight8; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight8; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight9; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight9; - checkable: true; - checked: true - } - Action { - id: preferences - shortcut: StandardKey.Preferences - text: menuOption.preferences - onTriggered: dialogsManager.editPreferences() - } - - } - - Menu { - title: "File" - MenuItem { - action: login - } - MenuItem { - action: update - } - MenuItem { - action: help - } - MenuItem { - action: crashReporter - } - MenuItem { - action: aboutApp - } - MenuItem { - action: quit - } - } - - Menu { - title: "Edit" - MenuItem { - text: "Undo" } - MenuItem { - text: "Redo" } - MenuItem { - text: menuOption.runningScripts - } - MenuItem { - text: menuOption.loadScript - } - MenuItem { - text: menuOption.loadScriptURL - } - MenuItem { - text: menuOption.stopAllScripts - } - MenuItem { - text: menuOption.reloadAllScripts - } - MenuItem { - text: menuOption.scriptEditor - } - MenuItem { - text: menuOption.console_ - } - MenuItem { - text: menuOption.reloadContent - } - MenuItem { - text: menuOption.packageModel - } - } - - Menu { - title: "Audio" - MenuItem { - text: menuOption.muteAudio; - checkable: true - } - MenuItem { - text: menuOption.audioTools; - checkable: true - } - } - Menu { - title: "Avatar" - // Avatar > Attachments... - MenuItem { - text: menuOption.attachments - } - Menu { - title: "Size" - // Avatar > Size > Increase - MenuItem { - text: menuOption.increaseAvatarSize - } - // Avatar > Size > Decrease - MenuItem { - text: menuOption.decreaseAvatarSize - } - // Avatar > Size > Reset - MenuItem { - text: menuOption.resetAvatarSize - } - } - // Avatar > Reset Sensors - MenuItem { - text: menuOption.resetSensors - } - } - Menu { - title: "Display" - } - Menu { - title: "View" - ExclusiveGroup { - id: cameraModeGroup - } - - MenuItem { - text: menuOption.firstPerson; - checkable: true; - exclusiveGroup: cameraModeGroup - } - MenuItem { - text: menuOption.thirdPerson; - checkable: true; - exclusiveGroup: cameraModeGroup - } - MenuItem { - text: menuOption.fullscreenMirror; - checkable: true; - exclusiveGroup: cameraModeGroup - } - MenuItem { - text: menuOption.independentMode; - checkable: true; - exclusiveGroup: cameraModeGroup - } - MenuItem { - text: menuOption.cameraEntityMode; - checkable: true; - exclusiveGroup: cameraModeGroup - } - MenuSeparator{} - MenuItem { - text: menuOption.miniMirror; - checkable: true - } - } - Menu { - title: "Navigate" - MenuItem { - text: "Home" } - MenuItem { - text: menuOption.addressBar - } - MenuItem { - text: "Directory" } - MenuItem { - text: menuOption.copyAddress - } - MenuItem { - text: menuOption.copyPath - } - } - Menu { - title: "Settings" - MenuItem { - text: "Advanced Menus" } - MenuItem { - text: "Developer Menus" } - MenuItem { - text: menuOption.preferences - } - MenuItem { - text: "Avatar..." } - MenuItem { - text: "Audio..." } - MenuItem { - text: "LOD..." } - MenuItem { - text: menuOption.inputMenu - } - } - Menu { - title: "Developer" - Menu { - title: "Render" - MenuItem { - text: menuOption.atmosphere; - checkable: true - } - MenuItem { - text: menuOption.worldAxes; - checkable: true - } - MenuItem { - text: menuOption.debugAmbientOcclusion; - checkable: true - } - MenuItem { - text: menuOption.antialiasing; - checkable: true - } - MenuItem { - text: menuOption.stars; - checkable: true - } - Menu { - title: menuOption.renderAmbientLight - MenuItem { - action: renderAmbientLightGlobal; } - MenuItem { - action: renderAmbientLight0; } - MenuItem { - action: renderAmbientLight1; } - MenuItem { - action: renderAmbientLight2; } - MenuItem { - action: renderAmbientLight3; } - MenuItem { - action: renderAmbientLight4; } - MenuItem { - action: renderAmbientLight5; } - MenuItem { - action: renderAmbientLight6; } - MenuItem { - action: renderAmbientLight7; } - MenuItem { - action: renderAmbientLight8; } - MenuItem { - action: renderAmbientLight9; } - } - MenuItem { - text: menuOption.throttleFPSIfNotFocus; - checkable: true - } - Menu { - title: menuOption.renderResolution - MenuItem { - action: renderResolutionOne - } - MenuItem { - action: renderResolutionTwoThird - } - MenuItem { - action: renderResolutionHalf - } - MenuItem { - action: renderResolutionThird - } - MenuItem { - action: renderResolutionQuarter - } - } - MenuItem { - text: menuOption.lodTools - } - } - Menu { - title: "Assets" - MenuItem { - text: menuOption.uploadAsset - } - MenuItem { - text: menuOption.assetMigration - } - } - Menu { - title: "Avatar" - Menu { - title: "Face Tracking" - MenuItem { - text: menuOption.noFaceTracking; - checkable: true - } - MenuItem { - text: menuOption.faceshift; - checkable: true - } - MenuItem { - text: menuOption.useCamera; - checkable: true - } - MenuSeparator{} - MenuItem { - text: menuOption.binaryEyelidControl; - checkable: true - } - MenuItem { - text: menuOption.coupleEyelids; - checkable: true - } - MenuItem { - text: menuOption.useAudioForMouth; - checkable: true - } - MenuItem { - text: menuOption.velocityFilter; - checkable: true - } - MenuItem { - text: menuOption.calibrateCamera - } - MenuSeparator{} - MenuItem { - text: menuOption.muteFaceTracking; - checkable: true - } - MenuItem { - text: menuOption.autoMuteAudio; - checkable: true - } - } - Menu { - title: "Eye Tracking" - MenuItem { - text: menuOption.sMIEyeTracking; - checkable: true - } - Menu { - title: "Calibrate" - MenuItem { - text: menuOption.onePointCalibration - } - MenuItem { - text: menuOption.threePointCalibration - } - MenuItem { - text: menuOption.fivePointCalibration - } - } - MenuItem { - text: menuOption.simulateEyeTracking; - checkable: true - } - } - MenuItem { - text: menuOption.avatarReceiveStats; - checkable: true - } - MenuItem { - text: menuOption.renderBoundingCollisionShapes; - checkable: true - } - MenuItem { - text: menuOption.renderLookAtVectors; - checkable: true - } - MenuItem { - text: menuOption.renderLookAtTargets; - checkable: true - } - MenuItem { - text: menuOption.renderFocusIndicator; - checkable: true - } - MenuItem { - text: menuOption.showWhosLookingAtMe; - checkable: true - } - MenuItem { - text: menuOption.fixGaze; - checkable: true - } - MenuItem { - text: menuOption.animDebugDrawDefaultPose; - checkable: true - } - MenuItem { - text: menuOption.animDebugDrawAnimPose; - checkable: true - } - MenuItem { - text: menuOption.animDebugDrawPosition; - checkable: true - } - MenuItem { - text: menuOption.meshVisible; - checkable: true - } - MenuItem { - text: menuOption.disableEyelidAdjustment; - checkable: true - } - MenuItem { - text: menuOption.turnWithHead; - checkable: true - } - MenuItem { - text: menuOption.keyboardMotorControl; - checkable: true - } - MenuItem { - text: menuOption.scriptedMotorControl; - checkable: true - } - MenuItem { - text: menuOption.enableCharacterController; - checkable: true - } - } - Menu { - title: "Hands" - MenuItem { - text: menuOption.displayHandTargets; - checkable: true - } - MenuItem { - text: menuOption.lowVelocityFilter; - checkable: true - } - Menu { - title: "Leap Motion" - MenuItem { - text: menuOption.leapMotionOnHMD; - checkable: true - } - } - } - Menu { - title: "Entities" - MenuItem { - text: menuOption.octreeStats - } - MenuItem { - text: menuOption.showRealtimeEntityStats; - checkable: true - } - } - Menu { - title: "Network" - MenuItem { - text: menuOption.reloadContent - } - MenuItem { - text: menuOption.disableNackPackets; - checkable: true - } - MenuItem { - text: menuOption.disableActivityLogger; - checkable: true - } - MenuItem { - text: menuOption.cachesSize - } - MenuItem { - text: menuOption.diskCacheEditor - } - MenuItem { - text: menuOption.showDSConnectTable - } - MenuItem { - text: menuOption.bandwidthDetails - } - } - Menu { - title: "Timing" - Menu { - title: "Performance Timer" - MenuItem { - text: menuOption.displayDebugTimingDetails; - checkable: true - } - MenuItem { - text: menuOption.onlyDisplayTopTen; - checkable: true - } - MenuItem { - text: menuOption.expandUpdateTiming; - checkable: true - } - MenuItem { - text: menuOption.expandMyAvatarTiming; - checkable: true - } - MenuItem { - text: menuOption.expandMyAvatarSimulateTiming; - checkable: true - } - MenuItem { - text: menuOption.expandOtherAvatarTiming; - checkable: true - } - MenuItem { - text: menuOption.expandPaintGLTiming; - checkable: true - } - } - MenuItem { - text: menuOption.frameTimer; - checkable: true - } - MenuItem { - text: menuOption.runTimingTests - } - MenuItem { - text: menuOption.pipelineWarnings; - checkable: true - } - MenuItem { - text: menuOption.logExtraTimings; - checkable: true - } - MenuItem { - text: menuOption.suppressShortTimings; - checkable: true - } - } - Menu { - title: "Audio" - MenuItem { - text: menuOption.audioNoiseReduction; - checkable: true - } - MenuItem { - text: menuOption.echoServerAudio; - checkable: true - } - MenuItem { - text: menuOption.echoLocalAudio; - checkable: true - } - MenuItem { - text: menuOption.muteEnvironment - } - Menu { - title: "Audio" - MenuItem { - text: menuOption.audioScope; - checkable: true - } - MenuItem { - text: menuOption.audioScopePause; - checkable: true - } - Menu { - title: "Display Frames" - ExclusiveGroup { - id: audioScopeFramesGroup - } - MenuItem { - exclusiveGroup: audioScopeFramesGroup; - text: menuOption.audioScopeFiveFrames; - checkable: true - } - MenuItem { - exclusiveGroup: audioScopeFramesGroup; - text: menuOption.audioScopeTwentyFrames; - checkable: true - } - MenuItem { - exclusiveGroup: audioScopeFramesGroup; - text: menuOption.audioScopeFiftyFrames; - checkable: true - } - } - MenuItem { - text: menuOption.audioNetworkStats - } - } - } - Menu { - title: "Physics" - MenuItem { - text: menuOption.physicsShowOwned; - checkable: true - } - MenuItem { - text: menuOption.physicsShowHulls; - checkable: true - } - } - MenuItem { - text: menuOption.displayCrashOptions; - checkable: true - } - MenuItem { - text: menuOption.crashInterface - } - MenuItem { - text: menuOption.log - } - MenuItem { - text: menuOption.stats; - checkable: true - } - } -} diff --git a/tests-manual/ui/qml/Stubs.qml b/tests-manual/ui/qml/Stubs.qml deleted file mode 100644 index 8c1465d54c..0000000000 --- a/tests-manual/ui/qml/Stubs.qml +++ /dev/null @@ -1,346 +0,0 @@ -import QtQuick 2.5 -import QtQuick.Controls 1.4 - -// Stubs for the global service objects set by Interface.cpp when creating the UI -// This is useful for testing inside Qt creator where these services don't actually exist. -Item { - - Item { - objectName: "offscreenFlags" - property bool navigationFocused: false - } - - Item { - objectName: "urlHandler" - function fixupUrl(url) { return url; } - function canHandleUrl(url) { return false; } - function handleUrl(url) { return true; } - } - - Item { - objectName: "Account" - function isLoggedIn() { return true; } - function getUsername() { return "Jherico"; } - } - - Item { - objectName: "GL" - property string vendor: "" - } - - Item { - objectName: "ApplicationCompositor" - property bool reticleOverDesktop: true - } - - Item { - objectName: "Controller" - function getRecommendedOverlayRect() { - return Qt.rect(0, 0, 1920, 1080); - } - } - - Item { - objectName: "Preferences" - // List of categories obtained by logging categories as they are added in Interface in Preferences::addPreference(). - property var categories: [ - "Avatar Basics", "Snapshots", "Scripts", "Privacy", "Level of Detail Tuning", "Avatar Tuning", "Avatar Camera", - "Audio", "Octree", "HMD", "Sixense Controllers", "Graphics" - ] - } - - Item { - objectName: "ScriptDiscoveryService" - //property var scriptsModelFilter: scriptsModel - signal scriptCountChanged() - property var _runningScripts:[ - { name: "wireFrameTest.js", url: "foo/wireframetest.js", path: "foo/wireframetest.js", local: true }, - { name: "edit.js", url: "foo/edit.js", path: "foo/edit.js", local: false }, - { name: "listAllScripts.js", url: "foo/listAllScripts.js", path: "foo/listAllScripts.js", local: false }, - { name: "users.js", url: "foo/users.js", path: "foo/users.js", local: false }, - ] - - function getRunning() { - return _runningScripts; - } - } - - Item { - objectName: "HMD" - property bool active: false - } - - Item { - id: menuHelper - objectName: "MenuHelper" - - Component { - id: modelMaker - ListModel { } - } - - function toModel(menu, parent) { - if (!parent) { parent = menuHelper } - var result = modelMaker.createObject(parent); - if (menu.type !== MenuItemType.Menu) { - console.warn("Not a menu: " + menu); - return result; - } - - var items = menu.items; - for (var i = 0; i < items.length; ++i) { - var item = items[i]; - switch (item.type) { - case 2: - result.append({"name": item.title, "item": item}) - break; - case 1: - result.append({"name": item.text, "item": item}) - break; - case 0: - result.append({"name": "", "item": item}) - break; - } - } - return result; - } - - } - - Item { - objectName: "Desktop" - - property string _OFFSCREEN_ROOT_OBJECT_NAME: "desktopRoot"; - property string _OFFSCREEN_DIALOG_OBJECT_NAME: "topLevelWindow"; - - - function findChild(item, name) { - for (var i = 0; i < item.children.length; ++i) { - if (item.children[i].objectName === name) { - return item.children[i]; - } - } - return null; - } - - function findParent(item, name) { - while (item) { - if (item.objectName === name) { - return item; - } - item = item.parent; - } - return null; - } - - function findDialog(item) { - item = findParent(item, _OFFSCREEN_DIALOG_OBJECT_NAME); - return item; - } - - function closeDialog(item) { - item = findDialog(item); - if (item) { - item.visible = false - } else { - console.warn("Could not find top level dialog") - } - } - - function getDesktop(item) { - while (item) { - if (item.desktopRoot) { - break; - } - item = item.parent; - } - return item - } - - function raise(item) { - var desktop = getDesktop(item); - if (desktop) { - desktop.raise(item); - } - } - } - - Menu { - id: root - objectName: "rootMenu" - - Menu { - title: "Audio" - } - - Menu { - title: "Avatar" - } - - Menu { - title: "Display" - ExclusiveGroup { id: displayMode } - Menu { - title: "More Stuff" - - Menu { title: "Empty" } - - MenuItem { - text: "Do Nothing" - onTriggered: console.log("Nothing") - } - } - MenuItem { - text: "Oculus" - exclusiveGroup: displayMode - checkable: true - } - MenuItem { - text: "OpenVR" - exclusiveGroup: displayMode - checkable: true - } - MenuItem { - text: "OSVR" - exclusiveGroup: displayMode - checkable: true - } - MenuItem { - text: "2D Screen" - exclusiveGroup: displayMode - checkable: true - checked: true - } - MenuItem { - text: "3D Screen (Active)" - exclusiveGroup: displayMode - checkable: true - } - MenuItem { - text: "3D Screen (Passive)" - exclusiveGroup: displayMode - checkable: true - } - } - - Menu { - title: "View" - Menu { - title: "Camera Mode" - ExclusiveGroup { id: cameraMode } - MenuItem { - exclusiveGroup: cameraMode - text: "First Person"; - onTriggered: console.log(text + " checked " + checked) - checkable: true - checked: true - } - MenuItem { - exclusiveGroup: cameraMode - text: "Third Person"; - onTriggered: console.log(text) - checkable: true - } - MenuItem { - exclusiveGroup: cameraMode - text: "Independent Mode"; - onTriggered: console.log(text) - checkable: true - } - MenuItem { - exclusiveGroup: cameraMode - text: "Entity Mode"; - onTriggered: console.log(text) - enabled: false - checkable: true - } - MenuItem { - exclusiveGroup: cameraMode - text: "Fullscreen Mirror"; - onTriggered: console.log(text) - checkable: true - } - } - } - - Menu { - title: "Edit" - - MenuItem { - text: "Undo" - shortcut: "Ctrl+Z" - enabled: false - onTriggered: console.log(text) - } - - MenuItem { - text: "Redo" - shortcut: "Ctrl+Shift+Z" - enabled: false - onTriggered: console.log(text) - } - - MenuSeparator { } - - MenuItem { - text: "Cut" - shortcut: "Ctrl+X" - onTriggered: console.log(text) - } - - MenuItem { - text: "Copy" - shortcut: "Ctrl+C" - onTriggered: console.log(text) - } - - MenuItem { - text: "Paste" - shortcut: "Ctrl+V" - visible: false - onTriggered: console.log("Paste") - } - } - - Menu { - title: "Navigate" - } - - Menu { - title: "Market" - } - - Menu { - title: "Settings" - } - - Menu { - title: "Developer" - } - - Menu { - title: "Quit" - } - - Menu { - title: "File" - - Action { - id: login - text: "Login" - } - - Action { - id: quit - text: "Quit" - shortcut: "Ctrl+Q" - onTriggered: Qt.quit(); - } - - MenuItem { action: quit } - MenuItem { action: login } - } - } - -} - diff --git a/tests-manual/ui/qml/TestControllers.qml b/tests-manual/ui/qml/TestControllers.qml deleted file mode 100644 index e9a7fb49e5..0000000000 --- a/tests-manual/ui/qml/TestControllers.qml +++ /dev/null @@ -1,160 +0,0 @@ -import QtQuick 2.3 -import QtQuick.Controls 1.2 -import QtQuick.Layouts 1.0 -import QtQuick.Dialogs 1.0 - -import "controller" -import "controls" as HifiControls -import "styles" - -HifiControls.VrDialog { - id: root - HifiConstants { id: hifi } - title: "Controller Test" - resizable: true - contentImplicitWidth: clientArea.implicitWidth - contentImplicitHeight: clientArea.implicitHeight - backgroundColor: "beige" - - property var actions: Controller.Actions - property var standard: Controller.Standard - property var hydra: null - property var testMapping: null - property bool testMappingEnabled: false - property var xbox: null - - function buildMapping() { - testMapping = Controller.newMapping(); - testMapping.fromQml(standard.RY).invert().toQml(actions.Pitch); - testMapping.fromQml(function(){ - return Math.sin(Date.now() / 250); - }).toQml(actions.Yaw); - //testMapping.makeAxis(standard.LB, standard.RB).to(actions.Yaw); - // Step yaw takes a number of degrees - testMapping.fromQml(standard.LB).pulse(0.10).invert().scale(40.0).toQml(actions.StepYaw); - testMapping.fromQml(standard.RB).pulse(0.10).scale(15.0).toQml(actions.StepYaw); - testMapping.fromQml(standard.RX).scale(15.0).toQml(actions.StepYaw); - } - - function toggleMapping() { - testMapping.enable(!testMappingEnabled); - testMappingEnabled = !testMappingEnabled; - } - - Component.onCompleted: { - var xboxRegex = /^GamePad/; - var hydraRegex = /^Hydra/; - for (var prop in Controller.Hardware) { - if(xboxRegex.test(prop)) { - root.xbox = Controller.Hardware[prop] - print("found xbox") - continue - } - if (hydraRegex.test(prop)) { - root.hydra = Controller.Hardware[prop] - print("found hydra") - continue - } - } - } - - Column { - id: clientArea - spacing: 12 - x: root.clientX - y: root.clientY - - Row { - spacing: 8 - - Button { - text: !root.testMapping ? "Build Mapping" : (root.testMappingEnabled ? "Disable Mapping" : "Enable Mapping") - onClicked: { - - if (!root.testMapping) { - root.buildMapping() - } else { - root.toggleMapping(); - } - } - } - } - - Row { - Standard { device: root.standard; label: "Standard"; width: 180 } - } - - Row { - spacing: 8 - Xbox { device: root.xbox; label: "XBox"; width: 180 } - Hydra { device: root.hydra; width: 180 } - } - - Row { - spacing: 4 - ScrollingGraph { - controlId: Controller.Actions.Yaw - label: "Yaw" - min: -2.0 - max: 2.0 - size: 64 - } - - ScrollingGraph { - controlId: Controller.Actions.YawLeft - label: "Yaw Left" - min: -2.0 - max: 2.0 - size: 64 - } - - ScrollingGraph { - controlId: Controller.Actions.YawRight - label: "Yaw Right" - min: -2.0 - max: 2.0 - size: 64 - } - - ScrollingGraph { - controlId: Controller.Actions.StepYaw - label: "StepYaw" - min: -20.0 - max: 20.0 - size: 64 - } - } - - Row { - ScrollingGraph { - controlId: Controller.Actions.TranslateZ - label: "TranslateZ" - min: -2.0 - max: 2.0 - size: 64 - } - - ScrollingGraph { - controlId: Controller.Actions.Forward - label: "Forward" - min: -2.0 - max: 2.0 - size: 64 - } - - ScrollingGraph { - controlId: Controller.Actions.Backward - label: "Backward" - min: -2.0 - max: 2.0 - size: 64 - } - - } - } -} // dialog - - - - - diff --git a/tests-manual/ui/qml/TestDialog.qml b/tests-manual/ui/qml/TestDialog.qml deleted file mode 100644 index e6675b7282..0000000000 --- a/tests-manual/ui/qml/TestDialog.qml +++ /dev/null @@ -1,94 +0,0 @@ -import QtQuick 2.3 -import QtQuick.Controls 1.2 -import QtQuick.Controls.Styles 1.3 -import "controls" - -VrDialog { - title: "Test Dialog" - id: testDialog - objectName: "TestDialog" - width: 512 - height: 512 - animationDuration: 200 - - onEnabledChanged: { - if (enabled) { - edit.forceActiveFocus(); - } - } - - Item { - id: clientArea - // The client area - anchors.fill: parent - anchors.margins: parent.margins - anchors.topMargin: parent.topMargin - - Rectangle { - property int d: 100 - id: square - objectName: "testRect" - width: d - height: d - anchors.centerIn: parent - color: "red" - NumberAnimation on rotation { from: 0; to: 360; duration: 2000; loops: Animation.Infinite; } - } - - - TextEdit { - id: edit - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - clip: true - text: "test edit" - anchors.top: parent.top - anchors.topMargin: 12 - } - - Button { - x: 128 - y: 192 - text: "Test" - anchors.bottom: parent.bottom - anchors.bottomMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - onClicked: { - console.log("Click"); - - if (square.visible) { - square.visible = false - } else { - square.visible = true - } - } - } - - Button { - id: customButton2 - y: 192 - text: "Move" - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.bottom: parent.bottom - anchors.bottomMargin: 12 - onClicked: { - onClicked: testDialog.x == 0 ? testDialog.x = 200 : testDialog.x = 0 - } - } - - Keys.onPressed: { - console.log("Key " + event.key); - switch (event.key) { - case Qt.Key_Q: - if (Qt.ControlModifier == event.modifiers) { - event.accepted = true; - break; - } - } - } - } -} diff --git a/tests-manual/ui/qml/TestMenu.qml b/tests-manual/ui/qml/TestMenu.qml deleted file mode 100644 index fe8a26e234..0000000000 --- a/tests-manual/ui/qml/TestMenu.qml +++ /dev/null @@ -1,10 +0,0 @@ -import QtQuick 2.4 -import QtQuick.Controls 1.3 -import Hifi 1.0 - -// Currently for testing a pure QML replacement menu -Item { - Menu { - objectName: "rootMenu"; - } -} diff --git a/tests-manual/ui/qml/TestRoot.qml b/tests-manual/ui/qml/TestRoot.qml deleted file mode 100644 index bd38c696bf..0000000000 --- a/tests-manual/ui/qml/TestRoot.qml +++ /dev/null @@ -1,43 +0,0 @@ -import Hifi 1.0 -import QtQuick 2.3 -import QtQuick.Controls 1.3 -// Import local folder last so that our own control customizations override -// the built in ones -import "controls" - -Root { - id: root - anchors.fill: parent - onParentChanged: { - forceActiveFocus(); - } - Button { - id: messageBox - anchors.right: createDialog.left - anchors.rightMargin: 24 - anchors.bottom: parent.bottom - anchors.bottomMargin: 24 - text: "Message" - onClicked: { - console.log("Foo") - root.information("a") - console.log("Bar") - } - } - Button { - id: createDialog - anchors.right: parent.right - anchors.rightMargin: 24 - anchors.bottom: parent.bottom - anchors.bottomMargin: 24 - text: "Create" - onClicked: { - root.loadChild("MenuTest.qml"); - } - } - - Keys.onPressed: { - console.log("Key press root") - } -} - diff --git a/tests-manual/ui/qml/controlDemo/ButtonPage.qml b/tests-manual/ui/qml/controlDemo/ButtonPage.qml deleted file mode 100644 index 0ed7e2d6ad..0000000000 --- a/tests-manual/ui/qml/controlDemo/ButtonPage.qml +++ /dev/null @@ -1,128 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of the Qt Quick Controls module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** You may use this file under the terms of the BSD license as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names -** of its contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. -** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.2 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 1.2 - -ScrollView { - id: page - - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - - Item { - id: content - - width: Math.max(page.viewport.width, grid.implicitWidth + 2 * grid.rowSpacing) - height: Math.max(page.viewport.height, grid.implicitHeight + 2 * grid.columnSpacing) - - GridLayout { - id: grid - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: grid.rowSpacing - anchors.rightMargin: grid.rowSpacing - anchors.topMargin: grid.columnSpacing - - columns: page.width < page.height ? 1 : 2 - - GroupBox { - title: "Button" - Layout.fillWidth: true - Layout.columnSpan: grid.columns - RowLayout { - anchors.fill: parent - Button { text: "OK"; isDefault: true } - Button { text: "Cancel" } - Item { Layout.fillWidth: true } - Button { - text: "Attach" - menu: Menu { - MenuItem { text: "Image" } - MenuItem { text: "Document" } - } - } - } - } - - GroupBox { - title: "CheckBox" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - CheckBox { text: "E-mail"; checked: true } - CheckBox { text: "Calendar"; checked: true } - CheckBox { text: "Contacts" } - } - } - - GroupBox { - title: "RadioButton" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - ExclusiveGroup { id: radioGroup } - RadioButton { text: "Portrait"; exclusiveGroup: radioGroup } - RadioButton { text: "Landscape"; exclusiveGroup: radioGroup } - RadioButton { text: "Automatic"; exclusiveGroup: radioGroup; checked: true } - } - } - - GroupBox { - title: "Switch" - Layout.fillWidth: true - Layout.columnSpan: grid.columns - ColumnLayout { - anchors.fill: parent - RowLayout { - Label { text: "Wi-Fi"; Layout.fillWidth: true } - Switch { checked: true } - } - RowLayout { - Label { text: "Bluetooth"; Layout.fillWidth: true } - Switch { checked: false } - } - } - } - } - } -} diff --git a/tests-manual/ui/qml/controlDemo/InputPage.qml b/tests-manual/ui/qml/controlDemo/InputPage.qml deleted file mode 100644 index cb1878d023..0000000000 --- a/tests-manual/ui/qml/controlDemo/InputPage.qml +++ /dev/null @@ -1,114 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of the Qt Quick Controls module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** You may use this file under the terms of the BSD license as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names -** of its contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. -** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.2 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 1.2 - -ScrollView { - id: page - - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - - Item { - id: content - - width: Math.max(page.viewport.width, column.implicitWidth + 2 * column.spacing) - height: Math.max(page.viewport.height, column.implicitHeight + 2 * column.spacing) - - ColumnLayout { - id: column - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: column.spacing - - GroupBox { - title: "TextField" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - TextField { placeholderText: "..."; Layout.fillWidth: true; z: 1 } - TextField { placeholderText: "Password"; echoMode: TextInput.Password; Layout.fillWidth: true } - } - } - - GroupBox { - title: "ComboBox" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - ComboBox { - model: Qt.fontFamilies() - Layout.fillWidth: true - } - ComboBox { - editable: true - model: ListModel { - id: listModel - ListElement { text: "Apple" } - ListElement { text: "Banana" } - ListElement { text: "Coconut" } - ListElement { text: "Orange" } - } - onAccepted: { - if (find(currentText) === -1) { - listModel.append({text: editText}) - currentIndex = find(editText) - } - } - Layout.fillWidth: true - } - } - } - - GroupBox { - title: "SpinBox" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - SpinBox { value: 99; Layout.fillWidth: true; z: 1 } - SpinBox { decimals: 2; Layout.fillWidth: true } - } - } - } - } -} diff --git a/tests-manual/ui/qml/controlDemo/ProgressPage.qml b/tests-manual/ui/qml/controlDemo/ProgressPage.qml deleted file mode 100644 index a1fa596f79..0000000000 --- a/tests-manual/ui/qml/controlDemo/ProgressPage.qml +++ /dev/null @@ -1,90 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of the Qt Quick Controls module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** You may use this file under the terms of the BSD license as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names -** of its contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. -** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.2 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 1.2 - -ScrollView { - id: page - - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - - Item { - id: content - - width: Math.max(page.viewport.width, column.implicitWidth + 2 * column.spacing) - height: Math.max(page.viewport.height, column.implicitHeight + 2 * column.spacing) - - ColumnLayout { - id: column - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: column.spacing - - GroupBox { - title: "ProgressBar" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - ProgressBar { indeterminate: true; Layout.fillWidth: true } - ProgressBar { value: slider.value; Layout.fillWidth: true } - } - } - - GroupBox { - title: "Slider" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - Slider { id: slider; value: 0.5; Layout.fillWidth: true } - } - } - - GroupBox { - title: "BusyIndicator" - Layout.fillWidth: true - BusyIndicator { running: true } - } - } - } -} diff --git a/tests-manual/ui/qml/controlDemo/main.qml b/tests-manual/ui/qml/controlDemo/main.qml deleted file mode 100644 index 168b9fb291..0000000000 --- a/tests-manual/ui/qml/controlDemo/main.qml +++ /dev/null @@ -1,161 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of the Qt Quick Controls module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** You may use this file under the terms of the BSD license as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names -** of its contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. -** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.2 -import QtQuick.Layouts 1.1 -import QtQuick.Dialogs 1.1 -import QtQuick.Controls 1.2 -import "qml/UI.js" as UI -import "qml" -//import "/Users/bdavis/Git/hifi/interface/resources/qml" - -Item { - anchors.fill: parent - visible: true - //title: "Qt Quick Controls Gallery" - - MessageDialog { - id: aboutDialog - icon: StandardIcon.Information - title: "About" - text: "Qt Quick Controls Gallery" - informativeText: "This example demonstrates most of the available Qt Quick Controls." - } - - Action { - id: copyAction - text: "&Copy" - shortcut: StandardKey.Copy - iconName: "edit-copy" - enabled: (!!activeFocusItem && !!activeFocusItem["copy"]) - onTriggered: activeFocusItem.copy() - } - - Action { - id: cutAction - text: "Cu&t" - shortcut: StandardKey.Cut - iconName: "edit-cut" - enabled: (!!activeFocusItem && !!activeFocusItem["cut"]) - onTriggered: activeFocusItem.cut() - } - - Action { - id: pasteAction - text: "&Paste" - shortcut: StandardKey.Paste - iconName: "edit-paste" - enabled: (!!activeFocusItem && !!activeFocusItem["paste"]) - onTriggered: activeFocusItem.paste() - } - -// toolBar: ToolBar { -// RowLayout { -// anchors.fill: parent -// anchors.margins: spacing -// Label { -// text: UI.label -// } -// Item { Layout.fillWidth: true } -// CheckBox { -// id: enabler -// text: "Enabled" -// checked: true -// } -// } -// } - -// menuBar: MenuBar { -// Menu { -// title: "&File" -// MenuItem { -// text: "E&xit" -// shortcut: StandardKey.Quit -// onTriggered: Qt.quit() -// } -// } -// Menu { -// title: "&Edit" -// visible: tabView.currentIndex == 2 -// MenuItem { action: cutAction } -// MenuItem { action: copyAction } -// MenuItem { action: pasteAction } -// } -// Menu { -// title: "&Help" -// MenuItem { -// text: "About..." -// onTriggered: aboutDialog.open() -// } -// } -// } - - TabView { - id: tabView - - anchors.fill: parent - anchors.margins: UI.margin - tabPosition: UI.tabPosition - - Layout.minimumWidth: 360 - Layout.minimumHeight: 360 - Layout.preferredWidth: 480 - Layout.preferredHeight: 640 - - Tab { - title: "Buttons" - ButtonPage { - enabled: enabler.checked - } - } - Tab { - title: "Progress" - ProgressPage { - enabled: enabler.checked - } - } - Tab { - title: "Input" - InputPage { - enabled: enabler.checked - } - } - } -} diff --git a/tests-manual/ui/qml/main.qml b/tests-manual/ui/qml/main.qml deleted file mode 100644 index 607bd624a1..0000000000 --- a/tests-manual/ui/qml/main.qml +++ /dev/null @@ -1,461 +0,0 @@ -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Dialogs 1.2 as OriginalDialogs -import Qt.labs.settings 1.0 - -import "../../../interface/resources/qml" -//import "../../../interface/resources/qml/windows" -import "../../../interface/resources/qml/windows" -import "../../../interface/resources/qml/dialogs" -import "../../../interface/resources/qml/hifi" -import "../../../interface/resources/qml/hifi/dialogs" -import "../../../interface/resources/qml/styles-uit" - -ApplicationWindow { - id: appWindow - objectName: "MainWindow" - visible: true - width: 1280 - height: 800 - title: qsTr("Scratch App") - toolBar: Row { - id: testButtons - anchors { margins: 8; left: parent.left; top: parent.top } - spacing: 8 - property int count: 0 - - property var tabs: []; - property var urls: []; - property var toolbar; - property var lastButton; - - Button { - text: HMD.active ? "Disable HMD" : "Enable HMD" - onClicked: HMD.active = !HMD.active - } - - Button { - text: desktop.hmdHandMouseActive ? "Disable HMD HandMouse" : "Enable HMD HandMouse" - onClicked: desktop.hmdHandMouseActive = !desktop.hmdHandMouseActive - } - - // Window visibility - Button { - text: "toggle desktop" - onClicked: desktop.togglePinned() - } - - Button { - text: "Create Toolbar" - onClicked: testButtons.toolbar = desktop.getToolbar("com.highfidelity.interface.toolbar.system"); - } - - Button { - text: "Toggle Toolbar Direction" - onClicked: testButtons.toolbar.horizontal = !testButtons.toolbar.horizontal - } - - Button { - readonly property var icons: [ - "edit-01.svg", - "model-01.svg", - "cube-01.svg", - "sphere-01.svg", - "light-01.svg", - "text-01.svg", - "web-01.svg", - "zone-01.svg", - "particle-01.svg", - ] - property int iconIndex: 0 - readonly property string toolIconUrl: "../../../../../scripts/system/assets/images/tools/" - text: "Create Button" - onClicked: { - var name = icons[iconIndex]; - var url = toolIconUrl + name; - iconIndex = (iconIndex + 1) % icons.length; - var button = testButtons.lastButton = testButtons.toolbar.addButton({ - imageURL: url, - objectName: name, - subImage: { - y: 50, - }, - alpha: 0.9 - }); - - button.clicked.connect(function(){ - console.log("Clicked on button " + button.imageURL + " alpha " + button.alpha) - }); - } - } - - Button { - text: "Toggle Button Visible" - onClicked: testButtons.lastButton.visible = !testButtons.lastButton.visible - } - - // Error alerts - /* - Button { - // Message without title. - text: "Show Error" - onClicked: { - var messageBox = desktop.messageBox({ - text: "Diagnostic cycle will be complete in 30 seconds", - icon: hifi.icons.critical, - }); - messageBox.selected.connect(function(button) { - console.log("You clicked " + button) - }) - } - } - Button { - // detailedText is not currently used anywhere in Interface but it is easier to leave in and style good enough. - text: "Show Long Error" - onClicked: { - desktop.messageBox({ - informativeText: "Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds ", - text: "Baloney", - icon: hifi.icons.warning, - detailedText: "sakjd;laskj dksa;dl jka;lsd j;lkjas ;dlkaj s;dlakjd ;alkjda; slkjda; lkjda;lksjd ;alksjd; alksjd ;alksjd; alksjd; alksdjas;ldkjas;lkdja ;kj ;lkasjd; lkj as;dlka jsd;lka jsd;laksjd a" - }); - } - } - */ - - // query - /* - // There is no such desktop.queryBox() function; may need to update test to cover QueryDialog.qml? - Button { - text: "Show Query" - onClicked: { - var queryBox = desktop.queryBox({ - text: "Have you stopped beating your wife?", - placeholderText: "Are you sure?", - // icon: hifi.icons.critical, - }); - queryBox.selected.connect(function(result) { - console.log("User responded with " + result); - }); - - queryBox.canceled.connect(function() { - console.log("User cancelled query box "); - }) - } - } - */ - - // Browser - /* - Button { - text: "Open Browser" - onClicked: builder.createObject(desktop); - property var builder: Component { - Browser {} - } - } - */ - - - // file dialog - /* - - Button { - text: "Open Directory" - property var builder: Component { - FileDialog { selectDirectory: true } - } - - onClicked: { - var fileDialog = builder.createObject(desktop); - fileDialog.canceled.connect(function(){ - console.log("Cancelled") - }) - fileDialog.selectedFile.connect(function(file){ - console.log("Selected " + file) - }) - } - } - Button { - text: "Open File" - property var builder: Component { - FileDialog { - title: "Open File" - filter: "All Files (*.*)" - //filter: "HTML files (*.html);;Other(*.png)" - } - } - - onClicked: { - var fileDialog = builder.createObject(desktop); - fileDialog.canceled.connect(function(){ - console.log("Cancelled") - }) - fileDialog.selectedFile.connect(function(file){ - console.log("Selected " + file) - }) - } - } - */ - - // tabs - /* - Button { - text: "Add Tab" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo" }); - desktop.toolWindow.showTabForUrl("Foo", true); - } - } - - Button { - text: "Add Tab 2" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo 2" }); - desktop.toolWindow.showTabForUrl("Foo 2", true); - } - } - - Button { - text: "Add Tab 3" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo 3" }); - desktop.toolWindow.showTabForUrl("Foo 3", true); - } - } - - Button { - text: "Destroy Tab" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.removeTabForUrl("Foo"); - } - } - */ - - // Hifi specific stuff - /* - Button { - // Shows the dialog with preferences sections but not each section's preference items - // because Preferences.preferencesByCategory() method is not stubbed out. - text: "Settings > General..." - property var builder: Component { - GeneralPreferencesDialog { } - } - onClicked: { - var runningScripts = builder.createObject(desktop); - } - } - - Button { - text: "Running Scripts" - property var builder: Component { - RunningScripts { } - } - onClicked: { - var runningScripts = builder.createObject(desktop); - } - } - - Button { - text: "Attachments" - property var builder: Component { - AttachmentsDialog { } - } - onClicked: { - var attachmentsDialog = builder.createObject(desktop); - } - } - Button { - // Replicates message box that pops up after selecting new avatar. Includes title. - text: "Confirm Avatar" - onClicked: { - var messageBox = desktop.messageBox({ - title: "Set Avatar", - text: "Would you like to use 'Albert' for your avatar?", - icon: hifi.icons.question, // Test question icon - //icon: hifi.icons.information, // Test informaton icon - //icon: hifi.icons.warning, // Test warning icon - //icon: hifi.icons.critical, // Test critical icon - //icon: hifi.icons.none, // Test no icon - buttons: OriginalDialogs.StandardButton.Ok + OriginalDialogs.StandardButton.Cancel, - defaultButton: OriginalDialogs.StandardButton.Ok - }); - messageBox.selected.connect(function(button) { - console.log("You clicked " + button) - }) - } - } - */ - // bookmarks - /* - Button { - text: "Bookmark Location" - onClicked: { - desktop.inputDialog({ - title: "Bookmark Location", - icon: hifi.icons.placemark, - label: "Name" - }); - } - } - Button { - text: "Delete Bookmark" - onClicked: { - desktop.inputDialog({ - title: "Delete Bookmark", - icon: hifi.icons.placemark, - label: "Select the bookmark to delete", - items: ["Bookmark A", "Bookmark B", "Bookmark C"] - }); - } - } - Button { - text: "Duplicate Bookmark" - onClicked: { - desktop.messageBox({ - title: "Duplicate Bookmark", - icon: hifi.icons.warning, - text: "The bookmark name you entered alread exists in yoru list.", - informativeText: "Would you like to overwrite it?", - buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, - defaultButton: OriginalDialogs.StandardButton.Yes - }); - } - } - */ - - } - - - HifiConstants { id: hifi } - - Desktop { - id: desktop - anchors.fill: parent - - //rootMenu: StubMenu { id: rootMenu } - //Component.onCompleted: offscreenWindow = appWindow - - /* - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.RightButton - onClicked: desktop.popupMenu(Qt.vector2d(mouseX, mouseY)); - } - */ - - Browser { - url: "http://s3.amazonaws.com/DreamingContent/testUiDelegates.html" - } - - - Window { - id: blue - closable: true - visible: true - resizable: true - destroyOnHidden: false - title: "Blue" - - width: 100; height: 100 - x: 1280 / 2; y: 720 / 2 - Settings { - category: "TestWindow.Blue" - property alias x: blue.x - property alias y: blue.y - property alias width: blue.width - property alias height: blue.height - } - - Rectangle { - anchors.fill: parent - visible: true - color: "blue" - Component.onDestruction: console.log("Blue destroyed") - } - } - - Window { - id: green - closable: true - visible: true - resizable: true - title: "Green" - destroyOnHidden: false - - width: 100; height: 100 - x: 1280 / 2; y: 720 / 2 - Settings { - category: "TestWindow.Green" - property alias x: green.x - property alias y: green.y - property alias width: green.width - property alias height: green.height - } - - Rectangle { - anchors.fill: parent - visible: true - color: "green" - Component.onDestruction: console.log("Green destroyed") - } - } - -/* - Rectangle { width: 100; height: 100; x: 100; y: 100; color: "#00f" } - - Window { - id: green - alwaysOnTop: true - frame: HiddenFrame{} - hideBackground: true - closable: true - visible: true - resizable: false - x: 1280 / 2; y: 720 / 2 - width: 100; height: 100 - Rectangle { - color: "#0f0" - width: green.width; - height: green.height; - } - } - */ - -/* - Window { - id: yellow - closable: true - visible: true - resizable: true - x: 100; y: 100 - width: 100; height: 100 - Rectangle { - anchors.fill: parent - visible: true - color: "yellow" - } - } -*/ - } - - Action { - id: openBrowserAction - text: "Open Browser" - shortcut: "Ctrl+Shift+X" - onTriggered: { - builder.createObject(desktop); - } - property var builder: Component { - ModelBrowserDialog{} - } - } -} - - - - diff --git a/tests-manual/ui/qmlscratch.pro b/tests-manual/ui/qmlscratch.pro index 5c9b91ee52..3287180d26 100644 --- a/tests-manual/ui/qmlscratch.pro +++ b/tests-manual/ui/qmlscratch.pro @@ -4,34 +4,10 @@ QT += gui qml quick xml webengine widgets CONFIG += c++11 -SOURCES += src/main.cpp \ - ../../libraries/ui/src/FileDialogHelper.cpp - -HEADERS += \ - ../../libraries/ui/src/FileDialogHelper.h +SOURCES += src/main.cpp # Additional import path used to resolve QML modules in Qt Creator's code model QML_IMPORT_PATH = ../../interface/resources/qml - DISTFILES += \ - qml/*.qml \ - ../../interface/resources/QtWebEngine/UIDelegates/original/*.qml \ - ../../interface/resources/QtWebEngine/UIDelegates/*.qml \ - ../../interface/resources/qml/*.qml \ - ../../interface/resources/qml/controls/*.qml \ - ../../interface/resources/qml/controls-uit/*.qml \ - ../../interface/resources/qml/dialogs/*.qml \ - ../../interface/resources/qml/dialogs/fileDialog/*.qml \ - ../../interface/resources/qml/dialogs/preferences/*.qml \ - ../../interface/resources/qml/dialogs/messageDialog/*.qml \ - ../../interface/resources/qml/desktop/*.qml \ - ../../interface/resources/qml/menus/*.qml \ - ../../interface/resources/qml/styles/*.qml \ - ../../interface/resources/qml/styles-uit/*.qml \ - ../../interface/resources/qml/windows/*.qml \ - ../../interface/resources/qml/hifi/*.qml \ - ../../interface/resources/qml/hifi/toolbars/*.qml \ - ../../interface/resources/qml/hifi/dialogs/*.qml \ - ../../interface/resources/qml/hifi/dialogs/preferences/*.qml \ - ../../interface/resources/qml/hifi/overlays/*.qml + qml/*.qml diff --git a/tests-manual/ui/src/main.cpp b/tests-manual/ui/src/main.cpp index 312b5f3823..a5061f4d01 100644 --- a/tests-manual/ui/src/main.cpp +++ b/tests-manual/ui/src/main.cpp @@ -3,88 +3,31 @@ #include #include -#include "../../../libraries/ui/src/FileDialogHelper.h" - - -class Preference : public QObject { - Q_OBJECT - Q_PROPERTY(QString category READ getCategory() CONSTANT) - Q_PROPERTY(QString name READ getName() CONSTANT) - Q_PROPERTY(Type type READ getType() CONSTANT) - Q_ENUMS(Type) -public: - enum Type { - Editable, - Browsable, - Spinner, - Checkbox, - }; - - Preference(QObject* parent = nullptr) : QObject(parent) {} - - Preference(const QString& category, const QString& name, QObject* parent = nullptr) - : QObject(parent), _category(category), _name(name) { } - const QString& getCategory() const { return _category; } - const QString& getName() const { return _name; } - virtual Type getType() { return Editable; } - -protected: - const QString _category; - const QString _name; -}; - -class Reticle : public QObject { - Q_OBJECT - Q_PROPERTY(QPoint position READ getPosition CONSTANT) -public: - - Reticle(QObject* parent) : QObject(parent) { - } - - QPoint getPosition() { - if (!_window) { - return QPoint(0, 0); - } - return _window->mapFromGlobal(QCursor::pos()); - } - - void setWindow(QWindow* window) { - _window = window; - } - -private: - QWindow* _window{nullptr}; -}; - QString getRelativeDir(const QString& relativePath = ".") { - QDir path(__FILE__); path.cdUp(); + QDir path(__FILE__); + path.cdUp(); + path.cdUp(); auto result = path.absoluteFilePath(relativePath); result = path.cleanPath(result) + "/"; return result; } -QString getTestQmlDir() { - return getRelativeDir("../qml"); +QString getResourcesDir() { + return getRelativeDir("../../interface/resources"); } -QString getInterfaceQmlDir() { - return getRelativeDir("/"); +QString getQmlDir() { + return getRelativeDir("../../interface/resources/qml"); } - -void setChild(QQmlApplicationEngine& engine, const char* name) { - for (auto obj : engine.rootObjects()) { - auto child = obj->findChild(QString(name)); - if (child) { - engine.rootContext()->setContextProperty(name, child); - return; - } - } - qWarning() << "Could not find object named " << name; +QString getScriptsDir() { + return getRelativeDir("../../scripts"); } void addImportPath(QQmlApplicationEngine& engine, const QString& relativePath, bool insert = false) { QString resolvedPath = getRelativeDir(relativePath); + + qDebug() << "adding import path: " << QDir::toNativeSeparators(resolvedPath); engine.addImportPath(resolvedPath); } @@ -93,44 +36,24 @@ int main(int argc, char *argv[]) { app.setOrganizationName("Some Company"); app.setOrganizationDomain("somecompany.com"); app.setApplicationName("Amazing Application"); - QDir::setCurrent(getRelativeDir("..")); - QtWebEngine::initialize(); - qmlRegisterType("Hifi", 1, 0, "Preference"); + auto scriptsDir = getScriptsDir(); + auto resourcesDir = getResourcesDir(); QQmlApplicationEngine engine; + addImportPath(engine, "."); addImportPath(engine, "qml"); - addImportPath(engine, "../../interface/resources/qml"); - addImportPath(engine, "../../interface/resources"); - engine.load(QUrl(QStringLiteral("qml/Stubs.qml"))); + addImportPath(engine, resourcesDir); + addImportPath(engine, resourcesDir + "/qml"); + addImportPath(engine, scriptsDir); + addImportPath(engine, scriptsDir + "/developer/tests"); - setChild(engine, "offscreenFlags"); - setChild(engine, "Account"); - setChild(engine, "ApplicationCompositor"); - setChild(engine, "Controller"); - setChild(engine, "Desktop"); - setChild(engine, "ScriptDiscoveryService"); - setChild(engine, "HMD"); - setChild(engine, "GL"); - setChild(engine, "MenuHelper"); - setChild(engine, "Preferences"); - setChild(engine, "urlHandler"); - engine.rootContext()->setContextProperty("DebugQML", true); - engine.rootContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); + QFontDatabase::addApplicationFont(resourcesDir + "/fonts/FiraSans-Regular.ttf"); + QFontDatabase::addApplicationFont(resourcesDir + "/fonts/FiraSans-SemiBold.ttf"); + QFontDatabase::addApplicationFont(resourcesDir + "/fonts/hifi-glyphs.ttf"); - //engine.load(QUrl(QStringLiteral("qrc:/qml/gallery/main.qml"))); - engine.load(QUrl(QStringLiteral("qml/main.qml"))); - for (QObject* rootObject : engine.rootObjects()) { - if (rootObject->objectName() == "MainWindow") { - Reticle* reticle = new Reticle(rootObject); - reticle->setWindow((QWindow*)rootObject); - engine.rootContext()->setContextProperty("Reticle", reticle); - engine.rootContext()->setContextProperty("Window", rootObject); - break; - } - } + auto url = getRelativeDir(".") + "qml/ControlsGalleryWindow.qml"; + + engine.load(url); return app.exec(); } - -#include "main.moc" - From 2296848fcb9f4255d144ffcc773703f1bd196ba8 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Thu, 28 Jun 2018 16:58:45 -0700 Subject: [PATCH 340/362] Adding potential fix for showing audio stats - depends on PR#13401 --- scripts/developer/utilities/audio/Stats.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/developer/utilities/audio/Stats.qml b/scripts/developer/utilities/audio/Stats.qml index f359e9b04c..e2291e485d 100644 --- a/scripts/developer/utilities/audio/Stats.qml +++ b/scripts/developer/utilities/audio/Stats.qml @@ -12,7 +12,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 -import "qrc:////qml//controls-uit" as HifiControls +import controlsUit 1.0 as HifiControls Column { id: stats From c6764c62445497e08617848bf9ad01a74fe3c281 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Mon, 16 Jul 2018 23:59:06 +0300 Subject: [PATCH 341/362] cherry-picking --- interface/resources/qml/InteractiveWindow.qml | 4 ++-- interface/resources/qml/hifi/AvatarApp.qml | 4 ++-- interface/resources/qml/hifi/avatarapp/AdjustWearables.qml | 5 ++--- interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml | 2 +- interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml | 2 +- .../qml/hifi/avatarapp/AvatarWearablesIndicator.qml | 4 ++-- interface/resources/qml/hifi/avatarapp/BlueButton.qml | 4 ++-- .../resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml | 4 ++-- interface/resources/qml/hifi/avatarapp/InputField.qml | 4 ++-- interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml | 4 ++-- interface/resources/qml/hifi/avatarapp/MessageBox.qml | 4 ++-- interface/resources/qml/hifi/avatarapp/Settings.qml | 4 ++-- interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml | 2 +- interface/resources/qml/hifi/avatarapp/ShadowImage.qml | 2 +- interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml | 2 +- interface/resources/qml/hifi/avatarapp/SquareLabel.qml | 4 ++-- interface/resources/qml/hifi/avatarapp/Vector3.qml | 4 ++-- interface/resources/qml/hifi/avatarapp/WhiteButton.qml | 4 ++-- .../commerce/marketplaceItemTester/MarketplaceItemTester.qml | 5 +++-- interface/resources/qml/hifi/tablet/EditEntityList.qml | 4 ++-- interface/resources/qml/hifi/tablet/EditToolsTabView.qml | 4 ++-- 21 files changed, 38 insertions(+), 38 deletions(-) diff --git a/interface/resources/qml/InteractiveWindow.qml b/interface/resources/qml/InteractiveWindow.qml index e8ddbf823d..c217238e93 100644 --- a/interface/resources/qml/InteractiveWindow.qml +++ b/interface/resources/qml/InteractiveWindow.qml @@ -12,9 +12,9 @@ import QtQuick 2.3 import "windows" as Windows import "controls" -import "controls-uit" as Controls +import controlsUit 1.0 as Controls import "styles" -import "styles-uit" +import stylesUit 1.0 Windows.Window { id: root; diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index aea5931627..39590748cf 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -3,8 +3,8 @@ import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtQml.Models 2.1 import QtGraphicalEffects 1.0 -import "../controls-uit" as HifiControls -import "../styles-uit" +import controlsUit 1.0 as HifiControls +import stylesUit 1.0 import "avatarapp" Rectangle { diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml index 5fff14e4a1..0740914440 100644 --- a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -1,9 +1,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 import QtQuick.Layouts 1.3 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit -import "../../controls" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit Rectangle { id: root; diff --git a/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml b/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml index 9d9db010fb..d3c9cd1d5f 100644 --- a/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml +++ b/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml @@ -1,6 +1,6 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "../../styles-uit" +import stylesUit 1.0 ShadowRectangle { id: header diff --git a/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml b/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml index f66c7121cb..36cb4b1080 100644 --- a/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml +++ b/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 import QtQuick.Window 2.2 -import "../../styles-uit" +import stylesUit 1.0 QtObject { readonly property QtObject colors: QtObject { diff --git a/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml b/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml index cb73e9fe71..8b28d4c66b 100644 --- a/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml +++ b/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml @@ -1,6 +1,6 @@ import QtQuick 2.9 -import "../../controls-uit" -import "../../styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 ShadowRectangle { property int wearablesCount: 0 diff --git a/interface/resources/qml/hifi/avatarapp/BlueButton.qml b/interface/resources/qml/hifi/avatarapp/BlueButton.qml index e668951517..0cc84d5ba0 100644 --- a/interface/resources/qml/hifi/avatarapp/BlueButton.qml +++ b/interface/resources/qml/hifi/avatarapp/BlueButton.qml @@ -1,6 +1,6 @@ import QtQuick 2.5 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit HifiControlsUit.Button { HifiConstants { diff --git a/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml b/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml index 1387c0791a..780981a5a3 100644 --- a/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml +++ b/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml @@ -1,7 +1,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../controls" as HifiControls Rectangle { diff --git a/interface/resources/qml/hifi/avatarapp/InputField.qml b/interface/resources/qml/hifi/avatarapp/InputField.qml index 905518ef0f..2020d56c96 100644 --- a/interface/resources/qml/hifi/avatarapp/InputField.qml +++ b/interface/resources/qml/hifi/avatarapp/InputField.qml @@ -1,7 +1,7 @@ import QtQuick 2.5 import QtQuick.Controls 2.2 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit TextField { id: textField diff --git a/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml b/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml index 4b868b47ce..6c2101498c 100644 --- a/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml +++ b/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml @@ -1,5 +1,5 @@ -import "../../controls-uit" as HifiControlsUit -import "../../styles-uit" +import controlsUit 1.0 as HifiControlsUit +import stylesUit 1.0 import QtQuick 2.0 import QtQuick.Controls 2.2 diff --git a/interface/resources/qml/hifi/avatarapp/MessageBox.qml b/interface/resources/qml/hifi/avatarapp/MessageBox.qml index f111303214..eb28745b1a 100644 --- a/interface/resources/qml/hifi/avatarapp/MessageBox.qml +++ b/interface/resources/qml/hifi/avatarapp/MessageBox.qml @@ -1,7 +1,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../controls" as HifiControls Rectangle { diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index 3446191163..bad1394133 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -2,8 +2,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../controls" as HifiControls Rectangle { diff --git a/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml b/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml index c2d84bb371..a2c84fad47 100644 --- a/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml +++ b/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml @@ -1,4 +1,4 @@ -import "../../styles-uit" +import stylesUit 1.0 import QtQuick 2.9 import QtGraphicalEffects 1.0 diff --git a/interface/resources/qml/hifi/avatarapp/ShadowImage.qml b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml index 3995446e49..51e1043702 100644 --- a/interface/resources/qml/hifi/avatarapp/ShadowImage.qml +++ b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml @@ -1,4 +1,4 @@ -import "../../styles-uit" +import stylesUit 1.0 import QtQuick 2.9 import QtGraphicalEffects 1.0 diff --git a/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml b/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml index 741fce3d8d..3968fcb1ff 100644 --- a/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml +++ b/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml @@ -1,4 +1,4 @@ -import "../../styles-uit" +import stylesUit 1.0 import QtQuick 2.9 import QtGraphicalEffects 1.0 diff --git a/interface/resources/qml/hifi/avatarapp/SquareLabel.qml b/interface/resources/qml/hifi/avatarapp/SquareLabel.qml index e2c456ec04..69aff47373 100644 --- a/interface/resources/qml/hifi/avatarapp/SquareLabel.qml +++ b/interface/resources/qml/hifi/avatarapp/SquareLabel.qml @@ -1,5 +1,5 @@ -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import QtQuick 2.9 import QtGraphicalEffects 1.0 diff --git a/interface/resources/qml/hifi/avatarapp/Vector3.qml b/interface/resources/qml/hifi/avatarapp/Vector3.qml index d77665f992..698123104f 100644 --- a/interface/resources/qml/hifi/avatarapp/Vector3.qml +++ b/interface/resources/qml/hifi/avatarapp/Vector3.qml @@ -1,7 +1,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit import "../../controls" as HifiControls Row { diff --git a/interface/resources/qml/hifi/avatarapp/WhiteButton.qml b/interface/resources/qml/hifi/avatarapp/WhiteButton.qml index dc729ae097..d0a4a152db 100644 --- a/interface/resources/qml/hifi/avatarapp/WhiteButton.qml +++ b/interface/resources/qml/hifi/avatarapp/WhiteButton.qml @@ -1,6 +1,6 @@ import QtQuick 2.5 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit HifiControlsUit.Button { HifiConstants { diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml index 2a4f2d0e22..a37a0ac756 100644 --- a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml @@ -15,8 +15,9 @@ import QtQuick 2.10 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 import Hifi 1.0 as Hifi -import "qrc:////qml//styles-uit" as HifiStylesUit -import "qrc:////qml//controls-uit" as HifiControlsUit +import stylesUit 1.0 as HifiStylesUit +import controlsUit 1.0 as HifiControlsUit + diff --git a/interface/resources/qml/hifi/tablet/EditEntityList.qml b/interface/resources/qml/hifi/tablet/EditEntityList.qml index d484885103..d2fb99ea0a 100644 --- a/interface/resources/qml/hifi/tablet/EditEntityList.qml +++ b/interface/resources/qml/hifi/tablet/EditEntityList.qml @@ -4,8 +4,8 @@ import QtWebChannel 1.0 import "../../controls" import "../toolbars" import QtGraphicalEffects 1.0 -import "../../controls-uit" as HifiControls -import "../../styles-uit" +import controlsUit 1.0 as HifiControls +import stylesUit 1.0 WebView { diff --git a/interface/resources/qml/hifi/tablet/EditToolsTabView.qml b/interface/resources/qml/hifi/tablet/EditToolsTabView.qml index 13b1caf8fb..dc7ad683e3 100644 --- a/interface/resources/qml/hifi/tablet/EditToolsTabView.qml +++ b/interface/resources/qml/hifi/tablet/EditToolsTabView.qml @@ -4,8 +4,8 @@ import QtWebChannel 1.0 import "../../controls" import "../toolbars" import QtGraphicalEffects 1.0 -import "../../controls-uit" as HifiControls -import "../../styles-uit" +import controlsUit 1.0 as HifiControls +import stylesUit 1.0 TabBar { id: editTabView From 82313f9464d5e3aa410845f68e13c1e02458f274 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Thu, 18 Oct 2018 22:03:50 +0300 Subject: [PATCH 342/362] update imports for utilities / all other qml files found --- interface/resources/QtWebEngine/UIDelegates/Menu.qml | 4 ++-- interface/resources/QtWebEngine/UIDelegates/MenuItem.qml | 4 ++-- interface/resources/QtWebEngine/UIDelegates/PromptDialog.qml | 4 ++-- scripts/developer/utilities/audio/TabletStats.qml | 3 +-- scripts/developer/utilities/lib/jet/qml/TaskList.qml | 4 ++-- scripts/developer/utilities/lib/jet/qml/TaskListView.qml | 4 ++-- scripts/developer/utilities/lib/plotperf/Color.qml | 4 ++-- scripts/developer/utilities/render/antialiasing.qml | 4 ++-- .../developer/utilities/render/configSlider/ConfigSlider.qml | 4 ++-- .../developer/utilities/render/configSlider/RichSlider.qml | 4 ++-- scripts/developer/utilities/render/deferredLighting.qml | 4 ++-- scripts/developer/utilities/render/engineInspector.qml | 4 ++-- scripts/developer/utilities/render/highlight.qml | 4 ++-- .../developer/utilities/render/highlight/HighlightStyle.qml | 4 ++-- scripts/developer/utilities/render/lod.qml | 4 ++-- scripts/developer/utilities/render/shadow.qml | 4 ++-- scripts/developer/utilities/render/transition.qml | 4 ++-- scripts/developer/utilities/workload/workloadInspector.qml | 4 ++-- .../marketplace/spectator-camera/SpectatorCamera.qml | 4 ++-- 19 files changed, 37 insertions(+), 38 deletions(-) diff --git a/interface/resources/QtWebEngine/UIDelegates/Menu.qml b/interface/resources/QtWebEngine/UIDelegates/Menu.qml index 46c00e758e..adfd29df9e 100644 --- a/interface/resources/QtWebEngine/UIDelegates/Menu.qml +++ b/interface/resources/QtWebEngine/UIDelegates/Menu.qml @@ -1,7 +1,7 @@ import QtQuick 2.5 -import "../../qml/controls-uit" -import "../../qml/styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Item { id: menu diff --git a/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml b/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml index 6014b6834b..b4d3ca4bb2 100644 --- a/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml +++ b/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml @@ -1,7 +1,7 @@ import QtQuick 2.5 -import "../../qml/controls-uit" -import "../../qml/styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 Item { id: root diff --git a/interface/resources/QtWebEngine/UIDelegates/PromptDialog.qml b/interface/resources/QtWebEngine/UIDelegates/PromptDialog.qml index e4ab3037ef..089c745571 100644 --- a/interface/resources/QtWebEngine/UIDelegates/PromptDialog.qml +++ b/interface/resources/QtWebEngine/UIDelegates/PromptDialog.qml @@ -1,7 +1,7 @@ import QtQuick 2.5 -import "../../qml/controls-uit" -import "../../qml/styles-uit" +import controlsUit 1.0 +import stylesUit 1.0 import "../../qml/dialogs" QtObject { diff --git a/scripts/developer/utilities/audio/TabletStats.qml b/scripts/developer/utilities/audio/TabletStats.qml index 2f8d212a2a..b50acabec4 100644 --- a/scripts/developer/utilities/audio/TabletStats.qml +++ b/scripts/developer/utilities/audio/TabletStats.qml @@ -11,8 +11,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 - -import "qrc:////qml//styles-uit" +import stylesUit 1.0 Item { id: dialog diff --git a/scripts/developer/utilities/lib/jet/qml/TaskList.qml b/scripts/developer/utilities/lib/jet/qml/TaskList.qml index 5b1aa0afb5..166f604666 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskList.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskList.qml @@ -12,8 +12,8 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 as Original import QtQuick.Controls.Styles 1.4 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../jet.js" as Jet diff --git a/scripts/developer/utilities/lib/jet/qml/TaskListView.qml b/scripts/developer/utilities/lib/jet/qml/TaskListView.qml index 2c75865698..0f083aa72c 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskListView.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskListView.qml @@ -12,8 +12,8 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 as Original import QtQuick.Controls.Styles 1.4 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../jet.js" as Jet diff --git a/scripts/developer/utilities/lib/plotperf/Color.qml b/scripts/developer/utilities/lib/plotperf/Color.qml index 15d7f9fcc9..1ad72fe2e6 100644 --- a/scripts/developer/utilities/lib/plotperf/Color.qml +++ b/scripts/developer/utilities/lib/plotperf/Color.qml @@ -12,8 +12,8 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 as Original import QtQuick.Controls.Styles 1.4 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls Item { diff --git a/scripts/developer/utilities/render/antialiasing.qml b/scripts/developer/utilities/render/antialiasing.qml index 1a8f9dac2d..5abfd30935 100644 --- a/scripts/developer/utilities/render/antialiasing.qml +++ b/scripts/developer/utilities/render/antialiasing.qml @@ -12,8 +12,8 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "configSlider" import "../lib/plotperf" diff --git a/scripts/developer/utilities/render/configSlider/ConfigSlider.qml b/scripts/developer/utilities/render/configSlider/ConfigSlider.qml index 41de77fb09..bf9089d82c 100644 --- a/scripts/developer/utilities/render/configSlider/ConfigSlider.qml +++ b/scripts/developer/utilities/render/configSlider/ConfigSlider.qml @@ -12,8 +12,8 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 as Original import QtQuick.Controls.Styles 1.4 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls Item { diff --git a/scripts/developer/utilities/render/configSlider/RichSlider.qml b/scripts/developer/utilities/render/configSlider/RichSlider.qml index 01b14f3d48..ff16cb32ad 100644 --- a/scripts/developer/utilities/render/configSlider/RichSlider.qml +++ b/scripts/developer/utilities/render/configSlider/RichSlider.qml @@ -12,8 +12,8 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 as Original import QtQuick.Controls.Styles 1.4 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls Item { diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index a9479b2935..64e00acdac 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -11,8 +11,8 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "configSlider" import "../lib/jet/qml" as Jet diff --git a/scripts/developer/utilities/render/engineInspector.qml b/scripts/developer/utilities/render/engineInspector.qml index 1b9941e64e..16dd8eb985 100644 --- a/scripts/developer/utilities/render/engineInspector.qml +++ b/scripts/developer/utilities/render/engineInspector.qml @@ -11,8 +11,8 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../lib/jet/qml" as Jet diff --git a/scripts/developer/utilities/render/highlight.qml b/scripts/developer/utilities/render/highlight.qml index 88d6a807ae..d8af2a828e 100644 --- a/scripts/developer/utilities/render/highlight.qml +++ b/scripts/developer/utilities/render/highlight.qml @@ -12,8 +12,8 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "configSlider" import "../lib/plotperf" import "highlight" diff --git a/scripts/developer/utilities/render/highlight/HighlightStyle.qml b/scripts/developer/utilities/render/highlight/HighlightStyle.qml index 371b7e81f7..475aadfdce 100644 --- a/scripts/developer/utilities/render/highlight/HighlightStyle.qml +++ b/scripts/developer/utilities/render/highlight/HighlightStyle.qml @@ -12,8 +12,8 @@ import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 import "../configSlider" import "../../lib/plotperf" -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls Item { id: root diff --git a/scripts/developer/utilities/render/lod.qml b/scripts/developer/utilities/render/lod.qml index 889d8db836..892b43d8be 100644 --- a/scripts/developer/utilities/render/lod.qml +++ b/scripts/developer/utilities/render/lod.qml @@ -11,8 +11,8 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../lib/plotperf" import "configSlider" diff --git a/scripts/developer/utilities/render/shadow.qml b/scripts/developer/utilities/render/shadow.qml index 464fe00eb9..a1d6777a68 100644 --- a/scripts/developer/utilities/render/shadow.qml +++ b/scripts/developer/utilities/render/shadow.qml @@ -12,8 +12,8 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "configSlider" diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index f74468a273..c150c523f9 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -13,8 +13,8 @@ import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 import QtQuick.Dialogs 1.0 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "configSlider" import "../lib/plotperf" diff --git a/scripts/developer/utilities/workload/workloadInspector.qml b/scripts/developer/utilities/workload/workloadInspector.qml index 2eaa9d8133..746a572f29 100644 --- a/scripts/developer/utilities/workload/workloadInspector.qml +++ b/scripts/developer/utilities/workload/workloadInspector.qml @@ -11,8 +11,8 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 -import "qrc:///qml/styles-uit" -import "qrc:///qml/controls-uit" as HifiControls +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls import "../render/configSlider" import "../lib/jet/qml" as Jet import "../lib/plotperf" diff --git a/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml b/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml index 033039b87d..753771987a 100644 --- a/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml +++ b/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml @@ -16,8 +16,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 -import "qrc:////qml//styles-uit" as HifiStylesUit -import "qrc:////qml//controls-uit" as HifiControlsUit +import stylesUit 1.0 as HifiStylesUit +import controlsUit 1.0 as HifiControlsUit import "qrc:////qml//controls" as HifiControls import "qrc:////qml//hifi" as Hifi From f7d9bafc7d629c4ae46bd0cdebc8d9fa8556f713 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Wed, 24 Oct 2018 00:28:55 +0300 Subject: [PATCH 343/362] add FilterBar & ScrollBar to qmldir --- interface/resources/qml/controlsUit/qmldir | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/resources/qml/controlsUit/qmldir b/interface/resources/qml/controlsUit/qmldir index 989115b8d2..d0577f5575 100644 --- a/interface/resources/qml/controlsUit/qmldir +++ b/interface/resources/qml/controlsUit/qmldir @@ -6,6 +6,7 @@ CheckBox 1.0 CheckBox.qml CheckBoxQQC2 1.0 CheckBoxQQC2.qml ComboBox 1.0 ComboBox.qml ContentSection 1.0 ContentSection.qml +FilterBar 1.0 FilterBar.qml GlyphButton 1.0 GlyphButton.qml HorizontalRule 1.0 HorizontalRule.qml HorizontalSpacer 1.0 HorizontalSpacer.qml @@ -15,6 +16,7 @@ Keyboard 1.0 Keyboard.qml Label 1.0 Label.qml QueuedButton 1.0 QueuedButton.qml RadioButton 1.0 RadioButton.qml +ScrollBar 1.0 ScrollBar.qml Separator 1.0 Separator.qml Slider 1.0 Slider.qml SpinBox 1.0 SpinBox.qml From fe4e4cfbd4b2bdf87d15f091d3e679be4314856a Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Wed, 24 Oct 2018 01:40:10 +0300 Subject: [PATCH 344/362] return controls-uit & styles-uit folders to preserve backward-compatibility --- interface/resources/qml/controls-uit/AttachmentsTable.qml | 8 ++++++++ interface/resources/qml/controls-uit/BaseWebView.qml | 8 ++++++++ interface/resources/qml/controls-uit/Button.qml | 8 ++++++++ interface/resources/qml/controls-uit/CheckBox.qml | 8 ++++++++ interface/resources/qml/controls-uit/CheckBoxQQC2.qml | 8 ++++++++ interface/resources/qml/controls-uit/ComboBox.qml | 8 ++++++++ interface/resources/qml/controls-uit/ContentSection.qml | 8 ++++++++ interface/resources/qml/controls-uit/FilterBar.qml | 8 ++++++++ interface/resources/qml/controls-uit/GlyphButton.qml | 8 ++++++++ interface/resources/qml/controls-uit/HorizontalRule.qml | 8 ++++++++ interface/resources/qml/controls-uit/HorizontalSpacer.qml | 8 ++++++++ interface/resources/qml/controls-uit/ImageMessageBox.qml | 8 ++++++++ interface/resources/qml/controls-uit/Key.qml | 8 ++++++++ interface/resources/qml/controls-uit/Keyboard.qml | 8 ++++++++ interface/resources/qml/controls-uit/Label.qml | 8 ++++++++ interface/resources/qml/controls-uit/QueuedButton.qml | 8 ++++++++ interface/resources/qml/controls-uit/RadioButton.qml | 8 ++++++++ interface/resources/qml/controls-uit/ScrollBar.qml | 8 ++++++++ interface/resources/qml/controls-uit/Separator.qml | 8 ++++++++ interface/resources/qml/controls-uit/Slider.qml | 8 ++++++++ interface/resources/qml/controls-uit/SpinBox.qml | 8 ++++++++ interface/resources/qml/controls-uit/Switch.qml | 8 ++++++++ interface/resources/qml/controls-uit/Table.qml | 8 ++++++++ .../resources/qml/controls-uit/TabletContentSection.qml | 8 ++++++++ interface/resources/qml/controls-uit/TabletHeader.qml | 8 ++++++++ interface/resources/qml/controls-uit/TextAction.qml | 8 ++++++++ interface/resources/qml/controls-uit/TextEdit.qml | 8 ++++++++ interface/resources/qml/controls-uit/TextField.qml | 8 ++++++++ interface/resources/qml/controls-uit/ToolTip.qml | 8 ++++++++ interface/resources/qml/controls-uit/Tree.qml | 8 ++++++++ interface/resources/qml/controls-uit/VerticalSpacer.qml | 8 ++++++++ interface/resources/qml/controls-uit/WebGlyphButton.qml | 8 ++++++++ interface/resources/qml/controls-uit/WebSpinner.qml | 8 ++++++++ interface/resources/qml/controls-uit/WebView.qml | 8 ++++++++ interface/resources/qml/controls-uit/readme.txt | 1 + .../resources/qml/styles-uit/AnonymousProRegular.qml | 8 ++++++++ interface/resources/qml/styles-uit/ButtonLabel.qml | 8 ++++++++ interface/resources/qml/styles-uit/FiraSansRegular.qml | 8 ++++++++ interface/resources/qml/styles-uit/FiraSansSemiBold.qml | 8 ++++++++ interface/resources/qml/styles-uit/HiFiGlyphs.qml | 8 ++++++++ interface/resources/qml/styles-uit/HifiConstants.qml | 8 ++++++++ interface/resources/qml/styles-uit/IconButton.qml | 8 ++++++++ interface/resources/qml/styles-uit/InfoItem.qml | 8 ++++++++ interface/resources/qml/styles-uit/InputLabel.qml | 8 ++++++++ interface/resources/qml/styles-uit/ListItem.qml | 8 ++++++++ interface/resources/qml/styles-uit/Logs.qml | 8 ++++++++ interface/resources/qml/styles-uit/OverlayTitle.qml | 8 ++++++++ interface/resources/qml/styles-uit/RalewayBold.qml | 8 ++++++++ interface/resources/qml/styles-uit/RalewayLight.qml | 8 ++++++++ interface/resources/qml/styles-uit/RalewayRegular.qml | 8 ++++++++ interface/resources/qml/styles-uit/RalewaySemiBold.qml | 8 ++++++++ interface/resources/qml/styles-uit/SectionName.qml | 8 ++++++++ interface/resources/qml/styles-uit/Separator.qml | 8 ++++++++ interface/resources/qml/styles-uit/ShortcutText.qml | 8 ++++++++ interface/resources/qml/styles-uit/TabName.qml | 8 ++++++++ interface/resources/qml/styles-uit/TextFieldInput.qml | 8 ++++++++ interface/resources/qml/styles-uit/readme.txt | 1 + 57 files changed, 442 insertions(+) create mode 100644 interface/resources/qml/controls-uit/AttachmentsTable.qml create mode 100644 interface/resources/qml/controls-uit/BaseWebView.qml create mode 100644 interface/resources/qml/controls-uit/Button.qml create mode 100644 interface/resources/qml/controls-uit/CheckBox.qml create mode 100644 interface/resources/qml/controls-uit/CheckBoxQQC2.qml create mode 100644 interface/resources/qml/controls-uit/ComboBox.qml create mode 100644 interface/resources/qml/controls-uit/ContentSection.qml create mode 100644 interface/resources/qml/controls-uit/FilterBar.qml create mode 100644 interface/resources/qml/controls-uit/GlyphButton.qml create mode 100644 interface/resources/qml/controls-uit/HorizontalRule.qml create mode 100644 interface/resources/qml/controls-uit/HorizontalSpacer.qml create mode 100644 interface/resources/qml/controls-uit/ImageMessageBox.qml create mode 100644 interface/resources/qml/controls-uit/Key.qml create mode 100644 interface/resources/qml/controls-uit/Keyboard.qml create mode 100644 interface/resources/qml/controls-uit/Label.qml create mode 100644 interface/resources/qml/controls-uit/QueuedButton.qml create mode 100644 interface/resources/qml/controls-uit/RadioButton.qml create mode 100644 interface/resources/qml/controls-uit/ScrollBar.qml create mode 100644 interface/resources/qml/controls-uit/Separator.qml create mode 100644 interface/resources/qml/controls-uit/Slider.qml create mode 100644 interface/resources/qml/controls-uit/SpinBox.qml create mode 100644 interface/resources/qml/controls-uit/Switch.qml create mode 100644 interface/resources/qml/controls-uit/Table.qml create mode 100644 interface/resources/qml/controls-uit/TabletContentSection.qml create mode 100644 interface/resources/qml/controls-uit/TabletHeader.qml create mode 100644 interface/resources/qml/controls-uit/TextAction.qml create mode 100644 interface/resources/qml/controls-uit/TextEdit.qml create mode 100644 interface/resources/qml/controls-uit/TextField.qml create mode 100644 interface/resources/qml/controls-uit/ToolTip.qml create mode 100644 interface/resources/qml/controls-uit/Tree.qml create mode 100644 interface/resources/qml/controls-uit/VerticalSpacer.qml create mode 100644 interface/resources/qml/controls-uit/WebGlyphButton.qml create mode 100644 interface/resources/qml/controls-uit/WebSpinner.qml create mode 100644 interface/resources/qml/controls-uit/WebView.qml create mode 100644 interface/resources/qml/controls-uit/readme.txt create mode 100644 interface/resources/qml/styles-uit/AnonymousProRegular.qml create mode 100644 interface/resources/qml/styles-uit/ButtonLabel.qml create mode 100644 interface/resources/qml/styles-uit/FiraSansRegular.qml create mode 100644 interface/resources/qml/styles-uit/FiraSansSemiBold.qml create mode 100644 interface/resources/qml/styles-uit/HiFiGlyphs.qml create mode 100644 interface/resources/qml/styles-uit/HifiConstants.qml create mode 100644 interface/resources/qml/styles-uit/IconButton.qml create mode 100644 interface/resources/qml/styles-uit/InfoItem.qml create mode 100644 interface/resources/qml/styles-uit/InputLabel.qml create mode 100644 interface/resources/qml/styles-uit/ListItem.qml create mode 100644 interface/resources/qml/styles-uit/Logs.qml create mode 100644 interface/resources/qml/styles-uit/OverlayTitle.qml create mode 100644 interface/resources/qml/styles-uit/RalewayBold.qml create mode 100644 interface/resources/qml/styles-uit/RalewayLight.qml create mode 100644 interface/resources/qml/styles-uit/RalewayRegular.qml create mode 100644 interface/resources/qml/styles-uit/RalewaySemiBold.qml create mode 100644 interface/resources/qml/styles-uit/SectionName.qml create mode 100644 interface/resources/qml/styles-uit/Separator.qml create mode 100644 interface/resources/qml/styles-uit/ShortcutText.qml create mode 100644 interface/resources/qml/styles-uit/TabName.qml create mode 100644 interface/resources/qml/styles-uit/TextFieldInput.qml create mode 100644 interface/resources/qml/styles-uit/readme.txt diff --git a/interface/resources/qml/controls-uit/AttachmentsTable.qml b/interface/resources/qml/controls-uit/AttachmentsTable.qml new file mode 100644 index 0000000000..fdd62b7c84 --- /dev/null +++ b/interface/resources/qml/controls-uit/AttachmentsTable.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +AttachmentsTable { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} \ No newline at end of file diff --git a/interface/resources/qml/controls-uit/BaseWebView.qml b/interface/resources/qml/controls-uit/BaseWebView.qml new file mode 100644 index 0000000000..6ff58d1e47 --- /dev/null +++ b/interface/resources/qml/controls-uit/BaseWebView.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +BaseWebView { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} \ No newline at end of file diff --git a/interface/resources/qml/controls-uit/Button.qml b/interface/resources/qml/controls-uit/Button.qml new file mode 100644 index 0000000000..ddadd0038a --- /dev/null +++ b/interface/resources/qml/controls-uit/Button.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +Button { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml new file mode 100644 index 0000000000..01ffd3c788 --- /dev/null +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +CheckBox { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/CheckBoxQQC2.qml b/interface/resources/qml/controls-uit/CheckBoxQQC2.qml new file mode 100644 index 0000000000..9950e4b47f --- /dev/null +++ b/interface/resources/qml/controls-uit/CheckBoxQQC2.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +CheckBoxQQC2 { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} \ No newline at end of file diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml new file mode 100644 index 0000000000..74a68706bf --- /dev/null +++ b/interface/resources/qml/controls-uit/ComboBox.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +ComboBox { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/ContentSection.qml b/interface/resources/qml/controls-uit/ContentSection.qml new file mode 100644 index 0000000000..d6cdd92722 --- /dev/null +++ b/interface/resources/qml/controls-uit/ContentSection.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +ContentSection { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/FilterBar.qml b/interface/resources/qml/controls-uit/FilterBar.qml new file mode 100644 index 0000000000..a9b3c350b4 --- /dev/null +++ b/interface/resources/qml/controls-uit/FilterBar.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +FilterBar { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/GlyphButton.qml b/interface/resources/qml/controls-uit/GlyphButton.qml new file mode 100644 index 0000000000..e6dbec69a1 --- /dev/null +++ b/interface/resources/qml/controls-uit/GlyphButton.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +GlyphButton { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/HorizontalRule.qml b/interface/resources/qml/controls-uit/HorizontalRule.qml new file mode 100644 index 0000000000..b0eaa56778 --- /dev/null +++ b/interface/resources/qml/controls-uit/HorizontalRule.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +HorizontalRule { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/HorizontalSpacer.qml b/interface/resources/qml/controls-uit/HorizontalSpacer.qml new file mode 100644 index 0000000000..f9bf73f6e0 --- /dev/null +++ b/interface/resources/qml/controls-uit/HorizontalSpacer.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +HorizontalSpacer { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/ImageMessageBox.qml b/interface/resources/qml/controls-uit/ImageMessageBox.qml new file mode 100644 index 0000000000..efdf513b19 --- /dev/null +++ b/interface/resources/qml/controls-uit/ImageMessageBox.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +ImageMessageBox { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/Key.qml b/interface/resources/qml/controls-uit/Key.qml new file mode 100644 index 0000000000..844cb0c560 --- /dev/null +++ b/interface/resources/qml/controls-uit/Key.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +Key { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/Keyboard.qml b/interface/resources/qml/controls-uit/Keyboard.qml new file mode 100644 index 0000000000..5c1622d594 --- /dev/null +++ b/interface/resources/qml/controls-uit/Keyboard.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +Keyboard { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/Label.qml b/interface/resources/qml/controls-uit/Label.qml new file mode 100644 index 0000000000..824f6436bb --- /dev/null +++ b/interface/resources/qml/controls-uit/Label.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +Label { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/QueuedButton.qml b/interface/resources/qml/controls-uit/QueuedButton.qml new file mode 100644 index 0000000000..4ec44c16d5 --- /dev/null +++ b/interface/resources/qml/controls-uit/QueuedButton.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +QueuedButton { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/RadioButton.qml b/interface/resources/qml/controls-uit/RadioButton.qml new file mode 100644 index 0000000000..dab757279c --- /dev/null +++ b/interface/resources/qml/controls-uit/RadioButton.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +RadioButton { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/ScrollBar.qml b/interface/resources/qml/controls-uit/ScrollBar.qml new file mode 100644 index 0000000000..71c4c956b0 --- /dev/null +++ b/interface/resources/qml/controls-uit/ScrollBar.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +ScrollBar { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/Separator.qml b/interface/resources/qml/controls-uit/Separator.qml new file mode 100644 index 0000000000..6be7d7cb40 --- /dev/null +++ b/interface/resources/qml/controls-uit/Separator.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +Separator { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/Slider.qml b/interface/resources/qml/controls-uit/Slider.qml new file mode 100644 index 0000000000..5cdd07a35a --- /dev/null +++ b/interface/resources/qml/controls-uit/Slider.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +Slider { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} \ No newline at end of file diff --git a/interface/resources/qml/controls-uit/SpinBox.qml b/interface/resources/qml/controls-uit/SpinBox.qml new file mode 100644 index 0000000000..56f360c286 --- /dev/null +++ b/interface/resources/qml/controls-uit/SpinBox.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +SpinBox { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/Switch.qml b/interface/resources/qml/controls-uit/Switch.qml new file mode 100644 index 0000000000..77d1d4e081 --- /dev/null +++ b/interface/resources/qml/controls-uit/Switch.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +Switch { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml new file mode 100644 index 0000000000..6ca7675db6 --- /dev/null +++ b/interface/resources/qml/controls-uit/Table.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +Table { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/TabletContentSection.qml b/interface/resources/qml/controls-uit/TabletContentSection.qml new file mode 100644 index 0000000000..0738426f4b --- /dev/null +++ b/interface/resources/qml/controls-uit/TabletContentSection.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +TabletContentSection { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/TabletHeader.qml b/interface/resources/qml/controls-uit/TabletHeader.qml new file mode 100644 index 0000000000..689a6e1dfc --- /dev/null +++ b/interface/resources/qml/controls-uit/TabletHeader.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +TabletHeader { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/TextAction.qml b/interface/resources/qml/controls-uit/TextAction.qml new file mode 100644 index 0000000000..617418d992 --- /dev/null +++ b/interface/resources/qml/controls-uit/TextAction.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +TextAction { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/TextEdit.qml b/interface/resources/qml/controls-uit/TextEdit.qml new file mode 100644 index 0000000000..99d6b43234 --- /dev/null +++ b/interface/resources/qml/controls-uit/TextEdit.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +TextEdit { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/TextField.qml b/interface/resources/qml/controls-uit/TextField.qml new file mode 100644 index 0000000000..8f44823c3b --- /dev/null +++ b/interface/resources/qml/controls-uit/TextField.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +TextField { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/ToolTip.qml b/interface/resources/qml/controls-uit/ToolTip.qml new file mode 100644 index 0000000000..29b98bf572 --- /dev/null +++ b/interface/resources/qml/controls-uit/ToolTip.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +ToolTip { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/Tree.qml b/interface/resources/qml/controls-uit/Tree.qml new file mode 100644 index 0000000000..3eca0fa1f6 --- /dev/null +++ b/interface/resources/qml/controls-uit/Tree.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +Tree { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/VerticalSpacer.qml b/interface/resources/qml/controls-uit/VerticalSpacer.qml new file mode 100644 index 0000000000..f05b83d9c7 --- /dev/null +++ b/interface/resources/qml/controls-uit/VerticalSpacer.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +VerticalSpacer { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/WebGlyphButton.qml b/interface/resources/qml/controls-uit/WebGlyphButton.qml new file mode 100644 index 0000000000..57201edcda --- /dev/null +++ b/interface/resources/qml/controls-uit/WebGlyphButton.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +WebGlyphButton { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/WebSpinner.qml b/interface/resources/qml/controls-uit/WebSpinner.qml new file mode 100644 index 0000000000..4ab8a5475e --- /dev/null +++ b/interface/resources/qml/controls-uit/WebSpinner.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +WebSpinner { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/WebView.qml b/interface/resources/qml/controls-uit/WebView.qml new file mode 100644 index 0000000000..4d2e37903b --- /dev/null +++ b/interface/resources/qml/controls-uit/WebView.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import controlsUit 1.0 + +WebView { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/controls-uit/readme.txt b/interface/resources/qml/controls-uit/readme.txt new file mode 100644 index 0000000000..8aa3714ff9 --- /dev/null +++ b/interface/resources/qml/controls-uit/readme.txt @@ -0,0 +1 @@ +this folder exists purely for compatibility reasons and might be deleted in future! please consider using 'import controlsUit 1.0' instead of including this folder \ No newline at end of file diff --git a/interface/resources/qml/styles-uit/AnonymousProRegular.qml b/interface/resources/qml/styles-uit/AnonymousProRegular.qml new file mode 100644 index 0000000000..09e6c1d0b0 --- /dev/null +++ b/interface/resources/qml/styles-uit/AnonymousProRegular.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +AnonymousProRegular { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/ButtonLabel.qml b/interface/resources/qml/styles-uit/ButtonLabel.qml new file mode 100644 index 0000000000..81f7928240 --- /dev/null +++ b/interface/resources/qml/styles-uit/ButtonLabel.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +ButtonLabel { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/FiraSansRegular.qml b/interface/resources/qml/styles-uit/FiraSansRegular.qml new file mode 100644 index 0000000000..d67fba71f8 --- /dev/null +++ b/interface/resources/qml/styles-uit/FiraSansRegular.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +FiraSansRegular { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/FiraSansSemiBold.qml b/interface/resources/qml/styles-uit/FiraSansSemiBold.qml new file mode 100644 index 0000000000..e9f49a5715 --- /dev/null +++ b/interface/resources/qml/styles-uit/FiraSansSemiBold.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +FiraSansSemiBold { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/HiFiGlyphs.qml b/interface/resources/qml/styles-uit/HiFiGlyphs.qml new file mode 100644 index 0000000000..129afb732e --- /dev/null +++ b/interface/resources/qml/styles-uit/HiFiGlyphs.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +HiFiGlyphs { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml new file mode 100644 index 0000000000..cf753d5c8d --- /dev/null +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +HifiConstants { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/IconButton.qml b/interface/resources/qml/styles-uit/IconButton.qml new file mode 100644 index 0000000000..93f37e9d7a --- /dev/null +++ b/interface/resources/qml/styles-uit/IconButton.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +IconButton { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/InfoItem.qml b/interface/resources/qml/styles-uit/InfoItem.qml new file mode 100644 index 0000000000..57397b4c6f --- /dev/null +++ b/interface/resources/qml/styles-uit/InfoItem.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +InfoItem { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/InputLabel.qml b/interface/resources/qml/styles-uit/InputLabel.qml new file mode 100644 index 0000000000..5b03ca1686 --- /dev/null +++ b/interface/resources/qml/styles-uit/InputLabel.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +InputLabel { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/ListItem.qml b/interface/resources/qml/styles-uit/ListItem.qml new file mode 100644 index 0000000000..e98d9bab81 --- /dev/null +++ b/interface/resources/qml/styles-uit/ListItem.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +ListItem { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/Logs.qml b/interface/resources/qml/styles-uit/Logs.qml new file mode 100644 index 0000000000..66d2eaeb2e --- /dev/null +++ b/interface/resources/qml/styles-uit/Logs.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +Logs { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/OverlayTitle.qml b/interface/resources/qml/styles-uit/OverlayTitle.qml new file mode 100644 index 0000000000..ec9cf33482 --- /dev/null +++ b/interface/resources/qml/styles-uit/OverlayTitle.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +OverlayTitle { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/RalewayBold.qml b/interface/resources/qml/styles-uit/RalewayBold.qml new file mode 100644 index 0000000000..2370401ba2 --- /dev/null +++ b/interface/resources/qml/styles-uit/RalewayBold.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +RalewayBold { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/RalewayLight.qml b/interface/resources/qml/styles-uit/RalewayLight.qml new file mode 100644 index 0000000000..0641a2746a --- /dev/null +++ b/interface/resources/qml/styles-uit/RalewayLight.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +RalewayLight { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/RalewayRegular.qml b/interface/resources/qml/styles-uit/RalewayRegular.qml new file mode 100644 index 0000000000..cf601f5c91 --- /dev/null +++ b/interface/resources/qml/styles-uit/RalewayRegular.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +RalewayRegular { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/RalewaySemiBold.qml b/interface/resources/qml/styles-uit/RalewaySemiBold.qml new file mode 100644 index 0000000000..a3c7cfe864 --- /dev/null +++ b/interface/resources/qml/styles-uit/RalewaySemiBold.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +RalewaySemiBold { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/SectionName.qml b/interface/resources/qml/styles-uit/SectionName.qml new file mode 100644 index 0000000000..b3cb143988 --- /dev/null +++ b/interface/resources/qml/styles-uit/SectionName.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +SectionName { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/Separator.qml b/interface/resources/qml/styles-uit/Separator.qml new file mode 100644 index 0000000000..9687fb0520 --- /dev/null +++ b/interface/resources/qml/styles-uit/Separator.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +Separator { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/ShortcutText.qml b/interface/resources/qml/styles-uit/ShortcutText.qml new file mode 100644 index 0000000000..921000651a --- /dev/null +++ b/interface/resources/qml/styles-uit/ShortcutText.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +ShortcutText { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/TabName.qml b/interface/resources/qml/styles-uit/TabName.qml new file mode 100644 index 0000000000..001c110a41 --- /dev/null +++ b/interface/resources/qml/styles-uit/TabName.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +TabName { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/TextFieldInput.qml b/interface/resources/qml/styles-uit/TextFieldInput.qml new file mode 100644 index 0000000000..ce5bc9edbd --- /dev/null +++ b/interface/resources/qml/styles-uit/TextFieldInput.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import stylesUit 1.0 + +TextFieldInput { + Component.onCompleted: { + console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); + } +} diff --git a/interface/resources/qml/styles-uit/readme.txt b/interface/resources/qml/styles-uit/readme.txt new file mode 100644 index 0000000000..105eda3c81 --- /dev/null +++ b/interface/resources/qml/styles-uit/readme.txt @@ -0,0 +1 @@ +this folder exists purely for compatibility reasons and might be deleted in future! please consider using 'import stylesUit 1.0' instead of including this folder \ No newline at end of file From 9623753416c7bba367ef02d3bc7d43eb3d489b2c Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Wed, 24 Oct 2018 22:44:36 +0300 Subject: [PATCH 345/362] change imports (how could I miss this place?) --- .../qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml index 27277a28f4..966f359e92 100644 --- a/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml @@ -14,8 +14,8 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 import Hifi 1.0 as Hifi -import "qrc:////qml//styles-uit" as HifiStylesUit -import "qrc:////qml//controls-uit" as HifiControlsUit +import stylesUit 1.0 as HifiStylesUit +import controlsUit 1.0 as HifiControlsUit Rectangle { id: root; From 4c6700b4973a0437dadc7bc5d8614f60155ad5ec Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Wed, 24 Oct 2018 22:52:28 +0300 Subject: [PATCH 346/362] remove deprecations warnings --- interface/resources/qml/controls-uit/AttachmentsTable.qml | 4 ---- interface/resources/qml/controls-uit/BaseWebView.qml | 4 ---- interface/resources/qml/controls-uit/Button.qml | 4 ---- interface/resources/qml/controls-uit/CheckBox.qml | 4 ---- interface/resources/qml/controls-uit/CheckBoxQQC2.qml | 4 ---- interface/resources/qml/controls-uit/ComboBox.qml | 4 ---- interface/resources/qml/controls-uit/ContentSection.qml | 4 ---- interface/resources/qml/controls-uit/FilterBar.qml | 4 ---- interface/resources/qml/controls-uit/GlyphButton.qml | 4 ---- interface/resources/qml/controls-uit/HorizontalRule.qml | 4 ---- interface/resources/qml/controls-uit/HorizontalSpacer.qml | 4 ---- interface/resources/qml/controls-uit/ImageMessageBox.qml | 4 ---- interface/resources/qml/controls-uit/Key.qml | 4 ---- interface/resources/qml/controls-uit/Keyboard.qml | 4 ---- interface/resources/qml/controls-uit/Label.qml | 4 ---- interface/resources/qml/controls-uit/QueuedButton.qml | 4 ---- interface/resources/qml/controls-uit/RadioButton.qml | 4 ---- interface/resources/qml/controls-uit/ScrollBar.qml | 4 ---- interface/resources/qml/controls-uit/Separator.qml | 4 ---- interface/resources/qml/controls-uit/Slider.qml | 4 ---- interface/resources/qml/controls-uit/SpinBox.qml | 4 ---- interface/resources/qml/controls-uit/Switch.qml | 4 ---- interface/resources/qml/controls-uit/Table.qml | 4 ---- interface/resources/qml/controls-uit/TabletContentSection.qml | 4 ---- interface/resources/qml/controls-uit/TabletHeader.qml | 4 ---- interface/resources/qml/controls-uit/TextAction.qml | 4 ---- interface/resources/qml/controls-uit/TextEdit.qml | 4 ---- interface/resources/qml/controls-uit/TextField.qml | 4 ---- interface/resources/qml/controls-uit/ToolTip.qml | 4 ---- interface/resources/qml/controls-uit/Tree.qml | 4 ---- interface/resources/qml/controls-uit/VerticalSpacer.qml | 4 ---- interface/resources/qml/controls-uit/WebGlyphButton.qml | 4 ---- interface/resources/qml/controls-uit/WebSpinner.qml | 4 ---- interface/resources/qml/controls-uit/WebView.qml | 4 ---- interface/resources/qml/styles-uit/AnonymousProRegular.qml | 4 ---- interface/resources/qml/styles-uit/ButtonLabel.qml | 4 ---- interface/resources/qml/styles-uit/FiraSansRegular.qml | 4 ---- interface/resources/qml/styles-uit/FiraSansSemiBold.qml | 4 ---- interface/resources/qml/styles-uit/HiFiGlyphs.qml | 4 ---- interface/resources/qml/styles-uit/HifiConstants.qml | 4 ---- interface/resources/qml/styles-uit/IconButton.qml | 4 ---- interface/resources/qml/styles-uit/InfoItem.qml | 4 ---- interface/resources/qml/styles-uit/InputLabel.qml | 4 ---- interface/resources/qml/styles-uit/ListItem.qml | 4 ---- interface/resources/qml/styles-uit/Logs.qml | 4 ---- interface/resources/qml/styles-uit/OverlayTitle.qml | 4 ---- interface/resources/qml/styles-uit/RalewayBold.qml | 4 ---- interface/resources/qml/styles-uit/RalewayLight.qml | 4 ---- interface/resources/qml/styles-uit/RalewayRegular.qml | 4 ---- interface/resources/qml/styles-uit/RalewaySemiBold.qml | 4 ---- interface/resources/qml/styles-uit/SectionName.qml | 4 ---- interface/resources/qml/styles-uit/Separator.qml | 4 ---- interface/resources/qml/styles-uit/ShortcutText.qml | 4 ---- interface/resources/qml/styles-uit/TabName.qml | 4 ---- interface/resources/qml/styles-uit/TextFieldInput.qml | 4 ---- 55 files changed, 220 deletions(-) diff --git a/interface/resources/qml/controls-uit/AttachmentsTable.qml b/interface/resources/qml/controls-uit/AttachmentsTable.qml index fdd62b7c84..e7fe874610 100644 --- a/interface/resources/qml/controls-uit/AttachmentsTable.qml +++ b/interface/resources/qml/controls-uit/AttachmentsTable.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 AttachmentsTable { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } \ No newline at end of file diff --git a/interface/resources/qml/controls-uit/BaseWebView.qml b/interface/resources/qml/controls-uit/BaseWebView.qml index 6ff58d1e47..61f428e9f7 100644 --- a/interface/resources/qml/controls-uit/BaseWebView.qml +++ b/interface/resources/qml/controls-uit/BaseWebView.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 BaseWebView { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } \ No newline at end of file diff --git a/interface/resources/qml/controls-uit/Button.qml b/interface/resources/qml/controls-uit/Button.qml index ddadd0038a..1d31f02777 100644 --- a/interface/resources/qml/controls-uit/Button.qml +++ b/interface/resources/qml/controls-uit/Button.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 Button { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index 01ffd3c788..c10bea08c4 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 CheckBox { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/CheckBoxQQC2.qml b/interface/resources/qml/controls-uit/CheckBoxQQC2.qml index 9950e4b47f..af758ee707 100644 --- a/interface/resources/qml/controls-uit/CheckBoxQQC2.qml +++ b/interface/resources/qml/controls-uit/CheckBoxQQC2.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 CheckBoxQQC2 { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } \ No newline at end of file diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml index 74a68706bf..8ac92909b6 100644 --- a/interface/resources/qml/controls-uit/ComboBox.qml +++ b/interface/resources/qml/controls-uit/ComboBox.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 ComboBox { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/ContentSection.qml b/interface/resources/qml/controls-uit/ContentSection.qml index d6cdd92722..d41b5ad8e6 100644 --- a/interface/resources/qml/controls-uit/ContentSection.qml +++ b/interface/resources/qml/controls-uit/ContentSection.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 ContentSection { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/FilterBar.qml b/interface/resources/qml/controls-uit/FilterBar.qml index a9b3c350b4..dede6d2ded 100644 --- a/interface/resources/qml/controls-uit/FilterBar.qml +++ b/interface/resources/qml/controls-uit/FilterBar.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 FilterBar { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/GlyphButton.qml b/interface/resources/qml/controls-uit/GlyphButton.qml index e6dbec69a1..3a0f8631a8 100644 --- a/interface/resources/qml/controls-uit/GlyphButton.qml +++ b/interface/resources/qml/controls-uit/GlyphButton.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 GlyphButton { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/HorizontalRule.qml b/interface/resources/qml/controls-uit/HorizontalRule.qml index b0eaa56778..7fc2269649 100644 --- a/interface/resources/qml/controls-uit/HorizontalRule.qml +++ b/interface/resources/qml/controls-uit/HorizontalRule.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 HorizontalRule { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/HorizontalSpacer.qml b/interface/resources/qml/controls-uit/HorizontalSpacer.qml index f9bf73f6e0..b4f372545c 100644 --- a/interface/resources/qml/controls-uit/HorizontalSpacer.qml +++ b/interface/resources/qml/controls-uit/HorizontalSpacer.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 HorizontalSpacer { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/ImageMessageBox.qml b/interface/resources/qml/controls-uit/ImageMessageBox.qml index efdf513b19..484a17dd7c 100644 --- a/interface/resources/qml/controls-uit/ImageMessageBox.qml +++ b/interface/resources/qml/controls-uit/ImageMessageBox.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 ImageMessageBox { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/Key.qml b/interface/resources/qml/controls-uit/Key.qml index 844cb0c560..c031c2f660 100644 --- a/interface/resources/qml/controls-uit/Key.qml +++ b/interface/resources/qml/controls-uit/Key.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 Key { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/Keyboard.qml b/interface/resources/qml/controls-uit/Keyboard.qml index 5c1622d594..2bdf682b9a 100644 --- a/interface/resources/qml/controls-uit/Keyboard.qml +++ b/interface/resources/qml/controls-uit/Keyboard.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 Keyboard { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/Label.qml b/interface/resources/qml/controls-uit/Label.qml index 824f6436bb..032367208b 100644 --- a/interface/resources/qml/controls-uit/Label.qml +++ b/interface/resources/qml/controls-uit/Label.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 Label { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/QueuedButton.qml b/interface/resources/qml/controls-uit/QueuedButton.qml index 4ec44c16d5..54d366a663 100644 --- a/interface/resources/qml/controls-uit/QueuedButton.qml +++ b/interface/resources/qml/controls-uit/QueuedButton.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 QueuedButton { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/RadioButton.qml b/interface/resources/qml/controls-uit/RadioButton.qml index dab757279c..46eb76e7ce 100644 --- a/interface/resources/qml/controls-uit/RadioButton.qml +++ b/interface/resources/qml/controls-uit/RadioButton.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 RadioButton { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/ScrollBar.qml b/interface/resources/qml/controls-uit/ScrollBar.qml index 71c4c956b0..1b21509819 100644 --- a/interface/resources/qml/controls-uit/ScrollBar.qml +++ b/interface/resources/qml/controls-uit/ScrollBar.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 ScrollBar { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/Separator.qml b/interface/resources/qml/controls-uit/Separator.qml index 6be7d7cb40..baeefc7895 100644 --- a/interface/resources/qml/controls-uit/Separator.qml +++ b/interface/resources/qml/controls-uit/Separator.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 Separator { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/Slider.qml b/interface/resources/qml/controls-uit/Slider.qml index 5cdd07a35a..1bcdbc39b7 100644 --- a/interface/resources/qml/controls-uit/Slider.qml +++ b/interface/resources/qml/controls-uit/Slider.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 Slider { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } \ No newline at end of file diff --git a/interface/resources/qml/controls-uit/SpinBox.qml b/interface/resources/qml/controls-uit/SpinBox.qml index 56f360c286..c202a3997c 100644 --- a/interface/resources/qml/controls-uit/SpinBox.qml +++ b/interface/resources/qml/controls-uit/SpinBox.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 SpinBox { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/Switch.qml b/interface/resources/qml/controls-uit/Switch.qml index 77d1d4e081..96ff7f8898 100644 --- a/interface/resources/qml/controls-uit/Switch.qml +++ b/interface/resources/qml/controls-uit/Switch.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 Switch { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index 6ca7675db6..3a4932302f 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 Table { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/TabletContentSection.qml b/interface/resources/qml/controls-uit/TabletContentSection.qml index 0738426f4b..70075d3858 100644 --- a/interface/resources/qml/controls-uit/TabletContentSection.qml +++ b/interface/resources/qml/controls-uit/TabletContentSection.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 TabletContentSection { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/TabletHeader.qml b/interface/resources/qml/controls-uit/TabletHeader.qml index 689a6e1dfc..53e660d708 100644 --- a/interface/resources/qml/controls-uit/TabletHeader.qml +++ b/interface/resources/qml/controls-uit/TabletHeader.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 TabletHeader { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/TextAction.qml b/interface/resources/qml/controls-uit/TextAction.qml index 617418d992..ce5a973776 100644 --- a/interface/resources/qml/controls-uit/TextAction.qml +++ b/interface/resources/qml/controls-uit/TextAction.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 TextAction { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/TextEdit.qml b/interface/resources/qml/controls-uit/TextEdit.qml index 99d6b43234..783950bd1e 100644 --- a/interface/resources/qml/controls-uit/TextEdit.qml +++ b/interface/resources/qml/controls-uit/TextEdit.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 TextEdit { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/TextField.qml b/interface/resources/qml/controls-uit/TextField.qml index 8f44823c3b..261a4162ef 100644 --- a/interface/resources/qml/controls-uit/TextField.qml +++ b/interface/resources/qml/controls-uit/TextField.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 TextField { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/ToolTip.qml b/interface/resources/qml/controls-uit/ToolTip.qml index 29b98bf572..a5f24c7974 100644 --- a/interface/resources/qml/controls-uit/ToolTip.qml +++ b/interface/resources/qml/controls-uit/ToolTip.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 ToolTip { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/Tree.qml b/interface/resources/qml/controls-uit/Tree.qml index 3eca0fa1f6..a21cdc6d47 100644 --- a/interface/resources/qml/controls-uit/Tree.qml +++ b/interface/resources/qml/controls-uit/Tree.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 Tree { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/VerticalSpacer.qml b/interface/resources/qml/controls-uit/VerticalSpacer.qml index f05b83d9c7..42b48b003e 100644 --- a/interface/resources/qml/controls-uit/VerticalSpacer.qml +++ b/interface/resources/qml/controls-uit/VerticalSpacer.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 VerticalSpacer { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/WebGlyphButton.qml b/interface/resources/qml/controls-uit/WebGlyphButton.qml index 57201edcda..1377b937a2 100644 --- a/interface/resources/qml/controls-uit/WebGlyphButton.qml +++ b/interface/resources/qml/controls-uit/WebGlyphButton.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 WebGlyphButton { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/WebSpinner.qml b/interface/resources/qml/controls-uit/WebSpinner.qml index 4ab8a5475e..a20597d8dc 100644 --- a/interface/resources/qml/controls-uit/WebSpinner.qml +++ b/interface/resources/qml/controls-uit/WebSpinner.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 WebSpinner { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/controls-uit/WebView.qml b/interface/resources/qml/controls-uit/WebView.qml index 4d2e37903b..b11dee7f3b 100644 --- a/interface/resources/qml/controls-uit/WebView.qml +++ b/interface/resources/qml/controls-uit/WebView.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import controlsUit 1.0 WebView { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import controlsUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/AnonymousProRegular.qml b/interface/resources/qml/styles-uit/AnonymousProRegular.qml index 09e6c1d0b0..87a91f183a 100644 --- a/interface/resources/qml/styles-uit/AnonymousProRegular.qml +++ b/interface/resources/qml/styles-uit/AnonymousProRegular.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 AnonymousProRegular { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/ButtonLabel.qml b/interface/resources/qml/styles-uit/ButtonLabel.qml index 81f7928240..d677cd06ca 100644 --- a/interface/resources/qml/styles-uit/ButtonLabel.qml +++ b/interface/resources/qml/styles-uit/ButtonLabel.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 ButtonLabel { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/FiraSansRegular.qml b/interface/resources/qml/styles-uit/FiraSansRegular.qml index d67fba71f8..65ee891443 100644 --- a/interface/resources/qml/styles-uit/FiraSansRegular.qml +++ b/interface/resources/qml/styles-uit/FiraSansRegular.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 FiraSansRegular { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/FiraSansSemiBold.qml b/interface/resources/qml/styles-uit/FiraSansSemiBold.qml index e9f49a5715..635523feb9 100644 --- a/interface/resources/qml/styles-uit/FiraSansSemiBold.qml +++ b/interface/resources/qml/styles-uit/FiraSansSemiBold.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 FiraSansSemiBold { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/HiFiGlyphs.qml b/interface/resources/qml/styles-uit/HiFiGlyphs.qml index 129afb732e..646cfc2988 100644 --- a/interface/resources/qml/styles-uit/HiFiGlyphs.qml +++ b/interface/resources/qml/styles-uit/HiFiGlyphs.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 HiFiGlyphs { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index cf753d5c8d..24b24cf019 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 HifiConstants { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/IconButton.qml b/interface/resources/qml/styles-uit/IconButton.qml index 93f37e9d7a..e60ed9eb10 100644 --- a/interface/resources/qml/styles-uit/IconButton.qml +++ b/interface/resources/qml/styles-uit/IconButton.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 IconButton { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/InfoItem.qml b/interface/resources/qml/styles-uit/InfoItem.qml index 57397b4c6f..d09f26649d 100644 --- a/interface/resources/qml/styles-uit/InfoItem.qml +++ b/interface/resources/qml/styles-uit/InfoItem.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 InfoItem { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/InputLabel.qml b/interface/resources/qml/styles-uit/InputLabel.qml index 5b03ca1686..89d74afc8f 100644 --- a/interface/resources/qml/styles-uit/InputLabel.qml +++ b/interface/resources/qml/styles-uit/InputLabel.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 InputLabel { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/ListItem.qml b/interface/resources/qml/styles-uit/ListItem.qml index e98d9bab81..8d72762644 100644 --- a/interface/resources/qml/styles-uit/ListItem.qml +++ b/interface/resources/qml/styles-uit/ListItem.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 ListItem { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/Logs.qml b/interface/resources/qml/styles-uit/Logs.qml index 66d2eaeb2e..d9453afade 100644 --- a/interface/resources/qml/styles-uit/Logs.qml +++ b/interface/resources/qml/styles-uit/Logs.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 Logs { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/OverlayTitle.qml b/interface/resources/qml/styles-uit/OverlayTitle.qml index ec9cf33482..51fc11a77d 100644 --- a/interface/resources/qml/styles-uit/OverlayTitle.qml +++ b/interface/resources/qml/styles-uit/OverlayTitle.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 OverlayTitle { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/RalewayBold.qml b/interface/resources/qml/styles-uit/RalewayBold.qml index 2370401ba2..1373859b32 100644 --- a/interface/resources/qml/styles-uit/RalewayBold.qml +++ b/interface/resources/qml/styles-uit/RalewayBold.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 RalewayBold { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/RalewayLight.qml b/interface/resources/qml/styles-uit/RalewayLight.qml index 0641a2746a..9573eb6649 100644 --- a/interface/resources/qml/styles-uit/RalewayLight.qml +++ b/interface/resources/qml/styles-uit/RalewayLight.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 RalewayLight { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/RalewayRegular.qml b/interface/resources/qml/styles-uit/RalewayRegular.qml index cf601f5c91..d5a66ff696 100644 --- a/interface/resources/qml/styles-uit/RalewayRegular.qml +++ b/interface/resources/qml/styles-uit/RalewayRegular.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 RalewayRegular { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/RalewaySemiBold.qml b/interface/resources/qml/styles-uit/RalewaySemiBold.qml index a3c7cfe864..874a48555b 100644 --- a/interface/resources/qml/styles-uit/RalewaySemiBold.qml +++ b/interface/resources/qml/styles-uit/RalewaySemiBold.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 RalewaySemiBold { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/SectionName.qml b/interface/resources/qml/styles-uit/SectionName.qml index b3cb143988..819ed87d8b 100644 --- a/interface/resources/qml/styles-uit/SectionName.qml +++ b/interface/resources/qml/styles-uit/SectionName.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 SectionName { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/Separator.qml b/interface/resources/qml/styles-uit/Separator.qml index 9687fb0520..9708ec4c26 100644 --- a/interface/resources/qml/styles-uit/Separator.qml +++ b/interface/resources/qml/styles-uit/Separator.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 Separator { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/ShortcutText.qml b/interface/resources/qml/styles-uit/ShortcutText.qml index 921000651a..16e1b2f1c1 100644 --- a/interface/resources/qml/styles-uit/ShortcutText.qml +++ b/interface/resources/qml/styles-uit/ShortcutText.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 ShortcutText { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/TabName.qml b/interface/resources/qml/styles-uit/TabName.qml index 001c110a41..b66192e582 100644 --- a/interface/resources/qml/styles-uit/TabName.qml +++ b/interface/resources/qml/styles-uit/TabName.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 TabName { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } diff --git a/interface/resources/qml/styles-uit/TextFieldInput.qml b/interface/resources/qml/styles-uit/TextFieldInput.qml index ce5bc9edbd..1498dde821 100644 --- a/interface/resources/qml/styles-uit/TextFieldInput.qml +++ b/interface/resources/qml/styles-uit/TextFieldInput.qml @@ -1,8 +1,4 @@ -import QtQuick 2.0 import stylesUit 1.0 TextFieldInput { - Component.onCompleted: { - console.warn("warning: including controls-uit is deprecated! please use 'import stylesUit 1.0' instead"); - } } From a3dfc873b7caa9471c330cf28784f7c2c545d55c Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Sat, 3 Nov 2018 11:06:21 +0530 Subject: [PATCH 347/362] rsolve rebase conflict --- interface/resources/qml/hifi/dialogs/security/Security.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml index 96a554838f..08605033a5 100644 --- a/interface/resources/qml/hifi/dialogs/security/Security.qml +++ b/interface/resources/qml/hifi/dialogs/security/Security.qml @@ -14,8 +14,8 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 import QtGraphicalEffects 1.0 -import "qrc:////qml//styles-uit" as HifiStylesUit -import "qrc:////qml//controls-uit" as HifiControlsUit +import stylesUit 1.0 as HifiStylesUit +import controlsUit 1.0 as as HifiControlsUit import "qrc:////qml//controls" as HifiControls import "qrc:////qml//hifi//commerce//common" as HifiCommerceCommon From 53742e90f57d096f8aa73eec369e108f636e2927 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 6 Nov 2018 10:30:10 -0800 Subject: [PATCH 348/362] Cleanup after merge --- libraries/audio-client/src/AudioClient.h | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 788b764903..5e7f1fb8a0 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -395,7 +395,7 @@ private: void configureReverb(); void updateReverbOptions(); - void handleLocalEchoAndReverb(QByteArray& inputByteArray, const int& sampleRate, const int& channelCount); + void handleLocalEchoAndReverb(QByteArray& inputByteArray); bool switchInputToAudioDevice(const QAudioDeviceInfo inputDeviceInfo, bool isShutdownRequest = false); bool switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceInfo, bool isShutdownRequest = false); @@ -453,12 +453,6 @@ private: QTimer* _checkPeakValuesTimer { nullptr }; bool _isRecording { false }; - - Mutex _TTSMutex; - bool _isProcessingTTS { false }; - QByteArray _TTSAudioBuffer; - int _TTSChunkSize = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * 50; - QTimer _TTSTimer; }; From c33f9b6ea311e8a88b7ad69e6a9c37da9fe56572 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 6 Nov 2018 11:26:09 -0800 Subject: [PATCH 349/362] Fix build error --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 07705c8cd5..bd126048dd 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2955,7 +2955,7 @@ void Application::initializeUi() { QUrl{ "hifi/dialogs/security/SecurityImageChange.qml" }, QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" }, QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, - }, callback); + }, commerceCallback); QmlContextCallback ttsCallback = [](QQmlContext* context) { context->setContextProperty("TextToSpeech", DependencyManager::get().data()); }; From 1444b3a18668961a9cd4cc3fea57c0442bf21062 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 6 Nov 2018 09:57:51 -0800 Subject: [PATCH 350/362] resource mutex issues (cherry picked from commit 750629a6194ec98534746a377b3ba027c1dcc20e) (cherry picked from commit fc5ab3289a5f88f245c2825edd085532bd4c97ca) --- libraries/networking/src/ResourceCache.cpp | 2 ++ libraries/networking/src/ResourceCache.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 7aad8d468a..392bc5b7af 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -740,6 +740,8 @@ void Resource::handleReplyFinished() { setSize(_bytesTotal); + // Make sure we keep the Resource alive here + auto self = _self.lock(); ResourceCache::requestCompleted(_self); auto result = _request->getResult(); diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index 33490301d7..c632399ad4 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -62,7 +62,7 @@ static const qint64 MAX_UNUSED_MAX_SIZE = MAXIMUM_CACHE_SIZE; class ResourceCacheSharedItems : public Dependency { SINGLETON_DEPENDENCY - using Mutex = std::mutex; + using Mutex = std::recursive_mutex; using Lock = std::unique_lock; public: From 378bf911d447d02611b1f65dabc621a93b4fd774 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 6 Nov 2018 12:48:17 -0800 Subject: [PATCH 351/362] Fix another build error --- interface/src/scripting/TTSScriptingInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/TTSScriptingInterface.cpp b/interface/src/scripting/TTSScriptingInterface.cpp index 6a5b72ea5f..b41f22759c 100644 --- a/interface/src/scripting/TTSScriptingInterface.cpp +++ b/interface/src/scripting/TTSScriptingInterface.cpp @@ -61,7 +61,7 @@ private: }; #endif -const std::chrono::milliseconds INJECTOR_INTERVAL_MS = std::chrono::milliseconds(100); +const int INJECTOR_INTERVAL_MS = 100; void TTSScriptingInterface::updateLastSoundAudioInjector() { if (_lastSoundAudioInjector) { AudioInjectorOptions options; From f22bbff505a6c6212b707f4fe0022abbf1aa7a81 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 6 Nov 2018 15:47:47 -0800 Subject: [PATCH 352/362] Fix MS19743: Fix case where user couldn't update certain items --- .../qml/hifi/commerce/checkout/Checkout.qml | 13 +++++++++++-- .../qml/hifi/commerce/wallet/WalletHome.qml | 1 - interface/src/commerce/Ledger.cpp | 4 +++- interface/src/commerce/Ledger.h | 2 +- interface/src/commerce/QmlCommerce.cpp | 4 ++-- interface/src/commerce/QmlCommerce.h | 2 +- 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 271aab87d1..7000eede41 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -55,6 +55,7 @@ Rectangle { property bool isInstalled; property bool isUpdating; property string baseAppURL; + property int currentUpdatesPage: 1; // Style color: hifi.colors.white; Connections { @@ -156,8 +157,14 @@ Rectangle { break; } } - root.availableUpdatesReceived = true; - refreshBuyUI(); + + if (result.data.updates.length === 0 || root.isUpdating) { + root.availableUpdatesReceived = true; + refreshBuyUI(); + } else { + currentUpdatesPage++; + Commerce.getAvailableUpdates(root.itemId, currentUpdatesPage) + } } } @@ -176,6 +183,7 @@ Rectangle { root.ownershipStatusReceived = false; Commerce.alreadyOwned(root.itemId); root.availableUpdatesReceived = false; + root.currentUpdatesPage = 1; Commerce.getAvailableUpdates(root.itemId); itemPreviewImage.source = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg"; } @@ -1181,6 +1189,7 @@ Rectangle { root.ownershipStatusReceived = false; Commerce.alreadyOwned(root.itemId); root.availableUpdatesReceived = false; + root.currentUpdatesPage = 1; Commerce.getAvailableUpdates(root.itemId); root.balanceReceived = false; Commerce.balance(); diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index d32017189e..eeee0f7262 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -29,7 +29,6 @@ Item { if (visible) { Commerce.balance(); transactionHistoryModel.getFirstPage(); - Commerce.getAvailableUpdates(); } else { refreshTimer.stop(); } diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 0c59fbc6d0..3512677650 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -454,7 +454,7 @@ void Ledger::alreadyOwned(const QString& marketplaceId) { } } -void Ledger::getAvailableUpdates(const QString& itemId) { +void Ledger::getAvailableUpdates(const QString& itemId, const int& pageNumber, const int& itemsPerPage) { auto wallet = DependencyManager::get(); QString endpoint = "available_updates"; QJsonObject request; @@ -462,6 +462,8 @@ void Ledger::getAvailableUpdates(const QString& itemId) { if (!itemId.isEmpty()) { request["marketplace_item_id"] = itemId; } + request["per_page"] = itemsPerPage; + request["page"] = pageNumber; send(endpoint, "availableUpdatesSuccess", "availableUpdatesFailure", QNetworkAccessManager::PutOperation, AccountManagerAuth::Required, request); } diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 427395ee11..715d6337ad 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -37,7 +37,7 @@ public: void transferAssetToNode(const QString& hfc_key, const QString& nodeID, const QString& certificateID, const int& amount, const QString& optionalMessage); void transferAssetToUsername(const QString& hfc_key, const QString& username, const QString& certificateID, const int& amount, const QString& optionalMessage); void alreadyOwned(const QString& marketplaceId); - void getAvailableUpdates(const QString& itemId = ""); + void getAvailableUpdates(const QString& itemId = "", const int& pageNumber = 1, const int& itemsPerPage = 10); void updateItem(const QString& hfc_key, const QString& certificate_id); enum CertificateStatus { diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index ffe89ffc5b..0ef26a62b3 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -441,9 +441,9 @@ bool QmlCommerce::openApp(const QString& itemHref) { return true; } -void QmlCommerce::getAvailableUpdates(const QString& itemId) { +void QmlCommerce::getAvailableUpdates(const QString& itemId, const int& pageNumber, const int& itemsPerPage) { auto ledger = DependencyManager::get(); - ledger->getAvailableUpdates(itemId); + ledger->getAvailableUpdates(itemId, pageNumber, itemsPerPage); } void QmlCommerce::updateItem(const QString& certificateId) { diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 2e3c0ec24d..c5fbdaf4a4 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -92,7 +92,7 @@ protected: Q_INVOKABLE bool uninstallApp(const QString& appHref); Q_INVOKABLE bool openApp(const QString& appHref); - Q_INVOKABLE void getAvailableUpdates(const QString& itemId = ""); + Q_INVOKABLE void getAvailableUpdates(const QString& itemId = "", const int& pageNumber = 1, const int& itemsPerPage = 10); Q_INVOKABLE void updateItem(const QString& certificateId); private: From 87760282596840a780b275628fa6b3a1b366f32c Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 6 Nov 2018 16:10:49 -0800 Subject: [PATCH 353/362] Prepend "root" to "currentUpdatesPage" --- interface/resources/qml/hifi/commerce/checkout/Checkout.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 7000eede41..098011f174 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -162,7 +162,7 @@ Rectangle { root.availableUpdatesReceived = true; refreshBuyUI(); } else { - currentUpdatesPage++; + root.currentUpdatesPage++; Commerce.getAvailableUpdates(root.itemId, currentUpdatesPage) } } From c40ded598da5392f4d49049667ab74f504667fb8 Mon Sep 17 00:00:00 2001 From: sam gateau Date: Wed, 7 Nov 2018 12:18:52 -0800 Subject: [PATCH 354/362] REmoving any attempt to fix anything in this pr --- interface/src/ui/overlays/Overlay.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index 1bf94adfa0..22cf924727 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -247,7 +247,7 @@ void Overlay::removeMaterial(graphics::MaterialPointer material, const std::stri } render::ItemKey Overlay::getKey() { - auto builder = render::ItemKey::Builder().withTypeShape().withTypeMeta(); + auto builder = render::ItemKey::Builder().withTypeShape(); builder.withViewSpace(); builder.withLayer(render::hifi::LAYER_2D); From d316fe15ce24fb406f449bf10fa4ec3f6eccf909 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 7 Nov 2018 15:02:32 -0800 Subject: [PATCH 355/362] fix tablet culling --- interface/src/ui/overlays/Base3DOverlay.cpp | 1 + interface/src/ui/overlays/Base3DOverlay.h | 2 -- interface/src/ui/overlays/ModelOverlay.cpp | 2 ++ interface/src/ui/overlays/Overlay.cpp | 2 +- interface/src/ui/overlays/Volume3DOverlay.cpp | 18 ++++-------------- interface/src/ui/overlays/Volume3DOverlay.h | 3 +-- 6 files changed, 9 insertions(+), 19 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 14c1f1a15a..8599e05332 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -290,6 +290,7 @@ void Base3DOverlay::locationChanged(bool tellPhysics) { notifyRenderVariableChange(); } +// FIXME: Overlays shouldn't be deleted when their parents are void Base3DOverlay::parentDeleted() { qApp->getOverlays().deleteOverlay(getOverlayID()); } diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 6f6092a42e..6cc5182b56 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -59,8 +59,6 @@ public: void setIsGrabbable(bool value) { _isGrabbable = value; } virtual void setIsVisibleInSecondaryCamera(bool value) { _isVisibleInSecondaryCamera = value; } - virtual AABox getBounds() const override = 0; - void update(float deltatime) override; void notifyRenderVariableChange() const; diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 190d9c3895..fa4ced84a8 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -60,6 +60,8 @@ ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) : } void ModelOverlay::update(float deltatime) { + Base3DOverlay::update(deltatime); + if (_updateModel) { _updateModel = false; _model->setSnapModelToCenter(true); diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index 22cf924727..1bf94adfa0 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -247,7 +247,7 @@ void Overlay::removeMaterial(graphics::MaterialPointer material, const std::stri } render::ItemKey Overlay::getKey() { - auto builder = render::ItemKey::Builder().withTypeShape(); + auto builder = render::ItemKey::Builder().withTypeShape().withTypeMeta(); builder.withViewSpace(); builder.withLayer(render::hifi::LAYER_2D); diff --git a/interface/src/ui/overlays/Volume3DOverlay.cpp b/interface/src/ui/overlays/Volume3DOverlay.cpp index a307d445c0..0cceb44a36 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.cpp +++ b/interface/src/ui/overlays/Volume3DOverlay.cpp @@ -20,11 +20,9 @@ Volume3DOverlay::Volume3DOverlay(const Volume3DOverlay* volume3DOverlay) : } AABox Volume3DOverlay::getBounds() const { - auto extents = Extents{_localBoundingBox}; - extents.rotate(getWorldOrientation()); - extents.shiftBy(getWorldPosition()); - - return AABox(extents); + AABox bounds = _localBoundingBox; + bounds.transform(getTransform()); + return bounds; } void Volume3DOverlay::setDimensions(const glm::vec3& value) { @@ -49,15 +47,7 @@ void Volume3DOverlay::setProperties(const QVariantMap& properties) { glm::vec3 scale = vec3FromVariant(dimensions); // don't allow a zero or negative dimension component to reach the renderTransform const float MIN_DIMENSION = 0.0001f; - if (scale.x < MIN_DIMENSION) { - scale.x = MIN_DIMENSION; - } - if (scale.y < MIN_DIMENSION) { - scale.y = MIN_DIMENSION; - } - if (scale.z < MIN_DIMENSION) { - scale.z = MIN_DIMENSION; - } + scale = glm::max(scale, MIN_DIMENSION); setDimensions(scale); } } diff --git a/interface/src/ui/overlays/Volume3DOverlay.h b/interface/src/ui/overlays/Volume3DOverlay.h index e4060ae335..2083f7344a 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.h +++ b/interface/src/ui/overlays/Volume3DOverlay.h @@ -24,7 +24,6 @@ public: virtual AABox getBounds() const override; const glm::vec3& getDimensions() const { return _localBoundingBox.getDimensions(); } - void setDimensions(float value) { setDimensions(glm::vec3(value)); } void setDimensions(const glm::vec3& value); void setProperties(const QVariantMap& properties) override; @@ -37,7 +36,7 @@ public: protected: // Centered local bounding box - AABox _localBoundingBox{ vec3(0.0f), 1.0f }; + AABox _localBoundingBox { vec3(-0.5), 1.0f }; Transform evalRenderTransform() override; }; From cee1454f6e963598fe6409224bd3c7f070760a57 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 7 Nov 2018 15:09:53 -0800 Subject: [PATCH 356/362] CR feedback - thanks Ken! --- interface/src/scripting/TTSScriptingInterface.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/interface/src/scripting/TTSScriptingInterface.cpp b/interface/src/scripting/TTSScriptingInterface.cpp index b41f22759c..6b1677aecb 100644 --- a/interface/src/scripting/TTSScriptingInterface.cpp +++ b/interface/src/scripting/TTSScriptingInterface.cpp @@ -75,7 +75,7 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak) { #ifdef WIN32 WAVEFORMATEX fmt; fmt.wFormatTag = WAVE_FORMAT_PCM; - fmt.nSamplesPerSec = 24000; + fmt.nSamplesPerSec = AudioConstants::SAMPLE_RATE; fmt.wBitsPerSample = 16; fmt.nChannels = 1; fmt.nBlockAlign = fmt.nChannels * fmt.wBitsPerSample / 8; @@ -132,17 +132,13 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak) { hr = IStream_Size(pStream, &StreamSize); DWORD dwSize = StreamSize.QuadPart; - char* buf1 = new char[dwSize + 1]; - memset(buf1, 0, dwSize + 1); + _lastSoundByteArray.resize(dwSize); - hr = IStream_Read(pStream, buf1, dwSize); + hr = IStream_Read(pStream, _lastSoundByteArray.data(), dwSize); if (FAILED(hr)) { qDebug() << "Couldn't read from stream."; } - _lastSoundByteArray.resize(0); - _lastSoundByteArray.append(buf1, dwSize); - AudioInjectorOptions options; options.position = DependencyManager::get()->getMyAvatarPosition(); From 501746b156c9c839278b3fb33384e6dc43330858 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Mon, 29 Oct 2018 16:37:10 -0700 Subject: [PATCH 357/362] 3D Keyboard --- interface/resources/config/keyboard.json | 2476 +++++++++++++++++ interface/resources/meshes/drumstick.fbx | Bin 0 -> 63952 bytes .../resources/meshes/keyboard/SM_enter.fbx | Bin 0 -> 33248 bytes .../resources/meshes/keyboard/SM_key.fbx | Bin 0 -> 32448 bytes .../resources/meshes/keyboard/SM_space.fbx | Bin 0 -> 30400 bytes .../resources/meshes/keyboard/keyCap_F.png | Bin 0 -> 1670 bytes .../resources/meshes/keyboard/keyCap_a.png | Bin 0 -> 3081 bytes .../resources/meshes/keyboard/keyCap_b.png | Bin 0 -> 2744 bytes .../resources/meshes/keyboard/keyCap_c.png | Bin 0 -> 3421 bytes .../resources/meshes/keyboard/keyCap_d.png | Bin 0 -> 2648 bytes .../resources/meshes/keyboard/keyCap_e.png | Bin 0 -> 1689 bytes .../resources/meshes/keyboard/keyCap_g.png | Bin 0 -> 3298 bytes .../resources/meshes/keyboard/keyCap_h.png | Bin 0 -> 1616 bytes .../resources/meshes/keyboard/keyCap_i.png | Bin 0 -> 1581 bytes .../resources/meshes/keyboard/keyCap_j.png | Bin 0 -> 2213 bytes .../resources/meshes/keyboard/keyCap_k.png | Bin 0 -> 2800 bytes .../resources/meshes/keyboard/keyCap_l.png | Bin 0 -> 1631 bytes .../resources/meshes/keyboard/keyCap_m.png | Bin 0 -> 2714 bytes .../resources/meshes/keyboard/keyCap_n.png | Bin 0 -> 2230 bytes .../resources/meshes/keyboard/keyCap_o.png | Bin 0 -> 3508 bytes .../resources/meshes/keyboard/keyCap_p.png | Bin 0 -> 2282 bytes .../resources/meshes/keyboard/keyCap_q.png | Bin 0 -> 3681 bytes .../resources/meshes/keyboard/keyCap_r.png | Bin 0 -> 2833 bytes .../resources/meshes/keyboard/keyCap_s.png | Bin 0 -> 3405 bytes .../resources/meshes/keyboard/keyCap_t.png | Bin 0 -> 1641 bytes .../resources/meshes/keyboard/keyCap_u.png | Bin 0 -> 2537 bytes .../resources/meshes/keyboard/keyCap_v.png | Bin 0 -> 3024 bytes .../resources/meshes/keyboard/keyCap_w.png | Bin 0 -> 4149 bytes .../resources/meshes/keyboard/keyCap_x.png | Bin 0 -> 3327 bytes .../resources/meshes/keyboard/keyCap_y.png | Bin 0 -> 2701 bytes .../resources/meshes/keyboard/keyCap_z.png | Bin 0 -> 2394 bytes interface/resources/meshes/keyboard/key_0.png | Bin 0 -> 3132 bytes interface/resources/meshes/keyboard/key_1.png | Bin 0 -> 1909 bytes .../resources/meshes/keyboard/key_123.png | Bin 0 -> 4399 bytes interface/resources/meshes/keyboard/key_2.png | Bin 0 -> 2864 bytes interface/resources/meshes/keyboard/key_3.png | Bin 0 -> 3341 bytes interface/resources/meshes/keyboard/key_4.png | Bin 0 -> 2126 bytes interface/resources/meshes/keyboard/key_5.png | Bin 0 -> 3071 bytes interface/resources/meshes/keyboard/key_6.png | Bin 0 -> 3631 bytes interface/resources/meshes/keyboard/key_7.png | Bin 0 -> 1921 bytes interface/resources/meshes/keyboard/key_8.png | Bin 0 -> 3535 bytes interface/resources/meshes/keyboard/key_9.png | Bin 0 -> 3617 bytes interface/resources/meshes/keyboard/key_a.png | Bin 0 -> 3066 bytes .../resources/meshes/keyboard/key_abc.png | Bin 0 -> 5346 bytes .../meshes/keyboard/key_ampersand.png | Bin 0 -> 3695 bytes .../resources/meshes/keyboard/key_ast.png | Bin 0 -> 2196 bytes .../resources/meshes/keyboard/key_at.png | Bin 0 -> 4722 bytes interface/resources/meshes/keyboard/key_b.png | Bin 0 -> 2839 bytes .../meshes/keyboard/key_backspace.png | Bin 0 -> 1987 bytes interface/resources/meshes/keyboard/key_c.png | Bin 0 -> 2950 bytes .../resources/meshes/keyboard/key_cap.png | Bin 0 -> 2088 bytes .../resources/meshes/keyboard/key_caret.png | Bin 0 -> 2209 bytes .../meshes/keyboard/key_close_paren.png | Bin 0 -> 2621 bytes .../resources/meshes/keyboard/key_colon.png | Bin 0 -> 1640 bytes .../resources/meshes/keyboard/key_comma.png | Bin 0 -> 1785 bytes interface/resources/meshes/keyboard/key_d.png | Bin 0 -> 2962 bytes .../resources/meshes/keyboard/key_dollar.png | Bin 0 -> 3481 bytes .../resources/meshes/keyboard/key_dquote.png | Bin 0 -> 1918 bytes interface/resources/meshes/keyboard/key_e.png | Bin 0 -> 3029 bytes .../resources/meshes/keyboard/key_enter.png | Bin 0 -> 2201 bytes .../resources/meshes/keyboard/key_exclam.png | Bin 0 -> 1633 bytes .../resources/meshes/keyboard/key_exit.png | Bin 0 -> 2466 bytes interface/resources/meshes/keyboard/key_f.png | Bin 0 -> 2112 bytes interface/resources/meshes/keyboard/key_g.png | Bin 0 -> 3480 bytes interface/resources/meshes/keyboard/key_h.png | Bin 0 -> 2186 bytes .../resources/meshes/keyboard/key_hashtag.png | Bin 0 -> 2394 bytes interface/resources/meshes/keyboard/key_i.png | Bin 0 -> 1625 bytes interface/resources/meshes/keyboard/key_j.png | Bin 0 -> 2070 bytes interface/resources/meshes/keyboard/key_k.png | Bin 0 -> 2389 bytes interface/resources/meshes/keyboard/key_l.png | Bin 0 -> 1907 bytes interface/resources/meshes/keyboard/key_m.png | Bin 0 -> 2438 bytes .../resources/meshes/keyboard/key_min.png | Bin 0 -> 1557 bytes interface/resources/meshes/keyboard/key_n.png | Bin 0 -> 2165 bytes interface/resources/meshes/keyboard/key_o.png | Bin 0 -> 3054 bytes .../meshes/keyboard/key_open_paren.png | Bin 0 -> 2656 bytes interface/resources/meshes/keyboard/key_p.png | Bin 0 -> 2821 bytes .../meshes/keyboard/key_percentage.png | Bin 0 -> 4316 bytes .../resources/meshes/keyboard/key_period.png | Bin 0 -> 1591 bytes .../resources/meshes/keyboard/key_plus.png | Bin 0 -> 1635 bytes interface/resources/meshes/keyboard/key_q.png | Bin 0 -> 3012 bytes .../meshes/keyboard/key_question.png | Bin 0 -> 2727 bytes interface/resources/meshes/keyboard/key_r.png | Bin 0 -> 1897 bytes interface/resources/meshes/keyboard/key_s.png | Bin 0 -> 3080 bytes .../resources/meshes/keyboard/key_semi.png | Bin 0 -> 1797 bytes .../resources/meshes/keyboard/key_slash.png | Bin 0 -> 2580 bytes .../resources/meshes/keyboard/key_squote.png | Bin 0 -> 1783 bytes interface/resources/meshes/keyboard/key_t.png | Bin 0 -> 2038 bytes interface/resources/meshes/keyboard/key_u.png | Bin 0 -> 2232 bytes .../resources/meshes/keyboard/key_under.png | Bin 0 -> 1579 bytes interface/resources/meshes/keyboard/key_v.png | Bin 0 -> 2671 bytes interface/resources/meshes/keyboard/key_w.png | Bin 0 -> 3466 bytes interface/resources/meshes/keyboard/key_x.png | Bin 0 -> 2997 bytes interface/resources/meshes/keyboard/key_y.png | Bin 0 -> 3072 bytes interface/resources/meshes/keyboard/key_z.png | Bin 0 -> 2115 bytes .../meshes/keyboard/text_placard.png | Bin 0 -> 1308 bytes interface/resources/meshes/keyboard/white.png | Bin 0 -> 293 bytes .../resources/qml/controlsUit/Keyboard.qml | 20 +- .../qml/dialogs/TabletLoginDialog.qml | 4 + interface/resources/qml/hifi/AvatarApp.qml | 11 +- .../resources/qml/hifi/avatarapp/Settings.qml | 24 + .../qml/hifi/commerce/wallet/Wallet.qml | 4 + .../qml/hifi/dialogs/TabletRunningScripts.qml | 4 + interface/resources/qml/hifi/tablet/Edit.qml | 6 + .../qml/hifi/tablet/TabletAddressDialog.qml | 8 + .../tabletWindows/TabletPreferencesDialog.qml | 4 + interface/resources/sounds/keyboard_key.mp3 | Bin 0 -> 9089 bytes interface/src/Application.cpp | 17 +- interface/src/Menu.cpp | 2 + interface/src/Menu.h | 1 + .../src/raypick/PickScriptingInterface.cpp | 12 +- .../src/raypick/PointerScriptingInterface.cpp | 30 +- interface/src/raypick/RayPick.cpp | 8 +- interface/src/raypick/StylusPick.cpp | 18 +- interface/src/raypick/StylusPick.h | 9 +- interface/src/raypick/StylusPointer.cpp | 34 +- interface/src/raypick/StylusPointer.h | 7 +- .../src/scripting/HMDScriptingInterface.cpp | 7 +- .../src/scripting/HMDScriptingInterface.h | 2 + .../scripting/KeyboardScriptingInterface.cpp | 34 + .../scripting/KeyboardScriptingInterface.h | 43 + interface/src/ui/Keyboard.cpp | 799 ++++++ interface/src/ui/Keyboard.h | 152 + interface/src/ui/overlays/ModelOverlay.cpp | 2 +- interface/src/ui/overlays/Overlays.cpp | 53 +- interface/src/ui/overlays/Web3DOverlay.cpp | 2 + libraries/render-utils/src/Model.cpp | 2 +- .../ui/src/ui/TabletScriptingInterface.cpp | 6 + .../ui/src/ui/TabletScriptingInterface.h | 1 + .../controllerModules/inEditMode.js | 17 + .../nearParentGrabOverlay.js | 6 +- scripts/system/marketplaces/marketplaces.js | 5 + 131 files changed, 3777 insertions(+), 53 deletions(-) create mode 100644 interface/resources/config/keyboard.json create mode 100644 interface/resources/meshes/drumstick.fbx create mode 100644 interface/resources/meshes/keyboard/SM_enter.fbx create mode 100644 interface/resources/meshes/keyboard/SM_key.fbx create mode 100644 interface/resources/meshes/keyboard/SM_space.fbx create mode 100644 interface/resources/meshes/keyboard/keyCap_F.png create mode 100644 interface/resources/meshes/keyboard/keyCap_a.png create mode 100644 interface/resources/meshes/keyboard/keyCap_b.png create mode 100644 interface/resources/meshes/keyboard/keyCap_c.png create mode 100644 interface/resources/meshes/keyboard/keyCap_d.png create mode 100644 interface/resources/meshes/keyboard/keyCap_e.png create mode 100644 interface/resources/meshes/keyboard/keyCap_g.png create mode 100644 interface/resources/meshes/keyboard/keyCap_h.png create mode 100644 interface/resources/meshes/keyboard/keyCap_i.png create mode 100644 interface/resources/meshes/keyboard/keyCap_j.png create mode 100644 interface/resources/meshes/keyboard/keyCap_k.png create mode 100644 interface/resources/meshes/keyboard/keyCap_l.png create mode 100644 interface/resources/meshes/keyboard/keyCap_m.png create mode 100644 interface/resources/meshes/keyboard/keyCap_n.png create mode 100644 interface/resources/meshes/keyboard/keyCap_o.png create mode 100644 interface/resources/meshes/keyboard/keyCap_p.png create mode 100644 interface/resources/meshes/keyboard/keyCap_q.png create mode 100644 interface/resources/meshes/keyboard/keyCap_r.png create mode 100644 interface/resources/meshes/keyboard/keyCap_s.png create mode 100644 interface/resources/meshes/keyboard/keyCap_t.png create mode 100644 interface/resources/meshes/keyboard/keyCap_u.png create mode 100644 interface/resources/meshes/keyboard/keyCap_v.png create mode 100644 interface/resources/meshes/keyboard/keyCap_w.png create mode 100644 interface/resources/meshes/keyboard/keyCap_x.png create mode 100644 interface/resources/meshes/keyboard/keyCap_y.png create mode 100644 interface/resources/meshes/keyboard/keyCap_z.png create mode 100644 interface/resources/meshes/keyboard/key_0.png create mode 100644 interface/resources/meshes/keyboard/key_1.png create mode 100644 interface/resources/meshes/keyboard/key_123.png create mode 100644 interface/resources/meshes/keyboard/key_2.png create mode 100644 interface/resources/meshes/keyboard/key_3.png create mode 100644 interface/resources/meshes/keyboard/key_4.png create mode 100644 interface/resources/meshes/keyboard/key_5.png create mode 100644 interface/resources/meshes/keyboard/key_6.png create mode 100644 interface/resources/meshes/keyboard/key_7.png create mode 100644 interface/resources/meshes/keyboard/key_8.png create mode 100644 interface/resources/meshes/keyboard/key_9.png create mode 100644 interface/resources/meshes/keyboard/key_a.png create mode 100644 interface/resources/meshes/keyboard/key_abc.png create mode 100644 interface/resources/meshes/keyboard/key_ampersand.png create mode 100644 interface/resources/meshes/keyboard/key_ast.png create mode 100644 interface/resources/meshes/keyboard/key_at.png create mode 100644 interface/resources/meshes/keyboard/key_b.png create mode 100644 interface/resources/meshes/keyboard/key_backspace.png create mode 100644 interface/resources/meshes/keyboard/key_c.png create mode 100644 interface/resources/meshes/keyboard/key_cap.png create mode 100644 interface/resources/meshes/keyboard/key_caret.png create mode 100644 interface/resources/meshes/keyboard/key_close_paren.png create mode 100644 interface/resources/meshes/keyboard/key_colon.png create mode 100644 interface/resources/meshes/keyboard/key_comma.png create mode 100644 interface/resources/meshes/keyboard/key_d.png create mode 100644 interface/resources/meshes/keyboard/key_dollar.png create mode 100644 interface/resources/meshes/keyboard/key_dquote.png create mode 100644 interface/resources/meshes/keyboard/key_e.png create mode 100644 interface/resources/meshes/keyboard/key_enter.png create mode 100644 interface/resources/meshes/keyboard/key_exclam.png create mode 100644 interface/resources/meshes/keyboard/key_exit.png create mode 100644 interface/resources/meshes/keyboard/key_f.png create mode 100644 interface/resources/meshes/keyboard/key_g.png create mode 100644 interface/resources/meshes/keyboard/key_h.png create mode 100644 interface/resources/meshes/keyboard/key_hashtag.png create mode 100644 interface/resources/meshes/keyboard/key_i.png create mode 100644 interface/resources/meshes/keyboard/key_j.png create mode 100644 interface/resources/meshes/keyboard/key_k.png create mode 100644 interface/resources/meshes/keyboard/key_l.png create mode 100644 interface/resources/meshes/keyboard/key_m.png create mode 100644 interface/resources/meshes/keyboard/key_min.png create mode 100644 interface/resources/meshes/keyboard/key_n.png create mode 100644 interface/resources/meshes/keyboard/key_o.png create mode 100644 interface/resources/meshes/keyboard/key_open_paren.png create mode 100644 interface/resources/meshes/keyboard/key_p.png create mode 100644 interface/resources/meshes/keyboard/key_percentage.png create mode 100644 interface/resources/meshes/keyboard/key_period.png create mode 100644 interface/resources/meshes/keyboard/key_plus.png create mode 100644 interface/resources/meshes/keyboard/key_q.png create mode 100644 interface/resources/meshes/keyboard/key_question.png create mode 100644 interface/resources/meshes/keyboard/key_r.png create mode 100644 interface/resources/meshes/keyboard/key_s.png create mode 100644 interface/resources/meshes/keyboard/key_semi.png create mode 100644 interface/resources/meshes/keyboard/key_slash.png create mode 100644 interface/resources/meshes/keyboard/key_squote.png create mode 100644 interface/resources/meshes/keyboard/key_t.png create mode 100644 interface/resources/meshes/keyboard/key_u.png create mode 100644 interface/resources/meshes/keyboard/key_under.png create mode 100644 interface/resources/meshes/keyboard/key_v.png create mode 100644 interface/resources/meshes/keyboard/key_w.png create mode 100644 interface/resources/meshes/keyboard/key_x.png create mode 100644 interface/resources/meshes/keyboard/key_y.png create mode 100644 interface/resources/meshes/keyboard/key_z.png create mode 100644 interface/resources/meshes/keyboard/text_placard.png create mode 100644 interface/resources/meshes/keyboard/white.png create mode 100644 interface/resources/sounds/keyboard_key.mp3 create mode 100644 interface/src/scripting/KeyboardScriptingInterface.cpp create mode 100644 interface/src/scripting/KeyboardScriptingInterface.h create mode 100644 interface/src/ui/Keyboard.cpp create mode 100644 interface/src/ui/Keyboard.h diff --git a/interface/resources/config/keyboard.json b/interface/resources/config/keyboard.json new file mode 100644 index 0000000000..ba113da1f5 --- /dev/null +++ b/interface/resources/config/keyboard.json @@ -0,0 +1,2476 @@ +{ + "anchor": { + "dimensions": { + "x": 0.023600000888109207, + "y": 0.022600000724196434, + "z": 0.1274999976158142 + }, + "position": { + "x": 0.006292800903320312, + "y": 0.004300000742077827, + "z": 0.005427663803100586 + }, + "rotation": { + "w": 1.000, + "x": 0.000, + "y": 0.000, + "z": 0.000 + } + }, + "textDisplay": { + "dimensions": { + "x": 0.15, + "y": 0.045, + "z": 0.1 + }, + "localPosition": { + "x": -0.3032040786743164, + "y": 0.059300000742077827, + "z": 0.06454843521118164 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.976, + "z": 0.216 + }, + "leftMargin": 0.0, + "rightMargin": 0.0, + "topMargin": 0.0, + "bottomMargin": 0.0, + "lineHeight": 0.05 + }, + "useResourcesPath": true, + "layers": [ + [ + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.5332040786743164, + "y": 0.019300000742077827, + "z": 0.03745675086975098 + }, + "key": "p", + "texture": { + "file9": "meshes/keyboard/key_p.png", + "file10": "meshes/keyboard/key_p.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.42067813873291016, + "y": 0.019300000742077827, + "z": 0.03744244575500488 + }, + "key": "i", + "texture": { + "file9": "meshes/keyboard/key_i.png", + "file10": "meshes/keyboard/key_i.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "o", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.47769832611083984, + "y": 0.019300000742077827, + "z": 0.03745675086975098 + }, + "texture": { + "file9": "meshes/keyboard/key_o.png", + "file10": "meshes/keyboard/key_o.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.49904823303222656, + "y": 0.019300000742077827, + "z": -0.01745915412902832 + }, + "key": "l", + "texture": { + "file9": "meshes/keyboard/key_l.png", + "file10": "meshes/keyboard/key_l.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.4439973831176758, + "y": 0.019300000742077827, + "z": -0.01745915412902832 + }, + "key": "k", + "texture": { + "file9": "meshes/keyboard/key_k.png", + "file10": "meshes/keyboard/key_k.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "y", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_y.png", + "file10": "meshes/keyboard/key_y.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.30902957916259766, + "y": 0.019300000742077827, + "z": 0.0374448299407959 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "r", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_r.png", + "file10": "meshes/keyboard/key_r.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.19474029541015625, + "y": 0.019300000742077827, + "z": 0.03745102882385254 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "d", + "texture": { + "file9": "meshes/keyboard/key_d.png", + "file10": "meshes/keyboard/key_d.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.16272640228271484, + "y": 0.019300000742077827, + "z": -0.01747274398803711 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "t", + "texture": { + "file9": "meshes/keyboard/key_t.png", + "file10": "meshes/keyboard/key_t.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.2517843246459961, + "y": 0.019300000742077827, + "z": 0.03744622692465782 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "f", + "texture": { + "file9": "meshes/keyboard/key_f.png", + "file10": "meshes/keyboard/key_f.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.2200756072998047, + "y": 0.019300000742077827, + "z": -0.01746511459350586 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "g", + "texture": { + "file9": "meshes/keyboard/key_g.png", + "file10": "meshes/keyboard/key_g.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.27622222900390625, + "y": 0.019300000742077827, + "z": -0.017457084730267525 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "w", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_w.png", + "file10": "meshes/keyboard/key_w.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.08203601837158203, + "y": 0.019300000742077827, + "z": 0.03743100166320801 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "q", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_q.png", + "file10": "meshes/keyboard/key_q.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.026292800903320312, + "y": 0.019300000742077827, + "z": 0.037427663803100586 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "a", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_a.png", + "file10": "meshes/keyboard/key_a.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.050909996032714844, + "y": 0.019300000742077827, + "z": -0.017487764358520508 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "e", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_e.png", + "file10": "meshes/keyboard/key_e.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.13773441314697266, + "y": 0.019300000742077827, + "z": 0.03745269775390625 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "s", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_s.png", + "file10": "meshes/keyboard/key_s.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.10659980773925781, + "y": 0.019300000742077827, + "z": -0.017481565475463867 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "u", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_u.png", + "file10": "meshes/keyboard/key_u.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.36423587799072266, + "y": 0.019300000742077827, + "z": 0.03743577003479004 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "h", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_h.png", + "file10": "meshes/keyboard/key_h.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.3321561813354492, + "y": 0.019300000742077827, + "z": -0.017461776733398438 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "j", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_j.png", + "file10": "meshes/keyboard/key_j.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.38753604888916016, + "y": 0.019300000742077827, + "z": -0.01746058464050293 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "c", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_c.png", + "file10": "meshes/keyboard/key_c.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.22023773193359375, + "y": 0.019300000742077827, + "z": -0.07282757759094238 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "v", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_v.png", + "file10": "meshes/keyboard/key_v.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.2767200469970703, + "y": 0.019300000742077827, + "z": -0.07291850447654724 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "b", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_b.png", + "file10": "meshes/keyboard/key_b.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.33254528045654297, + "y": 0.019300000742077827, + "z": -0.07283258438110352 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": " ", + "name": "space", + "dimensions": { + "x": 0.14597539603710175, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.27660465240478516, + "y": 0.019300000742077827, + "z": -0.12705934047698975 + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/white.png", + "file10": "meshes/keyboard/white.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "z", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_z.png", + "file10": "meshes/keyboard/key_z.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.10669422149658203, + "y": 0.019300000742077827, + "z": -0.07285571098327637 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "n", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_n.png", + "file10": "meshes/keyboard/key_n.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020292000845074654, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.38794422149658203, + "y": 0.019300000742077827, + "z": -0.0728309154510498 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "m", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_m.png", + "file10": "meshes/keyboard/key_m.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.01846799999475479, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.44464683532714844, + "y": 0.019300000742077827, + "z": -0.07282185554504395 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "x", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_x.png", + "file10": "meshes/keyboard/key_x.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.1630725860595703, + "y": 0.019300000742077827, + "z": -0.07284045219421387 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "DEL", + "type": "backspace", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.53203323516845703, + "y": 0.019300000742077827, + "z": -0.07286686894893646 + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_backspace.png", + "file10": "meshes/keyboard/key_backspace.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "Caps", + "type": "caps", + "switchToLayer": 1, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.02603323516845703, + "y": 0.019300000742077827, + "z": -0.07285571098327637 + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_cap.png", + "file10": "meshes/keyboard/key_cap.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "Close", + "type": "close", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.59333323516845703, + "y": 0.019300000742077827, + "z": 0.037454843521118164 + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_exit.png", + "file10": "meshes/keyboard/key_exit.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "Enter", + "type": "enter", + "dimensions": { + "x": 0.08787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.5103323516845703, + "y": 0.019300000742077827, + "z": -0.127054843521118164 + }, + "modelURL": "meshes/keyboard/SM_enter.fbx", + "texture": { + "file10": "meshes/keyboard/key_enter.png", + "file11": "meshes/keyboard/key_enter.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "numbers", + "type": "layer", + "switchToLayer": 2, + "dimensions": { + "x": 0.07787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.026323516845703, + "y": 0.019300000742077827, + "z": -0.127054843521118164 + }, + "modelURL": "meshes/keyboard/SM_enter.fbx", + "texture": { + "file10": "meshes/keyboard/key_123.png", + "file11": "meshes/keyboard/key_123.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + } + ], + [ + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.5332040786743164, + "y": 0.019300000742077827, + "z": 0.037454843521118164 + }, + "key": "p", + "texture": { + "file9": "meshes/keyboard/keyCap_p.png", + "file10": "meshes/keyboard/keyCap_p.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.42067813873291016, + "y": 0.019300000742077827, + "z": 0.03744244575500488 + }, + "key": "i", + "texture": { + "file9": "meshes/keyboard/keyCap_i.png", + "file10": "meshes/keyboard/keyCap_i.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "o", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.47769832611083984, + "y": 0.019300000742077827, + "z": 0.03745675086975098 + }, + "texture": { + "file9": "meshes/keyboard/keyCap_o.png", + "file10": "meshes/keyboard/keyCap_o.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.49904823303222656, + "y": 0.019300000742077827, + "z": -0.01745915412902832 + }, + "key": "l", + "texture": { + "file9": "meshes/keyboard/keyCap_l.png", + "file10": "meshes/keyboard/keyCap_l.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.4439973831176758, + "y": 0.019300000742077827, + "z": -0.01745915412902832 + }, + "key": "k", + "texture": { + "file9": "meshes/keyboard/keyCap_k.png", + "file10": "meshes/keyboard/keyCap_k.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "y", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_y.png", + "file10": "meshes/keyboard/keyCap_y.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.30902957916259766, + "y": 0.019300000742077827, + "z": 0.0374448299407959 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "r", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_r.png", + "file10": "meshes/keyboard/keyCap_r.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.19474029541015625, + "y": 0.019300000742077827, + "z": 0.03745102882385254 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "d", + "texture": { + "file9": "meshes/keyboard/keyCap_d.png", + "file10": "meshes/keyboard/keyCap_d.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.16272640228271484, + "y": 0.019300000742077827, + "z": -0.01747274398803711 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "t", + "texture": { + "file9": "meshes/keyboard/keyCap_t.png", + "file10": "meshes/keyboard/keyCap_t.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.2517843246459961, + "y": 0.019300000742077827, + "z": 0.03744622692465782 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "f", + "texture": { + "file9": "meshes/keyboard/keyCap_F.png", + "file10": "meshes/keyboard/keyCap_F.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.2200756072998047, + "y": 0.019300000742077827, + "z": -0.01746511459350586 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "g", + "texture": { + "file9": "meshes/keyboard/keyCap_g.png", + "file10": "meshes/keyboard/keyCap_g.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.27622222900390625, + "y": 0.019300000742077827, + "z": -0.017457084730267525 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "w", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_w.png", + "file10": "meshes/keyboard/keyCap_w.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.08203601837158203, + "y": 0.019300000742077827, + "z": 0.03743100166320801 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "q", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_q.png", + "file10": "meshes/keyboard/keyCap_q.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.026292800903320312, + "y": 0.019300000742077827, + "z": 0.037427663803100586 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "a", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_a.png", + "file10": "meshes/keyboard/keyCap_a.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.050909996032714844, + "y": 0.019300000742077827, + "z": -0.017487764358520508 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "e", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_e.png", + "file10": "meshes/keyboard/keyCap_e.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.13773441314697266, + "y": 0.019300000742077827, + "z": 0.03745269775390625 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "s", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_s.png", + "file10": "meshes/keyboard/keyCap_s.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.10659980773925781, + "y": 0.019300000742077827, + "z": -0.017481565475463867 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "u", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_u.png", + "file10": "meshes/keyboard/keyCap_u.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.36423587799072266, + "y": 0.019300000742077827, + "z": 0.03743577003479004 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "h", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_h.png", + "file10": "meshes/keyboard/keyCap_h.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.3321561813354492, + "y": 0.019300000742077827, + "z": -0.017461776733398438 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "j", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_j.png", + "file10": "meshes/keyboard/keyCap_j.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.38753604888916016, + "y": 0.019300000742077827, + "z": -0.01746058464050293 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "c", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_c.png", + "file10": "meshes/keyboard/keyCap_c.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.22023773193359375, + "y": 0.019300000742077827, + "z": -0.07282757759094238 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "v", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_v.png", + "file10": "meshes/keyboard/keyCap_v.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.2767200469970703, + "y": 0.019300000742077827, + "z": -0.07291850447654724 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "b", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_b.png", + "file10": "meshes/keyboard/keyCap_b.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.33254528045654297, + "y": 0.019300000742077827, + "z": -0.07283258438110352 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": " ", + "name": "space", + "dimensions": { + "x": 0.14597539603710175, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.27660465240478516, + "y": 0.019300000742077827, + "z": -0.12705934047698975 + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/white.png", + "file10": "meshes/keyboard/white.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "z", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_z.png", + "file10": "meshes/keyboard/keyCap_z.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.10669422149658203, + "y": 0.019300000742077827, + "z": -0.07285571098327637 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "n", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_n.png", + "file10": "meshes/keyboard/keyCap_n.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020292000845074654, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.38794422149658203, + "y": 0.019300000742077827, + "z": -0.0728309154510498 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "m", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_m.png", + "file10": "meshes/keyboard/keyCap_m.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.01846799999475479, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.44464683532714844, + "y": 0.019300000742077827, + "z": -0.07282185554504395 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "x", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/keyCap_x.png", + "file10": "meshes/keyboard/keyCap_x.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.1630725860595703, + "y": 0.019300000742077827, + "z": -0.07284045219421387 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "DEL", + "type": "backspace", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.53203323516845703, + "y": 0.019300000742077827, + "z": -0.07286686894893646 + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_backspace.png", + "file10": "meshes/keyboard/key_backspace.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "Caps", + "type": "caps", + "switchToLayer": 0, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.02603323516845703, + "y": 0.019300000742077827, + "z": -0.07285571098327637 + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_cap.png", + "file10": "meshes/keyboard/key_cap.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "Close", + "type": "close", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.59333323516845703, + "y": 0.019300000742077827, + "z": 0.037454843521118164 + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_exit.png", + "file10": "meshes/keyboard/key_exit.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "Enter", + "type": "enter", + "dimensions": { + "x": 0.08787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.5103323516845703, + "y": 0.019300000742077827, + "z": -0.127054843521118164 + }, + "modelURL": "meshes/keyboard/SM_enter.fbx", + "texture": { + "file10": "meshes/keyboard/key_enter.png", + "file11": "meshes/keyboard/key_enter.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "numbers", + "type": "layer", + "switchToLayer": 2, + "dimensions": { + "x": 0.07787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.026323516845703, + "y": 0.019300000742077827, + "z": -0.127054843521118164 + }, + "modelURL": "meshes/keyboard/SM_enter.fbx", + "texture": { + "file10": "meshes/keyboard/key_123.png", + "file11": "meshes/keyboard/key_123.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + } + ], + [ + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.5332040786743164, + "y": 0.019300000742077827, + "z": 0.037454843521118164 + }, + "key": "0", + "texture": { + "file9": "meshes/keyboard/key_0.png", + "file10": "meshes/keyboard/key_0.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.42067813873291016, + "y": 0.019300000742077827, + "z": 0.03744244575500488 + }, + "key": "8", + "texture": { + "file9": "meshes/keyboard/key_8.png", + "file10": "meshes/keyboard/key_8.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "9", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.47769832611083984, + "y": 0.019300000742077827, + "z": 0.03745675086975098 + }, + "texture": { + "file9": "meshes/keyboard/key_9.png", + "file10": "meshes/keyboard/key_9.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.47764823303222656, + "y": 0.019300000742077827, + "z": -0.01745915412902832 + }, + "key": "(", + "texture": { + "file9": "meshes/keyboard/key_open_paren.png", + "file10": "meshes/keyboard/key_open_paren.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.53364823303222656, + "y": 0.019300000742077827, + "z": -0.01745915412902832 + }, + "key": ")", + "texture": { + "file9": "meshes/keyboard/key_close_paren.png", + "file10": "meshes/keyboard/key_close_paren.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.59634823303222656, + "y": 0.019300000742077827, + "z": -0.01745915412902832 + }, + "key": "/", + "texture": { + "file9": "meshes/keyboard/key_slash.png", + "file10": "meshes/keyboard/key_slash.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.4206973831176758, + "y": 0.019300000742077827, + "z": -0.01745915412902832 + }, + "key": "+", + "texture": { + "file9": "meshes/keyboard/key_plus.png", + "file10": "meshes/keyboard/key_plus.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "6", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_6.png", + "file10": "meshes/keyboard/key_6.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.30902957916259766, + "y": 0.019300000742077827, + "z": 0.0374448299407959 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "4", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_4.png", + "file10": "meshes/keyboard/key_4.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.19474029541015625, + "y": 0.019300000742077827, + "z": 0.03745102882385254 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "$", + "texture": { + "file9": "meshes/keyboard/key_dollar.png", + "file10": "meshes/keyboard/key_dollar.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.13772640228271484, + "y": 0.019300000742077827, + "z": -0.01747274398803711 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "5", + "texture": { + "file9": "meshes/keyboard/key_5.png", + "file10": "meshes/keyboard/key_5.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.2517843246459961, + "y": 0.019300000742077827, + "z": 0.03744622692465782 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "%", + "texture": { + "file9": "meshes/keyboard/key_percentage.png", + "file10": "meshes/keyboard/key_percentage.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.1947756072998047, + "y": 0.019300000742077827, + "z": -0.01746511459350586 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "_", + "texture": { + "file9": "meshes/keyboard/key_under.png", + "file10": "meshes/keyboard/key_under.png" + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.25172222900390625, + "y": 0.019300000742077827, + "z": -0.017457084730267525 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "2", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_2.png", + "file10": "meshes/keyboard/key_2.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.08203601837158203, + "y": 0.019300000742077827, + "z": 0.03743100166320801 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "1", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_1.png", + "file10": "meshes/keyboard/key_1.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.026292800903320312, + "y": 0.019300000742077827, + "z": 0.037427663803100586 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "@", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_at.png", + "file10": "meshes/keyboard/key_at.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.026209996032714844, + "y": 0.019300000742077827, + "z": -0.017487764358520508 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "3", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_3.png", + "file10": "meshes/keyboard/key_3.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.13773441314697266, + "y": 0.019300000742077827, + "z": 0.03745269775390625 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "#", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_hashtag.png", + "file10": "meshes/keyboard/key_hashtag.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.08209980773925781, + "y": 0.019300000742077827, + "z": -0.017481565475463867 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "7", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_7.png", + "file10": "meshes/keyboard/key_7.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.36423587799072266, + "y": 0.019300000742077827, + "z": 0.03743577003479004 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "&", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_ampersand.png", + "file10": "meshes/keyboard/key_ampersand.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.3090561813354492, + "y": 0.019300000742077827, + "z": -0.017461776733398438 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "-", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_min.png", + "file10": "meshes/keyboard/key_min.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.36433604888916016, + "y": 0.019300000742077827, + "z": -0.01746058464050293 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "'", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_squote.png", + "file10": "meshes/keyboard/key_squote.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.22023773193359375, + "y": 0.019300000742077827, + "z": -0.07282757759094238 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": ":", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_colon.png", + "file10": "meshes/keyboard/key_colon.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.2767200469970703, + "y": 0.019300000742077827, + "z": -0.07291850447654724 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": ";", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_semi.png", + "file10": "meshes/keyboard/key_semi.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.33254528045654297, + "y": 0.019300000742077827, + "z": -0.07283258438110352 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": " ", + "name": "space", + "dimensions": { + "x": 0.14597539603710175, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.27660465240478516, + "y": 0.019300000742077827, + "z": -0.12705934047698975 + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/white.png", + "file10": "meshes/keyboard/white.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "*", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_ast.png", + "file10": "meshes/keyboard/key_ast.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.10669422149658203, + "y": 0.019300000742077827, + "z": -0.07285571098327637 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "!", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_exclam.png", + "file10": "meshes/keyboard/key_exclam.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020292000845074654, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.38794422149658203, + "y": 0.019300000742077827, + "z": -0.0728309154510498 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "?", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_question.png", + "file10": "meshes/keyboard/key_question.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.01846799999475479, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.44464683532714844, + "y": 0.019300000742077827, + "z": -0.07282185554504395 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "\"", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_dquote.png", + "file10": "meshes/keyboard/key_dquote.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.1630725860595703, + "y": 0.019300000742077827, + "z": -0.07284045219421387 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "DEL", + "type": "backspace", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.53203323516845703, + "y": 0.019300000742077827, + "z": -0.07286686894893646 + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_backspace.png", + "file10": "meshes/keyboard/key_backspace.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "Caps", + "type": "caps", + "switchToLayer": 1, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.02603323516845703, + "y": 0.019300000742077827, + "z": -0.07285571098327637 + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_cap.png", + "file10": "meshes/keyboard/key_cap.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "Close", + "type": "close", + "dimensions": { + "x": 0.04787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.59333323516845703, + "y": 0.019300000742077827, + "z": 0.037454843521118164 + }, + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_exit.png", + "file10": "meshes/keyboard/key_exit.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "Enter", + "type": "enter", + "dimensions": { + "x": 0.08787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.5103323516845703, + "y": 0.019300000742077827, + "z": -0.127054843521118164 + }, + "modelURL": "meshes/keyboard/SM_enter.fbx", + "texture": { + "file10": "meshes/keyboard/key_enter.png", + "file11": "meshes/keyboard/key_enter.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": "numbers", + "type": "layer", + "switchToLayer": 0, + "dimensions": { + "x": 0.07787999764084816, + "z": 0.020519999787211418, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.026323516845703, + "y": 0.019300000742077827, + "z": -0.127054843521118164 + }, + "modelURL": "meshes/keyboard/SM_enter.fbx", + "texture": { + "file10": "meshes/keyboard/key_abc.png", + "file11": "meshes/keyboard/key_abc.png" + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": ".", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_period.png", + "file10": "meshes/keyboard/key_period.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.38794422149658203, + "y": 0.019300000742077827, + "z": -0.12705934047698975 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + }, + { + "key": ",", + "modelURL": "meshes/keyboard/SM_key.fbx", + "texture": { + "file9": "meshes/keyboard/key_comma.png", + "file10": "meshes/keyboard/key_comma.png" + }, + "dimensions": { + "x": 0.04787999764084816, + "z": 0.02051999792456627, + "y": 0.04787999764084816 + }, + "position": { + "x": -0.1630725860595703, + "y": 0.019300000742077827, + "z": -0.12705934047698975 + }, + "localOrientation": { + "w": 0.000, + "x": 0.000, + "y": 0.707, + "z": 0.707 + } + } + ] + ] +} diff --git a/interface/resources/meshes/drumstick.fbx b/interface/resources/meshes/drumstick.fbx new file mode 100644 index 0000000000000000000000000000000000000000..0243d9fd1b89f0dfe6572a7ec293a38ec571ce53 GIT binary patch literal 63952 zcmb?k3p~^N`(Ia;P*h5yyE+vLxf|&wxs=nzy~VJwF`Kz=mCEVjQYw|px;vd36(Ykz zk;ICcSTlFAY?ztd|F7@1O23ni)A@g2JI?m~exCQ|c|Xtdd7jVb^ZjnwLn3`4NXXLd zTMjPW0)s=4kxQ3?G(bx=K_K%Pps$DO_ALi@LLt6T%6VjyXHc0Rcw^*v$liKyxR3 zV=!eaB+_mw2n5m@b8_lV1R80l3<80`W6n-@fWgrysNG}`2y|fVc^9Y;0`6-!1q1@2 z#++AlfCUA?!ekeJ1FnueO64urf{TvZq%It0s(61fIy&`EA(_d<5!)l3w_8%!TPTXjA|KDLr34=KKtMXe0)K^bM0<`8f8EJ_AV$S4DRm&m;^Wn0K2RLfk3uq9)Ov{JRm{P2pGg;D>N(+g$VKRMWTblP%xiB zUBAN-t?~}`%Q^t!%mwN5|J#S^R1gRVYapT~IZHk4goOFS;E*8c2U|pN2m%fFm8lNs ze|V~l_7<6h1_<<3BE}{pBnajs9gF*+a9;%SJ1a=pOaA~SwLzm0zRY?GB{w+d@)VTRtO3T zWE<}t4!=v;d^s_Oq&f%$(l^x8H`N31mTuZ;roYKd-$2(ye{+!>TJG7oW1+Xhf}lSk zJK^Z*pC&NNI2AY9M@V+moc{eX27o^zvSD~azINN;G(fQj0OA& z-!IN>l0(VEPIEk{iF(&aLB`clrrLt<@V32AQ~rG%$p%&5Gdu5B$@SMjo>=HZIF>jlGC;pLYsO3l`6k>8dZ21Pc8( z$$#v3pImxOzqSYj()Z{60yazed%xc#|FPeCx%5Vv)_^0vIYz1f-CiGPK!1g$+IogThg^h#&+~p7Dba$h|wZxB!zj*=ax-xkf*F z<2YMpEO|)(fbfM%6Fn4;4*r(>?0_j%p>8ZIZqLQPOM}ssaK5{Q#4}8DARjeyGM&w3!T&PPjo*GfEdO+s7b(mx>3JzcT?4 zIxYw_(g*4!TTcLE6!C3oA@!oEip&J&G83o)!$=ysHgH(53ku>B_>;McG@c=W(53qN zKyvza_^QZg0=ap{UIYT=i13B({&s!5FrGTz-5P1H3shf3B1HQ@Til zhWQ|2((QzC(iZ?^JXa42|0ibZ!~cV2E$ zl{xD-nYn?1eE_S~l{6=W4B_CX)kZ=#!jfYYj9?R*McJ5b^~((4(|8 zSp2=Y3nB<1mHR>Sz`r-&jqnK^uX&N0tb)sH?Y9~Yq!nqk{z2)!b`>~!!GC|ll=m(# z06++C34+3X$1An~%Z>A7_K?qwfb_c;fs&3@8y_EZFgi&3{$~)OatL|#11A5{dC^4W z^S)OBNPu6}-VzxC2@8`_*|!@3_XqxydPn-_>-d@AVYxsSyg=$0oA7{14v?R1P$(qq zFd7A2JYUvuOzZ+{6@9rpkc6J4=KsHXyl1}5d-62| zFgtO9MNMF~;A*{K!QTeLZUjOWs{b=78?*8HPeN3yR z%Myb$1H06eS&$`=P*G-(agW%g8=RBk?B_>ObYcyJWyXtbGA?hJ_=385fu; z0U2 zG=8crBw}1`{r^dFTy1asljOMCHpwOBrGM;zazgq-ky4gF7~xA6%UEufMIf*>`DGxw zgg|_t+Y!j!uwWR<2_6(Vo~8Wc81lseP)NRFpkRJ}aA;WAL1}XM>0%88%9#L=>qLM? zCjjI&5unYQGV{o%zd&jM_H}kc;r}eX()ygHY$VAyBY;`AGZOmMHo%1D7jkEZ9Cb`C zaz=!q{wu8J5*bzb5@Bqv|9K)pF8LeFFS`7PEcEgxsK1_qByy5two%6DI_`+xtR=I} zkHy9v&cQzw8+R<<{)yO6(hl{fV&f{Qsy#k8$oL6or(A4}EO7v_Z3hp4B4M)S@;DQU zqjIS+X-Zy4e;#A+wPi83L)IHGlakB*hYA0X1i?9Lk*Imr7Zw@V$nRAR?zG*sc zfL;EX)VQvD{WGa?!)x-gaW(u&bea52YFrKBKa(0)!`fvs$Lx`L8(8k{fFgpSC?w2h z%#7hz6X@FIGI;qtFJRX775+;$_*Lu^a%}nCQs73AVgglK_F4%p_G1)0&JtnZ@hioma>EULL+{DJBB4kSt0W~ zurInD357xZZGyn zHy`We*kf*K0@zH#-);hC zY5QSeu*0w*7%K9AKmGvmsa$@WOebK!@SFTn`ScP3_ksTGk`x#C^KZ83R=qrcM3dL=$i=_?ZT}Mp^hI*fpYGB*K*IuN1N$EGH~#^L;p!~g2#)E<42JLujL%(+aL=P`IBkDNZJ+*3k!pVLuJoc z0F1vqZSjw3FITRFJedI1?F-(RhfBshQSu$)0C=+QccTih!go)C{7@a)eWdCy_mRfD zvC);;MEYx|ceVi*>s!#lAro1d z{Ae^I<#6&^0^DPBL?DA9K@N~##?00GE{K6z0;o8K-cL6Z2f)+yWgO+j1eoVY%W((< zc#`9nLUNbBOegsY49HsFAbldo=r&$hf{jQO zT=e%hOu2ev_F>0Dk5TU*Oy{&wW;&pZPI8t$k|moUj)nU#nr3d48B$*HfE%oT?_lEk zelf|vIX*MLwN{Wl=Mk2*N%p)AaIaYDyL+Fz2bjlX58X%~MVa*7Md|nf?r=r}xk`R~ zmyzL{WPHcWoyWVc_(lHfacz^Vr~spCCh#cA_8^GAti}O-er*RF0S4m|ct)E50SlmH z$dB&`=o$Pv0e6Fm6Ig3FE&*rq1PF-6BQWqE2xuBkoIv0B1VUOSK%f+O+T}+ccx3qN z9-#dT0{4x^^#JJ21PI`OhbDeR0BQ8=1Tu{#?t!4q;}Wo*$RM@`vipw+7;OG^0^XY^ zPGE!axCD;ApMVD@0oxltA~5_P2rM<8I05GP1O`-&|8p8GACEx3@vnQJ?OzbcF&Wnb z`rHY4AP(5{_>l*~O@5ugO_PaxAjEWB0*BNl;DN2cw%?Bk7@Ph&0Uy(e6WC}rK7kn% zATZ7Bi~6AN3)$#@Ah6PG;sl1qCty8w0t70@BkemVU zWi@dRM1aR7aCFiH2<#rSwEAukgTcQ}AP_up0;bmE5(rs30Rl?Ew~Bt`0pROKzz)!_ ztjN||Pn-aMd;+aX6ChAG9)Ys|K%oC$5Fpx&>jC}A6CjWTtnq*3f#Wv6?tyHZiF+Vs z%eVyQ%$ooKXJA9~M+9uQ{5k=|mWdOvv>lfKNMQm5<^tbi{tc=DS;y(}={1*gBTgUalb^ZhjS<2VFy8oGtp4$5B9(cTU;vP7|NXg;tt{UZW1w*NW-!|f9%FnPzg1eS6p5JHsk z2vq+E0-S$ApyE#%0ia;c_;O2hcL?f=vh=%%!2UdF(wFnn9Z}$(XT$F@6P;25foALn zelo-d8s>Xwy6kME5(pGADfn98Ywd0G;}ujLMi0MPqkg(@P}R=;x0xTVB){oWRh_zQ z>mPdqh=;B7E-gK|f7V8Z!ZYXOq!-H{-aay0W36+7O`GSj%Sbzz{o0F+Ckr&@sAgYY zFs2~bS;L4N{Fzjy0dap*z8UL}nO>yF zgNe_{v?6p6Sh(tek=A9s^j1-O7k9O|V+UT78h=%NxRQ8P!C~mBy_TU@4mh;tLn*V) zsbiLLdVb_aWfRL1+HmE7;E9D_aiAm3q?;pjG1g#7a=RXF)E-HHTpf|?H?qQ~ zgZ1 z#pnC9)KafSe8?S4+^Ah=*}gUx(f!%Ys2lwNKNw|?qlFIcT2k?F&0yO@52wQS)X#zg zVU}(C5gipJe`PV_=P)uxU;LF7kuw*}+cFCEGjZC}%z^M5eFCglQZC8B(O+p7m%0r8L5|qpGnlWTH&QWZivO*J z3@d(Jao$w&TZ_b}#p7Df{RUxM)>ANpLxBwiTBShZg$?_%DmKT(Le@ z9@iRkR@YTsIdGth19!F@q}Mg_M=8EIj^a>M^Iw@C4rq6B3PgOYdV&D+;prLzi5Aqz zddzA;Gvb6QqRy!LKnqS+3d8CCU;(`3$mM_%D;P3%u_3q?vAVIs&o-0X$E@ZUeEgFq z2|$(*z%My}k&K^Oarwl0i2*n`ViTbeAkOWKcpl$KdOzyt)MuX)V>Z+fOVzr>>ak%T z!$V-1IY-KFx4c*BrJ+vDBYnu|7k{D$@w_PaO5;e18yba8pfnAcnIusW0TlUG5AjP-B<$+sWc5u4i6Qd$xr z8TN3^i5U)PV03>T$;{IecINl;=4x5F4IUkgA~CcdmY(3^3pcY|dh)sFA`cehnpxph zneXn#mIw@zOH^{+;ZLLa6|91naZs<0!bV|4^eNeS)q1O^n3;EsWjgvx%= zfmdP9>FqtwEc7z8B}qdTBlqFLUK2XDLdofNq^Um=Yt~c%L0Y- zlWXygym0aZ?6Mc$4=vwgB6?2@r@x_D_zSLZ8#Aur-E+VO83*1y&e>nvl-ZZxaUxHv zqN3rxx4u{3I)T{Tz-yD1%Df?-LTCXry1g|^0v*wwr_gf5q6DjeFcfbOZS7?ASJYWB zZ9*Sv@9(rQe{t-MGc)*?s}RNx9|aG>DVj8@AXcIYH$1Yq3Xs&SG{qnSe_ps;(Gg`l-R`0?U_9C7=In zOlTmT1efp=e92Ku1-nL+LZ8PQ4dtxE9{+smj;V4n&)Yl8BxHy)Qo=uO^1_Pm6j?NQ zu(Z+@{IDQKP14kp^N_zli*4kHU!WUZWBdZ5)d>!n&+_%}(54og!?`EM;f8`GLS~vD z*diwAFGN`XBH>f<*3z3%eU5 zh@U9X{od`$p_-M%07PRsl>Wk^vRE3V!Qzw*tb%rD!2wMZ-YEm!m?~^z>610@)h!8# zBkYJ5y)JwW!JPiP?X0(}A~5`utc6*)w)b_4Ee<2~E)nYlyBL#-y|0QeMGLaA*&Oo- zoh3`cJ55g7Cl$voDqz$9!hK@2>ohmT+-MwFomdeOUE5?&C=ID;jM!j#(#Hv3zQHNC zXI4R^9rlk~MW~@b-rdoVuEp>SN#@{@#{PSy6|pLo#`7i0xHQhlmH1lq62IcJjT>1K zGe&Sng-FHHxVwJ?n{E#nDBkw6s3zi%k?@o10tZv&;`6xdlQr4J9gCsqvw%~Oy#XvPnU@mZd5&PkO}&B|b}{#gZ~ALyRL;O@=J0wQI;z$V;= z6lbh~yKfRQq!eb{J*tRz;y~&4-YV^c;khNJxWgGEZ3E20gnNm@dc{uz>6vS5Tm*p; z>!MM_5WdR+6EnA7%)ov@?+5j@%IPsBv8}u^UO`N7=IJr#*#Ty2fjRZtHO8o@h|3$JsFA{ZWK{EP2qrMt4Lyw*4d?ZK9hN znb%B)Twp?xI#TmMK)l!DNOh$}iTj7XIg>+p`84I@(9q=5Yq zk>^as<}b>UYR@*!RqzTFX0$)6#A8eN{^UeKCtWPoZU(Q$-Wc zRK*d$*E@oEq|S@&q!~fL-k-0K=%s*i?^x81-N<{(I%7#LIWt9v-ul_x()ecM2lvv= z_(x7InuQ5fCgjGGfm)13i1l`i+{#5RM(o3`FDYU%O zl2$2APeaSc{0jFh5|A0ZaF4ir#U0u{-aTy7TK5f>+i;I^-Zfs%;dr+_?ulHGh?7>R z7E{rNkNx}O|82ZAwDVS@s7wUy;T}Ae9>fJ|HvSg+yl`_!iis$V#uTr ziO{_;TGoh>iEFb+9TF;Lu_DALZ#5c`@4ZqKSZjs~!!}@j59H=lYLTMEFEaU;+4m9a z9XP=#U6;C?=UExvWg_0C(LDCLL0Hd$5FBCF_W67*-+1K;5zin!rlbUn(;YEmty2Ctl+Q_F`;~OnilH$K$eakCMp8VBlSv5v&$Td9U`!qY#N$-LbT#g(P{A$@#Ov zY|4(DNQojVKdDQkSNxv6mW6Bk9Niljp=TM4I*DP5^olVoTw7-icQNe}F;`r~GHw+M zlnN!-DNGXmsDNeMYDtHD5-3G5N@X9#0wojPkZAfx9BC4lj(BFy6-_V3&=K?MF!TA} zq{M=RavFDW$PiTY8R4GV!iu)?;79!x-7cAS3|YT+6e3%MO;-YeRGkq)k^TtaSL0C7 z2GNGq;hP2FZ=);%6wG0kR(bJD@-jhY&%)iDW-Ng$Cg>b@e0YhXX6ql3KuNJy z|FDH&cVk`9U5@V?rOj`nke`$n!z`x_A63fBRE;W+a&wwia%c%5>r~6b%ajId?Sr-q z*bTi0cR8pxD*N9?MSW6P2eX_q`{?|<%(>Ick#6LvafcQW{7;oVyhzD?Hs|0LhAU*v z`n#MU{n-oNMujg_R)kq9oI5%#FH>!Md5jx*+O0!cgq~Ax^;0SAXA7^`G9n@CU*F}# z=qvlZjlwQeF@TkTb{}1o=QU?_Im%6MYRVxEg8Hdy{Y#Xy&*t6W+|-}Hx*=-gmFXz~ zB__M$Pv?0}_IozfO>d?F#Db7|+*Lo3VqX!o(4GN3qivI$kK%~89v303XQQ9;>+ zsToter>!|^Z#F@vqd5whO~`6(j@p$?$ZBtn zI+9JuYHN;i%O+%XG)F~c6Z~77qqb)g{M(zO{IUuDZOu{pvkCqk%~9dmgr3&sD93C< zPkVC|Je$zd)*R)PP3Y-pj>2XW)LWaQY_bXJ?afh;Y=U}Qa}iz9o$C%^V0D6Mh!X@s z>_aq3=?N24MiH(4P(+{N&UHV;z$ygM5Jw3II-8?bWfKI^2*$p`CtjBV8Se|9cx41K zj0&H4r35na3!iwU1v1cuPrOnC8Jxl=Uid(UQsI-9zPL{`nuN=7j}LAqaYF3%0C86c?>9m8(3Pxn$l)bCW{M&Jfm{))q)v zNkv@rj;Y+l*~}&LKbo5rVs?kHKD4%kmX&nGMe9uEI?iTh%=>6=T8P;d!g|-*0xK(N zh>P~0%B`Et%$Wbt+`JIu7{dD4+7ek-!j6kppUTyp&Gel2k!Vtgu?u0yruE$DD3h5VTK{uWJ&t^eubbFhth%83ZDY>wJYmV=AoK z%7AlM%)8A=8Bm*d9bF(;G}KT;6;0*^(E70_i}RS_BpWR^43WOfm_VdyC!eLzm`*S4 z7;vt7C(wNHz{ zv&oqh8dLT5Ep!2KHVj7V&$iZZ!w~N$U!%~N8nYd|(dCc&8ZNCwSo*IQ)81U2ux(!8XFWS9smcUcB zbnA<#(4&fZ%Wd8tK|KoB}9y7eDRoe|i{5)VxAbKi~ zo~6)qb%b^dc&eiKI=Y}niPTU;?K(yep!E-x=j1WNd9CVh7@}~%fI#$|Gb_Gd zFk1g#&+ObV#J>h65s0qKBJd+cp6G}q3N3qju{{HRK&|Bly5RmaL_-nv{i%WgT7TcO zv^-`wx=qOqLyQ_QBM?1fO>7wOMo?ly5!L8~PaZQop>+*`=;>p!g#mAzstlYyX`9Ck zuWemQAbN(GSTo>_lSdkgsQD*i^O%0`59#%ziIj)&6k4{@=ey{FKt*Bzt-ra<(G5c! zc^F5bWlsspLKiS*Py=ZFW@Y=`FvPk64Fb^O-^O)f!kPQT)Cs2Cfjq3Xwiqi6r@5^iK=U41qUa{8=LtOUo1ce4)v;Ho+VDa>`0kr;86kfek zMB1)TP7HX#EdJXfD&bUV9y2`fx(b0vbNl4LfEOr70jJ|L^O)g_uPYFUwC$f98SF!9 zs9Wd)>TFLKt^bKN5U7pU4G2Wq{!d#O@B$^X`XXv@ymB5h-2A#Ff!MSuU`I3!uAX-X zT|iX|4xsgqSO>ach$U~%P-t-NyxZsks>+4{TEDV25Swvt&QWO0Ifi%81?ICI0%-k- z*1(Xy_2v%>jk(b9HoCxESt)?l?`RzhbbLdLK%^~p2C|F8pSBD*cfq^6Xk+Tk{D2&4 z$rK|Pt-s}@MjkU9Na`3O_l5?6NKTP{(b{pAei%8LlHH0>ZJf$ztyQTdCYLU z^)xpO5yn}ZZA&(iJb$u#=M(f8O-IB))2XC=)!w~V;0h^PP@e~?U{pAh@oV%_Q zXs$H$ridz1ehQ=Yi;sHbF~f0NfIufL1&mHl&Y;kk+Ap^=;9Nalz%Wx>18DtqTm0NG zxrn7|{b(XP`8mHEaZGLmZ5E;op zHJW}KXg#wUNbfeA+%QCH@5WbFhoIeGKI#Rv=T_tGX`O_+BzGH zW=43@Qe6U(=JN+&^r?p%il~++59Tq$$xBxgh_tXjY#4CvNHX?p~11njTCN3aI9VMXx-$3FG4 z?!lrwRM~|))9m$V*w|5;gc{8)#z=aY85?~n&jntou*mL+S`f10tZRAgdg!8>rMAZ%CEY1NCYn7r$Z@ zm!%cIFfXo&E+)svJg(B zw#?KyzVqhkRhe5a*saQ#{rDwhRpdP9ts7T8xN~mSW@kI2v_J1;ZZNEW9_|+4qu!6I zcTkC)t^ab4U{Twg9VV4CD@|T3s@qt$QvGW8rUNtc;+kHsy5d}Zdg+CL!>4u5`|h>V ziM#3-;rDv^AG{NDDz_|{yKQ#p%H*xHU=Nn21R-6wS)F{~r*!ARt#nS%?K2viU3N@y zZ`y8P9st!L_Fg}0Ib2Wn8rk@Ik}~qjElvN6De2woJJNe5yLhzEnlx!h?Rm_W89vL- z{;~X&iR#YfCk;1MFHa+mE{N<5)1%+HVr20w?&6-h6~d#t{)$M~-ebSSX~;mMsj|Vs zR*_&)l>^I)Q|E2493I+)^G%K|eie8wB-xm^xXPrCD2#JS@3s}cnA*cT;dHfI)q4Ou zc}jnhY0RZVTM9FC)XJ7G2vd9X{Lw|<3kU0r?fj<|gejPIB8};Hk8WXhL|=4%WgCN^ zMPaW=+T)<)G~m+*Yh8aK-~g~wj3Z}YqS_*~b=SHU0aGCsbb zeXJzi58Y^$SNf@3FUZ54e50 zKkrSYZgcPLG%fzZL&?iV49b@!hMZG77kb7Z-RHQS?xtF3wrWH|#-{)8=ty4$4x739 zV#xA8R$e>UZnyI4@AcJ7&uZ?!uw&A;c`oH{ul_tV<!7 zZTL+UC~o#6{9NVG6F%vO&bg$wX{zl;o`>m%U3%6u^#2~j!ZX6Z2l0C3K|8H!^Y-kX z8MyY=ai!n))~-Hs&tEaY@KMHs<`3JsN2P z`}Fm`$vrR09>-^{N}sX(^yn_SJC`NQ_r2htd;eut^7{ujE}}287cRbc z^HlA-x6k%JdHY(~t*IJr_UW)n$#8CJvVZ}MCA^(fwp-&A9D%Ji9ya@fU=bI1z~>Ckkir_bi47H&D3 zd2QPq_RMAHPOe{mW?}ozrKjiB7sl;=Tn(MioA>yd(Z$D!NJ{Sc-;R9Py+kRfYtgkP zn|0<*4ho*1Z=_;J9w$MRLy{{ryG(AyJcHG3s*BXsf3Yfi&h;$6xnmLseA(%F^#^TM zH+4Qf6En-TR%4gz4pTxc0+)9*>>PWeq;0uNOx?ch zfv!-il%}a0lLBY1Iydd3^~yc#+ZI1hdA14OCb_kCE4Ni%#(*atRlYo=@#P-`iUEN@ zlmuB(0~)AGzvjCwNcw$5+4nf5-%48o`_|^`KLP*+eky#-g{jitg$5o5IU+v`{Cdv< z2S`W=4DJv7J3C0(j~+{ZR9X77rjg%mK3|Jgaynj)+g30y1DDE z$;UpguVGGGaz1Cy+!brk>wfdnyK`|%m#WnQZ`kVCe_V01X-b}R zeCwn=JEz(=dW6*tTblP&o3j3Fsd?CgPsx2;$4-l`drCpDxUu9GhlblZ+0g-5gCWi* z#mf#Cwb|<~J8H0SqN~R6;200b;?Jv6Mrl>dr`tvi9S2*5u4l1^Q6Bwq>q_@^#1Dp= z_V3A(gk03-rLWsa;B=3bp6_v_8>fJrY_vmJKGjhb+I_9a=s`T8W zn~_M?xVwX)rlH;mjl4D$PDDUD3WgOXcT;(_L6(xT&@=HS^t~iBHYP2Tv`s@hrQs?4 zn)Lx;VYc2v9nVh9dyMxTDuHNAjG=OoA^0lYjD*`tmH=k+5p?;9uRg(-oAjzyi6}f8 z`<+Taj7Os261%~TV*nV+yYM|C13VP_YB1EaOh|d!=4gq(9(pFe+n?*M_6O;1kFBAV zuFge?vYd9?7(3FIvn%>ul3_9y>;}ofz zVxot{i_RqN5*k`+crM179A60c0^^Ur9ojzckWHJtU41 z1LYgUI#N%vung69q7k^eqGB-AbnpNS$e zvqJ0d@I*=P5ti7eVglzR5Z?Dva=Z@@dIHfE$F4;fS0QrZNA#_9b*wh=_>b0NR}VIr z^wtY4xCpX4vn-V*a&g)R1lCM0JW`}9yV%{7;*FalQ3KA~;jaq$c8|q3fWBz9Dc%y; zXO6dqc4|W!eb=r|d!f^iZO3hU_F^`5mO6Z};~H+EcB+~}j`r$k%WIWuwXWg4HuE@S z-yBK(=m$-%yS?)P;lZI4sc9a0NGLIjNgEnTLk0;cy0wT`jM`hm)|?#`qlM{Did!sB zBXU+qhe_Dex+Ps0#Cx`}hIZfR$BD8*$qf2&}&LAtd@fI*R5!k@~hLzN$ zfb{koJVJkJT~byrc2`FS@;HmT`VH$(tTmu34PhyMSSEZO6JcVtIm<(mNHO+`XsYP0 za!HDVpZ0mh8^*3nP^(IE5#tVb#-GQ*(H2%iPr(|Q{=!YBNUdBWv3_Q473Z%PJ&9^P z$sI+JOkd@}*qE1tgF~_PjVR$&~4Pe>kPz2;e>KQ^k2G58|5A zM_=7*>@6JJ>X!6On_HMHv2X4vDTSm~mwxW(=+3g_>{Cc!vg?+K^}(g&yj*p>Szk@v zGI4STJATlqO2^T-M?FkS%j?nTpZ6%{gYknA!q5gs#jIB6nN{ei z$knAuYFTNPyyC_lHN44Oljqt<3za_=g;$YxyHqGdGOK5|SC&;`sIo~W`9i#1V_e;$ zp;+-iVN5zIJ_ZlgjUTm5ta4(*VqOlK#Iv(H$QNy6v~fjYcWWy25h#!6H$vM-N1eIq zFP4=ifoY7cP-V6OZS?mlBYxCV_Db?5EsS@us8zyXv`4aHv$Z()db_J=ewF7(?=ugT zvMo!`v)mK>eKRFt^$fn%!BVH{!NK#99K2IOgoR@@`+4V~f+!C|5*=-!T#MJ-C4$`_ zd=sOgJp#FE42{LZInA16_*uNB=MF8&K;hjqx`(7WEj}murI`jY-HDy{^ekS}5;4qs zx#%_5#NIpj%FOs~K?JwYKv%;Hvm5PUNUy5+_+oKowLxu?-qXIyt05MM3Z9me1?%Mf z%2(t=rD4ra&$6!BSs2o*I*JXfHacd;vol;qjy>vgWZR2Rd6QHH+t)Lot2w_Zcqd^@ z=xmQZt6OL@ny`=^A+V^!n`e^zuh8ew&vPt?@;OBn!t-=44xZ7GD;7m(;46pA+G5DK zE6e))US3YHGvm(7ZA^3JPg6&vW!%+ul719g)@zvrb>QJ;vN?U zGDK6@IxG5|+eMV zT89h4M)hVGnBqn{Ug?uGS?B8T8Mf@S3q7HUXS>OUWG!-t7{$}}s(KrvfuUVR#$c}* zpHc&Y1&@sN~Mm`X?1#o=fbd8AXrmoTcaeE zy?{*BzcR$WIJnywcUQBgAx0xhe1m6bLG6!-8Si^}Yx|-n+w+Om33?qiyh&)1Z zqW1@0@xl)p?qT8Tn<6j`pS#e)+r}+k0{&tmMc{gZ>NSd9L^fU$5r3{qa=(LjrXu@3 z`&f!rT8T$DH}H33z0ATr-I#*n0mi1xbM%aXO6ocV)aev|@31=|$E&;(T}(pXLK>?h z5TTXx6FnBJgr!zhOy^tGp`gxgm<1MsK;M8`_-e?ZTMDdMi9Z zQ^^^Oe{rqxb7ZM&hmL1^=LOv9`^MN(Yc{_09%oqCc>!~EN73{1;`4O-{q%6E>Asw> z%op^J_oDt(Odlwu7??<`Y7Y!VnMn+5)q7R5aAzqE0v$NJCF4%ENbKfBb{hR~Bf7?< zS5?Pd*wo>DQ`DS_{+%5>$UWdjI<6a9{^&A}L5|4HF3^98D|T;O+^d?EFCHANP^IY| z=Q#BUY+Xt0Rp9IP>2-WC{l19)+N_xAG8jPc7dRD>pSz>!%RJjpRMCq_?+E!qjG`_< z;7Ir8-VnWt@ZljOA!P-tuMyCD>Oxr50x@HU|hi_1rF9a(|P~qs;SkiMZq=eDZ0Z@+Z~! zn9VUf;o=i?w{%aT(6d~}vpt!PHnxyFb0bS&k)y~_Y=SoGv@N#LBP)`YF1dhdyQMp7 z#HVd^Be%4Z55UBBsV1~TBs9KuKRJMta97C&oI)i9u*l~(|b3Kv>G2=dXp1W zB1zSH=ufM%0U9**_44fQwAAO^w%2sUIGnwLbXQkFj>CCY@=WakuT+(2||$87zbD+_t=a*5$$O3wKm@Uqti{HtetAK8$_9 zO{-OBM1Xsm(-z|tlo9th*wIN&gbicDY~IASyA`>1JOZ}U2LbK=gUZe=NcpNSG4+y zybo$`XTye?4)Py%Q%BpNa?ROu3I3a~1!9)iW+YH*^L~PM3Anv>X9Yg#Afg2HKIsyJEFJQ65{i!K_vz$LHvYX_id3VP+BLPwQ)VChbE)7IR_ ziAOAhMV0R*42M!|R{avHahZahdaRmImoB!eCC`k1kd}-_*YH`dxPD9ruAQ*A!mLGW z_%h<93D1a63uchkrGHMpsiP@%Q1pO%=`!Ibp(-XpO&TK$yo9hpI%OzjbN0Q+<$$Kd zktCy>$kSc)4h7Hl;nrN5aegMx_LT}I00`Ai%-gXnywH*I_O=P)J!gB-^Yfzp zCbszg8nX+7ly+7SX{l#>GWl*nphPDJQQauKxufX04Jn!`NJ@@+qWy@&`EU&_^)q?% zDr{RMerF4VeBEBNI!)N}#y|SLzrVniw2K>9ze<@;D+rQ+-wx%L^A!M&)Jy1taoXs)3&8{Ry&Ify(wp(FPd&e75VO{C2#ZpQ?FEf zlg~fG9=ybWVgow{#p?9Ud$6k=K_tcqai_O-;Z|O6m3d$3bZLBPMGOY3`Xht`k1jD* zIRa{YzJc?mOM-RtNAcG@Ldl^2Uz8#^%xtaTJ{ z2Fv`qeY&zx*D6QwyCq)3m2?3o4Ur*H&tWRKM!X_MIt(1VoN90nV^aYASZ4SAhxnzA zf_T8L46FKuB+nd?Q&DGHC+6OR71fv!aTmRbM4|En6g>w5ha8efMx}yQR8a-5GnaPX zpYAOfY^z}i5=B!AIBH!HMM>h8xcsD`VnOSCj!38TD0zfS-;bX%M|%%eaIJ}Zz^Zrh z4&j#eMf{+HG23EEHw{XkZ-|j@WYQc3?g^%AGuNz#GqX34gtfUDfKj{|lrPs-2K!U@vT z^yW!c0fysfk-BuW?u&Lw9gN-4=<-;s@}y;Fs!3<=(}CNFTV3FJmag&A{7~BIb5Yx= zK2`x;mQ&4Y&-8B;sPgznYWSA`HLAJzW8_nh6HoLo*6~Yc0(k7$^{(PoEFjF6krbgz zX67t$B^iM!J8V7M3!Riij+FBQ8Vqs}Z#ke!wV5jR@n{7sOOi0E*Q;8EE*Nmg$?B=f8H=@EUOA>qUT$9WOtXg{qw~}7MadIEh1*hnS`j)(8DaAe+jM4yGy0f(6|}6BZG0ep)vHud{0sW z#b7b`lE|f9s&M`t+>z1^U;;;_cX?He|EJu?i6^wP>xH-b^CWf$C^@gSkuO=t-y0B} zM8Xq#e@HIqhMF~S*de5r-3v%8e1deI;B9P+@9#->#ow=Ak}nKYQ|0Q^)i>TiM0@o& zj4WJ|q=0+EHwZNwAa-}=_Ay!p1GssSjUZu>0|6g9qbPF_y~p(@3jH3~)%# z%*R;b3PbToR*&UMAhh{3qESxSPzwA#5ICKfTAlpNYPJv0={it`vWJ~&tIl7;*YV>~ zr&lnxnK0v+xtU2pyy^wUeL#M2vy5i2!&Bem&qt*I43_YN0y&@8i@Po5h3pfrfZu&V z!dr!v7OVA)KPioZ+~K4Fps2(=iA^2BJr?#{$a$sUIqWb%WPXg7?CxtK(fPUWh~4q1 zW474FQrZaq;t}Dud0r#&=${$QjVR2E;Ej@W>&PR@QYFHJQ;q=Z>W3#gv^^Xha_(_e*;{sofVglUU*;wrmrGMMleP zdzmJJRNLncyawN=*U%wO7q$1O3$F=vQ+pOF=9nK~cgUs*X7~L+^l;+e2~&GMoYv}H zUd81dBZpTq)H7Tqj_WQKH+D>-DVtQs9Ql-6mMKUtR;dZ2|0bQ$9oV~vv4|jHx9E^B zTb$PobsK1c&mnP@dR29zTBci3Q#tE+0f~<}fn?}PX>B_YV98!`Uwm+=z9}poaBi-H zD=A-l151@FylE>MZ`0P48DF>8q!*mBTKqZYwU)YpGR1>j82kwc4UpK}7}RGHO5!5IGXA9M&tfiWgN>1Z=IMawLL)5J(ghujAhSGO1kao~6;$d*g8A4S|%yn^0fl_;paNUMS%5U+%(Rv{fHB>5C82)6g``UYd zLsPQSSc5MArMjP%9xJ{(n}#g4DTN7?TECslPR=H5r>Q>zi2JD7^nffQ+gGb2FvsR9 z;a{o8E(N6POCq))f&Ewa{A_Wazx`>T^=*bV@{3x!Q)-E8lCeojyJRM*Gg-NtPAgUK z*S3t2soyE_yH(pCbG3j!~%yYyw--%!4EeF0vw*5X{%Akapj z&X}IDtFxumbf|u=idjI?)W{eu_OF#6}nrCJkjt?|fpy^Yu z)D3?e2IF)0rIcc5Yq|yMWl!fq2CvDkD3S#!4P6kNnW`R2yyOrNAo4sT%%eM>qqj0# zTywY)r*ZDudzQs}>BKFRypmkJ5PgsISkxn--y2XJ9b93pEu4h5n_Q&pLenm|sw8xB z@xw{98g<;4T!(&n@RPZ;?`bvstTb4oQ~z9%*;UuIK(9Jmd`9f7C|S_`L8YopHc95A zafyvz&qbYh3|;Nti+tsi8N3Lue0J=_bj@7<_VYr(kEM!z`6-_jrx6!AzR0U$CUwvj zro%{zQ_{y9wKRFV9u!S32w!WyqCKyFg|c&MGr^sVo1!h{i)VQY&Pbbml>Swec3W*5 zd(reIFTE6YMUbNHKwMM0{+=iczL;J@!Kp2ykFJ6?KjWpBfA1TJ(2!Jj!@}~ z0yX$0xV=}?f?2T?hFbU~)=X8e#%DXb$?K8^V-8-QhTniU?-5nyiX|Cd4F0WefM(ZR zQ}e{*A>NYC94II8ImiA52Ck_pS0qoF1dIc z1U66Ry8lG$#ns}L{|)^LzYBMs5n`ilFv^y1_u$C_@&C3E4n`ks@K&y(@u4@wwlyC3 zM{2b>kaBX5sK}RRbD2O}TYP_I`a*)~4{|yK)*1{9Wg{MniqDv~$^tk~G?E(nRBaF8 z`C#4QTzpy+@Jpz@zvj^F>T*W1e(CAVP04cYK+LfStllIRq|ZMfUq{kkF(n5JOADdeUy5&9R(&l zg^skWi;?0TRKj@9nf_P{LLNKgW93Yh5L24|d$6)UT>$+++g$IKneByMk2vD`3JUrB z<(EWd&WZ%$VBE9bNgXjBU8*x{k{ypKg{c>r6@6x%KFa^5S@ux|!m@*Rp?lV~*#3TD z$;)r2U<>jUAs=xIW3F3wok5-99DYJv7LbXOSl5|%eLF>Ir}pEsRs=GBmVfKthwP(N zw4o9?8V8ET6iClqNSq}<%7C$D(WR$>k~_opn6r?f_R#z*6VC?jacPGd{UrgZ9GJuc!5&XY#cWTYffm5^{4@Xkr- z@BBoXlI>Ehh9^W|&fwWz>PB6OVhaifh`@L&9b4eMO{BR% z+PNAaVz785DpvRSNVd0S=xbk!5q5hDelXjqR&cQ zH9wr?-Fzx0IBAYdl@nfE=CC9ucg8{g+;_m0Fa=#jx)-d9`>7sYgn4rq*bD4I-SAD!uta&Mk)Q*VoFip!q z#L;doX7LG-(>zfGfdQT#46Z=q+h;wxNGZ<1Sj8j5Wh9S6*?Y zcYoy@nDI;!`ExSXOeI{yw>bt2MA2&|0drLq$o#ZKvBKe{Br^UHFfVni(nhv-Mf#`p zv=+ed`zvYlEpG%a#gnz2=De!@#c}vkFK;tL)MmhKVAm`;8PDa(KhNwa`mBuJY#q5@Hydyi`bqK9bY+e+GtD-xAt#=eNbHeko4$;)NFjB_%8rziQcevX z3`|JBV_HnFNastwOCim-#rdUQIij835y%H!sw&z<%U?>iX}fuKEP6a;07Z7W&y~jA z(x&O%mRxepVks={3NYC@o3;+t5MSW19hcOp9pQ+|;wmI+)^1=TU^b0U*dbqpU$3k7 zP?xea4N_iZ1AX;tE?(>DSZyaOM4Dd}XQQh~Rl$~tG=W~9Dv`U15uvYE6trakWPlVBn9WgbFR?ZT_{ zB2j(%fe9IT&<8euOP$XG9VMhqAup7Zf4Rl&yVM)>ttSacdZjw&L}gK6apW&2rYUEt z?C3sr5d|^xZ7*G=Pj>^R5h$ty_Mq_33GG9=JDei?I>@Spvejtt6rDd3Zoc6fKm<5;!n_H7FC6acX0 zu*dP33OXTIg92_QY!bG>>ORN%PS5W?rht0ZqKB&noPAMRuim4Q-~^gZn=qOQFhdr@ z{+_Q9v7k}Mh0Z!tdSCiqO>2S4r{YdHYx`Bn16R{h6ubpGk7T}1eOSaVjN7Q4noR#Z zF;D2imAoD0hqH+v~1%QmBc()?aNtK04g7K1c@ zs`w7$P!|B*)E2Vbx*)eAgsYNzF>zTd+DUxes=-La_M?Zg!a zck12x`G1tGRED)sq6EP&%xs&L zJ>mtAp|haCBLth1ydc54HnWWU_&68%QTZpmM;m-_lF}wv-2!&tpB|Tb2YhA8B?afw z!~c+f(0eqgb5DQ;_mCh|GX)ya_gfwEwoAsGrLg<9?wCqqA_%@Lm?#cPi4YrJO9 zpw8pLBK?Flc=l?h)pWd1gSJP?9_h?W8Z%;NgM4|!9%!1^Dp&*7h43+?o+ycO z%K1bhcCl>xM;_a<&e9Q)zxZ#v|oaL?-NdtWTZU3)Tk*^(At=pOCc?~U0ZK+*Qm5yjDPPBZ z-hlsvVA*e9b{jVH>$n3U1cYDvq-(0uq;E-Zt-zrW*df^GisT(lf!1Hg?biw{(`nEN z90(!Ib8+nn+#f=S+X7sg&;9n~B7rFNE8r4t8=&rLFAuKSd%i!}?N=by40GTT=f8)O z*DLsW+LS&e%m9p_x%}AoVS~Cp;`EgB{jv>zl^oHkm=36@B@?DzQ{}kZv_Jj$X?49>b96hZWYuA_N ziaO_YtR$EoMm3M4r+Ijt_$rQ`#$4A*T19Y8Xc4lvw7VvR^RGTk=r7sM3DBP008V`V z`n4sGSJ3iTNiaR5CKEW34rT#Y{8wylcTKQPF5?_g$M=^s6$0`u3r_-)DN=zh8jxhh zx?2wu`k(h7)Y=!94~XT^0kOkV)@sAy?D=d*954)&J4w-)3Tm8 z%2$9UE_E$%g6`i7{M()mNIN*61hPV89u6j|9AEf~W+|N#`6ICb)AQO>?S6nG|ur#-eQU*HOw zopOHBV2cDU+)e?nowq4fGH^CiBFU?hHk?55c&daxDc}5Lrz~kLEj z;AzF*%V;g~J$M`JY);^v-r8EzPDMJtZ<-v}@7-@^O1s3N!BSL>$%Jnub6KKS53YD% zWSi12sxl77{ZXQH^%&SZc{)_L> zDc=f<`K8{zPoDqP{jLG=s%|1r{D&{$8s@r&)Xm#T!#l~Sl}_P%2oehZaYV2B@H&wS zQQ7r*i)Oxuk7pc>7GIOWbY8A0rrqYhXg$$_3j{cfEe-CNW4f@`jc+H$ESy|OUAhQYyfG)#+AHaFV;g-o~UQWEcfE#XCF zI}Rvs(qp$iw}$1&zd31R?f`{VS#*+^qzfET9aF^9m_{l*18ovz_Jf0KTQnD?(lu0C z1v60EiPf~Wl|Zj3E({Ka)mN*}kVM`6Ju%gToKzLjDvofSUd=Vjp)1^QTdisBigkVB zY`YqTN1vD^tWtPBzsPgzQ!SJ$Ls%UAd0z78_o8O7Nx7idG5!!dDK z$Nl4O&!gI4ls;9gz%j5kwty-4a!Ct=UfV-VwyKq0u$Q?k41zw$wzza1S zT@XY)vn2fn9g7mxGlN|}M5Rl=EG1-Rtk&R4^EXgoj9pwAJw8!Llv=WF_@$*kC=jsg zY(hEyCAS>ElYZ6(-$R5_d)yvZ!UpjOzB|{3MaM#K*)$c`Nor)PESwg{DF>l-4&9Zj zcz?1`8NO+b*W|4VcwR#C9S+GM{YRlp5=@A`J)e%}oPgSO#+?$b@Tryji=XAnIm?Ut z38&t^n4S`=p*(Oo`9w(I%htM(x+&J*NL-Eu7SH9WOV)Xb9`?}7JAHXa;{qy9QkOA< zVJ^|R=XqiA9n!XHCU5B>zZRZt+-}%^R5~h$j{jdNuEl)2C_RzVVtYoGE=znV^AbZN z!>EK7uQQ4pI9Mn;|Kf6a;%E?!YZrtS%v?p5?jq zs)!ucq%s;U}^t+KEQJTE>t!~Rac_<*mCjpJbK-GL+GX}+3vv*k8Z zo)?y!H^udoJUbCMZVUo1T8D()QAG z+gjzJ`Le0Laq>grsY_sRY+1hIkgsei*Q~Q62pr&Z3SL$w;^rJ`Yt? z@lnCe*bHpMecO%(Y-y$?v?@~`?-kzRBWeqxw%j1e*W$2cQfz4^);Unr<`^j2iQR0I zPL`r}y`*Cw+R8$pul`t=?C~t;il~d+(Kpo>AwLvi9^mr5tSSy$c2;pHy98U7s5q2O z#g=&z)c152xdBGGt4j`K6Z)p6n9AF!wL~ZoooiO$2Zd)YNM8hnHyt)VIna*nfWn=t zZI65WFTM9E?zJ3UdTtx4O7e)UR(H`SNlwEgykA;isp2Y}FHU|aI~Q9va7aA0!8BIv zeTxvU*aj7)*AG}eZBQN(Pi6L`2hb3j=nC78`2^Yl@l;(sT};Q(R;llvtFb-q@hnn) z=%C_|7_gJt0w4Uy-a5Mk8)=+7;Yu8oGk- zFZ+YO(k`96l?kN9H7nqHvhfhESxzTE6&+wHUKeWkVc8vp8xgW*j4my3`E8cXVc*s%@ zC|q(?z7|&Nre1*c_>LsEUPfOAs{#sRTL2UeBr7&l};C%#cs@ zVB0bm+YADg=_gS=l+CE=NP7NbjcsVhF;T?8cOF8s3R}M-k7w29^wuLHw`mIN4)Ii` z&@3E>FFWR@t!Qm={v%)OD)~oSBl(I$0O_ETds-zBD&aNAC({?>=~YmkAH}1(P-s>$ ztt9q-Frftpz%kGELjPm{Zfx1P8r#sV9cC4_mN?C!Bn80P&~59hYFpvbQN*L4zKCls zLZ?5iv^{PcE8e4OP_~!I&qs?Iusv!oVkocS6gO7+@>DY`bop*dn%ZMO-zP^?enjw4 zh{r**I6J$D#_j1d!nH{u-xJi^Wyv8GrlBUl2 zqid%k?E_}b(q3EZ;gFpaYv4lL zA29LV^w<|3q0DmNpG$AyEdlM0=0oR|;Ari}rzR#-wr(4_O3kHDwX4)T^Q_J9n)zYt zEpUw*{bDu2bm9E2gZ2I%RvY$~2KEJi{J$OhCw*M|arMV`re~b@SXnQyD1n%s{@jO1 ztUi)cUcS4W(b}5Z%J2Pz^(Q*^ztDCkm*1D~wX$AlQ3^SEI?;z%SzVD+&fU#rw2E`Z z{N4{)hh`9MtG};zbXmJRq@uMdw`vK*~09MOtVXYa(h2x9iKrA zt`4eqbXm6?S<%YMWi5fwPuE?Dh|LKV9xs>dmIPU**{8s`y&td+%pjUqe^c*RvZi29 zMeBpy2TLIKrx#v`h`kZob-cW0cTJFGfSoUt+xsc&uNg$I>TUIoCBGN!t!Qn`ZCnB& zozA`x5t|(yH^k(`rb)BX$jDPep5aZut_3&1tI(5d+tIULG&6++7)D3AJ;Ea(kz;j?N%@ zRJ+$ZVt>c%t!Qn?ZCCpWNy)1N5nw&1WGs2M0MiZei^id8u7l)(zq{zRK@3)pi9{C_Cr9?rlms+f!8+74ED z^e3iwTfo>3_-iO-C}*IEDwg25=7SaX{fU9y7PRLM_^T-8Ud}*1Rm{h69R@2<{fV4z z3mV%2pMg?_a|TMOVg-(CI#^-Tp9t%=AUt_@xx4+M>%+oeXm;h0ul3PuJdk|)y*D=KZZDuQhtuzS}p8F z#2m_X-pR7M(R0EN<2H|Shl{V=xkC`sqoRn@|FM9g2g&InfO%^r7uOvXG)!#Q?WweS@p=18vdE>`M|o_aq_ z@H|T1IX0_W*n@~Uk?S1HO3m!4_s8(&Q6%Ts@@k;QpH#C%FF1R1l{iXrTF%3?1=Ukr%}!}$A9_kt&-a^c$6hV+N>#P_; zhEo#36%ul!tLCUR@L0V3EldC5&KcN7El=)dPgmR}W_B6CN z#hg#Z*f+uj0XgMOrGSQf2vW8^HK?gHt#lJZSm%_*_CP5^? zph1wbl~xR4xl9FQ}dDG`JJIR=XhgW2= zUjRX?fuI?kWXvic=n)XK9tg7NBxA~fpk5&8m-FRKiT~>)W99%s58)90(%*rgixC&Q zmTq4j?~}2(o9w=e0Z*;CS?-kRPejkmfbf@wTQNdPoU$4Q(B2tSyUFfm40!7IH_M$i z_z}?;|AX+ChFCE|a-Fgo+R@$_^Sa6Io(#C)+RcXblpA>=)lD0GiTNox5TpaniV=Rq zDT{p^?M=DRO~x!?!UaJ$%A4x86Z2EDAxMV=D@OPUr!4j*v^S-wn~aHK!Udgx0HA>c zK{^mpgPIneTIkIP|HmndU556i{Mt>%9Am%*-(;3IEes&$`~C|-7VfrUgx_$=Vn0E9 zQxM%`Obr7rI0py>67zlkfglTaS~0?}I%Tn|(cYA;-Afz78$|(SxMcPfq@R@MI^6TYIVcx|2=Ian-VT2VUJjp4GeGTnRsp}?V zQkZZ-0U+R$aRK9t@J-Hdz6L=ShFLMfDNb4Je!v}clQGT=xBzpryh-3k%x}I5K^7vd z7~zFZS!^NT4!X&h1O{Bt4+!`Z^P4ju$U>AABb)}f6`1qoEcPF0Z_2@LGG+=BE`Z!9 zZ?f7>%y-U$AY)>!7{WiDve>5qchF77xG~{^6M%p(G2b}{f{ejhF@%Slve-ufchF77 z*fK+|vFEXFAO!B2l#lQB#t zTo8V}yov2i%%|LdAY-De7{YT-S?p}U9dwg1g-p0W2nhHP^C_7SWK4_|Lzw22#V!Th zN+5>dLz&V|#+WkT0_&UQO|E{#{Dl7?NZB4MhVYhC7MlyWgKjd$g8>($0RsNS{Df-| zq%72mAuMvrVoLyb&`rkJGvI=No4!jmX#w9QtPstOEDfA$f0nP>8+*Y|#4M}nh^JWx z%w{SQdw(x#K^^g`lH%u=Y|oK3w=Rn;~%jQ!C zzK_D0ary_XsWpC_GFd70pv_NAhu&QzjwYj4+N^obkI~GHQgzb$2TKVxel)456wibG z#6&5BNgNreN8@K%m7w2zKzq1m3_n-+>hPahIrdpa@V>BE)RE)0Kf{R9jJ zHaZ^dduw2i=ar$Mp71clRM3VoK5BaT(q zhfhbOG!ry=#!yPY1D)4X(&v5oYnl7MrgdYqwDyA<%!bke_PTyOt*zTGy=^L^A1gA4 zccA!4yAk;MQ>&3Xw7WL|-{J4`+_wi2iCAnhNw-TlP?f+2vtQj9uDzjZdTC-}BDp!7 z*oe)c4&TsiAN;#+`(WUy)n-&QJURpwzA*%Yihj#&rsWE+X{^9GKM!r5@*LC))Z?U$ zx_!-e!9zpBgWpN**Ee-rfXvsa1qxqp?#f-!AsF5MzB^Hz6TK*60KzuC_qFwI_!;Dmv8Dy$4wUt*f;~s#RhonTNaJJ;D>&6f~Ei)PfptC z`8BXS0wM^G4vk(J9UU366CI7v-U6PJO-xKe!lSjGU~PCbA{Y^=%aC5>&@nYrSMMK~ zn3zlf_m5xYvj7YQh?%kq9SW4bHV@!$kD#C-4Z!hBwvIZWj6mvMM1a%*J|sF49v%fO z3h~pM+#Lr~0+ys0!JRT|IE4}1wPcR7BEQPgMI2j2hGe~miX?%iyjw+zKrO(2)RQ)9 z>t|a?RLIVd(2(f3H&21m^aLFE$|0Oa9A9P|17R5$`nDhxlMF!E@BA7F`&Pw;hJ^1% zL>f4tvtUXFAViaf?HmEZ$2srrfDS@en{2FH1K+j<1{TW!s6VLp9UPD`wdZoi$!#qd zactbqCcOu4gGstPyc@S8j=xC8xXT7Z-`3qsdzgZ|IPTZa7~gLdev z+)*&Na(fgE4(GnR`-!ElHfft?#-Ftx*nAE+_EVpOdf&nQ7*qRo-Z-gU2SywlwU_hW z1GSx?DevZfez~nP&0W`Q0%Wb<6*_AR&_@Z?_Fs*L0k!i+1G;)vYLC;wNwxa6!^Gqq zs2kYFZPG?@Il@hQ94x@Q*2{)eaX$BAAA7;$XqSrohn^xy>( z;V?@<(|N&*6#vSGU&}U z+!?lWM5~%#I8I3TV8pRO^7Ebd0LhU%69GvDsAWQ0)vi0nK=`a^Y#=yN4E)8ua^wEO zpk)mQ(=tGzuXq#`?iP&`g?=#N*icwk{2ov^T|5yebbzLfy*bepZ7*!Ks`X#FHFiMX zMD@P5*Eg`oC&d^Z^TE)!^%E;g4gAFA4aWS$(l`CYF)$?qI20wLz`>@B6ArTyUBt1$ zv8Uucz>x!*@^1YYA*f|S{KWK9V<0RAL*EvJ-)ROQ?5{Nj!jd;Z_!~^g0E7=|qkzy^ zI!+KQspA15g8CjnCVN`04VKQ$QCJKmw#C~7a?9VfXAFyh!;*!+9% zf!v6Dx;(raxpkmv6VM-iq5Rc~ImQ0&(Cp^yGF`UE-geG;`19m7F&voTk753RsrNEt zcSL1uNPd1WPDn6d#IZqA^WZ%|GQDCV{L>~-%Y?M7(G|u(NCrdS76h9r1Ak$1$hf~S z=xxivv^M92*GUweJChG|-fH>#?+eS|$X9Np;3R_znzxTaS=QH)xD5 zw-|4X3_7nzz_bidK+s1)K~|@$oAD0w?(YAs@i2Au&ufA=&+2LeX2kYuG#Uccy*^ z7QK(2891Rwa-&YjV7}UeF-mhnV~#@JKa6pb#{?sejl2cS_ds4GQu<=&1hJJpBp1D0StXx@+NZ($eU$roIHc6Ha3`+0ePh@!YWCnH*zp$@N~h8qcgyOyvi4&kavSKPVzWl#IcdL^u>E1FaE_u zkXH<9nULm>{lXXtCat>Aw)v4!2DN3ay}N6X1l7_9npY)SC_u#zq_;a*bhP3x>WeCeRK8hw+8AQHNnL1N_Q zVbLO^BpAFSdf=2P9yrS8xT=@Qo*zgP@r^;f&%2Fu5R9S`@@aR>jP*kKRV2bV+py16<*Yrw< zeyryks9PNp6oigKj7!}yz77$va)IR`Ky|;tcSWNjJwMj2JOLEu8}?nkaG?uE2cs|7 zWezBL?FV}M;E{;%U2*>}9VR9wy|8b4bUFG}r{fb76O&)VLZYI8#U6l@Z3w`SC9Acj z=uWhz3>Astk7!UOaAnd)-AXIEG9RzT3PX)xIgMB2hXP&6FB-PdV;_7MuqFUd=DSho zouP;~iyzGD8)M25$0?!i=J?*k#00W3JS0r_tzBGH z#U_&gDXoCETn`F|^^G98V5G&sVj1gEyWpXo(eTJ+%b%)(^%;?He`nzU9a7FEcSQStisK)>2($?Xn<Om>KXfGyELrmlJPM%=))l$-r?!wrKb8#)Uz@fH6&{Y* z1uWeXHD9EwWnfL5Nvk}8bwNBlhSrc-uaC6ZsF;4Ohas^zqhk6mvWLVj8x_-kF)$=n zX;e)A#>tSF(5RSx?siD*>t16y)pt~e#9WMu>5G4e{9Q)HdWC;X1MZ8V0{qLUn0~R8 zA+cLV#qP2}ZF>1$)|8*Nlf z|C#ZSSc*|G{nv#oq-~t9pH24jUEIe<(L3mSt3Ikqk^!FW+EPY=;?`sJ>(djf;H(is^5$*%D&~qW=(K zNNkN!G5!0;Lt-eSV)|DEhs4eq75n)^&^CSQrAEc{Z@&+TwHp_9{p+qnVm?O2ex3UvV+EprNuX`|)b|<{)4$I%BzD@U*uGhyHog2Eqhc>U2gUSx z;TRRuKffOm`?%j&P8WTk&jR?of1y#aFTMiB^d+;!sF=wVP)y(4Bp4ObKh7Bv``4(L ze!_J~tjegEewt-Stke!hRmNT^Y<%Yz`jdU=FVu^0N{)#v57 zQ8E3T;gDFKQ8AM*!7S+In~aLhl7hDBOJ>r5v7GLUKCd0v4K?~d7!}ix|Axdy95dy^ zryl`b->5wUy0IY86`Zn4H=@;c=Jg%p&*j4c9|D0Ru4aMJftL+F`7I63~WEXw?)dr_jG;6MW38E?f(E_>FI0$ literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/SM_enter.fbx b/interface/resources/meshes/keyboard/SM_enter.fbx new file mode 100644 index 0000000000000000000000000000000000000000..119e3fc5356a9dcaf271190d3e64912fdba4a3f3 GIT binary patch literal 33248 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 SA;rMJz~JfX=d#Wzp$Py6*tu~4 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_a.png b/interface/resources/meshes/keyboard/keyCap_a.png new file mode 100644 index 0000000000000000000000000000000000000000..f409254292dbe07c27c37341049f2c1f584f1f2e GIT binary patch literal 3081 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 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_d.png b/interface/resources/meshes/keyboard/keyCap_d.png new file mode 100644 index 0000000000000000000000000000000000000000..4eccee2dd533ea24e6bca5e6ab3114fce37db803 GIT binary patch literal 2648 zcmeHJZB&eD7{1@v$UChvW#|LVs1|9gLHf{C6Ka~3eAGyXKF~y%3Z*bLZ|UP`%0%a+ zXlccUP_n0dw39>#Vc?Hv4mbJm?sFLc#1ZyNSD%=+H|_F0Z5|-*d^zXcIwEUgmNnA{e1g^?YvWf>tea*$zxV>& zMkUh{4V#d%Z4J+!eZMbr)dt(kgRxW60xt(=<)}kjMm`M3FyhcW!5EzYDBu8808Cre zzlY=De09_}(y?(={MG5-Ob7HEtovNZ_?`cv<4YXpPXDActL_-1UWK72OL_8PiuyxR zWkHWKMa0S%6Vu)EQ&LhgHFg1+8Qj*I8n;jNfr%8pD+c^N81JY z#1h5Pi!c*oQEle^yJgA16rp`(G|7XK@>{n`=1`-qq6vYGr92Mj#EBEL{iFP6kO**a zE^YHKjjw~~Pj}n8PeAYvhnf1T|Ir+R;CGVJw!uD%qJE7YRp#QU7WR|?BD?L0GOB)CPGCV-Pft%NPriNU zX|IAcYt{sK90BHKy2{9{Usf^J{w`U^X7%;h1MhqT@CdQ$8yIM6-Y-?EUZu}RA++uT zzd|0}jLT%3gjzvQK^uj*Hn>TN7)`H6-yEcFrmE^WHd>#AORSeedL1iu@( zSM;>Hy1Fxho{hmd%lfgFmX`k7i^{ejbI8c9*=nFEr`~2_U}+U+Ahk*NrxM_?cNTZl zTr8;~fyZOMvxrd@R<2mzB2QLV0gh@GP}x!bEVIrR zFRJL~ka4;_%;-R3oza8CASV9Gn^A!lPfP^n!DTsW{|TwUJh-8d^U~mrh6))kUkgT^ z#MPZr53QXtnXJwt*v~e{GtCkOtJdAN$4+P_g$M{>*jUON?G@0?M0#^jup(X;jP_Qj zDFiU|Zi(V>`fIHukF__F7V*O6)Wc;CO?+p&(-@rlnJ0T4Y3kfHWoeElgFr^`;1JED z9b}C{P@utJAo}CN0AmgTe)rEFa?Dj9Rw{Spr-Jxqq4JijwSu;)K$9yqS^+N2rEhoJ z3I!cKE?mFv0g5%)@3OPAJ6xG3Yt=MLxdAt3dT%pQmG#Ck!T;1V$ zd19uzJx^>PTRGU)Jq3kpS&~=b74D02 zpGU{A8#F$5`f6r_wDSI;-9dA+^8{9U<3U!>CU{)cBzTu_M3RS&7u;qNxY1Kvs@ zK>ZjMB1%)w(p459brjZAy=idRXEHBxq34Klux)S*g8_-MW~_Jc)o3f4aCFV+km|Qq ziZv*XmC&3zO%OcJD%IRm-SIXFm?FZ7v~j8bRNW~p7lF9;O#fkeSI>a}a?+*)!x#Q~A9=Q?%3_owMY{W9vlW2_ae=xh zh*{lUL0>iGxZvOYJT8d*Kjy#Xi}PR1>2VI{zI_5B9k>f;9upnkMFjw0yLqj=x;!Z1 EPktEd`Tzg` literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_e.png b/interface/resources/meshes/keyboard/keyCap_e.png new file mode 100644 index 0000000000000000000000000000000000000000..09bd5bc289d89475106bc4583820f10cdaa0f7f4 GIT binary patch literal 1689 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|T42r;B4q#jQ7Y59YQQ2sm7{Sk$1I>Q#_F#ldsglD7#; zOB;Gc8b2JW{yz1?jHllFc7$FPXE^^kmxY0ep@D%xfq{VoWG(4Ta`YfsMWJ>MGF*&g zJH^@=m?#K3utUfXd6++7G?^x$Yd^pq-P4fy_IKUsQdgI>Tk(3=SDg}>@#SsK+i9;~ z?d>;wqd&cvg@qK$3IxKPiu`Bp%)LG5yqKUF2V;Y<{ieO;#SP{%jEv~klac}ttUt=J zW9jwRTi^eF+AkF$zP-Qi=8|@9&o09@ zT{ngYKN!xOkMBQCiZu@^x&;l^md5fjo_p+O(9m8)T6AEEIxIq@#DfBZ8|6k|Bt+6& uK#}%_f!3}-Ug3+BdB`kC>wgO-FjNN{?^~8w#mvCKz~JfX=d#Wzp$Py-;?-vW literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_g.png b/interface/resources/meshes/keyboard/keyCap_g.png new file mode 100644 index 0000000000000000000000000000000000000000..9c52605428d9c8b82cf213068b1cb6e0ddde9f7d GIT binary patch literal 3298 zcmc&$TTl~b7Cs39BccH`aw(b!B8b!!wOoY+xfN{zS)_s(xrJCn5D2#rNCZ~_gHT1l z1q8Gfwty6}lC=SXEjG$UWLQ)#*&u?Dg>VhT07-W2OlO>(edyxDKAbtv02kl696`Qe&Om1fQ0vke(zEZUFrb9faZ3R_NSN#T2e?H1)RfULMeF9^C47< zFC`>ACH5)hFo4!2PgiHZ*K})hOCf@7i(NIH8v@WlnrbW8?xZNWs%sxhFsDf zwLM4~uQ6$6dZzaCH{VnYrQgOCju&cKcc%1+qkkf{G&h%mkE5z#BcK2fKmyQ)b;18H zUHjvb|9Cp;Bc%IH@%1bJd&7SB>wM`y=`7vVsI5Lb^78UZy7#pHD#KtfL}k^xlsf;_ zd9x+DLMr{+vPEl8W0%HQrd+zP5V~*f_<8P7ePw@9g92G}Hb#mooPLxxLqM}j>PQbj~^O@E?ITy~{OQX`$7!8h@4zEaY)5nF81nb6p^6;!I9=Ea~)0wRl7ma53 zGObI#+H+%=aD*GQUl^{BsQ4+=@BAS{xmQEjq)5;bVC=``a_yY@1M8O8)}~bm6e}mv zx}>JxqTZwC6#sD6GS$mIgh3u{~U_Hu5gitL)9BNK9LHy#4DB#>n`jq@vd5 zEHS?qw+}2VBKoLQYKr7|cC0*K7W!rLmw!E_9;{Bfx3sFbeoe`fukr}f2Mu%M0$f;F zxVP=#WU;b{CRO#u?D!=ss{MHk7vm3pfn>ckyO~K`dK!t?woCIzWW{7?htbvvsUUmT z=g13EB9$k!Hq5ngGR>z9oIPY8UEQY1I#S#-A;vU1yy2jkUhLTz#`iY2C*mF+S}}n+ zQHx4E(aU#yyOYdb$b2+XPMlq$MG{ zJB_wjhJ3+ve!Aypvh(ZJhFo)JwsQZnXGmJpM%(6rsKWFargNe~)i*vqKC=*SH7^Sc zQwQd^=>rq`;YFSIKHZ2U-D)3Jbrun_F{8R2`Oln#w|uH7BrMT|lNcmPJ}eTMIUV5+ zJuui+cA8`qI4UOw6gm=#4mG;9V17#=5VS8c2PI9tI23EalRtl~roYj^8O+P8s;b&O zF1*u2v3%*WZcb~Sa#_U!pT`gs6!hvc9twIsdD)>_!m_&2ND@|UB&P!p1-_%o^68uJ zcjUX%k!?~Oij{Lfu>nz0+nTQU%fi&e#6;6BG~6pl*VcdXtwY8mzTuWDSh z914^(D=YRzZlsWU9pS93?EcY5Y4nto2CQw4``vHTPhX+8w36$uE?0F&a7s!_WQzOT z-TuH}h_7ULlL^|2naU>CUE)cE50*-$KAisLmRFT{16K3EO6svtb&}~=!h=%%cuY!e z1+#Bf)~#qjvRnwrYrHM5qPbQp6S})%JZqskNB_Yhy*jK;%{aI_f`dM$tqmPx3n#8^ zMqA;CuNKc(Zhu(%40VO7r!CMsILYB~jBr1a>jgg>D{@?7u6DUb#SsTYr*!v$#drQS zubxR z&b0DJ8`0oaKpCLfe`OcE2@MqNKGw+%X8{d{fQJ5aq&k1jUJ_J?s=b&>Fv!~|h@V-a zlbuoUPeNzo$qn1aE}!8)NVIcwTw2%#`GYJm&E7Eg=zP@O6Ah8EQY3;kETy;TpWKxG z1>WN3n@^)jFyhT~K|UVlwho=3@QFxBHKK+U?l5>V?-VK?lq@eU##i3fMFTEzuXdkA zOruL3kIbWH$OGa$XnRewFDp*M?NhTF#b{(EBdrv&Ks;RN(lUy|bjd22wnl?WG#C-p zI>+OZvXYsbGK7-1NiO^}rvf3hgq8Q5GV+k2qm$FKiN*fNo1Jy)qgM6x^+S@TDPKJJ zT+?m08ImL>of1@4Rdq)!UUkeGNLyQZ{Me1r(%j4tg^N6$x0O0%9Jk}`Zg!Lo9Yq7+ z3NzaP2`m6S0Q&tH`oF{Va6Sly4Bq82x!yk#obhk#Nl(%M literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_h.png b/interface/resources/meshes/keyboard/keyCap_h.png new file mode 100644 index 0000000000000000000000000000000000000000..f29323da1173bea026ac0f1f5f0ff97d51fae35c GIT binary patch literal 1616 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|V9T==L4kpR945(n z4%F)@Cj<*7ay;Nh^CkY&m%#Xs;+hqM7 zaB*OGpR~+Cj`jxm7`FpDYfW!Ab}AYBGCpuv*Fd7J7*S1{qcOD~;GozDOjGek4r<6z nsC^h_L*hc@!R~$rW`?GV>faV^YB|8bz`)??>gTe~DWM4fe!z+W literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_i.png b/interface/resources/meshes/keyboard/keyCap_i.png new file mode 100644 index 0000000000000000000000000000000000000000..721e6b84b1a7626aeadc8439c65bf580709b88db GIT binary patch literal 1581 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|V=MPZ!6Kid%1P9OP|K5O8%g6-&}AVJmUUEZd}Jmhi~+ zz|7xq|DLMuc00m!;D3%9g8%~q3j+fqg98Iu%z@SJK$+ty*DgRo&_P0+?2v~=7>p*< zBn<6r+h#ZHoV)${^W$fGI2d}CZw*SU?7J{jvged zI2t%9O0Ja0IR4D?;z%CTvuQ!UKFCyUb88W0s5`;Uff2&^jX=*)6l?cE4nU-^B$`RI zLhQ+cLhTLYB{U>kDNJ(;l%;s`wV&5xWMepJC4PT@ov9oH0|SGntDnm{r-UW|>3f0w literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_j.png b/interface/resources/meshes/keyboard/keyCap_j.png new file mode 100644 index 0000000000000000000000000000000000000000..7186df71c1026d15ea1426e95e77882ae4318d6c GIT binary patch literal 2213 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|SS(hG8{;(RA-$d${;&Ov>W|(gxKG)|BSSdpIArFf%7)_>0=-L~4Y7JRFOg@_? z{P}TcsHF3z3%B2YcVqnbIr`%3qkneX*DQJ{B+$UXpuoVu0ZPW?Fv-z_WK{!$1QXGT z_JnI_C>tYVgN~_hvVZ`Ch1>b_=h+xRNxosbBo7-{%Jm-ei|@aW2{1P3>|}3XV5ncL z!PVTr;4p35?c6i{(^E5f1Q||D@^+BvzgM^4oa@8;^)BnhKR@1m_nec$97cz_NA_$v z-`p7e{y|8MT@=2rHMv*BvG z(zqA9cGaNJ=LqqfTI}4co`0zm>F4|@?M}ETIxZ9B_&$|^B zQWH1*I~-V3Tkxrt_g{vI)zAA?e?FX_wXCwTl5_pU{$D@meyqsxf5^iSWhTS-SUjUl z>qy%6+c$%sobXM!B>KN(TX{ikZLYr}!-Y*(v!?#A=4t;w`Q(v#P7DiDC&r)u#3$C> zde>#&9cG1x|K}FoKY!1(yS{*pVeX5Jqkhp{i+1eazkRjO6r*$7abfw>z;YY*I^4bsofXDu?mE43keE^;j4_S*U05#qBr!_m;c; zP>Fosm&4in@9Xp1Tw!Evc+<-B=bhBVPB|8ahWs^5Oz0Vx=o|qlvnkf@Oi>ZQz%;e}{g3sptlC@%BU|{fc^>bP0l+XkK^(*8l literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_k.png b/interface/resources/meshes/keyboard/keyCap_k.png new file mode 100644 index 0000000000000000000000000000000000000000..69667f42e0c4bf63668dee1391701b5659cbddc6 GIT binary patch literal 2800 zcmd^>aa2=P9LIleXT^&UwlpCRxj7;&!C~&0A#4kQT#6_sXzMTop;}T=X<~R z`}y5_?%ltB-3zmP=lKG_EZ*waBmfc+4@$iD%v<#t06dl(mz=v%{7P=Va927!pC;as zPUne)8R<#s!nA^%i|J7SzVGm2pWh_CK58`P7~%Ag$T%^Ns&=Lq6K!4D9pff}U0LAMx3bExQv^^5c!l%F4<>Xwa!@fD1qY zAOj#XYW}x!K<;bu_b=z(2fAtEGbVqxU^6S+UD>^IbhJEFaq_Clpa36$!s0skU*GasNuNv->XLY z?Joem*RFF6ewUx=#3Z6LXwrF>%NRiU`eay~#&z`fulOovQRvVLr3ZzQoHK&d)Kpum>!?|*C+w@8=eNvr z%S>An$#8xxZtv&FyXAV8cbJP~(n~6jBt>V-T1o}9(Q?`qj#r8}(;kL;b0wVBC*X`a zHnX-oL4`ue=OV+HH0&9-LGwie3MGd}OU>a8lP~wj1$CDjRr07~H$ZJb+j!x&F z<2a8EeUMSdra~FZU7qEjbzUJ#_cp^SEm$l6vp=y>Yu#hn4_v>dAI5rF2Z_9~N>7u< zrL$!b#a-~NyV){+RExER042*=dwpE&3;_zSDja9td6zA6#(Ecoyt8|!vsLsOArUIo zjIEx4{HwDOMNsce=1KFigL?@}w6Slc;OEoxJFgTnfU@dXe*30rLc*x!vbp`^99;_m ze|G|2J5>JNcTBfNY^O$-Z=?X) z(E(&==(}AQzN>2y3SYH^b+Zz#B8Wmd9x!Q-07M~OQrjJ+I~;h*n4N+Z1Sx=f#4KMC ztW=w)*tR6IKi%k)oJ0oFiu!e?)KPZDPR~oYo)zA`Lr8_mzTTVo;A`8=*>qgU#UwrD z@i4G45oi4>pR6OKv(p6qRAP>Go&cGtRa95t78!!6#e@ld$dk$EAI9IaIY!%GB?E)t zlTY$YV6NE}Wn36&c~$unZQX=@akL*72Ad5v3}H}+rSb0Z<6q}o!fCgNgsb)V;Oi@ft-fajC(P`#ss~7btL#*EhLba= z|50b&NFo_g;7o9EzDnHE#aV2>KtN+WEr~4&EL{K;U4&nPK27k15s?yjqT+gdB?1-+ z#y0W!!)545TnJDS4H{D_-KO#aTpcF})--=&`-lpty6>eA2`u!6T%YyBWo6A4Z~O?E zX~a9B`47^8l33LeQ=6Vc40^7*Fle9*CBuzOgT^}Lvi$Cu_hux6geVRj2&N7av~E1L zYxB)+w@^VOM%C6SS750zyVbo{OzeJjSHQj~KQ7Q4l9>K=>WkhQ=vfI>rO1@~hvxNU z*!S353*=iqE=5o%*>pDrpC8xb8?Dk2{ORv^3?9<{LF4qcCFad^8)G_4Qd>-Y;6dO< zHC2>(8B5<1cFA+D{gSuaOQt%K!7%zHuO$QH*zx0C(AUe>;Kx;eF)Z1MJ_GlmD+*pvgV*ha$lJ$n|(dl`R_pfETwewmD||zTW_K C1v!ZT literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_l.png b/interface/resources/meshes/keyboard/keyCap_l.png new file mode 100644 index 0000000000000000000000000000000000000000..d59c24f7e24a7223186b2907e536504da6623385 GIT binary patch literal 1631 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|OhQr;B4q#jQ7Y&vLdHh`3!W+|V#3iT#*Y5_i}N)g>a< z6`RYA6_ozX*)0C#^dl#;Or=N62Y%Q1GAJ-GaDXgqU?7V*u-Y3ab3EnR6(|TgNQjdi z@~{Yl(PWy0q5V!oHq*NG*LJOY{qFqkb>5mY1M=Q+GdXSek`74-&Rs>;9j3 z|9fBO=bcPUL>u>@LayZ@o0sPKIwRHRZ>Ctr3k!xZR49f!kQs$=Z4{;@rUWMP0}~Pl zWXA5#SY8q@V-&SbB~z?aTTM_Tf<+MfNQGihP^&@&0f7Ly>e$p4 zh)64)u|j;HR13jYL~4W}3ZWEHgknVvHby`xBs?#0lXNbOfBL60*qQ$6oH?`m}=gKv~%OSDHo zOyu_H(CEmhl(_on_W=ZNoJzN>dPh&28*4@2`U=8b*j2Toq8ef~ReCVK` z-Bp!JRmq%1+OPZA1_ z>8t2CU2{*UX>82W^F@#ui_DWsrJvEx$S(IE!ra|&Y_^m^DTq4_hQgC9UP05|6=MhK zntRTIFph`bq(e0*zRe%k^PM0n0*W(nb)Ad}Ot|lY^Cfw3&fw#o}IrH%q`CCQCP%?7-?CF+Ow^10vC$XY42FJA_9!5yv!<%P~A0E z-L@T(&tk$eHqu(jMVvsl2brf(DBcb~7CJ~s5HTXS))`wT<3g7dY2l%P*kq!?LGwUo zu+`ZCTr{O4C0B;%2%eJ-O%@LDjEv;uq8GFb&6hH4QILYF* zOyap;sA+BehJv)E5*`UPTP|u7ccslJuM~j@jHWpC07FYeH0I3su($8b0Wh98k)NMr z*|*nR`;jrobM9u80s6$GB&TJ*+g*`ry=-hIDf0tmgH6}Xn>TSzM|bz4crv&^eGV@= z=DdFq>F6Y75#~}L&cDx>G<8~QBRz%aiI$}@fAI3q~gKY%+X8L zs0Jm>1Fo*f-R?n?DXwUOL~t+*kF`yqN>H=~nU`CCW*~PN+h6d6Kk|5|8%2p#B`TG4 zHj?x4^rPB_TU5nKgK>6BXvPKBk&*ef)R9wTDf2yucAI>29g5GD>%wK;=gKG3*C_Cy zN3EStpVm_7?hJ?e`uh5sn$?F{4(cHTK@b*8Wq|}0LFR@hDjTzv!H5`h8NDF@@#&4^ z?xoL3m29{-zx$+hc=+;wJg6YFWPG~-IyTOauw%@zt{7K28X(iIYJ#3+rSwNPVq>*RSJf3Ka}k;<A%Ff_8`^WCM&|sjH)fF%!xKTQcA-G$R?$Wr)7nIG8 zKU1&W>W*Yv)4U^SXN|%498|K!c@6bZN6ouF;X29X^5Qf{4JyuTu1R=uU0W51mA0Se zQ*698C!_88P<6F$tyfgpro_tO2C5-gXp$^zpGjA!5W($VAN%MTU${@`(M3#)swnnJ zcc5spqsKT~&5PfzUx!VIex32UmYuh93nI8G*J&Brc(NQyL3xeUL5VL0%eb0_hfj8w z&)pIBqZ^GU`b1z1nqC$5sY}J5g$hc6ExRtTI${bgLPr=o!^a&-nH1&{Z`9rDdKjyu zXV&dr+8%{O$=>*^C_x#P_qEw>phqr2%lcs z7n+ZvQ|$0B!d}=Z&U**=|F`fmKc#xlpW0c+O#m8=pU F_zNQ|_;COL literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_n.png b/interface/resources/meshes/keyboard/keyCap_n.png new file mode 100644 index 0000000000000000000000000000000000000000..950fb88e41b66abd8f22d6ac9beb1a036f6cd853 GIT binary patch literal 2230 zcmc&#ZA?>F7=CXl?FCD7*;x1()f?CZ-GaodQ;~&REFdh!f+ny#D8&pF9? zpZ9&v^St+*Gt%S)AMYS<0PsoND^3Mq0s3$>eH65(2>=2Wap{Wvd50B6biNEk*?Czq zGBKAvB1@Ig*&oXLWzhh5dZJi#p!n`zV<~d$UcXbn);D2?@GL@fE-;;UjPqt$AN~sG z1&_~8dj3EJl?4=cwt~KOxcb7-TXy>1WTW|XsdYhSGdgCTZBP|26|w*W3V;A$!@8FL zFP!ZjdAH$!xgi~+`1+N18RnYTnQ1j#mlad5YPpqba?UM`)G0FT$4NM_ZTau<>Di0V z&O7I#&ssMvov`KQWQ;B^+Nz9u>`#-Fo?=16(RXWm1r%tf^4m+F_$R2~k>Hr=_t_Im zqgyDz!@gET)tfQ4O8anjK9ctkSd@6wAw1HN&&RslZ2(<6dS@5sr5I^?JZq7u_`5!|-dU8^IeIFhQ(#lEo zb>k-?AgydwKWcq__>K0hEGYQY-fkg3A7_8#__Gm*k%`B}e!-ggt{n(*8i}DBxfZDL zg)50By)(M5M5vBrfpC{Hd?sg*n-&K;zKly(ZHNQiW*K(^sYrp2-^-ma1GC%9rB}!! z^Cq90r&AikG#TjFH3H3O!HgHphQ^da;){lUvH~b!dCc~^%)*;**yvvllw^Q!*zzi> z{#zt!fwae>I{_-7gadp-EwCXc^$GGsTKO@T_Mw2hq|Y_V6PwMxlZ7EyL@w?i)FU;Z)N}R7POO~7$e~SLaX)>8+=ewRBR(_YcEe-I#7S(@# z$OUuF>%0jG_o}-Ocl$Y7qhl_eEZvKtJy3nwJXNqYegFU< MF)mr$9DAtjAC2nT7XSbN literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_o.png b/interface/resources/meshes/keyboard/keyCap_o.png new file mode 100644 index 0000000000000000000000000000000000000000..2c1755c8877805917a20d32661f936d5988dcff2 GIT binary patch literal 3508 zcmc(hdo&d28^_-n(+oujmc8qUqJZfI;=gU~;$6i@*Q0673) z06@Me{Qu$rvZ3TRj-xgp-QS9Tv-1BM_HD-fi~TQNUX4%Kf`rM+%3?fQSmE4821_Hk zqHiiu%yO@Yj*3z|ayWcs$FSAiPklUIMQqA=e0;oTrTfy-5`Q68UTm9jKTW!*s3?lY z5zoLR!5K&Oaf9vG2L}i9j*52}rAZf&$xN4UQz$rVKAatiw;*Bths42B+7ds%)J*=+ zc^sg_ZN;6P&e9_Ob&hzZhtS^csDc9Mep@kv!AP{`uI9G_C_qHM5xd-njPl=}wUq>+ z?S9p165{82`)k6CgsebHa?>1H3PR{WtTlfygu3!)GQlyp)24E3F6@yRVr?ddCM?A_ zm?=RBs_M&R66tIRLoCK#4xMK(e!*Km>DVTB+dUtX`G8hihgY*8bzQ3cP5r70NOa0v zWipwIJ4M9mEs|h*z^{685oh}2mlekDJu$~5{*!=^&n)aGk5 zd2rXGC!uco*BRP3M8vw-O81Mk=j$p>px{V`#K*v2SH*P@-r_*k&vIbd(KDaPWYW|{ zO-JVUul0Hz=&CVOgb2H8mAJR1&t3Q%IxH^f_;Q4;E%J{lx-FfS4H{-?Qqa@MGnlK_;(1cR#(O zaNfr!ywruaI3VQrO;$F%j8E7+kFWO&>T^ei8x3|;#+|)p-A;Wb-4{GG0 zaphk}F{2D)Z#pGCg9V^;t-9LUwz!c)Tfh1fW@BiBgYV<`$iVETy`zVpu_>MNTk;8S ziS_+SEg#}rB3G;=VxKN-H@cW!zHEtx(;?{)llIA`72M2$<6^^I**fN)-uuQ;Hx{ zk_|}6KZ~9Au!m>@y)dl(%;hVf8%3p3)2j?sv-Z(W>|)MMO+2LN-F%sF4(b=%>mqg+ zn*(}}Nhlw?h4GavPZn6PEH&$0yUwJHNA!u`s z!bi9h(olj59Uo2OsB(1f?ag>~wnX541-MFKJogOiPJI0Mz?+tc!`=PN)Q4yRlVie& z7i?V-)^-G5rmFx0ka7%gg31Ba5kB8&EPIY$=#wxMI*}p|?rKJwjv7h;C`w`X)4;@) z&kt`TkZRr?fY|^;2}210dGE>=e4n09B*jfg%YhL5GX5D1KuW&!8eI4d27}U5EIPi^ z{HF(%*!Ozagy7s@OP5dp88tOwa}l@mW;977{5YqyU*f6W{H ztLndNY+YEO{FLGcN0lnt-E6FhOiJ<^OLF4Y$6e1{lLRt(jWyLcx6TG4Yhr3JCOLWb z@*mi9P{01(s+`B;W2kDknHbtl+BFsM(MVZ(o6kMH_KJ#%Co}zvi_yZJJoPnyI4T!o zuB84aFFJZQpK?yGtE;QmhdJfb0fL_2-`lGT(Si?U4Cz?hGE?75>B!1ly8}UQn?Jz8 z#@3#njrGyCY4A2^Ps%7RE*^ra8UrWjFy{9kG3J7k@@hF4fkrmd9^bL9@13BwP_x1Q z$s=S|pHCS(gz=$iQKJJO^Bm}7W0?Gc46V4O1wuhV0fj=*E5$2k*{_30O`~hOG&Ck~ z?-R=UJP#DmRP6nU{sPuw;+40b7jZKS1QmeT4d=B5+~TqQkXY*A;pJQNe${DMOrnKz z%OMDiFfiHFhI}}=h->~3;aHH0%}@I5oouly+iLZfq-xl!;xR8tp*6LpzCN?sbo8RP zmzQ%{Uw!bqvFA#X;B4O5_Sbk1MaIfXe1InAYL-0Sn#}AbA38-m!I<nHrES4N-iB7cDpz7Bm%KT4(#hE2!8tHQKZmxCbb>)IHxnd2QGWV z9uEUsBhyTTIv^rV;$e>lgdi(jkX36xJedvXT4rcNWo3~(SiYld($9;oY|=L^bPA*t zuHWMtCAiUye%qUYoM$B*(Qa*D0_Cao)c4#M$xU-~4P($6gKF@u(#>^X9B04nstRBC z8nyPA&R{TJ%&7_z%$7>b1UlfET?PA-P>4gY#JIATXt`1`H{1{b1zDYhdm0czQXtxb zG#huSoE?&7U#cvU5lMn+PtW`wLYQS84?$$M$K`K<0ww*I9FAAZ!+Xe%xfq(!EYoEo zP9Bt}1kszDo0n1(8^jJ0q?5XEVQy}2mm1Gf5}5cV&+|<0cXrQZ*#Hov$A!T+GQ4hl z{*wdY5CQ<207$M6_TLhI3-e!3Z4d|04JE&EoUj4u{+8iel%LlU%cH^2>YE?bE^Q44 P000NOlgFOgps)M~_BGVb literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_p.png b/interface/resources/meshes/keyboard/keyCap_p.png new file mode 100644 index 0000000000000000000000000000000000000000..366a9ef3ff594dac68e666b81bfa347f8fac82d4 GIT binary patch literal 2282 zcmeHIYfO`87=6BvQc5TGLv&$4X;Gt5SVxqR2?k0F+KM8VmB0{6)vBO!krv8S_+$f$ zVi;bi=%ks>IbebUTDdqbZexfH2}1@7t8yt=UwPlzZecAqFoa6|}Rdt-MC>DZxuIKyb+nVuJ&-#$tRIhC=RxvWF@Y z45R@eI(b3CLj=LCsX1FIrVfA=w(RRWO(($a<-vxjj}I?>nrBr%RGPOo?Yyw7DJxuX zt3<68xry~uxoWa2fA`bRtmvK#T zz}dOH%juA*TNEx>3C_;N9Ywn1gSwR1GbC8DbM6u%4zFI34RzyRMqN}TH=h#e%}p?v zCHn+a)HDo>Hf$jPvC{m2=0t=$5FjJkL$1)o&j1G2MbrG@Jytl>(Ql_(|J+!QfmCFr z{3enB8SA;r*srCntRA;pi$ZR+#_oFiGfR-hPZ^%A3nai53!DlleDb?q-#al-YsB{x z?nQ*9oDZ8TU4|mKt>oy@gBsGK11?4>8T-q-Dry=Ixo$M-8q{bi2jUr{l;tJ}UK-pf zo|743fDX!1=`}-N9~QH9Kwz(jQfAjJy;AJpX8hMx#+j(xEQN)jr2luFBuOMxI&e-bzJq4>!A| zG4~r{C1lcTk^J{Q{WrRH!1$Fl$2{`4>$Q#TF7LBvPgj2xwH=|edQtSvRyPL!@1BKr l_TN>%$mOvxI&PTu#Ss`v40Xs2eAx^D!1eL-E@4Hc{Q>m=0S5p8 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_q.png b/interface/resources/meshes/keyboard/keyCap_q.png new file mode 100644 index 0000000000000000000000000000000000000000..2d58bc2b46cb5d2cfdbf58abc76e3763756e9d1f GIT binary patch literal 3681 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 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_s.png b/interface/resources/meshes/keyboard/keyCap_s.png new file mode 100644 index 0000000000000000000000000000000000000000..365a833af2441a593c95a3b45e1b4ed3dac1cf6b GIT binary patch literal 3405 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 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_v.png b/interface/resources/meshes/keyboard/keyCap_v.png new file mode 100644 index 0000000000000000000000000000000000000000..1dbe395005c5da1f53826fc94752310af14369e2 GIT binary patch literal 3024 zcmds3X;4#H7Cu?%1QV9D2+}A47X&8?ibEq?0*bg{L>v*t4j@an4M_l77LgYbC*8x0 zAPz=oBsf-!3L23Ta0Ff?0%Am{Mx@z5dj$0_Jn5PSeRJmsblU+RC9Cxmd_fUu z8PX^e3MWUtRz+SdWsR}3gER(M3`rVD4%j`q!fq(uDOhfT#|U9&FGXDm`F{`G7N}sC9kvikoHO0*9Srq|gO6Q4mKn zOkfxKR}aJnOw;S8?eS9chMt7mk*2oj$_7pb;DUG8WDAla&eoVb2tXd8){rOSzrLQ; z=*cehpWm4^z#$4sFG-SLIZ)oeZ7H1G=7608{)L*yJneM!#!AX2wGTM^t2){JFYp8P zsI!KK>n*m(OjcEtvJ_aif3;24gm}|_5 zf(DA1WFVH=nOb6fBTa{+NWR`3G4GSA_U&j9%B(B@W@~JTZGP579fzCp&=RuvJUzv3 zUa53)Q_EZFI01UTlqIt#q|Go-A;-7h5ZruGTkCzlu%^4aJ6Q6v>^L?Mo6|M5BEzR1 z?KK1~&10##VS}(z@ig8`FCL!n_9*7&MtuI|A1)&?B*4(rb?hBw8Zt+obdA5z)OuoI zBm<Xtl9#*235AVhK-I`6h8a39u#kZ$rRH1GHrga4&IBPv(`Cbt?B^r7pP6daf ziqG`V@F=wEm{W;~Z`sdvI7v5GM17M{e;hf@Sm$_fjNPh@(pFax1mb5I`lD}2lnkUq zTB>SWTeFLpct8UbTQ4f6><1BJNVs_FUf#QR zuj)6mrY{Fp_xzCzk!$kwM#^wwzc5vQHpIN3cUjE_$4A|!tMafC4FL}_gS%Rp8JieJE@6l&R=)lnj+eT=`sK({*YmDoT4YZI6f5RI%)b1!S#Jwu_pA6L=|< zIjY^P`6D?493uSVGZQ4bdh6F&nT?K!ClQvcEoCW`(IH+`j0WGQ^$&-U=}MW-&8;xo zt5n6M0ge!{hUT+x0&~Nbwe@3U_$wnhw>ruS?U}gbsGs%aau)P58yNfBSspo9iF!|C z9=Yy)P*9Kwd$6m!yGm&p`Y^~8^%Gg#2z;dL3I z!SFAkCAM5?krT=H)aM3Gn+Z6CRJ@raVh;KS5(Qq*E#kV zi(r*mJT!}{#U$SgNX)z$9^6Mx$2Gs1HY5y$s{KzMTeY?0Jl!%; zp^ThytCeB=@)O38aJ4V6KN&ELbD4>hw9(7?%4?sTbzVG<`!zM-i&4RNcj( zRMg!lHwsJC9Y_I4uNc@a)bI#cUlA(PK?0{3&E)||x)_^!ziw%`>(>!KB^V7U$FC(2 zoP~(O5b6TdjEJdLX8T1GBDgrJBd9u|Z`MdVOCDgZ8Q5yrpKjgr^t;)}pBZ?^ZMAzI zOTu-E?iYpR%e%Pcx|i`~_>~;<4_mL|UPY|bo(I-Xzgp(Is?*Kyk)KE1X&db;tx6f? zJ4ZQL&^M1m!48eP(LWOhm+pXq1DXv4U9JTuG zcxbsKA)a*2CgOq6V8%P9$)Qi+_#0oN2--7|j&>2K5bw- ze;fD0?3`caERFBM<#JzFou2EE+@EE}h0~Vz?#;pDjRA-NIA8%RX1W;6N1=>?j{E&x q)xUkp|6BO~VTr~V5;$>|&b literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_w.png b/interface/resources/meshes/keyboard/keyCap_w.png new file mode 100644 index 0000000000000000000000000000000000000000..a71de5124d9c153c076ceaa22550a6617a37b514 GIT binary patch literal 4149 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 Ypx$t3IWFmu2LJ#BQG)&JHpQ_14`e)Sg#Z8m literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/keyCap_y.png b/interface/resources/meshes/keyboard/keyCap_y.png new file mode 100644 index 0000000000000000000000000000000000000000..ed68e21384a58128e8b737f998abf650aa2b6bb1 GIT binary patch literal 2701 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 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_0.png b/interface/resources/meshes/keyboard/key_0.png new file mode 100644 index 0000000000000000000000000000000000000000..ff852cda9dbc378f423851fc44298a9595a6d1d6 GIT binary patch literal 3132 zcmc(gYfuvC8pmG{)AiEP)I6EHaLP?nldQZU36|PwNv7p}H>E^t3C#=P70k+XQ_fj4 zDpM;fn#>F@*;y}OUOH;X8k&)UXcsd?F_eW6*r}QQaK7e;op;`Oo-e<5{_}fgp0_9j zx5w1T#s~ml8u)`>C;(9K`5JyM**3fD008UK{Ug)Ek_qXj;!_iV@6lv@0wVB4d}2aq zLj2LplYI$j0E|U}e!d4OuV#hP0~?~&({L!W*1_ZW&v+>63XM#3jFwWDu1)^_jtK z&CzwL9MWc*6}Wf;RY`6Y=5n=T%iW>ZIW=4akp0YHFj&&+X;ogKx!(Esm*4t*E714b z(sM1QSvuDlxAn@t9Jfda=jm0Ajv|6v>B*%CAY&1zrdSZy;(|mv<#oHAvo2T+K44r5 z30d#)A@Xn~&zHHF@p5wt6D<=6eV*ETW)A zME50|G!xHr($<>(S?J9Vb8~aQ2}!{Q>{u5e$`V5JK%r2MIdb+;Qd3o~cG*j-QYs(k zg)4CtghrK2Cfav|(IXNTaka!j@*T!x2^!8Q{O$w4YQt63H3I64XRn)Y!w38<% zYrBm=-tK2lSgOOrv1pqT{DwR((!1AbEv3`94etzHzG-Lm2C;R-YHaZAGcBu~9bJca zUFeVU)DpK{ab^tcB1kF53jsM5kr1q)LRX1P^r>c_aHK~d-;nuhA(0SH@?{)zrjc4d zx>xXTx6gi+aVn%hNwZP~E)B+4ut$Q+|CeLtvmoo8qAc)#o~ zSK2L*gVcR^GjN>9inR0TO4{KlV`h0*;?VI^WzPuPC!EXi)@J2p)2hx+->IDTSQlgX zp;jR-is5N_!3r$zx|A?ep*u@=+%(tCEID(F?PdlE#W0b9fV1Zb_ zNd1&frw^=4oH}-AwH^Y5JbXU*U~}CL1c-!OWW{TEwgn@fgGTHBi|F_vW*$YbL``rK{ z-k8b=HUr1+2iT7?$9YfPV1^(E`PQYvEgJIpiUSn*JIb|2r0F7n?Oc=mKKxZ%n}DcL zy88l3;hIyWj7QcE3*@R&2m;93yVbJ{a>d) z4(Qh|a0y{ge!A{FpjcYv3n)As4j1~JPX(8}^GOe$_0+>q|I^~);+wGRD7;07ki=F^ zatZRu$@o)U1M+3D81Z&pcx|~q2nVv$muer@ZD%r>oBzSiepJKNb?@b@nXs`Oa0QO`xtMHX5|T0SWX}kx#X?Ue z9rK9DkJkqn%dn&MqkCgv+bp5IRL}*O+=$S&ETYyM(5yfk`MK@t!Wmv;dy~?%jB+UmTLYmBQ4Fs%L{|i^(Ek0BXFu2HGFtB zSq^ss6!iv#=3}jO2CDKhqlL}Oqf+6>re7q572J2no7Vss5N&wWuh!%R8(^w=7RciT zLYDFQdy<{oQy)Ub8yyu%DPFef9wShEW#6vZ}1Fn{gIkJ3nc(IO?M7f%?%}p~w*iKKzR(C$+?g>#B?o4r}(@ zuq}mzYm8Zz)^{3#pyr?I)f%k} zY3-s7myqUD-4ea!Ff1H|Ji43{Q9IJpEus1Mq{ zbVMHUnG{@o$4$<97W_u%`W=OJhuIol79741CpBz2V(1SPM zNg1p3YmsaF`U00nztI|k;pbWHr6%JO6Ukb|RJB`m4?#OQ8PDXi+UWquR(lzmtnedU zG42Be5P${13;-DTs`l^2VJi#2VjMtLpze$DuQ4xfS^k~_Vs4-LBe!s>9{>OX{c(N| IcEz3k6MG5?0ssI2 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_1.png b/interface/resources/meshes/keyboard/key_1.png new file mode 100644 index 0000000000000000000000000000000000000000..7115e92be8b2bdd84f739daefd0f55ed8623ff6d GIT binary patch literal 1909 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|UFbr;B4q#jQ7YFXldS5OKJeI+aO-QR~1u28~881JM8` zXI2MRGg$}rcM9(qra$1DBlvrA|I{!g5AJ!z|JJUFTT-6xVeR7GW@&Op_?oE`UfjZ7&aVc(`9L^}THK z=f{(C5|TZE_rEW5Q2J!^-;<$icPz(22}z#y4IW3+Hvc^Njh(UK`{mNRSzA}FI;HWz zdOoAUpSSyC^ydCO$lI{xN~!H!zubBOhD$UOSgH6_^B8!-ua_#eye!H3-7L+(X(`wL`|rK>y$TE}C9n3!=oNn5 zo|`SDz@Va-_h;2AzvYvI?Kv1c=X{^NY~|};ReOK+$0{%^QEJ(6|NU9Ud8hpu896|i zvw?v@fq?X)P!Kuo=+*a6`9UuG zSRupzZ~pxG+w4I+`}{x2%sotZ7(6n*-p|{d%_GA0OAfp0%v<$H$X*nfcdUf4xYf=68juyxRfk%-h@6z5f5> z>SA`eIp?46E8nBAzJd6}RAE;y&{n=FLg&xshmn6gJsMmpSy>Q4jTGv{Gbz^YM!6BN zv_h6i6ndb6GTSNFu7HtcNc9q0jw4q4|8J@X7!2DsT@_%<6=h&xVDNPHb6Mw<&;$U< Cyhs-S literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_123.png b/interface/resources/meshes/keyboard/key_123.png new file mode 100644 index 0000000000000000000000000000000000000000..07a3187a70ec9b39052d4d5d7445c9c8c75a7613 GIT binary patch literal 4399 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 Fe*p@B`1k+- literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_2.png b/interface/resources/meshes/keyboard/key_2.png new file mode 100644 index 0000000000000000000000000000000000000000..99a7e650f1431b60189211951446818a086a3b61 GIT binary patch literal 2864 zcmc&#X;hO}8h#Vmj37&)ATC5uJ4ZrUMyV5)MA=7CVXqF7!1fBV1S{3k%A)0FbK*b|(h1GCqw8WhBt9(&2bWR4^UyafKE} zKS!s9B*pa5?E!F4Je-evB|e`M3uB(U12R7O3Rk2u4TphfT&t<3Coy?Ud_?%c>UteF}j*Kp{od6G@u^|V6O~K!m zj%-c**6H{ySof3Tn|A*Fg}slP%(O*1V1I4_AM^|x8KvlV&M*%5+N>)^`B7aH(sEYC z7|IUxtE4WwtYkZNd2h7d-0z}0&6o=^nZBA_X)IygKYaY2;NlTnON^H;xO*8*o9x@S z&%}iLjw#aCw$|8X#Ob=;)>`Ze_~IB=gP3Y(A9tBQS~EiEXjRk2LW}272|JLaAnkQk z=KN)CEhP+tK{&C*eA~Rf|{GNh~sT_;n)C!*c4)T@UM*t2?@D9li9na<1Mwmgcr=k zpnCu0kR#ixRh?%A)9;!Mt*M;Q9W|)cO0(Q1!rf)Xyk>;(f}=D1VZRv`!ggFpFzOlz zC$E?%6`;#&38Ye%W1(9kR#+Eh6KfWM5o$)YSpVx$@}8Idy?vn{yw2RID5j|b>NP*L z{)S_5yH^}(__}$mCnOl%hd~Y)FOWuUhE7vt(q!8_mmUv_em@=yhMqmF)#shzGTuaG z<9M#aV=WAFAngy#?p98za!sP2oFz@1t1^R}H2Z!UlQ$0wl_=|@LQ>$$c17`i(5b<=P*T5BYl7&1L|}woCh|9x4i46(6Zy`TzxI%;x4Z%JcM(@=Q&ObZIR6{Nk`L~s301~Gd{ zam)9No~k+AqZrBBJCtwy6oFc5*ZGaDpaRVDcd5+*Jj*dDi@wNqty1mq9F4He>NJ(u zrEt}68p}6cjA&kg&d1vgFpG3WYX7m=DQ`Cvx+$$*4=jpG$EvW%~`BSTX%Bvoacp%eNs=Qle+ur&tE4?UOH*&=o2-0cfqzmPz$;BXZM2-Q zA-)1G-k6X+n_-lsdaZRvDKiC!L+bsBHD&EbLC-+?9F?X!;grEUGMumQ{nRlkNM0;4 ze>P=g2Oi3}!=E&6!;&AJQXYpo3WvkliPZoqAQ1ig*~A$$Je2ht1nB^%fH{x>HV|U| zMkbrYBM_Y>+J^h_bJEE(6y)5ICW_Ey9y;~M@yd_|2VR;=9pxbcDgc=G)J?$6aDLSL zQ&pAL{lA?4V&zxEU%l~XfBx@~TcndWy#Fm7)Y)3~P1BLUwUY=2t{hU{x|wmT3;@8x M<+Ss+CxSlv0qVJa4gdfE literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_3.png b/interface/resources/meshes/keyboard/key_3.png new file mode 100644 index 0000000000000000000000000000000000000000..5ec80db3e01555227ff2b9b95886f333a519f00d GIT binary patch literal 3341 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 YK$iZ&p3I@_5dZ+U9Ujhio&3-I4|zN9)Bpeg literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_6.png b/interface/resources/meshes/keyboard/key_6.png new file mode 100644 index 0000000000000000000000000000000000000000..bf4e81a7a1171dfa395e69f9b845ef44c11ea632 GIT binary patch literal 3631 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 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_7.png b/interface/resources/meshes/keyboard/key_7.png new file mode 100644 index 0000000000000000000000000000000000000000..5d9765b37e87c4926961c38bc4d514643e8eab52 GIT binary patch literal 1921 zcmc(fUr19?9LK-+y5)5E2P9@fZYUB_h)`hBXjhx)gf_FF3?H^|Mv%?_X-c~^2!gU*q-X#!raWUIz=Adv{WK(<=mM~(I_F&VX-8@GRa>PUStX7OcAcrrcF|sFx0Tda zyteCs$W)Ut>rCB;FCTj=9&w2`ZGltF7&AFgi?+^)W^ZT2`GT~E5&2wNr)#{_e0g7f zJP9gd9l~;HT|Y_pwBKD)a(e#*a6mku0GP14JBx#n6<$8h$xydpJZ$ru2NvGJZDKNU z#qTQ+yEqp$(cFG1d+oZfX5{E<)sPasw*#LSn!p*~AFfv@kTOc%9Mi!nxxk0wSO^9c zTFCYV6|7P!_>cusZpOh+Wa`S}p@2=Q$~(D#@PM~xzQfS=O%nV4i^+HPE}iARb}W3q z_SC(r7*;93V%S*>KGdYfursLDpoIvAbYzebCmnYa54>U~sbKh!|HoQiS#koMGQ%RM zrQo$^GVQen3F=3^aiSpmZ zW?gEOG-hX_GIo@#on>f{FfkYmW?W`wPyaog^MA2#HZT4!e&>1Ke4poczRz>c^U3$Z zx+p7ZD*^zNU5`2X000Fa9^yl|sO?Y>0MJTwI+b`LE<7gtyi+_rVOI`=J$xYy=Szrial->H$5ZKG0_> zY3pChKl^C=t5-Mahf!yJLm%4qsARr5Xt6gvmM#sD_P4~1V1Xn>Hen2q0Kfr&01Wt~ z^#3czZ0_@$mm@b}-QSLXvhyDY_A!29rGJn|eM{v^zLeaMt!$fDH7RFPC=@=QKYyOS z+?ScO63plGUxrhS6oYkz1HmJ|ri=E@o95iNy}GWVqVkqsr}2QzqH>nxa&a$;x>hL5=*+T>$#~+wzqai^-ED zB{c9lC|F)&mz((tm*obq(ynK&yP|%Jss3Xi()`Ke$0>EandMb}b)2chH*N*`z-pK^ zBovcUU-8A-g)gL1x&uT9f_d2f7?rdk(vo^U@+{49$eK!w5}<)s**;e8ZHt~0rqaDb z2ng}Gv%K47V51)i2Q`c>XyBzFT5WT#iHc-Knm^#MDrv;pEDD7Z-Lo!#5pq917zqX5 zrlzL$u&9>7LgyM2_I%&=uRO7kVp>H-g>{k4aOk4L86~DFX!|{lF56f=UoX#IUtdq6 z#hlOs@i+YH$~ds##`vTpzw1AF%Q z^;mfrsEKm*Ks+&nQ5BVt<6J{XPuF#Ri&BsVbLl?eM|M)42w6t~6eu7r3#X>el{n=P zU^o(=76pO8j*Ff#PwE3wH-I@IsFE0gw9}iCZPDy7E})c z99*1Av@TXgz{DZi<&=DF$gdtSXweq==MmF<4f4RW@ctP74?m=km8!U#|Vglgco^P+NxY%Xe0{`L1#XR8OTecMmhRax`l5OM|Qs6HX zCK5=fEOtGKMDqQ`cRa*ni%YEgXGiab-2JY04BxguEnUl0b^%T0cZ@=|2zn!bNJ@{5 ziwn)*IfNm=jPc^nbqgt#@pI&f1g(G{Sau5cM%9t41(nEVZnWw4?xBeje@~7A!Y3 zG-RB2z-rEwvnQf0+!me3N@ri%y_Ix0JA%<=+Wfg-im$p8mAkPt)hCuOE(`kSRnLuZ zOQ!djBLK?3gx%_LeY$MPOSd1?W)03B-yu~6k}Jyl*7SjwE9?O3RL67~{@juas4m$O zcDV6Xyj3FG4hnp^HUrxnv07w5Z*|;zMr-T+tXkD%h_}CB`3Wa#V*c*()3Zo9Q`-tk zq<8a0?#}YF=Gwb6eIPmzrd^ChMFyj4+LhOw+j0BDnw$)=w2Uw~EVJW8Tb!raLKtJ} z$cOvM*ms5sHuiVgk6Y_z1Iz(SOG{}6V|(3`is#lGc#;>Hk^b4Z+1u>u&Om_&|R zstAmhIoEi5i0=vp_S&jzWOaR`i4i1B^8Nkl%5LsL0;}%1@1h3rkA`MNBGDZq?ZOvg zLMoM;-J@uSLZQmp_(8YFZ!g=!-e_b4=C37fNC|DbZ%ug6+f(vBetiE<(uwpm zBacX8%<MkJ9MFxB8lMGA%TfetS^qZs5A|rejx*!n1fdxENqs zUhij|aVR-C8D~TaJf=x=C>--QP=B6}h65S;s;sz?@3HQ8z3BiMYq&`MAqYtVmta4n)zL2yhCaN&KYcHSaH=HPr>HY@NCQF)o{Mz3wW0Mz-nLnM-t*j?P7|Cp zIo!5f6$;`E5>SV(_LdFD9n@4RxN{}I{+3#RKA2z8OO})7!y8zaiQMRLEP1pWQ}odG?uE$g|N1_&VZ8vpEf7FFc3Ud`$Y zM1wX55{a~*kMHkVbSW57+BJ} z%r$88!i5WX%J}hrWhJpjx=;Wlq zs;(5*f7rZwXjUe3u4$s$RlJ-Or7i}nckmnhjCW{2h&4Om8bMsSu6rp@U(BJ5J znq%V@Ek;3zbazT$!QA8}HQaj@NK=3+7;vlOq@hHyxuHdzsEI|hR)Pj{^+$IDVd!Ax z+xW;O7cdHNuiI#}y7l1R*cOG$S@rpViA$pg)w}eBQ?-ghqg@uqwZYe~g<}0Gewx~c zBjr|V_YMJq%w`<(9I^%rWv&Ky00aY@{)5PSAI$r9D+atK>f{g@CT=x$P>{dl+UnPw zYeo?nNKhM6xmlG6Xn@x9pY2TXjd272H7t+~bu+*;=(^`Gg9VpNt3@ zvx%xS3g-**x7s=o%*-flAgwgOqJ!dTQ2M~6P-X=swx}1u!l_)sEscx39pM;IZC~*+ z+_s9;`R=3{VZ)osok^kv39bN)^6`0zM6*aKWO#Pli?t?3W@@TdhPGH0)CMcHL!Qok zLpavnwKBaR_un-$Dx4ct>X3?Ca$6~C5HLz7XdwT)tN`%=xIZQR6wUv%A_9rxrr3J( zc)A&~|NH)K5>GcNhyN3v{+ps7A>!X?h?h4`6Ct2>>2}B|AKxMX0C07}IyTs!{q8Rj CDBSe` literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_9.png b/interface/resources/meshes/keyboard/key_9.png new file mode 100644 index 0000000000000000000000000000000000000000..89a6397c8210e9b324bd21f6bb13e2482927de97 GIT binary patch literal 3617 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 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_a.png b/interface/resources/meshes/keyboard/key_a.png new file mode 100644 index 0000000000000000000000000000000000000000..74d57d5bd4b1bb2b884a76011c21cb117ad612bd GIT binary patch literal 3066 zcmd^>Sy0ni7RFCvz_5s*En+~4(qgNa78*jJfdoY`vIK1rM8%cG7(tP+1(GOJW@Hfy zw3PycmKLnBfu^I-VaEc3Mr2SB0tRH86cQ335FnELr|c>o=LH}7VIFRM_vLr)t?%AC z=eyGJ$6!TM5SWMj6=umLK5OppTPrZCL zJoHHD*$W9*UxvB>801ks+>gfh$s|LU`{?G}A=MLP!pGmvy$y3+uW&ua8lThn5<|5H zYithw!i_9F{QA3ojS2sF{Dhus-$Lvit!7)yB{}Q3S-Do7?(ek>eA>Ml1_}@c8c;w1 zFb2TDinf0*j`_OFe>D!Mudwbf$5-zB@`Ekw3M+jjZv4N$O<}5=7$d6_Gb&G}k!xoP zo(GpMg!9EdS^!`*HS5gNZQwut zi{*NbT@HgdecHs}Mr2k0c+7AC(E$wswzv$rTz*a-HB|QqQx_7o*Q|NC@nHs0gIbp3ujGf0>!uR{iu!qm|6=Lo1>18jEwYfB6N?zl(nsTH|w=@dj9~!lSdn3{I8}kY(oezwalUn8;=Chguh}=zfj1wqlfz87Kp|9kYt>8#+wgbx zdy-E7IAA~EEKN&7w2~ayB?_P%J@tD2{!Nxl(IHj#iU=bIy=xh@Ol{(#t_k#hP&DYZ z$?LAMSSLQYt0E|GyblXUD#|loMPvV&7+?Q`N7lEUDSSSEvQ`l-NUwUM90@78QGw>7 zO;KXof>K}PH^-DFe8=vt;jq{s6y>q6b#}f>;`Uf%t&tH?J5i?dLLX-Ox>KsoncR_< zN~~}wE^Z>m8O?=8)-$mp)d5i4`7J=hQlPMP=H#p6HD0+}feUy5Sw4-3V#(&EbxbZUJRYb76sxbzEFrLRsbO z$X?^}Y}Yl#oe>^-&iQ_(y5^fuT>7LU)3azg{$y)_N?g?RN3K(NrWwD<1bXWpys0Rt ze|}<;_MQ=of!Nv}4x26Cx!WIm|N0U0o}L~ak5^0%LQU1jE;X<R z!`@ouFP}@y_ml3S&1x1Ka!OrYTzst8dmF2Ih28Yf-Tpf()(%8gDKoY9zRC~Hh?DNf zH0jWt9S5hW1Og$6e>CG79KAYE?l>Jq%pI&xHaFremT*+`z`#J|Vr@5lk!}p%bw=2~ z;+IP%V-*U8WY`kJRSN_H@q$8?Sl8cwA}&f7RmlE>U!JRx$bvF1W6_j7V}gm9E2L+N zwP?yIR~HwCYGaM0Y($HPuHIJii046W zb)X!N9@rnV3m=P(eOQk#XESA-SaESR&G#V zGFL1)&=35w~?j#p0Is#c%UAMPY%l-v==h_6vyiH(9a;WUg)(^EM8{O;*(gh76R&5!Hhv zv3W*o*=^tJFtsI=my+5}NuROz<#!@9+OIF)Zzt$Ks{dekyE@p$)X~w=d{Fd5SaMar ztE+1}xyh^Oa8j1zz0;Sf!qTX> zrPXI3vzOv;yVrI~I|}%5w5;Z66bjDF=dD`l^jsdg%eulWWemA1MVC)p))gkQuf+dP egV4eb#9%Mj<;D5XNV5q50MrA19%cK^-1slYRmbE2 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_abc.png b/interface/resources/meshes/keyboard/key_abc.png new file mode 100644 index 0000000000000000000000000000000000000000..5b7f1bcb0fac2bbec87755d94c1c430b2a6cde6c GIT binary patch literal 5346 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 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_b.png b/interface/resources/meshes/keyboard/key_b.png new file mode 100644 index 0000000000000000000000000000000000000000..50b607cbd3f6e5e6bec11845cca09a68200da6ec GIT binary patch literal 2839 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 KT-G@yGywpG#*C~0 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_c.png b/interface/resources/meshes/keyboard/key_c.png new file mode 100644 index 0000000000000000000000000000000000000000..da8a4a4f2d531742844622ed80c5de4646949f6f GIT binary patch literal 2950 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 Jm$nEi{{oXY0UQ7T literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_close_paren.png b/interface/resources/meshes/keyboard/key_close_paren.png new file mode 100644 index 0000000000000000000000000000000000000000..6a93b0fe05348fdf8344a6162784517cdaea991d GIT binary patch literal 2621 zcmdT`ZA?>F7=G{N0xgJEtFR%$MUmha7Ly@k0(SxyBH&O_6qOdK1huG$2rJMQu^>3w zxCDzhi63E;34Tn`QKeYL&LQGt+CstN5VgXf3IY{-yIh@#Ci^k<$Fd(M=gm3qbIx;~ zJmD9W`+Hw5{Lmf*mHkHxbymJbA8oN`ii5&8bHJ~5@ zzyW9!tM?B-1oPHb9|@;@0OPlI|7bX%A0gc5M?Y-tQO+p*BP(!;Xv{j2b>=hb7#ktSij<~ zbj3Nb9B}Rx)@;Wj?dtyBTvU`7$u?FbCv0*+sMIdMs9>V^vTHDSLRf6{#2@d>tQjK7 zdo~HEC?X$%#)|1MZwlH@ug5?FEJWlE5x}7V6FgX0o}RAlANFN1!Q)2JWe$ojTQ|mr z2_`#B$Pezogkx2emD-)CXP~15h&EO8uG&d^foOYDTb$j?b72Pq3iN>mUNkV(DilR? zdgHYOrA8K)1_;i+A9q>ZKq*&n~GcY(P@jC4g7D@$+2?=0udqoLv{A^plU`@QYlY*!df<|+CH@TIC01h(dN-+b%;zpdr z6d_;$U;qdZ(Amta+dr9{9zC)>WBlHW7Pv^0&|gn+?yBO49h8AK)SE-(GV9|4y~d%S z)Jxrh672Pt1WCgTN&u&T4l9o9MO)&UWtbh0`jt2HR<#ikAUkHv?w-0uK z?D!UaU4!Gj_I7Om2B`<39ogX9j(M{GwwZ9A9O?~0G&uIqV3<*392&aY=MEec5YaIg zsal&~UwFhg#tQ|yeZ1v(Pg|cm$c8XR++!Qc5Sw(qS!OjR0z$RMf*?5i3hM%Cg%ZVW zHQ&rhGutAfvs1?AYZtPG7~Bptj}uyT_VwMgnF_KFgK$;JI#>jFOtwB>NOI4uZhd7a zegBnBWfiBzH1J4vQ5y$>T7FY{*$E1O*q5$Yn^C1x-jL7D^|7>A8WP5jn)16-GyT8o zb@zm@=Yv7%gUZxVCnk9C{ms$fDF$i~-aQ)5Fs;bH-CR3|x0xI&T()1YHnl`Mbe7RJ!L*3<5Q<-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|OhEr;B4q#jQ7Y4JWcFinv~UyH>D)MMZ&eiXtz!$H9a5 z3k{sNTs>>~(?9OQ_O2!CJAN=eu-UK1Ai%)D!oa}D;J`o@b6~YQQ092bwF^)XbdV4y zJLF*z2BXO|i9+oRj0BRvgZjNb2RN@q6`xx>ImpZFWC;huo!h@(-;cU&C7FCB%J+D4 zB@+`lspx@q9oKomqH}w{-xc2dF(=$5g7JaFyatL=GtLC@?m;S(--c_miu2}92@3LB z=FITm-o5z$*KS=qDY5ukmX+KYVSxq)1_cHN4p8PLo_YUca7EvQ;&*%B&%4o^zFX-K z7X$kz78cSCLa~%$?W5%ax%mm64Jpjn|Bpq2;k~KmZT~0xDj66U7(8A5T-G@y GGywn=Zk|K{ literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_comma.png b/interface/resources/meshes/keyboard/key_comma.png new file mode 100644 index 0000000000000000000000000000000000000000..69da507ec60038d15f5183d3bff4c588e0f70135 GIT binary patch literal 1785 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|VP^PZ!6Kid%1PAIv>uAkh%GLP8?|>DDiB5-|^&s zlLeY~Dl!BtD3tlEFT=+kyeQ}ObAQF)1!k8`w&u>RT$nlQ_~XQY8;lO6_W%FeC@?WL z)W0m+`6RyJpEIuvW8mVudE&`2Rj>ZATQg_`dueX|wSLZj1%`lCe_s7Bv6}0{$=Ja3 zG{3=_qxSx6AtnaaQ}-FH|1Xr&Yg~D*Y`3qbuI=8q^|9Ay3Y9Sg%#uh>`^v8v@mG*R zBlsWFfuHmdKI;Vst04nX{djJ3c literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_d.png b/interface/resources/meshes/keyboard/key_d.png new file mode 100644 index 0000000000000000000000000000000000000000..557f3816f070ce467eca46e65e5bf8ce64f86204 GIT binary patch literal 2962 zcmc&$Sx}Q{7QKHGFb09N+W>6lk^G5TgyUCAKI64KWI%AVd^HR3HI?1Og;obQRU}IE4@W@YOjl=exJgt-9x) z@%H>)pI|}&0DbpuEe@hSQ|HgZRU|-w20#SB!JO!ya#YmEOhrYq;o;cYpa?Bzg862Um1Tb0zd7YQ z=*eE7j>?m~eY>95Ox5yA#2*9Zx3;!A7GxNa;4atyW_!G2fk-CU+M(r3{YNj0BVBY8 zYlEhG&d1qzVMBtSyiQ6^mN3W`*^Tquv|2xN^1@4>S=Gb*uya83*h--kGN%>og{rd` zU2;O7X8*>WO`JE$8jjvKH;YJ(cK(6B_Xza^sx=O@DP5S}cHBsFsJ+B>RvHtr*MR48Z zDvFJ544WO%_vp&Ry$*({PIR35ErYBlP;-wQ*<`dp^5rUT{G;Xbc*nJ41}-j<<$>c> zKVwyJUHpk#zLJ?gMx|0kbiQ4k%*s9MU5t@n;iJ<4uzR6UZ7%S1 zKW3zVx@!C?5iF06inx;%;>ZQMCwx>JKKm%YM%Wum+su!U^TZ{3Yo;X?} zwp7HfB!YpfhHVuT6yz^>V_{)Yw;2a42kf@2`Y;C)4E&VOMM55V62Tx|x5sAz9_BSI z033=9PazXQ>7!Y7xf{_%dT7``R;5rAqhp3cu~GsB4;*ieT_Xy@pe8|%DINygO2kAU z0*|2?EL1!%~ZU>%cmx1I8s(Wka7JS^^cQ^9noAU+1r|F*KZnA<`RPt(X`vXj=7JoCm(Gb*Py7ZG~#c)XL0 z2uji4k_+qht~+NkooOAK4npqeL_qX=%G# z7{8pk+`<(u|L86(yQ1VAq!bCHww~Kfs=DQ`%7U9U2g?{4afyl1Yge+K`O@09rHO4; z=dh!@Ivpt#jY_>}_)2DGrk9u3j&q+C&kb}>G-=X|ap>;m)(%6NHUM6);Gx%f_I4s| z>LV2H@}Bm_wr(o6;;)~aj}azs%le>H{vxzE{;wC&4~lozt;d1EK}I^Ih0a$85OCV#c$04 zLE}=ppX@Im*?dQ^Rki-ZAuHlanRyJ}DXf6+4Hli5QTcjV1RXy~CC7k&5={ zty{L#C{>N&w?gM(KpmpPkZ- zpSHWxwsf)t!Gm9^IP!q8rP9N;ykq!izW%hLlQFP;jCYM>*z7doUK*9iW>@T8M^!qe z7^yZFv`VGntpdfqvRB!SANueO0)ZRa~eHjrf-O_4y>BRq-KaN-1eRrjFI1R003}z^>n%OUC8m@0g|(h APyhe` literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_dollar.png b/interface/resources/meshes/keyboard/key_dollar.png new file mode 100644 index 0000000000000000000000000000000000000000..0105debb68db7fd3bd1c3d209956013ed76c3b52 GIT binary patch literal 3481 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 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_e.png b/interface/resources/meshes/keyboard/key_e.png new file mode 100644 index 0000000000000000000000000000000000000000..1b356d9d5b4e2e207f7027410a16925164a01876 GIT binary patch literal 3029 zcmc&$Sx}Q%8ohrA5)ergP{55vK@o(g7&alu;($m55hKbfF~}AI$P$pmwrqq5Xlr*s zgy7aGjch7Si2`AhfeZwsK-oG1VptQ95Wpm4imvI0d6=m#dFZ-z>$@-K)>r4d_neb? z((Bj;H9a)|zy|k!xcC471FIXcT2l3#O8@}639k5rQ>2RtNg;6r;1os*CFr|HhY$%q zgpjZlax1|efb~i4E>6D5{nJCkr$@$ja6)Q`b-3r)=fTCW6UtcSqkpV$Aa{mlP4us`c}-n2$I>VH3*;;fZ&)g}9^hj~1~9I|^ttdJM> z)6KY(3)SX70?v-yC3J5~v138|2vJ5ZVH{I3T*qRuh(scV(-OZVlSS>Dtv6$ETf^e0 zR7IO7HK4*cywbn@;=%O(1_bc!NVc!7sWB%HDhR5!g&ob!u4Wohv2b9D79%qn^Q5Gi z?M6xr2IBw?a7^^R5dcKnUf{)&r&BM7@H7Z#6Uk(>j{5XQI5=FoCo69uC?Mdpz^G?x zp`c*vmh}dp+zCA$+?8vDVL51kXQAA(6cwM3)nx}(9Lxlr348iQ1{+UGerYGuh z27`eXmq{D&YGSF}=fEvOUuZ?FH|pn+Z9v;d~;u?Ynner<9}v^vKhjLx68 z$JuPwBKw+up)^0UmE)~Ni!$g&TPYiC2OHb zbSkj6ogL|i(V{GJNhH!t3>>uOZ>jPxG2iKH+RK z&*fUOx0aWZK8U@wgeolysuO;`V6xv1P8<5xXk4teDWjn;>adQwgxobM>_h-R;J%MG zL9daPrBf&`ruqyrXqH;SNC_5LYQbQD256U&2;l2G5})hzAJKkBipQ0zKs1km)(z9x zRT($s)T2H+EW#dcs!dz|>FZZBt^{qc7x^Mna*20016Mtrh`5oJ#}j0` zjSgAxUQA0DMTd>>P&svSfzswAd${M_Xhk~$#tDq5^xtc-QzPRy0|ZFjJ4g5Cj>Q$w zX*~nYI6Do9=DV;ZlTIr+LfGx5h^b1Ai+dk_=$pKjd+Vc}7wnWd+uLdkBc{}Jg}=H! zK~EC={Fc4{HYwK~pnLjkNyT~i~RFR+zw`>H6MB=JCRL>lQfjWYv z&aR@4Y-n+KI3&I*gKpL4Z?g3v#3o!VC^7`I_wrUSdf82P*aeZC>PHG3)6~x-lb`bA z3u{%+v8FvKDJl7Zx9xc*cidkSJapwqiR8B)1c<`1y4{(^Ey+VMRZ@BS-Ve$rSlxKW zuE2|WA@L5h$ymgVERVMbn;d&0Ds@tfOhu{7aj$P?J+?45ytKogrVZYf1eP%_TM=)5 zVy~B6SgGxe&|fFF=Z#)}vdyEvq4{aBGN+|0riyzcXZ0J(5&yEtJ#d26jd&Mzn7(K} zXGcJPTcjMZ(}HOH({0*LW#&-jvdt8^&=~HmWz>#p{kydZ+fCa1>H#ltzG0jf7^{tvMmR8&-VC(|ysaz6GnzU@HkNpZ z^R7fgCEM*$A+G9-rs9f?Bjg9twz+zr4+sbd3L+`!Ftj-6{CP3uNJ|Krj0rbh=NcqN*7=DO$v)L>!L4D zb@s$xi9QDh5;u78%E&+!iMum!S;`jaHWgFT(#}(dt&uRmX&F9n@^8*4P`T<_O?HhQ yy+-)|vG~6j9oHBV*BsOSVw?ZP6RlFVmjlM;&TMpcRVxPo0C!g}m(rubKl~e4s-)xq literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_enter.png b/interface/resources/meshes/keyboard/key_enter.png new file mode 100644 index 0000000000000000000000000000000000000000..cf935fe07e68d6d899b9ce200a13d0ddf56fb590 GIT binary patch literal 2201 zcmeHIe@v8h82`NQ9d|Dn-AmaV=jOe$g<)-8LrOH`y*y`7N+TIZirhJ0qXD}^Kw_QT zcSehxt+s5oMuwF-+j*QLCW4GNq$ygNweA{6a+m`3`s2zj;pFbx>!t(7ZJqRIe?0qs z_TBS&p3nF5-Ja*|7uT&xPSPd;NX~iAm=Dm9gOQKdwX2Q*NUdINs(#73wYt`_O@LvG zb+f?bR9IdS@&(J5y2?J`X@FUgV>GOPt^dj|zIXb~bKg8)X`~$LcEu=a&AIxA_{3^H z@nFJY_hDsjvqJqu{@h1)HP}<$YxR^Zy#Q@FtU*N;k|Gs zwVEZJxd+9Qy3d*pdKT8*780lPyUoK&aZX#sic2T=o)vQYQg8KjxT+HT!u&nLs%K8T z`q8J4HXYSjDQe0q`7JE&HQp$!iHHl$?8&H!W3W7e0fVTy7` zWGyk^G$Ao7-FeD1rISQYg$jlQ)hr|p2anGGRK}t)XD~1Q+mRdL`s=4fzQ|kS3ZzS# ziT(1A5A4@jsiGK0k>Ififyn{N9qBr-r)eHbY*I<^y*K(< zV9n5}a?$(isou2EL?n=&vQZZaw$$w#JTzG5pcpT*7rnsVj2aI8c(}q>GWfv9`5oO@ zeD;T`zuWEez8^~quQh0~`)e=K>hAw=wB}6LWA%kf4qC(t2HN<8s7mF6%@jc?QGsrz zwD>od1ac-VQ~t}i+n$j%ak8enol0lAoDkYx&RW7?NUJX{1m)jxvp@6(*rSw3rzDNZ z?g$5CISHyMZnMIf%^+pIIcz}V&yX}U->Wh?SS)-}oQd+*xy&j$l*t|V()749+wO8o z%L~Dr-oVE_Y^Ke?S*`@y|Nl;#=IEVW5csY>Ne{!0jE0Il#|HqAvwEG;^{jd4KSw<-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|Ohgr;B4q#jQ7Y5B4%E3b;BZ`!cjRaI`q2wXn_HYJ6kr z#G1@i?`nTaz4VM(=5mC);rAY21_cHN4v=LH3}i6}R(k_wj;CC^0tG<_330MR9u{FR znoN^WwNE&l%yj8?wfy~W_p;ZTsVo%|3{&_S9(U^F+oX;|dIF3<3-c zEDQ{c3=RxLG8^)3)fDzxKL41TeYPPbROt{GgE;X%v47fb@MQaUyM1r>UXRN4@T%iD z%o<;*d3<~JTEjM8&y&Ies9{KS03DE*6P|Ey&gUQ7wx&63(h^K$ZQ!nCVj|gls6j!o zc43MN1j_RiCB+dWCMe8P6l&){%Tq+B2Us$K(Ih(Lz<)a?9tQWfO6#x9|7FF%z`)?? L>gTe~DWM4fmm{&g literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_exit.png b/interface/resources/meshes/keyboard/key_exit.png new file mode 100644 index 0000000000000000000000000000000000000000..4c06660d569be01afdaf2bb290eb1525c39f5ff5 GIT binary patch literal 2466 zcmd5;eQXnD7=PYg?dtTTn=_7LvP&>VO>~lM;3lI7cG+g)n2f{*m3FwYu45@5(+MQp ziw1Ef>P%!yOr~eZ#JP@^NLC5V7RMUR5?3v@i`~`%%NQK6sX0gv?`qz+!Qf}K{X_n^ z+*PIIgY)!Rw9e@tVWhA%V<|hXLU~yZet*zd(xvjlnyAxJ7 zdNw(^n%0IU=Q?LYwmS?;@G>jxv(=ga!He0g^7mG16+WuC;#I&Q4E3OE2GDUBhc<^LOwvjd-f zIF|)=X~r{V{zk(vgpys(W6Ak%KEhX54Yo(i7A^*sPABBxM&IybUa9=;gUp3pYH|1I zxgTAYwb2=VD>I#xl6zeJ-wqB}h+!pg?Q{hoM_jQq^KZ$*cc8~Zk`En-*%=c|JtwfA z9M#Mi7N(RIzE_Te9$$;unTH301`bXHG;>|ES_)`-NRH#DBp)(jIjBwFTPlW?7SK6V zVGZN<1`S4t*_rKeJEI55hmJ`;q~qV*cHjPn$1i?gB8HU}m+Q}*6m+}_Ug=X5ON}bK zf9eMjnH_2#l<A%6+! zS@jmuEba*D5%~~p!on~PLR6n1Jelh=E4#I9CB$zo& zG}d{X$dA85IH%2fomtS#uG&GK!kfs82nnAftZED3n}L*N48WNr}QBu%c1U| zfV~1`4a;NnmHuOJs&J3rjKQIn@{9?*L4yl)SdIhBgI_a?h>mm#ppepn4IJ16mJloX z(BB7O)FPD8QNkiJv#WKf7~KGcQzLUw?G$_4<3r{56$^ zlskwT?_T0iZzE$!=7>yqipUM|%+=)~%-=Ww`OTLoCBg iH_pHe!{JbLKQM4ya~B+PKS2J*pr*3cdU%cF)n5UT`Lr_t literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_f.png b/interface/resources/meshes/keyboard/key_f.png new file mode 100644 index 0000000000000000000000000000000000000000..93306c60359d04f8e1c8a07dcc5167bbb0da082b GIT binary patch literal 2112 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 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_h.png b/interface/resources/meshes/keyboard/key_h.png new file mode 100644 index 0000000000000000000000000000000000000000..c73f37c271fad09d1dfd91c54c42528baf87b75f GIT binary patch literal 2186 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|SSSr;B4q#jQ7Y?*=|fkZ8DAdc68_<3{E;3M(B}9Ef0y za$LZ)foUt#My7>K&a6R9vMKu`)AkhHo!ZL6WcmD1_tgDAZ&c3z)2N;r`Hx{iZCw-- z10#b21A_nq0}BIL%z@R;LYdm|9|@Q>BoAzVMx?MX%u?-nX0Sk8{_p&G zO1_uhe%EqfSYXtAFu{EG^Tcok6F24zyKu(BKVia^Ml;`h@;q$ZHa~J&Wc?e)1NWBI z&st_Z@1HzxfgJ;5L#^L`Tfc{K>%Tu;e$s*A!Hnhg8zOX$J>K}8iLs%~_y5%2zkmPI zvtnXwIREb5JE@{SwO@ZSyyswI*!|&0baCAI6r(~l2F8Y4x?~H#Qr5`Te(U`|aLEFHTGQEWiA)V8=gsj^ESPw=T$(VOrEX`_KCI z>+|#TBd7h%{Z)TQ=g0Bff8IBi78q(UG5i*E`r&wu~q%EaHWC@>VP zI3&Oy{a0G1mH+07EPDqA0frk}bxt4pa{ovFw)U6*|NPnW_jCaV2V+C5|Nr*(c3pWD z&q;aPcRy`D^Sn5por$5{Zh6C3ZX>i^C7aDu$f8yWQ?v4xz_MdFlKQ8dCICE+-6BEPK&c{3U<=-uo zVqk1=%l~&hN?f~#!NJ-5|7M-l??0YDe_q#!iNW{E`|r9;91OWz{ys1Lf7X(Lg+Xf7 zyW}tHn4HKc2q`K&VdYQM<10cJObTV5SH64dc-QaLrY&3yx=*N?-H>t~(aZ*hh|%hS r++r108pCKZ(+Qc{f85h&WMc?yv=V4+Yq4TrU|{fc^>bP0l+XkKroGA8 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_hashtag.png b/interface/resources/meshes/keyboard/key_hashtag.png new file mode 100644 index 0000000000000000000000000000000000000000..df673653b0416114f905965f1ade51d11897a0b3 GIT binary patch literal 2394 zcmc&!Yfw{X5Iy&jU_h{e8LXJpMihU5j}(hMhCmc8;se8@z)Tf`5~+ZM$kPGJMG@bb)2#v61}`=0^en6fM~B~-pCB|Unp42xsr8)SmOEzz;E z5LtB0HpLa055O4?T)H?cqqFbs4MnGrua3T{!8Mj*PL+qcEQaky2#M+#@*CS}xpwDs zAI)m?{lj^0h*fmcfY-^#tFHTX(9+6do8wUHcZpAefEeHcER5^;|H4@lYktCT5EG!z z%=q}39~ErWjx*E0;XU~xPuAR9zo%Hw%%UIYYCg!HnO0&n8c%#TFh9#%D|KJJuR`r4 z^7Z|MEv@e0PuX?CS)Y>=<=h*gch>LB^46;72@+^JvhsGFimq1hv%NWp3DawRV&@D+ z*gtm5g*dOMsOXMitFERl;@n68%IjUJ^P$OoE>`KKVhFYq;Gl+fTyvGAAWpS`7)k8n zC)xy15U*>qf&EKp9YCVPLelffYYwUnwS)j&6^3PxLz90z_yZG?x`i$aDS|y%czP-s zJwy@*Vd-qiCP+<15J$-@Aqyu6s(@!`_5c@=`EiqArKBKkZrs+{>@&26N}%n~>Y69% z`DM6aMFMQV#ff-R_cRDFh0sU=l5(XAc*)%mqkO(B@hT4jBpGyc^DIh?$b2`Ie&fQM zsfP);XC`rK>+^QG4i9U;O}a5SG!!l^sdJn$BfwF)`4FFdHeRLcH)gi{cGI)4<=zcN zIaJhZ^1QyR)W!3<{$m-s{zY*mXCK*zcO)zdR@z(^juZssI$?0vzg zr|jrQ797O*xF2xD(K93oyAATI#}6N{WmqX>gfl+O+M6l&T@c)pX(>RlV&jpiSWoz) z+`EY|Qzd`G5pc0zkSCEsSZU8g1US%M7?H2A#VX1Ut;%b1v@0_#`6N=;5l8QqD(FcB zRX@Ay&;o-)RntJWch~o~K3-i&{@l>iG{+}qo)@e?(y6Ztwyu5Z{Z80DO-EJ=-$D>>6DvtxZ;sF z?Of|LU{N0T!0`O}bFbcr!j_GFuJvuF$qS0fU!MBZG&LhOhlQtKW&34zZmVl|Vg6hm zrFT|e7C!r&i&ir!N2=Pcw_Fln(9c8vHN%xg@&M9l`88XZ9SKWe7pKMhra9=COfJds zzXAkR*8S3h8SAT;fUn4trL7n+^{4St&}i|9GLJ6-Vt@12sky6NFQ5o8d7^ r{<-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|V=CPZ!6Kid%2)9^`B>5NNwN_b5|Plj@R|lC(_E^kmM{ z&z}9bXFFZ~;o$~}nLimH)b3Mb5MW?nVPIfna9|*dIk4ItD04jJ+65>GI!K6<9rCaU zgVAJ~1k>JdicdJ;UR3t)ocDXTra60co!P>4bpEG~x0{R48Mg6io)i^mU|>*SVBi3y zOd^?o&iX%GdA@)BL1~aC1qRII)}a1L)!^&et=rCjyBD2pwq(hNsWx9JwHC#RL^>5k z50VcX7-qo3h{7O-YnOX%!1y%w`qtWSHvY^(p()Em8RAZm6TT>p9)unxtz>~EMR+)o lrk#Otq?Sf?yICa|3>y_Lb9_0?!oa}5;OXk;vd$@?2>=FFoU{M{ literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_j.png b/interface/resources/meshes/keyboard/key_j.png new file mode 100644 index 0000000000000000000000000000000000000000..f723de1f55df97011773af238e7fe85d00f387bc GIT binary patch literal 2070 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|WaGPZ!6Kid%2)KFpo%z|e4U*(-*g2RRMu9P<>;v2>~z z%wRBbym8c#5?%Fj5e7;1DN2h^Sfy7eBeo|z^!d>%ip}|HB6aQBg%4ldxXxcWiQKf0!30IrJU7H>M9IyVq-L5 zA<^F$VT3!_4n*AAX3C+!P~)c9a__jBXA*-T!;g~9^;4FuG<0Ejpp(1(wqkoUqd}s0 z>~%@jWvg;;pFVwB@jSzy#spS|^vtbMyfgo9)-i75VCY$|b9&XPe_7wwzpgqVY0Hps z>kiW+n-+m*pDX{Q?u@asvpbiz+5XS-imUUV|Ga$p@`1_k%B;H?SQy;I(*O7Wu6+0I z-MaPblX>dq{(8LU|A!9-HSg;usjOeW-c63N!7wybbp4C^-~O4i_T^vcoc!;A?|k{A z$8yZ1oBJ6KWbm~+@A+f@Z+YeS-+R*;d}g;Fn6s zh^tNaO?hnwi;GJg<~{%a=eEKoo!dEP-+n7IFdbOg&3Gek`~N4!iKm|y?TxEG%gw>? zd`a%@UF+u0pI^P_g`w}|+qZ99{NDYFo$nw^b)2C1W@p=3E#-AS-%~-v9^_SjC zc@~C-`EQt*$jq!TZ7`b5qFaE$nfT0$k%+;0p6I+!6YUMemOR*Pr(C-Nu|?pA`L*H+ a4ENTmO$eFAah!pHfx*+&&t;ucLK6VsNvXB~ literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_k.png b/interface/resources/meshes/keyboard/key_k.png new file mode 100644 index 0000000000000000000000000000000000000000..aa3e806a8d135bf1b79703fde235ea1d067f9b96 GIT binary patch literal 2389 zcmc(feN0nV7{=e*b=i$fq@dv=OiO8CP9jQFn23~*A{Z%?iAutD!4=$=p+#Y^fl&FX zfZ7g(3D}h_C=7;d5MeNFMZ^}IA_E}^6R0KbQniH@TP*EeZk@(te~o6@A2;{hbDrP( zKIb_%r|3P-j^)c-mI1)>i15&801A)~O1?5(LYe@;IhDOX_5H(1sp$zv5+R6t_&_2( zB01q;Vsv5x_luO?#BBhmJ|Z+|PsY7RzYnA+_pan8oEuGlVEguQ+ml=Q%idhIzX9r5 zM_#{keEfJ!?#>!&95Qf~>Fpe4Z(D8fn$VvabuJSddD+H`rx$r!#bGcmnQ%q;0CWJ9 zw6m1qQkYj-S&0M1D)ZLkd@G`R9>bE8f0^Ra4(=uP0$=RQ_M=E!r5d69K^o@&?$JVR zxnK}itu(Y1fA>e{lg{Q3nn$W{-Eh9PFAhPhwEa)~>an%OO1DWI|6Y?RKLISC#=8yb zPKD=CT7Z}%W>TPXPt2tWjl1N>vBwoR&zFS)y(~fJ8|ko+KV3!-v}cR0L9>_qwCB5D%2}TRdHNl=ijgEc4^sshc4Z z6jasbu7YMf_0uUEgwH+HqMe_eonq1;e2Xd12K!pDJz!1*E`1sJfNJu}Ap3ls1@p{+>I50@DFAxP%c&ttq7_aP2WtvU*_$&?j_4@2pG7YI46! zsdoC?C@MsAa%?a+k$dUoD#^;FxN*CmNzgIT)`Saz)q-Oy+1ii~F)Tn=1S}7UMADTuQPSSt zK6xKdSgfnMP!vTBtf}t4?SSwzhxAu`&F;XfYa2`E6*y6$rC1hhONHpIuD0F%$Zdqw zY>Lz-2h%}RUP7nBlc5c!yv80ni0&FIkFtQ?s8Am(TtoTc4R;Xz_Tb`)5+K=fp7C)8`q zNAEibfYpcR>>iot`gA-iYaeTR0i0T&Yw7dZGwj!-))ud3GnNtMN`bX?M9-I3PRCI< zM}8FGgz&Qlb5slz;bUR36~xooZzkCyvV^oiE|;&9zLP=Inazht;Tff4(AeYQOr;%! z_mV#J>oRCVX<||x-Py|)>94ziC|b+-SQU_hf@;-e)Fqoz-_W;_0iviyM*Sx)nxYV( zpRbOi#;ynh`uR!rohcrVj9}+%NsTF6LvpniH%5}aKpgmqcal7F89LK>+tPV$HqBC= zJ58GQUnN6ljQ1@oxQAnqC6Aq5B{g5w1=ApW#=|kFyB{I{k1XZit9xflYI+V#F-{Nz zD|TE>e>yiex44)!?~4b{YqqQwj<-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|UFLr;B4q#jQ7YFXr9~kZ8EL>=Z*9(+9SD491PNOg;?~ zjl8E=W^g7jpJCh3IFrGcaj#N-?iGzKr9HLhlp7;{E&cI%>-rO?-c#5?%Fj5e7;1DN2QBhRH7 zKXzhR5PV1}!D!~5-I^MEB{IP!i z=4=kehCD9yzxMHuxf%SA>nd1j`(9S#FSn36Bg5DbCeiEWEBEgEY^MuF`rB`x6?w7h zU_!w4(&YBtF?xX_lGb4iAHGWM<4zH5U~q^|iBw}yVEA&e#47jGyfZILjEY$pZY9~x zH!st%cC6HsW4c^e8hgC(&W*(h^#Ku#4$&@wTjJL1GC453SZs5C`Q=RGg<;+fmW7O|k#XyFVdQ&MBb@0R0&yz5oCK literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_m.png b/interface/resources/meshes/keyboard/key_m.png new file mode 100644 index 0000000000000000000000000000000000000000..2e83b7b214666b41f24db1021b1ea570d9ece81a GIT binary patch literal 2438 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|Tdqr;B4q#jQ7YANp!#iZmp?ezmHWrpLbSvZyhx={f)T@SQhqHbO;lGaR2?ud%z&@ZkB+ z)3FQ+3=A9~*EBGY#T;1e4U{>aa_tHf1RW&A$qso~gu!SsO`=dc0~78f@Z`nz-3?+t ze$V~?@BZ1dXY;n-uDtpo;r#jYn{Tf9GwW)C0>hG!yj}0>!nb}Ex6iPed++Yuqx`+~ zIzM7J7IZJNu&{XXwd&*hUs)z{{iko*@m^ZR*4+PWg2$Q-T>S?g7VP-ZUr|-HYu~;; z-JO4w?ekKMW~%)8n!K&MySu_||Cx}De}Dh|_xSPRc41xthHJ_U8-@M-{U_U7_wnfL z&wVVGl&wBTtL|)mso<(Yp&55g(%`X6jUCG45eX$UTzRtA4#V_y2Y_olhnE zYLf2V$Vp}1!{@;8LokW^_$lwY9rXv->VT$u2&V$Y|y@mBWs{|hFh zo6UY|5Y6+fu!-S<+02|9=HcPt@8gBvynp}P;OdvD?%YfctB-s;|4QZg*|TSF+eFT7 zsEt<>V7PYg=FP~w{NkS8_BBcX8iRl6E#mWHh%aXL?*JbB_DF3@dTU0ND?|9EkoVT6z9W>d2n3=`VC zl{VbsKBTiFvHkGF%a<=JCHtsNKDKVvj#US%_Rc#lefl$3!=2LIcQ0ma`BB=u*m+}? z^bd=jqI}PqAA(|KQ(0A2Rek;cyQ_~K&*}?}ex2~QuD-R|lD~)HLecf>*Tu!fr_P_p z-+uTP8@D7M8Rxf4%#|=>F_KTU;g`bYRHn{59>(vDb!k zPTh|dajng^x*8qg$PkhIUVqUf(f3y!85jf@7+4q>7#SQ$U<-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|V=JPZ!6Kid%1P9OP|q;Ba*`{N}o%<=^TAVeb!1-uZCO zKkVHx(~{xF`AjATMg|841_1^J76!7I1FM~dGRIS_oq>^ppaX|E`5_OBFc?jyNfc@q zATQY<*-BChYKRU!YQ2M%iJ^gkL4kpR1C%=mGB<2I%~BIyp1w@>!OR(w+6*}-xH%|F z^c1HX2L>PFosZprMEjm5+8c-sd+fGTu3dqsn8#^5<=Q#$8csyQM0MK-e(wfGhC@Gi W<;|Dv&tPC+VDNPHb6Mw<&;$VfXLQd1 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_n.png b/interface/resources/meshes/keyboard/key_n.png new file mode 100644 index 0000000000000000000000000000000000000000..be1a6a9e7a52b5b517fee281f23fd19d6fe9d257 GIT binary patch literal 2165 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|SS+r;B4q#jQ7YF9tr!5ovh%b^cy+)gxR-xTbKma82Nv z!X>0Fpyi-Ff$sv>6|OBUB@=9wjjgT!S+DV8n*Xd)Z%+Y>k-YJ9UQdJS(%AnD3#$KZ zW?^7rXkcJaU|`??xrlToIeL(+qEI^r87@Y$onq|_OcVqi*dgSHJj@?3noN@@)UJR# z**J97MKM)8ei|AY+TY(_|F=1A{q*ORf3}2iFqmYXeG;p$Rx~Yje)Z9#Nguy_5m{ed zUjF?b`@A`GWaQ=f*_aI`j*y&DG~?&*8LaekI|cMIMIH~a{1!_&z?Oi-dh>m zbTOl(tnAs_$J~W{4SKuw?dv-xBO~LZHu>!G|5{OxKU&P&JTaY_u_4T@{>)#AYw}iC zKL6v9k(Kpb9{jq8!NDoXYQ5Y4)2F>x>x<0SdK9W-@v>NG#{2cFR?RrkeKcwIeB(#Y zYwWiF;9z)DbiCd|#*KN~pIuT8zhsymS!KSxaar(s^g5iQFH4)lqoP=So?B+(f3ocn;0BkpW5V;{%if4KOQ0+4EN*T zWUE``Ca<2u>AoP=r&Ek>UKO4Hf6LkR3mX|OSZd0rb>@Dq zeq;scCE~2oo@Szii#f>Vvk+xn6N`agt1|+W|xxN z|KIQLN^FhLo9=t}bs&QP!;QupH~a7Yys`5Ck>tn1&dS?s%SAXC8*;m2f83aOKc(aT zy?beq+a9K={5EA}Vc7TfOMmPS-s9!-o8(*`VwgMo zpMBhVZYdtNFj2;aD_^Vju3o)5ZT{ykUrHvo?>1y$Vp!X~_v_jI-~Z!-?{hLRHf$4d zy?CN^cK(yfdlexL3=2*g-o3|p+iVvT6GQLYf7`!5VQOG-*tPq=@89Qb`)}R9zx@OU z!;*~8>$4jf4otuDZRsyQMn#4RD{Rh3*VctIDw0v$k(s?<+934YkC%le%(6M(y=X(_ zg>8Fxty|0Fz;4OH0#ikL`AdyLM1f%%g98JD00RRH0|O&j%z@R;z(|qfDc9~mLC`@$ hoXn8__jWo1Gs8q@CF|#@%RtQu22WQ%mvv4FO#n-)*EawF literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_o.png b/interface/resources/meshes/keyboard/key_o.png new file mode 100644 index 0000000000000000000000000000000000000000..883feec8715fc0621edb9d9150f2f51643195407 GIT binary patch literal 3054 zcmeHJYc$l07T+^xXmY$WLSC_SDsJe3$jne)d8O2$bFNojx$;UVGiGp4Xol&Wipulk zNDqw8Tt$OXIF7_5hh#92Ms770w@AtU|s3|mX6uj z@EfKBY6H^!Tk-WP|8-$sah;tuNLT#NXH#?qSu*5{tLy4A2{~;Om+FuS$*a))x7&l) z9N4_28CxhC)=#SDa2Pg}s?&8-A+$T`YMMGaPYPFpJ}rG9;PIlr&1E-zNHWXi4GlFF z);v}>ObsQLc1_2zg(FF=Sn!Ofr;y3kw_F}CE18NMzUkm{%^j`~!EgDnBy|6P=9saA z5q6KUv6hA{@U`T`gh2lL_b3qu7>cYqeNtPzT;O)J<#^GU)#g*s9lpRwt~#DZMjXoE-F(f%FO|y&L;~yl zdzJDHGBC`WlDK-P(3+or1`PNdyn(m0d@w%6=?piUoUf5S<|>c!IC~&}mBJxKpWBHB zW7Wfx+brn#TQ2ydwW-11QZ&(b?wIwSh*;FG=S0Upbl(!TUxU_sXQzy>AsQ%^KFlA= z-#Gh@e#%R(9-NJ{%nxRLo{!Sh(NM2uvvb>n`w5l-`wF>amS>y3CwF2ZXqYmY8?tza zvgvNu*`-aZwV|7eFsH!N?PWb9D{F6uh*?q5(b3@(uNG#a_V%<~Q^BD_hpP3`-$yCm ze4(CcudYzrbaVA-CuM$|);!ijK<2ma2uZ!KVhF<@7AO{)e+&`Y?(VqeWF%#Tu0&Hc zTqG^y(G{J9COuuVP$eo3JjtTM=#PbDtz#;7Mrcy67pj5sGMKfH8O!FKqa(Q&h`$+F z$9N;R{iGUlmAfQO&JjlwkkcZo&3YbP)7`z9I3VGQ!$eX!rpqHsyWT8J*p)$;n5vI~ zz3lpWw;@|LFROL&@$x>rf%y3`hh#-q`CinO@cRg%woc(`O^q;bh|kxJ z4)A%`7&yXDA6->tD{oLEuefc~!hO-agnh3g1WF4kpKcL})Tmv^Uo1)~7 zu?5{$p-@o&mM|wQGKmF z78!y?-l+H=y5Q+J7HW~(eiVT^E6BA=&ugA!R(h*wo^PDUkkbXu|%3rIZgf+WOz8s zzL>6XJ4@>(ce4IR3?L>e1qB7~c)@gzI7z}tyZL73!b=g8$z*ZP@JC0VP5WX1DeIjn zAwP9$OAfnhe<+}vCkA`&;64z*n8bWJDZ8>3;7W)sNoDs|6&Qolw~4yYcf zHGDTW7qxF;RZ0(_c2@-&z0RlkmG%|aX`vM1{|0O$=JUVdpw_cOz8#|E2U+19Dv6bL P4gkQ(&h1ErtzXJtJnXg| literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_open_paren.png b/interface/resources/meshes/keyboard/key_open_paren.png new file mode 100644 index 0000000000000000000000000000000000000000..9dcee0b9a0b0e84bea376265a735a0a07238fb65 GIT binary patch literal 2656 zcmc&!TToM16y5g{@bZq}qgoypwW%$JfVLJzNkptLWzbQBwFrb~w1_+dQo&q65lTl; zoF<&gkMZs*eWs~??_4?oV?`(v-Q*IsAN zF5Mcw+0kyX9RN6nh6HZ|K!G|K6%H=mcm)6!Wo`OAYkO)^R!(e2B5X)VjZ4HsQ)2mv z+Y)0Fa?^iITnAu>h6ZogaiCu}@JqTXVgV8BJ)h%F%oo>kwpyd!^xeo6yFFZ7f`-l) z*D15C3%&8RzH52=Pb~VWtE;v*Ft5Z}pYMDwfpI`K^t3|;1!diTjM)&AlWi`5QF;gf z9Ns8>yKsDV;%5)XW?|io<8SW#zYq3rd_yKOIG`imTXwf5{lTY~f zm0aSxGiJpW5JaV3S2d;7NE%j(fpl|ng zG{3&|f{MQJae+>H{^-%8h7olnhX&jFM-1BWwP`#qw`1s+RY4b@dq87$PR_!BtGg`$ z(@UY^(dcNVZthRjd*yYFL;~NwRT~k zP@yuL^L2j;8z1{m$D>4TaqMytSzr}qiARYs5h?vUTN7>R6-4=vOq>DY0S%em{KHpb zP-@u!Ts(=0^suzhVAU;?LGR3fv`J?Mh#zW%s-`Xr)j2cTw9ALo%A1PCfM3#=v8$@n zCpH5v6^TTB1yv8*sgb$H#u!b%A|=wk1e7`-GBq{zibwZzhVS+sgZaxJzINA6uow`c zd;Qukn@2HYknr)bp+kyt_k)?4@yJ}bCplw|-y)3QJ`$X@4yO7}2x~o1*yY&vvmsyi z`7|4YWeWz)1|u5<*%5>AbhY#=ip9jtt~Gnoz{P%q+`&yG=ODzL)_P&ujK<7!q)aYv zATUS}_`9`soCZ3C=pKkzQmd-!*W;Thm>Ov!i+~fCukIKmN84Fc2{X^?g5*Pl1Guz4 zMj;Kfc}MEN4Q|Jm?}f>Kk;?HeWT#$nYbj!GKrIx+%PK!|HyROq6@Fn^KYy zKZFzUtnx+_HF>j4R_eZIA%+mA!ePiikj-N~)Eg|2indcCtfJ2!XDvG}LT>b-$N%f6X5QN4Ga*10+5 z&wR9o{(Y;T#d~^sGV$&t`;yY;+qaz*FD4JH>k*_l`XNePF5C1Hh!}8b##A65OM4$&p)(7+td%<;J#<${ za7SC&cB4sH7*s^ADX)+4-Uwn4pUrS_R@*Lrt@o!hAf$7M!$W!1U3qTz=!7jeAUu>f zyiwWKR^$a?(7D4n%t^huWS%QroERIM7dT{>Af7e8`ni<6eP zYs*@~ikNtkq)t1W{cb?hH%`;$!Nru`_VnFXBBaeN(M|=n;1clS_|MgQ9 z@X23^5%LQzSJf154y$6~#s?WP6MyODGgA-Pefky}j5er-<#nU2d>RlRG+NVUxxQx& z{}wm^bO0Iv@~g#$7x@w10>W?|jmj{mRomi0nAUpU@=Swk!G?*ae_-4q^N7sM<4 E3thGQkN^Mx literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_p.png b/interface/resources/meshes/keyboard/key_p.png new file mode 100644 index 0000000000000000000000000000000000000000..8d55d351cedabfda1e3a0883cb13f8d2f5f617b0 GIT binary patch literal 2821 zcmchWYf#fy8phv*ATfdjTah4Zf+7{_5~wUviXo!hrYsj55fKQcTx!)2E(VZG{t#SI zK@b)gxvGqWl>$XT!eW{@5CQ@MDjRE>k`O3Rpp>RXAPw1JJJZ?j%&VD#^-Y>s- zpYuG=xfbB(WrQ=w0f3Q@_wf?|Xjnhk^()o7oB8oZ(L7U9c53 ziuh=6eX4$tK_<@KV6M}C$~XGqu3S5;+Le>cj^=iI9~$$owwr#DA=DW|`;^cLg#eHM zuj7~YT zEu`hkgQ{tcVV_E+DkalbwQGcrO!08wnBnj|_r=gsgd?o+1WPgYY1=dYJ=UY(Q=c_Yzd}Idb}i7kXWa3R{Y`$I@|%A_!VnPY4@9?^ zT_GnsS=XZFOz8{>15fv8HQF_?u!qsizmQD@`&GJcS*YGLf z3b&9T;EwZB(;R8H(KNer$pj56<-XQCg&N8iRjJ2J;6P;ZNgF)WHQHA5+(Ge;mF~p_ zryJqY=oT9_&qhH{u{3PIA5?$IrBb8Om@RTRO&{itoGWXT&Wag<-NtE@BS*v!{L@lV zZx3!eTUVeC$0n>kS&OjG8PRofgdq(#WApp8=MR>Cs425$;d1iH+m}iWc=>CRTbC)- zLV;j1Rg{so`+j8>P?erFfh9+OHp`z$EK|vdVOyziC)goU@~X1mXt_&iZ(1z9!YBd^ z;E37;*VKdx+{7p{H5H@5qL6!@x~~@I2I89aoPTJ_UM&h9k^Uk6PC@h$J-9h0Q184+9ZSlK}(csC! z1D`yB}VsBl&o!dG;~}W5sodC zetyq+X^0|Y>6@Vt*Wa7o*x`o})AniH2LKPfoJ6}H8;2K@E7Dhfx-8cR48%W@&A*;! zA6+;@ZwTu6iurOTan1$qg!rU^+}uP9D!MR?F_p!tRYBz#M9n9?;eVcLBp=1J%4`uJSVS< z^PtOybYLjarOgBny*kBT$9X~Hug3CqBq$J?X}xL!i#4>#rmG;jR6l3Q?w*;P6t~ea zfTIn@v0W-xl?E8NezHkArA`?wo9Lip;Aw%CPj1J~mG`=o!_)pzDccd_Y3FZB)XZLv zeWjoLr+g3%{y}t`_+ZF}M2Xic>5p2GW`CR)8yoA%!GN8OPode3{>O(~U(Ecxyx4Wb z4GW>+g7Ewl>Q=WWrv24K{umL3;0XB=ab?N-lxz!wwXfATf$G6SlgVTUU#~4cxOnSy z!^F%uMLviwMUvL4(nT3*JXIP+x1#3Vt)8893Sf@JVce=rLVkClT{ak9R68>!SU&7i zm?LSA9lIG@*lc71{L6ir^CF{o9F_`z2fzTJU?byinPoghhM{>CTE%c4g*VBN@9nCW zG=c_TaVya1&8z{`rd0j|FaH;@|FxdqzV-i&|1T`qc+S7n<3HDg-|B`j1LElY1A)D? Q5dZ)m55MD$?h#pk2i4?4z5oCK literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_percentage.png b/interface/resources/meshes/keyboard/key_percentage.png new file mode 100644 index 0000000000000000000000000000000000000000..fbe1fa959975b50926d92b6a67e70340b36a9493 GIT binary patch literal 4316 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 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_period.png b/interface/resources/meshes/keyboard/key_period.png new file mode 100644 index 0000000000000000000000000000000000000000..ff726df39b64f8e6650da997339242afc8383173 GIT binary patch literal 1591 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|V<#PZ!6Kid%2)Zsa|zAkcQv;*>%U(_~p?BQEJVi@yXq zWqyvUKO6sJaz;(cGv{rdXs+?Og! zRqvNQK4!znNKVf9AaCEA&b@R>M}*bNt+#rst&J{oF-U)6VHw!0LP0pef)_@U7|bwj zFq%y54&+5LlC9*V5hU#bWF#?aXm7ah$H>OOsw8pdSzFp}1_lNOPgg&ebxsLQ0A@&y ADF6Tf literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_plus.png b/interface/resources/meshes/keyboard/key_plus.png new file mode 100644 index 0000000000000000000000000000000000000000..3eab84c34d621c9b3431a897f0b740cf8bd3fee0 GIT binary patch literal 1635 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|OhYr;B4q#jQ7Y5ArrENHko$RlBro^9PJ3(6=GWnTfV#~t7z{udhz#zcDK#*Bya3#vO z@wBJr-`m-`hR5b?UVKoVuvL)s^x>-!)4Gg#&ogK7>D~KYdwW~kdY#ioeUA?wRb+X7 zXlsQ)+rOznL0-Jl#pev&n3sgFYar|%u!A3j$h9o2S8OSby=^v2PWH0`&jH_^OiaZ1 z2aks-)~-ys5%}CkFvckKfCFW=Q?6Zrgmj5L9*IuY*tN3|4rBb0;K0DJWBzsqW(JAN X+Ddy0r>bP0l+XkK`dOi4 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_q.png b/interface/resources/meshes/keyboard/key_q.png new file mode 100644 index 0000000000000000000000000000000000000000..fc733fd7fd9918056c61702f3233b4ffae05893b GIT binary patch literal 3012 zcmc(hdr;F?7ROJ1gohC@T13q9h$0VN3n~F4MiLZ-(2^p>6;LXIAgGvt7$1=2Cn^dG z8BwD&25=Z&?G^-z6k|+Wgj7O6i-16RD56z}LfMdnN3vr(&d%=a&g{zU>>qc|y?5q& z=gjAR&zzGP%<#9s5pV!tK@ac?0YKpWGJ79V30uAcfTby0!&1Iv#-$wFn;Z+i`);?^9?=TGuk(s)DuCFJyW-SN~<82q|=Olv>U{&UbUtI z%AgE(pQ0PzUmR90`@zUNYT#_1Xf){U#TX9p(ZtN9hd*x=>SYjM*QQ+fB3Ku@grZFqn!5dZG$>bDe$avwm35X zm7L0(mC|!l#C<}en6n`3?mb*LqMjNac5!%FWzl6pANTg3JjJ$)%(f+qqiy`1ViLY{Qz3{@$gCi^*cT4xY2Xvul{iFJ9Q zB(>p?Pl22nUfI=Ti$U9a4|z$xVtR`@t%YK-IP5?ejtH#4q(NS4>MldKF6_XK7n*dC z@%Rs7a1|<fOyS=r<(YzR%Kb$>sjj=WAZ>WsyQ{r`OMfP9LZZ%zcpm7aTNy z|5fF;dI59g%ZYl?S)e84lJO%%^6# zUzbSlshtqKR5Lv6#I2p)n6SY8%*}9b5u^S~R_=;$!6phNZOqR7G1?ft{dHZA1fP+? zvOrD3zx&g0t3K(8VQR`-sPxg}nzWbRnfCXb2O1|Zt#g;Hmg)w!P2l4A36_5q&Oy*&&+Ig}3O3#-F-vfkLdf zuR=RZJ<4b+HGYi4hcf6ao#^wpdabRGeH^~gFcFfFtG25Obh-KR%uvH?JF42vz zpudrNxLDh&cP9g8Fn4j9zODSNBE=n#fQuzfIVAU1qF`@v}1v123?N!wa`>AVz}InDkTCd?gCn*VsEsy zj9JBx(eWuBZkCgdHkY!=W15Q_-1>FJ`mwyZGW0$N&Hnq^;WZK}4n!x@IG@Yx# zv!rxsC2&BmriVMCpD9rwqGN58NVK8WC=6mP;!4wNU@!q2TzP`o~SuhbE zd5@^?bd5wtMm82~Gs2)=)t3L?-SZq^!PZ%%kqX}7ZhSg z)F;Wd+^G_x!MqAVk9gXc_xJZ#ulBGC!_YoUtm|!zUN}FU|Fd1nkw+?aWB?5YlSkQN zN?V<&+_2~qPVNe+-VO_BM7_!H1^kZ`K9u!;a{DhJ{&(_>k8STCaDRv&Iqc0*4w$*l j8UMR6<#!_FT7%gdu#6#iJr$G{0RYgqGW;qwN2UJ_a}S@( literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_question.png b/interface/resources/meshes/keyboard/key_question.png new file mode 100644 index 0000000000000000000000000000000000000000..57f518721333f4698abdc2e415d2ce44a97bd49e GIT binary patch literal 2727 zcmds3ZBP?t65fO$DV8KkR2nltl`j!=v-%df7H`|_v4-2nRlPvXZL+) zcZ-7pc37Ab%mKiHzSAcJ011l+y(n@Bw0Z#eAj3BzBQzy0Ba@LH3*KL*#KhXtlNj-_ zA+d}vv(xUydI6Zz=|0}OvmTGj)k%+;R-C~hzR53^-HBict53Wa;{XmEKR5&N$h)W$ z@7Y=&vzT1s(Qqp`Y-S84OGwI%qFmT@Tzc2%S9QuhQALYmHaJ+x2UZ-PjOugCUy!5g`rAiPfjPBEH`&QdiJej&c^bCQ7&9bh24ZT<_ zmZ4d-5PlXV6biq&X_6GXNxm?XO2u3yqF;f~NGCZp&9{O+G{EZ82^H~_6?D}C~Ib%zM6_I@@#Hy{%L$# z#JaZr-aSA;`_?mlC6q^_HAzC5n}1$JcRr2UO|$x!{%g@n3KQ?^;^LCL{Eju8x-z$q zYqMF>bUS8nt=Jk)@miOvmF}7umONQ|EmSbCH_Xj>jK_Z;y(%=Y}S$@o$NSOSgaDuy8AE;oGt4H@< z(FyB6gdj%-(P(%-saR_Kv3G3v+7CMb2Tpye_tgyr=}tbyQsdCkM30mQcE@?K*@mv%iBc+W0UA7Rls zCbfC_{ZG@H+$fw`1NVb8&p!oTaXRI(^mYvzx@BT9 zt;O{E2UZ9UL6$6JU~Sv-NuT)wuRmQ>-alo70%oE+afGZN8)2;P{A_TL+vR3~xGPM= z`?e_`c(z(Eev*CHKjC-&p(pFi;;-LWK8~Q;`#DtPOGUk7XAHX)j&;!DNyIRwSFuYa z8J>bojb3Ybz=+ZAsEzUe6Nezq{y356%sN49N+16a5~k~H-tK4c2oS=kfi z4eJWp(=nWpGQ;hdG&Z|>VqFSF5J{?<_XS>8ms^#egMFD{zn8TmJw_g~wi9vRS*G6d zcSDzr#%4SF(SnfGXa-^UE80Raqh*w3=+7l%D~%6b1xh>D<0sB1)b*^-)9ncU`HOl# z_Bev-yLG{_f{v!D6wUJFZDXZCJzUEcDU3zEa|QJVRmSs@Kok(%^h$TkYJ+BG80l`t zQL+0AjLE`}^^IoG6E@@@O<7T3$R%gC+8{W)_ByY$kDjz_OS95PPu?0%TWiI2kTiwY zg|$bu1mllyujxyStB=6~b3miHcuF%BFiH=T&r|#d zaJW^&Q&S&ln}YEjqlW^gfAk~7JtVtsV)`r$$5FD0dYO?yl!J%*$f+UO@S>!AsvkkeL zkTzv7Trz{>vV|0(Y|)$lP`eEZ2+t*4mvE&2NH8w4$f=|=FZgp}+r!oVBX+J-ngMnajF`U4QEK?uJ(X1IIQ}LwY+N0^-qK@9)QPNXwwZkDanhJmeKmj1&4b{$(bCZ!} zstl(6jrr_#UT;9-wPXC%S-b%}9PwJCR~x+g_O*M#JHr3(=Jp%!Q41orsTJgNI1GY` RSrh;O-8aDJ;-~xbe+OGEGsXY_ literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_r.png b/interface/resources/meshes/keyboard/key_r.png new file mode 100644 index 0000000000000000000000000000000000000000..6277c640978d83298abdb27c9c8db0ee56a9c428 GIT binary patch literal 1897 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|Ps^r;B4q#jQ7Y59Zz~5IOL0$tl_X2ZCqV(wIK5888|& zeq@=!nZUe7%2!kaEOy1@~{Yl(PWxLp>_e>$>u@*mo%0Sp2r`5 zEZX_!*JPd3U5gAPSs3;?MIFo(wEuAA&$Gs*_e<|SEV%Lhd;Py{I;ZdEJ$H3vc+kyv zn7#QRv;UgcRU0Gz{Jx)WHv4MU*N7Ykh6mAJtM_Nl`usCTgoD9l;4zS zhLuG=0C|@6xkd1{^_^)V`o?x8=g-o z`&(e4leeatg<;KPC2`lYc8S z2=p1-b1=A=%s%_>{tE^shQ^f0ubP0l+XkKpqEGn literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_s.png b/interface/resources/meshes/keyboard/key_s.png new file mode 100644 index 0000000000000000000000000000000000000000..1fc108539141e31c575f9ff41b1eaef9c22bf2e5 GIT binary patch literal 3080 zcmc(hdr;F?7ROIwf)S%UL}{_5FUAS*qzS!$L>rQ{^;DfbIzT6zW3bw zIroo~{q?nEnCxFE_JzSlA5?{RQ8HssuWF@mNct~x2EG-6`rkk??4hNP)d9yca zcEn=amXmGBl@{00oUY~^`nryFcl0f`ixq1MIwrp{XdHUJeV4m=fWQBj@Ig7nTMhaE z7yuNIVNUK}D<{wI^7EJL&ja0mh|ih)9|ilv&heyq$^rGizd-gIWGM>CQY07O2v?aP z2!nB+gDA1Jej~!N{WBaNn@eu&zS7YLXHt`rVvG{2E?OleCuch&^~XXdCw}WxC=#f= zQCBF%$TVCoH`0NxQ;YnzPDBFhEjsv51TBBJWZ+uF{8z@c{Mbt*stz28~W3zMz z8p0fm@#_ja?4`k+&2HLua+SCMg@I0L{V|$jF||a&2Q;)kl`DC&QAvPXj)pBL?}S_=R$0o$C}wI7 zPj)xwX097IBi))P2J75@TQ%pVk4=T>5iiY}*qC6qnV-7L<#JlQ{a&?v8&@ zS63%_u&tA)J^65GU(hPT=Sz_@g;lNQk6L-;^=V~gWs$v!2vWj-qdiU+BsvyHITUGs z@M`hl_Hc0UdSmwjGt#^7G~;L1UPvSI0yeH7RgTTP*87pVDAP_AHa$u`C(n-4iXsxO z7uNJdWpMk36T(@f?@?rp9kNSXJ|vr22rZ=nk~@>2B~W4SN>geEm0q=_rdew z1eCcM9t~ysY^2HG9ItZ&{lRQo`hjy=uPr^eiDES`p`jN_=R8MrQ*2s;o)I7b{}OE2C`3Z&LhtP3{aVPSG?sS10CMyDCV_$_4V5& z8Pg|g{aQ*Tk*mgxO=hX)Xavk<+Na{JmO?ZJI>QOZ_zZ5GdP2Qm;1Zyiya@Mc-^LJm6F43zcHQgiWn70!XKYNcG5*A3+ zdxh~1MNfvtEuFAG`BsdWuaKW{25J07tE9xlOj1{hX(b8=s;0HS00wjqqz}X|8fJXTNojb`kO}0=o`lO@V}52geJhHdw59EaQv2`t1E-0PLxu!Z0i0sIN}UpyrRz8k_v( z#_Uti*4h=+Z`Rmy@nDx7r|o5og&}99d#cl1Dz_iiI)lZGgr37eG_4oXSS%K$tU#=T zA7wKdraxv>R#i1+;UL&xLGrn3x$)g$MY0#JO%^j~*iy5G(Y~6EoT&AC<(l2G6?DI7 zdvBbsr`;}!s1=-O=}0^^Bo4T8BOI?DpHh9;(b-SG<-0+?1`W})>oW}Mu}UAC184bs zz8FbmFwS2kT`Z8T{rlVa2)#uZcopeTl;BuA_JEO>?hF=e7K^n^uFS7$4eCR~?mG&8ra!ta3 literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_semi.png b/interface/resources/meshes/keyboard/key_semi.png new file mode 100644 index 0000000000000000000000000000000000000000..5cb1b495a4b4f79db88a8a913014169269d1153c GIT binary patch literal 1797 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|VP~PZ!6Kid%2)9xS}&AkvVi?aa7{`9%X?gjNIJl~ zl%%{PW?%35Hh%D19=!HzaQlxefqAM542%p83=9Gc3@i-9Gyk1E#&IGca@ymM8GIjh zul%0J#lRWK#6+Sg=r&^4{)5lV&4F#}HGP>-FGDM)*;VhCIsD=OWm5Z4<33-~W)>Fm z0{?*iU;d?2J}OQ>ee{k11ILPMR`(v(*v%GVVt9G(-#Z2YhK`k6zdgKJKQF!6i9uo3 z>#tV#4*ueo^x0b;tT6j;-Tw+3Hfx*t{^blim>V+W?0L&J{A*@#@R9$=v`42~Q;|X7 z!rLTxNve}U7Q^oE- z_3!Hwo>kh(@VU8Y)iE=8oDvcsy%a%7gcNI+V5Haxa&r?n9Z+cdFe+38ZmniuX4t0L Wsxx)s?;Q*b3=E#GelF{r5}E*Qq!^|E literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_slash.png b/interface/resources/meshes/keyboard/key_slash.png new file mode 100644 index 0000000000000000000000000000000000000000..be7b0fecb486566ae909baecac777cdb4e78e496 GIT binary patch literal 2580 zcmeHJT}+c#7=Bx;{S+%7!d6h!4jB?tEr<(>MCvR*O%Vo)0tyvqVa%}#gF!QBD{=B6 zL_lIeP@6dsBLM>_3}JjAVilr9$Y>NTEr{6QPeEKM{af0N-Rur8cC(9<_gtLkoaa36 z^PcwS+QmX08rQ=tVjSNSO;PaKd#wZ4*)Kifzg@!1<9G&2^oCY!xJR(Y3#Iw z6n-Q>fhSCV!1o7O-oj?>Igs=8_4Dqm>S!m4n)k?wxjd45kQ_0fXM!;nK)>`P(ik*z1FPdbiQB5?OgYE{D&k+E%G zdHIVxXF|8%sRRT!9E$p&)vCON8)Pz>>>`m=H7RU=zSVN-2oE2X4Wtwl`U1wjG#X9{u%Vq0zo0B*H0e#4nTtm%RtxtB)hOMsZHg z4%xtZi{3Mcsr_`{&`Eo7SZ#TG01>=ho_<9?wgH8g6fCKRURdlXHFYw zbA?UxylDzS@*;YE;*1NSTU<3MY#oWap(rh)x0+L{Ts^Feg_p=mQ#1X2otOk}J{?F$ zNK}+m)D{!O)G}-bdvfqAT?k@MJy*lEJh4*(ZCgb@Uud*zbVcVs5 z{>@|8#jvo|1W9FP9M#(SB_Ux$+tnQ0tStg2?_T=r+gr76opbl{yohd})zM9F zb%yu)*Iv`hz&nPoQEb|P0v2l3Tpb+5bnhNyAW-P%x@vsKsUzKk44oXo42Kk1s^^w+ zwY?H(ep1mM2stoE9i^KxZWK#?P2tBh%=G$hDgNpxt`ETsH~Xw>#wajW zKH0|TJOQN_)Pbr%X}cx4Ilc0k_eR>=hnqRLeEhMm7zM1jhC3G+#C&$HVTx|b_l2{~ zeS?ApJ0&J+sPVlBwktw$tA>lhS)VgWL)&e7ep4*LF4b_g%b1t@nr_b85t$jM5?&d0 zYu%(GcDV!UP;D)P*!iUbaG#u>_SU||{pZ%2X@I(LaP&TcdDhQZ1xF-+%A%yAfyrjM zI5L^U${miK&(a0k%pH)cX}!LggC8ZaLT&6;-|{yco?l9!?dHoBu0&j) zq@i{YP}%K1xME^YQ_erMqr1-FcTxy8AF@ zqDWv(4K^@7o}_wuNXicn4;Sf|SOySmWLpQ)f!Pq}r#@6o3RQupFYj1ivdU3p-G6oG zZ2OaMucpq-*((8KE3(uaymM|Dk;&Uy!LcR!PMtF$-SLO^IMq`{#+2;p19rYIBsTdQ z3K*vO-vbaRi{bwzkTy1mnd{EMRTK&99N#5Jr|b1aVeB9K+X<3Mcjc7}FVhvz(=BQ1 zJP`11oVDBV2*K=+YN#ZMlU^oL2RRd_m2^`+l)f4l&rO<3<-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|VPkPZ!6Kid%2)9?U&tAky$KptNCw<4lGd3O;Po2F%h2 z*wUE$CkXCw)Y{tawU)*I(Us@#t!JN}d`REAam}<;Nro5aXA3fLfUIs{U{GKni#f2` z6)1B&<=QzY2s&_xlOOW12!qjNngrACkoMlqAwO&F+O5a4?e@(O-MMyJsv+yHO>yhz zvpuM=ndhgzS;^ep;mgt~pT6AwU(KEn#=)?pX!l*E(<}{g7EFuuXP-@*_k8*381Wm7 z4qBh5FF#$jdv2s6!-P{`t9o^c|1~o>m>ra6xc&Cp8UcnAi?@b(b*`DS?Bossh7-ya z|BNk|oEQ{l{{AZ$Y1X%xL4o1IW4;5*4jc?AzrXeWVTfQ5U^t;x@z1z|g@du7#=7Bw zmpQ|W_Wex?`=egh?dR7kb4_4psAQ48C<5FaNMJHrPaEZ@u;)sl4H3 zS2cr5xR<32-$&U4>wORWtSQrT3A@M1WB>18e94FF%pc?}nV19^7+4q>7#SQG7>HrU zJ)Am0#k}vaiSPQFJ3J>(c|T;dddk5;q;6a`QLMd(awCZHK8-xUz(|qplxue&Efru1 z1}s9vq$eb8M5jR{JxELnwX=|$f*v&bGqN!}`CF{Y;b1t2fq{X+)78&qol`;+02ll3 A_W%F@ literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_t.png b/interface/resources/meshes/keyboard/key_t.png new file mode 100644 index 0000000000000000000000000000000000000000..c2082b8f51223e7033afe73b82a3cca584bdd803 GIT binary patch literal 2038 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|WaEPZ!6Kid%2)ehj>oAaUT~rDBFV3ONVZS~!nz_6QgF z2^e)6G+HnwHs>VFIFK`8S;gDRZJ`Taxlelj^R#tK!Fsd(YVj(~yVmVzykLL7m%)L7 zL4bjQg@J*QEat#!XJDkr@sw+KpdjcVAx?J4!y}Bmh=XgxrTsz4TuDaRV3DI|&z`;L ze?P}8t%JcqJ2Z6a^PQFlX3t?_q9oZg>@`+iP5yz-AS7F z)h;vETd5l(QXP+(xd$&5exmmy=mMPGbn$@bg2 z(@!ssxADEqUt?Tw>JERyx|5%4tmex7E%~qXf`54fr9n?r%rz9OT4lt-!tiT_2A8t~ z!-D=(n@m95jIB|UOiT>5Q{Kz|n)0o*NRVt0+sNjS9vW&Y z)w}N>UpzlQ|H5_7tnLgSE*Dtb$uZ+&a$tC|_+v#)b#*r1hj2f}i?y%6{`&Rn*XHxA zu7M`=&ren()fB$XSQ~mSuz2Ek|wU2!9 z@^!#`Lv|*H1MWqV8U&ITq4YsQTzdnavVoA_AqsF7EqJsS fOps^dVc5Uw-;Y%q?4KAI7#KWV{an^LB{Ts5EkS@@ literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_u.png b/interface/resources/meshes/keyboard/key_u.png new file mode 100644 index 0000000000000000000000000000000000000000..657527f6c0068fc41a5ecbdcdff232defe858b02 GIT binary patch literal 2232 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|Q5tr;B4q#jQ7YFBabNU}#7zp3D@|WW^BP@PeVdLHj_6 zLJZ?FrgNeUq6%6YtPE5yG;U(npLx1fF@ih$_iwpBA|{)r+^pX^$#r?=ENg~+^Xp}p z7#JBG7#IW?7+4s{Vh*f!7Rnq?v33SV3W5$C;^c=sEW%(knI=)FT>x*g@i@lc-moMj zwDeaTP56&enYJ zXeZZiv2PbN)nunNQ0hd=eE6g5+wZ;23`_^gL#}UaW?*7?d;4u!Xs9J02ZKub?z?Vx z|2=-ZxVeYHVUh8d3hDKHGAS$!Lc9)3ejiBOyzD;*gNp7Ym0F{jE_?SdFgEPAIdA{z z)aLx zMowE>!pG9EUd`vg?W(oW5`WV-M`|2=$+ISG_4Vu5KYY&GpS=FMbpLThHa~OoFNYNs zOkD%Fh&DX_+i)^PXxq)VWrxqlKmUEOZ1>)i+;WHXU(_+IV|cLtP{$uxxqUjPi>&5a z-Muqm_JJi8tOfrL{pBscG}DP;LG9s(1|LuUKK?(Bk%i&k@d_Ki%6IuM*%%uls`kds z%GRlzr$60WorAHV9Fo+j}&r zaP@4)%{O&={?$jl_52AE-PiH7sPgH{GLLh=e^>ALXYn^bhDCuv;QGg(r!OBajPCRQ z)!zBvGCzkeTs@BsWN)FP+P&Fd4hP7weKGiGUmy4WW&jJrx+NN2+qZAuq!V4kWt@{` zBDGgGrr_|;8x5~x<~r=&!#B%6bN~JIVXMDZ)qdQ*qeZ*Z;PU(LzjN0Mp-}q>EzN&u+p|hAEW8%^h0lIP2?GNI NgQu&X%Q~loCIIFy+yVdq literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_under.png b/interface/resources/meshes/keyboard/key_under.png new file mode 100644 index 0000000000000000000000000000000000000000..3694dd31090a560a802191acaf243c676263769a GIT binary patch literal 1579 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|V<>PZ!6Kid%2)9^`FM5O8%g72C2lkW)91Np|hD2Z{Vb zX<7>Rk4!Ikc;*aq!`~P+1_1^J76t}J1_uVRm;$Fc?jy zNfc^lU>x8iAiyBZzyZoB4Gatl3}i9M(f;9XE|b`$sM*E;{!9?OSp9vY0K5ZvO*tS%9LEcio5p7Y4%FAXkBh+d_C?fqvQybAv^rq0v zIkniDUMSM^Vk%OpMqAP}Ct)VD8k#0zMq_?^aQwS}u;=VQ`^P=cJ@?$t^W6J=o^zj@ z?eDkB#E4=9046@Gy*B_LFfLdK{hKlCiafps0Rqqf$N>5yPj74Igw&*R-{@#obV-4bDD`5pRzO4>vV6z1%mT$V-IheS*4sFH~dGM$Z` z=kKZsoWKW*WBKXl1Krv~4J%(;igyQ6adUXHwQ}?1@M4K>f>gtq28PhJQGJkQ>dcNg z;^yw|?%~m^vw^Fkqop*Jw!RP3hvh-)FmX^RO{^_V_(=~DHpkMmZ-c0$Q%!YJDjC%L zd=p32@eVJ=SNCtnjJb&fL^!n~WWI0Q=_)7UlMb{gqE>4_U{QQOoq4!)iR7=t7f~;1 zp^)^hr>AG{a!y2x+OhwJq&x8@DZE%f4jmt(Jo!BZxx3n=5aswZ`<#9cD<$?ijadBnuA@*cA;QKsVE0Yh(llAghr-LI>QrqwyKkbW>=K=wDqcmxjE+K zb5pq!Z^|*RP$C?iV82W69g0JN4R1 zGVs#b8Q&QkI*l*-F%EGwzb57jc+DA2v6X_GH}~`}WGknnYW0Gkv9P3sc28SBjM+)p z5%!dp9o8lts1?ov8n->k=6#q-u@7$;-mvi2#BYkpXaQb5U<-{k%-Q>P)!}YLqbgj2 zvw?=UC)r?rnIBsqVZasvuK)+)z%IL1DH@TfBrLL1x#bl;i@6gX5f;{zcA^^(Aqf3z z<=#QTxS)qxirT=U`Qpo3g+d|dp-OPerM5GFn6rF##p?|ZIyoT(VQcbLm-l@bALv_g_Xf{R zDQxQ`Gic3mK#(!M+N0R09b*qC!~Z{k@%(=NjTSz}6sq5#2nqPP7Zfg+cQgY4_^kBv JzPy6I|8IdU_TK;i literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_w.png b/interface/resources/meshes/keyboard/key_w.png new file mode 100644 index 0000000000000000000000000000000000000000..15de9b25a8cfe59c3350c498126de9eafdf30158 GIT binary patch literal 3466 zcmc(hSyYqP8iscwQ)O-|qEbVl1=BJWgh0RqtcuhE4$z1L1PR8&V60F;f&39HLP4-1 zpalY_)`AJsA`BvsKv7W!LyZW5LKIAx!jQy<=}-Q%I9;pfT%8NJI2U{E@8BScaf%GQ}THFt-}qa$k7dyEbR27_V% zU=D!Jnx^%X>tX)usSV-)y&>@%#~B;2?k^kG?fl;^u5WOEV;jVE{^ty%uJe*LV#D#p z1R_y8WJ$DDTRpX>^(j}WnBc47gK%2*sgMvVbTx!WpPfzT2rH(IyuG~xMTlJPxO)0I zpFhsw@%A`E&d4#ZhY!;}($f+S5C{aVRL1M{yT6yi0f`U<#l^kquz!(Nl%12q$(fs* ztHPEOiRf2g^6QwfO#Z&c#zvKytOQbHkxm|6#cDO>OKNLFcw(vn+OvQQAauhMl@t;va?({;fBX| zNo1Yl-?qSsmo80J8=%M&+Ni)>r|9hC38h0N*1?ajdlP$&BL~Vuf80;TD`D8fRIhf` zi^>VQa6tE)!vi~aEiq+s8vL!VRqmS$ll}&lpkaT^nb}KN*V~Cf?ybk%kq`LEdDT8G zUnISm*GG|?A8|ZeE5EPWb9mPO1tO=xA9oaC&0WUczNO%cwL+E_7H;erz?3P5{SH$d2>kryvVGd{FOl*$Dso3PF4`gnKuZo7$b_dnQYX`{uOtP@{n zZw(v2uJ|bu>Fo4|PMD%Ukkh4)^wCT@tnenk4H4k?48)v2K7(2B^SGS1{2 z_v~SJWUc2iMuTzs1!DEAzVL02e`laVE}P#%ONv%tG#DcSL{%E-T~w0Ie%>v6YDHfz z=!WoAh$KS%c?_# zVo_9Zx0rYUNNc1|Iwc z1~_s}!JxLVJ|<}#F4J#u{dDvPH+n_8n22s8r1P1>ipeOxKk_7}@CvB;uGZ;d(jrAr zDAn)i=AP^yxfdf-RrJ1zT64;JY(|)1Qr>az_0q}5#Fk}@OP7U1a`S@+I(F9@L$R*G z>fW7K&p)vY8Tm$fmy>)YM>98qG&+v)7ET%^p$= z_u3#9z*Ga~&(21>RxE_I@X+U6uwz;^Ypec z8h41|7_@4BvR@Y)3E4ZK$(Tp{=6pRgzDY%FuH!RF=^|&t13>zVEFtOg*>S9Q*-Xtk< z@r_={zqO4RK1}$OX^U{5`79q@fb)3m>@I>3)oO_9t4<7Kpl#iZc_)$l z3TyS%7mSLXj*QD5xW(4weBTXaf!CD@k);{RasrGD1)JrS4Wv!CoOuKq_VsF}cDGG~iD=&Ru% z99r|H9C#%|v7H zw78^%Iw3NcALCopO%3c|0J2eyj6E;)n3M8ftqgm&8R(-^*z*RB!RNUB=XSG3N&~>) ziH1}y*TcUufCd04;0b^cSXa4Gyv_Un94t4I$^VLU8w{2k#LfQ`FxS~>ow;;I^?4h} XH&uO{;pSNe002Jw{q|MuC8zugEve^% literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_x.png b/interface/resources/meshes/keyboard/key_x.png new file mode 100644 index 0000000000000000000000000000000000000000..d81a423f3b3651de72b207bed25a6dc7d38976f4 GIT binary patch literal 2997 zcmdT`Yg7{07Cs;)7AAVrn~VZ!jP_8nOv_71Dm6)inW?F$J;2ZmwU9JX8EbrGjWylG zu?&0pno+aI#Yd%HDst3B$-1P4T8f5>PZ%X+maa8x&97Pa=CAwXti9LT>)ZSL_Bm(m zowb*`%Xk@T834d|_cvbq0Dyw|1D`iZD9>^LfF+xJfbAC(!9I291Ox1##~fy0cOO4= zl(CO-h@KeN#2^5$JaD(yj{OO5hu@9I^*u*7#3@?W^UOZ3*kdU9ZV;^7S_1qaY+&8s z&vWCgD1^Dia=4o{j)^~*sC?jATw*HS@uX?MSWAkGfzyqv)oNHneOv0IB>)O|0AK-t z&y)J!-7mrXsnjCj@GoHer|OG_1Lz`Jw?O2}JO77{KkOG~S|l9%m(NCm(ob7R(Ek49 z6&S=)syU2Dot@KZqaOZBvqxP%r)x7i5j}#;}F|hKUFs{$~7d_{qjaRd_-DE_-PL-IelmsJ!j5+ z&hWw<9B_O3`x!N~2uzrNr&6gLp~gH6CI{$?rIA6oDa{Yg;;?U-Tdj6Jmx+2WdhF8% zS?7q^hh-SObnS>H38Oa!zwJTM1peuv5vS=4shDxKY<%AU7a8Lf+MkEyMbhE7A3j-DLHMEYqNkqZq z3jDdg*sZVb4WPTPMak_(=th}p?=u6Ox>h3L%8{xAPkoNKTqRqi4r{#tR@~g&%q6xF zxetR!ICU?K_WPUHY?VFndD~)!v_{IzQ9<7sRqT*yvMhlYIl$M3Z-}?Z9&Xn+^_f6H z9NE{)(d7MO?I+*HXM!0nOK#_I)F|`istY@1%V)zM`_N*?#ZP=%Kp|B*?WUy)!b9QS z`Z)C<=FvH07>Gh2s0{CD3fWYK+b>x|A+cMcU7Ex4>myd%Cwz5tJYo2QI}`}bY8*8} zO;$Eny7$9Fu>?>^Y#(}3ze1DM7435AnP7x-Y-$(iNl#m#W@3j_Om3UF9j+w~Sy4!A ze|q_V=os7f%~xIX8l3H_kyAqLQ}Xp`rra(9NFF#Fd^5=#7Nrt5_HcXq`<+P_}jygXOjhR(Czmz z@d6%?U%Og%D!F95^sNpL3wie zWF*s$LLy{3Te<$rGPQ<=(m5IMI9W6__s+cy!me^53c}+U5u23xiRsA#0)j#!{4+#; zrSar@Q^2fjJcz6b9JDKvmL>}bW)u>k=!NGIB7H%e@ig)3GabHx?`d$`34vVRmcP&2)NI!R`nE%J5V#$a3SbIyv{gCr>KB^oO z3Vzi3IRd;bDJ$!ZO^{-$-m#kRO$FEji4DKGe`aRpwaF6Y^yy-0Q^j614~IpI>6;>Z zE?pejb$+L<$^{CJR|S`|bQ|sp%F0eBCr@9O#^>@9VB!x|a+T|6Pm_!@p*Qj%4ckK< z`E|6?f1oa)4#YtrywsI4%{Fw-eau9^g(>>@w;aeDzZ(kf@zDzx%VsZEM$ z9)xr&P0%Q92wh3ehwLeq*0s1dAi$HV#{>;H-DKv;`;3Y(M~(<3vei zTVT>_WxN_bYGDco-2t&xj2fB+dE6)U>fE|5SnacSHfgLXm>1tFN8B5mm#jM;l>3G& zyBe|Tk|C*<41EmUywq zlo2%RZv)1bG>+sw4pYT$<(`lQ3aJKBM~yPo`K=K$(Wq40k?J5~p&D)ZhH_=r6)a$P z4uqKE|1ACD@zw0Qui7YuDe#RcgC%mK%7Pb zwBJ~+No#cVdDXJ`Y2WCWJr4K^C^^t%P6wpyaJy8B~?w_E3n zEz1}|`7kU1-5EG6fB+HzNC05qi`>P+$1D~*L>D=O7YYCC5&qw-75}GOT!TutwgJo@ W?s*+|hRFf|fZb%OSB1yHwBG^ucD9EA literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_y.png b/interface/resources/meshes/keyboard/key_y.png new file mode 100644 index 0000000000000000000000000000000000000000..cb85af5b322234afe8e41e3d2c24f2ff8aafc1a4 GIT binary patch literal 3072 zcmc(hYfzJC7RS%~a*LpbtO!D_NtG%Q0YzL{MG{)kQWWG;i9`YXxi%TfTaW_bp&0+{<)hr_548ox=?Gk%0b0jfw=ZhIZ8Hnm`rlI|Ga-u+V$24`{EF>Rx7*(bO3Vz zLjdHX!ao)VWJbl$9H-Afy5AK4Xyt!z*vB>QcWj2Z;r|=~8#R%MJSgv;Mb00sdM@LZ z(H0o;ml`Y8k}OqI`sG9p#|;y1STH+ZC^Rt1l&E`pb_v&y_8+S3O04n!_lP?{5t;E2}l8S7vOwa;_<&vg-cyU2a znPw_irjg)66jAQJ%Gu%ATG4KN_r_{whf71SY*{*pM|}mJe#eQ#kgK}Vu{FkWo*_&; zxlcW%)u;btZmP;*-pgF{Ub|((r$Q&h>6I4-}0 zKG2MJ?iu`m5WcRp*sZd#tH~dmv@(Qe{+sOPkC<8fEp3>#f9Vp+PW8)|%j+c(pC48p z#NQ70n!C15o7lrw&Nkm?PCsZkW3>wR0LnMg3sq-Aj^A^H+qenB52R-K&tmrCf==U@ zMfg#mukpV|m%ni77tWROI0pz2NMX>b?^4_3@&KjXJG^c02zr@O6Vk_wjt-YNTBZll zq!mM~b*YQ88C^#jCQgSL!IMORcVoFWOM)+kZH4Dre^ljq>=`G&!`osu(W`tXO?Fs4 zqHYUacG*oPXe}g~*+a3vv|XAK^;yl}-^K2%5!?d`ELBq;1_6|DvQIY2ywsx3L32;1 z(|Lr*>J%eC&=&XH@Uu)mku}R6ii2|)s-%N+yB~cvF)us>Cqc2_q-|20g7L$q!S*hc_c_Q$@WbN zA19LyQIOPZ&^B{9Q#hA2fRPQsheH}0u&}#{>`t6*ghI(35h2&7D9O=`+hF*LM1e8) zJH3r~($B>EydnG;h?BnCs=}+)yFv|aM>&JKN$JiK$;Zp|F*- zeu*gWy$;S-h8y0UWjdrzDDRO*tz58>by7>*4s9#Sy&F@CXg^P<&j%OpN&m4Q-1Ng9 znKM?XJeD|ZhQe0X@!YtBxL1!g5_?fvF_ci3;@vp48gDd(i^U_FxD&T70mAQoawI)d zOI#)H>pb}(_+n^dtx=Th$BqGA{v?*#saG;GRa!)U zh!LRbOJ>m>t15go3Z;H3YR`wqvz-G4OlQ3STPs#v{}GwsZGr;E@(bwOBMok{h6@C! z^76BIp@|moNZ=~>Ys9XfK)X=BS4k^j+}9=TmSssm?sou@2lML%>F!1fwr%S zL}FS5FN@~^1c>ZoO>G}=S8Z+yayz#%R! z?(C%Lhq7cHWg+v{;a7bZLf!?z*+Os!ykKC~&wYe=$+@v`0Ip zYHH=xV`=TJImELqwy&Cb`kjfqjAJ+Ldv^>6P7=SFv}2n?v1fk~magaz$>OQ{Y6-KM zA2#5jU%5J&4$7en$sQ((LWE{wqNZbzO*RKtCw4wh`}*D#AH?jIiXjYpS{Y&vuItsW zI>$^9e&x7YLKIDZ0pc#QA>`>)tS-}|@hYFQJTL7CCV=8Dvr zz7O+A<`H|Rl6fvR7bcHEo3p3i^t3ka&$-zQ_VaHsu85bM9HlfUW82yek55m#31zod zC9<9Gi}H0-uQ}&ev|>jNoWG>=0GP~87Xu1>015y>@BjY)@MD<&_zG1z{J~~vCO-2w qn=(VQG(+6{-=^umGZkFGEJC6Cjbp}9M~DRg@bdK8c;k!EqrU>>4D2BQ literal 0 HcmV?d00001 diff --git a/interface/resources/meshes/keyboard/key_z.png b/interface/resources/meshes/keyboard/key_z.png new file mode 100644 index 0000000000000000000000000000000000000000..462531351db199b49c68afdcebcd304daa202f80 GIT binary patch literal 2115 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@GfkCpwHKHUqKdq!Zu_%?nF(p4K zRlzeiF+DXXH8G{K@MNkD0|Wb0PZ!6Kid%2)2Ik&&kU8+MM2o@ML-Gbs7Mo%7EXUpj znoVtvXBMOd7@u%7U@<+49oBH=goWM_rG_e zywG!@8EgFizeophSQsu{Eu9OZH*LQE{`bEA=-s#9+S%E?QCwNf?$Fosw}D~8VgaYn zP>^v2{(G2|^e>xSEtTC{Q~9m@momeId6n~KFEjkc%;Zp3bBmvY!SdCvJc+g)TaVT4 z@YZWQ*|bq!fT1Ym4g0!JTphpd|Mr3moLf2X_BK@@6)6)MJ$~?X%^;?l$uW_gU zu{sBa6Bj={kgxri{ieO9g7wH=PDK-6;k|cm@G~)Z8iDjKd$r39Y)0PCHRd2+E_qd8 z;ZlDzIdaFEt)RHpmIC0WM(Hb;0R>yawm^zQ~d-x#qM zjf@*uAt|-seT{_7>#cY5?%%uj?!opC%Twmo$e1`R+rz{(umz(4Lp07Dg*~5Ph~mnS z7z)vIEJOi?+8cH#K6E{Tf|$zz`!6`;u=vBoS#-wo>-L1 z;Fyx1l&avFo0y&&l$w}QS$HzlhJk@)pQnpsNX4x;cQ*DeH4t&Q_+NbyOS-@%#?%0& zt$|Fb7Z?>&^bShCzUm)-D}n99y7Ma!9Ma#hXq>!o+MoxQ`rV88VJ??j1R>nUT zkl}%qVoT;MF3*ihRz59^45G_!gmcbMjuaDQW4NOn?w;2#3%_SV@XfcF4&_PBa$s)N1&2ea4+>XQH^Z`sSFsBm?SD4dD zaO->q^dLkFQ8X?*fXn7?%e~9U5Rl$(b0<-e#hHQO%1uT7@`%eOwfAQ;GdxJ!=}^=j zayRj&0goFa!-@?K^WJY*_u8hDk)iz5+lkyR?SIYs7P}fUJdoRIRa8BtVAA)>d8%`i zmzta}?zvdNz%bqIhKX-~e%sWmiStFGr={LDQ)Xg_S@`~Up$vEDhD}y)lJ9>njlC+t z@L@Bn8ILokb)swe4R()y}pNo?{(JaZG%Q-e|yQz{EjrrIztFdX-EaSW-r_2!}>BLf4+VS@wvpDQx=g*aXI^eXRV zWnkc7U|?iWU|?VYnJ>V=z{KD{nIN^yK{!J`ijj>WA&vQO32T210|Nttr>mdKI;Vst E0L)M=YXATM literal 0 HcmV?d00001 diff --git a/interface/resources/qml/controlsUit/Keyboard.qml b/interface/resources/qml/controlsUit/Keyboard.qml index 9d4fd33022..c38631ff79 100644 --- a/interface/resources/qml/controlsUit/Keyboard.qml +++ b/interface/resources/qml/controlsUit/Keyboard.qml @@ -36,13 +36,29 @@ Rectangle { readonly property int raisedHeight: keyboardHeight + (showMirrorText ? keyboardRowHeight : 0) - height: enabled && raised ? raisedHeight : 0 - visible: enabled && raised + height: 0 + visible: false property bool shiftMode: false property bool numericShiftMode: false + + onPasswordChanged: { + var use3DKeyboard = (typeof MenuInterface === "undefined") ? false : MenuInterface.isOptionChecked("Use 3D Keyboard"); + if (use3DKeyboard) { + KeyboardScriptingInterface.password = password; + } + } + onRaisedChanged: { + var use3DKeyboard = (typeof MenuInterface === "undefined") ? false : MenuInterface.isOptionChecked("Use 3D Keyboard"); + if (!use3DKeyboard) { + keyboardBase.height = raised ? raisedHeight : 0; + keyboardBase.visible = raised; + } else { + KeyboardScriptingInterface.raised = raised; + KeyboardScriptingInterface.password = raised ? password : false; + } mirroredText = ""; } diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index 6314921286..08aa903ae9 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -127,6 +127,10 @@ TabletModalWindow { } } + Component.onDestruction: { + loginKeyboard.raised = false; + } + Keyboard { id: loginKeyboard raised: root.keyboardEnabled && root.keyboardRaised diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 39590748cf..9635681c34 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -19,7 +19,7 @@ Rectangle { HifiControls.Keyboard { id: keyboard z: 1000 - raised: parent.keyboardEnabled && parent.keyboardRaised + raised: parent.keyboardEnabled && parent.keyboardRaised && HMD.active numeric: parent.punctuationMode anchors { left: parent.left @@ -204,7 +204,8 @@ Rectangle { property bool isInManageState: false - Component.onCompleted: { + Component.onDestruction: { + keyboard.raised = false; } AvatarAppStyle { @@ -235,6 +236,8 @@ Rectangle { avatarIconVisible: mainPageVisible settingsButtonVisible: mainPageVisible onSettingsClicked: { + displayNameInput.focus = false; + root.keyboardRaised = false; settings.open(currentAvatarSettings, currentAvatar.avatarScale); } } @@ -344,6 +347,10 @@ Rectangle { emitSendToScript({'method' : 'changeDisplayName', 'displayName' : text}) focus = false; } + + onFocusChanged: { + root.keyboardRaised = focus; + } } ShadowImage { diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index bad1394133..cd892c17b1 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -14,6 +14,22 @@ Rectangle { signal scaleChanged(real scale); + property bool keyboardEnabled: true + property bool keyboardRaised: false + property bool punctuationMode: false + + HifiControlsUit.Keyboard { + id: keyboard + z: 1000 + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + property alias onSaveClicked: dialogButtons.onYesClicked property alias onCancelClicked: dialogButtons.onNoClicked @@ -314,6 +330,10 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right placeholderText: 'user\\file\\dir' + + onFocusChanged: { + keyboardRaised = (avatarAnimationUrlInputText.focus || avatarCollisionSoundUrlInputText.focus); + } } } @@ -340,6 +360,10 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right placeholderText: 'https://hifi-public.s3.amazonaws.com/sounds/Collisions-' + + onFocusChanged: { + keyboardRaised = (avatarAnimationUrlInputText.focus || avatarCollisionSoundUrlInputText.focus); + } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index ed4ba66b2b..874c1c53b7 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -40,6 +40,10 @@ Rectangle { source: "images/wallet-bg.jpg"; } + Component.onDestruction: { + keyboard.raised = false; + } + Connections { target: Commerce; diff --git a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml index 6cd220307d..b0f17ff841 100644 --- a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml @@ -88,6 +88,10 @@ Rectangle { checkMenu.restart(); } + Component.onDestruction: { + keyboard.raised = false; + } + function updateRunningScripts() { function simplify(path) { // trim URI querystring/fragment diff --git a/interface/resources/qml/hifi/tablet/Edit.qml b/interface/resources/qml/hifi/tablet/Edit.qml index 4acced86ce..099c53cda2 100644 --- a/interface/resources/qml/hifi/tablet/Edit.qml +++ b/interface/resources/qml/hifi/tablet/Edit.qml @@ -49,5 +49,11 @@ StackView { if (currentItem && currentItem.fromScript) currentItem.fromScript(message); } + + Component.onDestruction: { + if (KeyboardScriptingInterface.raised) { + KeyboardScriptingInterface.raised = false; + } + } } diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 0f26ba20aa..b8972378ad 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -56,10 +56,13 @@ StackView { Qt.callLater(function() { addressBarDialog.keyboardEnabled = HMD.active; addressLine.forceActiveFocus(); + addressBarDialog.raised = true; }) } + Component.onDestruction: { root.parentChanged.disconnect(center); + keyboard.raised = false; } function center() { @@ -218,6 +221,11 @@ StackView { leftMargin: 8; verticalCenter: addressLineContainer.verticalCenter; } + + onFocusChanged: { + addressBarDialog.raised = focus; + } + onTextChanged: { updateLocationText(text.length > 0); } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml index 57ca705352..a5d7b23df6 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -242,6 +242,10 @@ Item { keyboardEnabled = HMD.active; } + Component.onDestruction: { + keyboard.raised = false; + } + onKeyboardRaisedChanged: { if (keyboardEnabled && keyboardRaised) { var delta = mouseArea.mouseY - (dialog.height - footer.height - keyboard.raisedHeight -hifi.dimensions.controlLineHeight); diff --git a/interface/resources/sounds/keyboard_key.mp3 b/interface/resources/sounds/keyboard_key.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e2cec81032389f719f90dd60b0ae4ca10719e725 GIT binary patch literal 9089 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 literal 0 HcmV?d00001 diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bd126048dd..ccefb3c772 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -187,6 +187,7 @@ #include "scripting/SelectionScriptingInterface.h" #include "scripting/WalletScriptingInterface.h" #include "scripting/TTSScriptingInterface.h" +#include "scripting/KeyboardScriptingInterface.h" #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif @@ -205,6 +206,7 @@ #include "ui/UpdateDialog.h" #include "ui/overlays/Overlays.h" #include "ui/DomainConnectionModel.h" +#include "ui/Keyboard.h" #include "Util.h" #include "InterfaceParentFinder.h" #include "ui/OctreeStatsProvider.h" @@ -953,6 +955,8 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -2327,6 +2331,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Preload Tablet sounds DependencyManager::get()->preloadSounds(); + DependencyManager::get()->createKeyboard(); _pendingIdleEvent = false; _pendingRenderEvent = false; @@ -2436,11 +2441,17 @@ QString Application::getUserAgent() { } void Application::toggleTabletUI(bool shouldOpen) const { - auto tabletScriptingInterface = DependencyManager::get(); auto hmd = DependencyManager::get(); if (!(shouldOpen && hmd->getShouldShowTablet())) { auto HMD = DependencyManager::get(); HMD->toggleShouldShowTablet(); + + if (!HMD->getShouldShowTablet()) { + DependencyManager::get()->setRaised(false); + _window->activateWindow(); + auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); + tablet->unfocus(); + } } } @@ -2633,6 +2644,7 @@ void Application::cleanupBeforeQuit() { // it accesses the PickManager to delete its associated Pick DependencyManager::destroy(); DependencyManager::destroy(); + DependencyManager::destroy(); qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; } @@ -3113,6 +3125,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("Vec3", new Vec3()); surfaceContext->setContextProperty("Uuid", new ScriptUUID()); surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); + surfaceContext->setContextProperty("Keyboard", DependencyManager::get().data()); surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); surfaceContext->setContextProperty("Users", DependencyManager::get().data()); @@ -6848,6 +6861,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("LODManager", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Keyboard", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Paths", DependencyManager::get().data()); scriptEngine->registerGlobalObject("HMD", DependencyManager::get().data()); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 16e8af5683..2ca997a1fc 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -356,6 +356,8 @@ Menu::Menu() { qApp->setHmdTabletBecomesToolbarSetting(action->isChecked()); }); + addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::Use3DKeyboard, 0, false); + // Developer > Render >>> MenuWrapper* renderOptionsMenu = developerMenu->addMenu("Render"); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 1e9955a760..f1d56825b5 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -210,6 +210,7 @@ namespace MenuOption { const QString TurnWithHead = "Turn using Head"; const QString UseAudioForMouth = "Use Audio for Mouth"; const QString UseCamera = "Use Camera"; + const QString Use3DKeyboard = "Use 3D Keyboard"; const QString VelocityFilter = "Velocity Filter"; const QString VisibleToEveryone = "Everyone"; const QString VisibleToFriends = "Friends"; diff --git a/interface/src/raypick/PickScriptingInterface.cpp b/interface/src/raypick/PickScriptingInterface.cpp index 26b5aacac5..6e979d2d91 100644 --- a/interface/src/raypick/PickScriptingInterface.cpp +++ b/interface/src/raypick/PickScriptingInterface.cpp @@ -31,6 +31,9 @@ #include +static const float WEB_TOUCH_Y_OFFSET = 0.105f; // how far forward (or back with a negative number) to slide stylus in hand +static const glm::vec3 TIP_OFFSET = glm::vec3(0.0f, StylusPick::WEB_STYLUS_LENGTH - WEB_TOUCH_Y_OFFSET, 0.0f); + unsigned int PickScriptingInterface::createPick(const PickQuery::PickType type, const QVariant& properties) { switch (type) { case PickQuery::PickType::Ray: @@ -137,7 +140,12 @@ unsigned int PickScriptingInterface::createStylusPick(const QVariant& properties maxDistance = propMap["maxDistance"].toFloat(); } - return DependencyManager::get()->addPick(PickQuery::Stylus, std::make_shared(side, filter, maxDistance, enabled)); + glm::vec3 tipOffset = TIP_OFFSET; + if (propMap["tipOffset"].isValid()) { + tipOffset = vec3FromVariant(propMap["tipOffset"]); + } + + return DependencyManager::get()->addPick(PickQuery::Stylus, std::make_shared(side, filter, maxDistance, enabled, tipOffset)); } // NOTE: Laser pointer still uses scaleWithAvatar. Until scaleWithAvatar is also deprecated for pointers, scaleWithAvatar should not be removed from the pick API. @@ -416,4 +424,4 @@ void PickScriptingInterface::setParentTransform(std::shared_ptr pick, pick->parentTransform = std::make_shared(pickID); } } -} \ No newline at end of file +} diff --git a/interface/src/raypick/PointerScriptingInterface.cpp b/interface/src/raypick/PointerScriptingInterface.cpp index a44d14b4a6..f0edb4d9ef 100644 --- a/interface/src/raypick/PointerScriptingInterface.cpp +++ b/interface/src/raypick/PointerScriptingInterface.cpp @@ -16,6 +16,11 @@ #include "LaserPointer.h" #include "StylusPointer.h" #include "ParabolaPointer.h" +#include "StylusPick.h" + +static const glm::quat X_ROT_NEG_90{ 0.70710678f, -0.70710678f, 0.0f, 0.0f }; +static const glm::vec3 DEFAULT_POSITION_OFFSET{0.0f, 0.0f, -StylusPick::WEB_STYLUS_LENGTH / 2.0f}; +static const glm::vec3 DEFAULT_MODEL_DIMENSIONS{0.01f, 0.01f, StylusPick::WEB_STYLUS_LENGTH}; void PointerScriptingInterface::setIgnoreItems(unsigned int uid, const QScriptValue& ignoreItems) const { DependencyManager::get()->setIgnoreItems(uid, qVectorQUuidFromScriptValue(ignoreItems)); @@ -50,6 +55,8 @@ unsigned int PointerScriptingInterface::createPointer(const PickQuery::PickType& * @typedef {object} Pointers.StylusPointerProperties * @property {boolean} [hover=false] If this pointer should generate hover events. * @property {boolean} [enabled=false] + * @property {Vec3} [tipOffset] The specified offset of the from the joint index. + * @property {object} [model] Data to replace the default model url, positionOffset and rotationOffset. */ unsigned int PointerScriptingInterface::createStylus(const QVariant& properties) const { QVariantMap propertyMap = properties.toMap(); @@ -64,7 +71,28 @@ unsigned int PointerScriptingInterface::createStylus(const QVariant& properties) enabled = propertyMap["enabled"].toBool(); } - return DependencyManager::get()->addPointer(std::make_shared(properties, StylusPointer::buildStylusOverlay(propertyMap), hover, enabled)); + glm::vec3 modelPositionOffset = DEFAULT_POSITION_OFFSET; + glm::quat modelRotationOffset = X_ROT_NEG_90; + glm::vec3 modelDimensions = DEFAULT_MODEL_DIMENSIONS; + + if (propertyMap["model"].isValid()) { + QVariantMap modelData = propertyMap["model"].toMap(); + + if (modelData["positionOffset"].isValid()) { + modelPositionOffset = vec3FromVariant(modelData["positionOffset"]); + } + + if (modelData["rotationOffset"].isValid()) { + modelRotationOffset = quatFromVariant(modelData["rotationOffset"]); + } + + if (modelData["dimensions"].isValid()) { + modelDimensions = vec3FromVariant(modelData["dimensions"]); + } + } + + return DependencyManager::get()->addPointer(std::make_shared(properties, StylusPointer::buildStylusOverlay(propertyMap), hover, enabled, modelPositionOffset, + modelRotationOffset, modelDimensions)); } /**jsdoc diff --git a/interface/src/raypick/RayPick.cpp b/interface/src/raypick/RayPick.cpp index ad12db4df2..b6adba4a12 100644 --- a/interface/src/raypick/RayPick.cpp +++ b/interface/src/raypick/RayPick.cpp @@ -10,6 +10,7 @@ #include "Application.h" #include "EntityScriptingInterface.h" #include "ui/overlays/Overlays.h" +#include "ui/Keyboard.h" #include "avatar/AvatarManager.h" #include "scripting/HMDScriptingInterface.h" #include "DependencyManager.h" @@ -40,9 +41,12 @@ PickResultPointer RayPick::getEntityIntersection(const PickRay& pick) { PickResultPointer RayPick::getOverlayIntersection(const PickRay& pick) { bool precisionPicking = !(getFilter().doesPickCoarse() || DependencyManager::get()->getForceCoarsePicking()); + auto keyboard = DependencyManager::get(); + QVector ignoreItems = keyboard->getKeysID(); + ignoreItems.append(getIgnoreItemsAs()); RayToOverlayIntersectionResult overlayRes = qApp->getOverlays().findRayIntersectionVector(pick, precisionPicking, - getIncludeItemsAs(), getIgnoreItemsAs(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable()); + getIncludeItemsAs(), ignoreItems, !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable()); if (overlayRes.intersects) { return std::make_shared(IntersectionType::OVERLAY, overlayRes.overlayID, overlayRes.distance, overlayRes.intersection, pick, overlayRes.surfaceNormal, overlayRes.extraInfo); } else { @@ -118,4 +122,4 @@ glm::vec2 RayPick::projectOntoOverlayXYPlane(const QUuid& overlayID, const glm:: glm::vec2 RayPick::projectOntoEntityXYPlane(const QUuid& entityID, const glm::vec3& worldPos, bool unNormalized) { auto props = DependencyManager::get()->getEntityProperties(entityID); return projectOntoXYPlane(worldPos, props.getPosition(), props.getRotation(), props.getDimensions(), props.getRegistrationPoint(), unNormalized); -} \ No newline at end of file +} diff --git a/interface/src/raypick/StylusPick.cpp b/interface/src/raypick/StylusPick.cpp index c495ddd194..0416563272 100644 --- a/interface/src/raypick/StylusPick.cpp +++ b/interface/src/raypick/StylusPick.cpp @@ -21,11 +21,7 @@ #include using namespace bilateral; - -// TODO: make these configurable per pick -static const float WEB_STYLUS_LENGTH = 0.2f; -static const float WEB_TOUCH_Y_OFFSET = 0.105f; // how far forward (or back with a negative number) to slide stylus in hand -static const glm::vec3 TIP_OFFSET = glm::vec3(0.0f, WEB_STYLUS_LENGTH - WEB_TOUCH_Y_OFFSET, 0.0f); +float StylusPick::WEB_STYLUS_LENGTH = 0.2f; struct SideData { QString avatarJoint; @@ -64,8 +60,8 @@ bool StylusPickResult::checkOrFilterAgainstMaxDistance(float maxDistance) { return distance < maxDistance; } -StylusPick::StylusPick(Side side, const PickFilter& filter, float maxDistance, bool enabled) : - Pick(StylusTip(side), filter, maxDistance, enabled) +StylusPick::StylusPick(Side side, const PickFilter& filter, float maxDistance, bool enabled, const glm::vec3& tipOffset) : + Pick(StylusTip(side), filter, maxDistance, enabled), _tipOffset(tipOffset) { } @@ -90,7 +86,7 @@ static StylusTip getFingerWorldLocation(Side side) { } // controllerWorldLocation is where the controller would be, in-world, with an added offset -static StylusTip getControllerWorldLocation(Side side) { +static StylusTip getControllerWorldLocation(Side side, const glm::vec3& tipOffset) { static const std::array INPUTS{ { UserInputMapper::makeStandardInput(SIDES[0].channel), UserInputMapper::makeStandardInput(SIDES[1].channel) } }; const auto sideIndex = index(side); @@ -114,7 +110,7 @@ static StylusTip getControllerWorldLocation(Side side) { // add to the real position so the grab-point is out in front of the hand, a bit result.position += result.orientation * (sideData.grabPointSphereOffset * sensorScaleFactor); // move the stylus forward a bit - result.position += result.orientation * (TIP_OFFSET * sensorScaleFactor); + result.position += result.orientation * (tipOffset * sensorScaleFactor); auto worldControllerPos = avatarPosition + avatarOrientation * pose.translation; // compute tip velocity from hand controller motion, it is more accurate than computing it from previous positions. @@ -131,7 +127,7 @@ StylusTip StylusPick::getMathematicalPick() const { if (qApp->getPreferAvatarFingerOverStylus()) { result = getFingerWorldLocation(_mathPick.side); } else { - result = getControllerWorldLocation(_mathPick.side); + result = getControllerWorldLocation(_mathPick.side, _tipOffset); } return result; } @@ -236,4 +232,4 @@ Transform StylusPick::getResultTransform() const { Transform transform; transform.setTranslation(stylusResult->intersection); return transform; -} \ No newline at end of file +} diff --git a/interface/src/raypick/StylusPick.h b/interface/src/raypick/StylusPick.h index cd01df20e9..3e0ee452e9 100644 --- a/interface/src/raypick/StylusPick.h +++ b/interface/src/raypick/StylusPick.h @@ -58,7 +58,7 @@ public: class StylusPick : public Pick { using Side = bilateral::Side; public: - StylusPick(Side side, const PickFilter& filter, float maxDistance, bool enabled); + StylusPick(Side side, const PickFilter& filter, float maxDistance, bool enabled, const glm::vec3& tipOffset); StylusTip getMathematicalPick() const override; PickResultPointer getDefaultResult(const QVariantMap& pickVariant) const override; @@ -71,6 +71,11 @@ public: bool isLeftHand() const override { return _mathPick.side == Side::Left; } bool isRightHand() const override { return _mathPick.side == Side::Right; } bool isMouse() const override { return false; } + + static float WEB_STYLUS_LENGTH; + +private: + glm::vec3 _tipOffset; }; -#endif // hifi_StylusPick_h \ No newline at end of file +#endif // hifi_StylusPick_h diff --git a/interface/src/raypick/StylusPointer.cpp b/interface/src/raypick/StylusPointer.cpp index 4ba3813c4a..caa3151cc5 100644 --- a/interface/src/raypick/StylusPointer.cpp +++ b/interface/src/raypick/StylusPointer.cpp @@ -17,9 +17,6 @@ #include "PickScriptingInterface.h" #include -// TODO: make these configurable per pointer -static const float WEB_STYLUS_LENGTH = 0.2f; - static const float TABLET_MIN_HOVER_DISTANCE = -0.1f; static const float TABLET_MAX_HOVER_DISTANCE = 0.1f; static const float TABLET_MIN_TOUCH_DISTANCE = -0.1f; @@ -28,9 +25,15 @@ static const float TABLET_MAX_TOUCH_DISTANCE = 0.005f; static const float HOVER_HYSTERESIS = 0.01f; static const float TOUCH_HYSTERESIS = 0.001f; -StylusPointer::StylusPointer(const QVariant& props, const OverlayID& stylusOverlay, bool hover, bool enabled) : +static const QString DEFAULT_STYLUS_MODEL_URL = PathUtils::resourcesUrl() + "/meshes/tablet-stylus-fat.fbx"; + +StylusPointer::StylusPointer(const QVariant& props, const OverlayID& stylusOverlay, bool hover, bool enabled, + const glm::vec3& modelPositionOffset, const glm::quat& modelRotationOffset, const glm::vec3& modelDimensions) : Pointer(DependencyManager::get()->createStylusPick(props), enabled, hover), - _stylusOverlay(stylusOverlay) + _stylusOverlay(stylusOverlay), + _modelPositionOffset(modelPositionOffset), + _modelDimensions(modelDimensions), + _modelRotationOffset(modelRotationOffset) { } @@ -42,9 +45,19 @@ StylusPointer::~StylusPointer() { OverlayID StylusPointer::buildStylusOverlay(const QVariantMap& properties) { QVariantMap overlayProperties; - // TODO: make these configurable per pointer + // TODO: make these configurable per pointe + QString modelUrl = DEFAULT_STYLUS_MODEL_URL; + + if (properties["model"].isValid()) { + QVariantMap modelData = properties["model"].toMap(); + + if (modelData["url"].isValid()) { + modelUrl = modelData["url"].toString(); + } + } + overlayProperties["name"] = "stylus"; - overlayProperties["url"] = PathUtils::resourcesUrl() + "/meshes/tablet-stylus-fat.fbx"; + overlayProperties["url"] = modelUrl; overlayProperties["loadPriority"] = 10.0f; overlayProperties["solid"] = true; overlayProperties["visible"] = false; @@ -72,13 +85,12 @@ void StylusPointer::updateVisuals(const PickResultPointer& pickResult) { void StylusPointer::show(const StylusTip& tip) { if (!_stylusOverlay.isNull()) { QVariantMap props; - static const glm::quat X_ROT_NEG_90{ 0.70710678f, -0.70710678f, 0.0f, 0.0f }; - auto modelOrientation = tip.orientation * X_ROT_NEG_90; + auto modelOrientation = tip.orientation * _modelRotationOffset; auto sensorToWorldScale = DependencyManager::get()->getMyAvatar()->getSensorToWorldScale(); - auto modelPositionOffset = modelOrientation * (vec3(0.0f, 0.0f, -WEB_STYLUS_LENGTH / 2.0f) * sensorToWorldScale); + auto modelPositionOffset = modelOrientation * (_modelPositionOffset * sensorToWorldScale); props["position"] = vec3toVariant(tip.position + modelPositionOffset); props["rotation"] = quatToVariant(modelOrientation); - props["dimensions"] = vec3toVariant(sensorToWorldScale * vec3(0.01f, 0.01f, WEB_STYLUS_LENGTH)); + props["dimensions"] = vec3toVariant(sensorToWorldScale * _modelDimensions); props["visible"] = true; qApp->getOverlays().editOverlay(_stylusOverlay, props); } diff --git a/interface/src/raypick/StylusPointer.h b/interface/src/raypick/StylusPointer.h index ff60fd78e5..64e2a38bed 100644 --- a/interface/src/raypick/StylusPointer.h +++ b/interface/src/raypick/StylusPointer.h @@ -21,7 +21,8 @@ class StylusPointer : public Pointer { using Ptr = std::shared_ptr; public: - StylusPointer(const QVariant& props, const OverlayID& stylusOverlay, bool hover, bool enabled); + StylusPointer(const QVariant& props, const OverlayID& stylusOverlay, bool hover, bool enabled, + const glm::vec3& modelPositionOffset, const glm::quat& modelRotationOffset, const glm::vec3& modelDimensions); ~StylusPointer(); void updateVisuals(const PickResultPointer& pickResult) override; @@ -81,6 +82,10 @@ private: bool _showing { true }; + glm::vec3 _modelPositionOffset; + glm::vec3 _modelDimensions; + glm::quat _modelRotationOffset; + }; #endif // hifi_StylusPointer_h diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index ea24d6c793..f2f8d3b8d4 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -119,8 +119,11 @@ void HMDScriptingInterface::toggleShouldShowTablet() { } void HMDScriptingInterface::setShouldShowTablet(bool value) { - _showTablet = value; - _tabletContextualMode = false; + if (_showTablet != value) { + _showTablet = value; + _tabletContextualMode = false; + emit showTabletChanged(value); + } } QScriptValue HMDScriptingInterface::getHUDLookAtPosition2D(QScriptContext* context, QScriptEngine* engine) { diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 2c0a3fe45f..6cc695762b 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -355,6 +355,8 @@ signals: */ bool shouldShowHandControllersChanged(); + void showTabletChanged(bool showTablet); + public: HMDScriptingInterface(); static QScriptValue getHUDLookAtPosition2D(QScriptContext* context, QScriptEngine* engine); diff --git a/interface/src/scripting/KeyboardScriptingInterface.cpp b/interface/src/scripting/KeyboardScriptingInterface.cpp new file mode 100644 index 0000000000..b26e1ec378 --- /dev/null +++ b/interface/src/scripting/KeyboardScriptingInterface.cpp @@ -0,0 +1,34 @@ +// +// KeyboardScriptingInterface.cpp +// interface/src/scripting +// +// Created by Dante Ruiz on 2018-08-27. +// 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 +// + +#include "KeyboardScriptingInterface.h" +#include "ui/Keyboard.h" + +bool KeyboardScriptingInterface::isRaised() { + return DependencyManager::get()->isRaised(); +} + +void KeyboardScriptingInterface::setRaised(bool raised) { + DependencyManager::get()->setRaised(raised); +} + + +bool KeyboardScriptingInterface::isPassword() { + return DependencyManager::get()->isPassword(); +} + +void KeyboardScriptingInterface::setPassword(bool password) { + DependencyManager::get()->setPassword(password); +} + +void KeyboardScriptingInterface::loadKeyboardFile(const QString& keyboardFile) { + DependencyManager::get()->loadKeyboardFile(keyboardFile); +} diff --git a/interface/src/scripting/KeyboardScriptingInterface.h b/interface/src/scripting/KeyboardScriptingInterface.h new file mode 100644 index 0000000000..1ab91ea7c3 --- /dev/null +++ b/interface/src/scripting/KeyboardScriptingInterface.h @@ -0,0 +1,43 @@ +// +// KeyboardScriptingInterface.h +// interface/src/scripting +// +// Created by Dante Ruiz on 2018-08-27. +// 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 +// + +#ifndef hifi_KeyboardScriptingInterface_h +#define hifi_KeyboardScriptingInterface_h + +#include + +#include "DependencyManager.h" + +/**jsdoc + * The Keyboard API provides facilities to use 3D Physical keyboard. + * @namespace Keyboard + * + * @hifi-interface + * @hifi-client-entity + * + * @property {bool} raised - true If the keyboard is visible false otherwise + * @property {bool} password - true Will show * instead of characters in the text display false otherwise + */ +class KeyboardScriptingInterface : public QObject, public Dependency { + Q_OBJECT + Q_PROPERTY(bool raised READ isRaised WRITE setRaised) + Q_PROPERTY(bool password READ isPassword WRITE setPassword) + +public: + Q_INVOKABLE void loadKeyboardFile(const QString& string); +private: + bool isRaised(); + void setRaised(bool raised); + + bool isPassword(); + void setPassword(bool password); +}; +#endif diff --git a/interface/src/ui/Keyboard.cpp b/interface/src/ui/Keyboard.cpp new file mode 100644 index 0000000000..677d384a17 --- /dev/null +++ b/interface/src/ui/Keyboard.cpp @@ -0,0 +1,799 @@ +// +// Keyboard.cpp +// interface/src/scripting +// +// Created by Dante Ruiz on 2018-08-27. +// 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 +// + +#include "Keyboard.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ui/overlays/Overlays.h" +#include "ui/overlays/Overlay.h" +#include "ui/overlays/ModelOverlay.h" +#include "ui/overlays/Cube3DOverlay.h" +#include "ui/overlays/Text3DOverlay.h" +#include "avatar/AvatarManager.h" +#include "avatar/MyAvatar.h" +#include "avatar/AvatarManager.h" +#include "raypick/PickScriptingInterface.h" +#include "scripting/HMDScriptingInterface.h" +#include "scripting/WindowScriptingInterface.h" +#include "DependencyManager.h" + +#include "raypick/StylusPointer.h" +#include "GLMHelpers.h" +#include "Application.h" + +static const int LEFT_HAND_CONTROLLER_INDEX = 0; +static const int RIGHT_HAND_CONTROLLER_INDEX = 1; + +static const float MALLET_LENGTH = 0.4f; +static const float MALLET_TOUCH_Y_OFFSET = 0.105f; +static const float MALLET_Y_OFFSET = 0.35f; + +static const glm::quat MALLET_ROTATION_OFFSET{0.70710678f, 0.0f, -0.70710678f, 0.0f}; +static const glm::vec3 MALLET_MODEL_DIMENSIONS{0.05f, MALLET_LENGTH, 0.05f}; +static const glm::vec3 MALLET_POSITION_OFFSET{0.0f, -MALLET_Y_OFFSET / 2.0f, 0.0f}; +static const glm::vec3 MALLET_TIP_OFFSET{0.0f, MALLET_LENGTH - MALLET_TOUCH_Y_OFFSET, 0.0f}; + + +static const glm::vec3 Z_AXIS {0.0f, 0.0f, 1.0f}; +static const glm::vec3 KEYBOARD_TABLET_OFFSET{0.28f, -0.3f, -0.05f}; +static const glm::vec3 KEYBOARD_TABLET_DEGREES_OFFSET{-45.0f, 0.0f, 0.0f}; +static const glm::vec3 KEYBOARD_TABLET_LANDSCAPE_OFFSET{-0.2f, -0.27f, -0.05f}; +static const glm::vec3 KEYBOARD_TABLET_LANDSCAPE_DEGREES_OFFSET{-45.0f, 0.0f, -90.0f}; +static const glm::vec3 KEYBOARD_AVATAR_OFFSET{-0.6f, 0.3f, -0.7f}; +static const glm::vec3 KEYBOARD_AVATAR_DEGREES_OFFSET{0.0f, 180.0f, 0.0f}; + +static const QString SOUND_FILE = PathUtils::resourcesUrl() + "sounds/keyboard_key.mp3"; +static const QString MALLET_MODEL_URL = PathUtils::resourcesUrl() + "meshes/drumstick.fbx"; + +static const float PULSE_STRENGTH = 0.6f; +static const float PULSE_DURATION = 3.0f; + +static const int KEY_PRESS_TIMEOUT_MS = 100; +static const int LAYER_SWITCH_TIMEOUT_MS = 200; + +static const QString CHARACTER_STRING = "character"; +static const QString CAPS_STRING = "caps"; +static const QString CLOSE_STRING = "close"; +static const QString LAYER_STRING = "layer"; +static const QString BACKSPACE_STRING = "backspace"; +static const QString SPACE_STRING = "space"; +static const QString ENTER_STRING = "enter"; + +std::pair calculateKeyboardPositionAndOrientation() { + auto myAvatar = DependencyManager::get()->getMyAvatar(); + auto hmd = DependencyManager::get(); + + std::pair keyboardLocation = std::make_pair(glm::vec3(), glm::quat()); + float sensorToWorldScale = myAvatar->getSensorToWorldScale(); + QUuid tabletID = hmd->getCurrentTabletFrameID(); + if (!tabletID.isNull() && hmd->getShouldShowTablet()) { + Overlays& overlays = qApp->getOverlays(); + auto tabletOverlay = std::dynamic_pointer_cast(overlays.getOverlay(tabletID)); + if (tabletOverlay) { + auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); + bool landscapeMode = tablet->getLandscape(); + glm::vec3 keyboardOffset = landscapeMode ? KEYBOARD_TABLET_LANDSCAPE_OFFSET : KEYBOARD_TABLET_OFFSET; + glm::vec3 keyboardDegreesOffset = landscapeMode ? KEYBOARD_TABLET_LANDSCAPE_DEGREES_OFFSET : KEYBOARD_TABLET_DEGREES_OFFSET; + glm::vec3 tabletWorldPosition = tabletOverlay->getWorldPosition(); + glm::quat tabletWorldOrientation = tabletOverlay->getWorldOrientation(); + glm::vec3 scaledKeyboardTabletOffset = keyboardOffset * sensorToWorldScale; + + keyboardLocation.first = tabletWorldPosition + (tabletWorldOrientation * scaledKeyboardTabletOffset); + keyboardLocation.second = tabletWorldOrientation * glm::quat(glm::radians(keyboardDegreesOffset)); + } + + } else { + glm::vec3 avatarWorldPosition = myAvatar->getWorldPosition(); + glm::quat avatarWorldOrientation = myAvatar->getWorldOrientation(); + glm::vec3 scaledKeyboardAvatarOffset = KEYBOARD_AVATAR_OFFSET * sensorToWorldScale; + + keyboardLocation.first = avatarWorldPosition + (avatarWorldOrientation * scaledKeyboardAvatarOffset); + keyboardLocation.second = avatarWorldOrientation * glm::quat(glm::radians(KEYBOARD_AVATAR_DEGREES_OFFSET)); + } + + return keyboardLocation; +} + +void Key::saveDimensionsAndLocalPosition() { + Overlays& overlays = qApp->getOverlays(); + auto model3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(_keyID)); + + if (model3DOverlay) { + _originalLocalPosition = model3DOverlay->getLocalPosition(); + _originalDimensions = model3DOverlay->getDimensions(); + _currentLocalPosition = _originalLocalPosition; + } +} + +void Key::scaleKey(float sensorToWorldScale) { + Overlays& overlays = qApp->getOverlays(); + auto model3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(_keyID)); + + if (model3DOverlay) { + glm::vec3 scaledLocalPosition = _originalLocalPosition * sensorToWorldScale; + glm::vec3 scaledDimensions = _originalDimensions * sensorToWorldScale; + _currentLocalPosition = scaledLocalPosition; + + QVariantMap properties { + { "dimensions", vec3toVariant(scaledDimensions) }, + { "localPosition", vec3toVariant(scaledLocalPosition) } + }; + + overlays.editOverlay(_keyID, properties); + } +} + +void Key::startTimer(int time) { + if (_timer) { + _timer->start(time); + _timer->setSingleShot(true); + } +} + +bool Key::timerFinished() { + if (_timer) { + return (_timer->remainingTime() <= 0); + } + return false; +} + +QString Key::getKeyString(bool toUpper) const { + return toUpper ? _keyString.toUpper() : _keyString; +} + +int Key::getScanCode(bool toUpper) const { + QString character = toUpper ? _keyString.toUpper() : _keyString; + auto utf8Key = character.toUtf8(); + return (int)utf8Key[0]; +} + +Key::Type Key::getKeyTypeFromString(const QString& keyTypeString) { + if (keyTypeString == SPACE_STRING) { + return Type::SPACE; + } else if (keyTypeString == BACKSPACE_STRING) { + return Type::BACKSPACE; + } else if (keyTypeString == LAYER_STRING) { + return Type::LAYER; + } else if (keyTypeString == CAPS_STRING) { + return Type::CAPS; + } else if (keyTypeString == CLOSE_STRING) { + return Type::CLOSE; + } else if (keyTypeString == ENTER_STRING) { + return Type::ENTER; + } + + return Type::CHARACTER; +} + +Keyboard::Keyboard() { + auto pointerManager = DependencyManager::get(); + auto windowScriptingInterface = DependencyManager::get(); + auto myAvatar = DependencyManager::get()->getMyAvatar(); + connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, this, &Keyboard::handleTriggerBegin, Qt::QueuedConnection); + connect(pointerManager.data(), &PointerManager::triggerContinueOverlay, this, &Keyboard::handleTriggerContinue, Qt::QueuedConnection); + connect(pointerManager.data(), &PointerManager::triggerEndOverlay, this, &Keyboard::handleTriggerEnd, Qt::QueuedConnection); + connect(myAvatar.get(), &MyAvatar::sensorToWorldScaleChanged, this, &Keyboard::scaleKeyboard, Qt::QueuedConnection); + connect(windowScriptingInterface.data(), &WindowScriptingInterface::domainChanged, [&]() { setRaised(false); }); +} + + +void Keyboard::createKeyboard() { + auto pointerManager = DependencyManager::get(); + + QVariantMap modelProperties { + { "url", MALLET_MODEL_URL } + }; + + QVariantMap leftStylusProperties { + { "hand", LEFT_HAND_CONTROLLER_INDEX }, + { "filter", PickScriptingInterface::PICK_OVERLAYS() }, + { "model", modelProperties }, + { "tipOffset", vec3toVariant(MALLET_TIP_OFFSET) } + }; + + QVariantMap rightStylusProperties { + { "hand", RIGHT_HAND_CONTROLLER_INDEX }, + { "filter", PickScriptingInterface::PICK_OVERLAYS() }, + { "model", modelProperties }, + { "tipOffset", vec3toVariant(MALLET_TIP_OFFSET) } + }; + + _leftHandStylus = pointerManager->addPointer(std::make_shared(leftStylusProperties, StylusPointer::buildStylusOverlay(leftStylusProperties), true, true, + MALLET_POSITION_OFFSET, MALLET_ROTATION_OFFSET, MALLET_MODEL_DIMENSIONS)); + _rightHandStylus = pointerManager->addPointer(std::make_shared(rightStylusProperties, StylusPointer::buildStylusOverlay(rightStylusProperties), true, true, + MALLET_POSITION_OFFSET, MALLET_ROTATION_OFFSET, MALLET_MODEL_DIMENSIONS)); + + pointerManager->disablePointer(_rightHandStylus); + pointerManager->disablePointer(_leftHandStylus); + + QString keyboardSvg = PathUtils::resourcesUrl() + "config/keyboard.json"; + loadKeyboardFile(keyboardSvg); + + _keySound = DependencyManager::get()->getSound(SOUND_FILE); +} + +bool Keyboard::isRaised() const { + return resultWithReadLock([&] { return _raised; }); +} + +void Keyboard::setRaised(bool raised) { + + bool isRaised; + withReadLock([&] { isRaised = _raised; }); + if (isRaised != raised) { + raiseKeyboardAnchor(raised); + raiseKeyboard(raised); + raised ? enableStylus() : disableStylus(); + withWriteLock([&] { + _raised = raised; + _layerIndex = 0; + _capsEnabled = false; + _typedCharacters.clear(); + }); + + updateTextDisplay(); + } +} + +void Keyboard::updateTextDisplay() { + Overlays& overlays = qApp->getOverlays(); + + auto myAvatar = DependencyManager::get()->getMyAvatar(); + float sensorToWorldScale = myAvatar->getSensorToWorldScale(); + float textWidth = (float) overlays.textSize(_textDisplay.overlayID, _typedCharacters).width(); + + glm::vec3 scaledDimensions = _textDisplay.dimensions; + scaledDimensions *= sensorToWorldScale; + float leftMargin = (scaledDimensions.x / 2); + scaledDimensions.x += textWidth; + + + QVariantMap textDisplayProperties { + { "dimensions", vec3toVariant(scaledDimensions) }, + { "leftMargin", leftMargin }, + { "text", _typedCharacters }, + { "lineHeight", (_textDisplay.lineHeight * sensorToWorldScale) } + }; + + overlays.editOverlay(_textDisplay.overlayID, textDisplayProperties); +} + +void Keyboard::raiseKeyboardAnchor(bool raise) const { + Overlays& overlays = qApp->getOverlays(); + OverlayID anchorOverlayID = _anchor.overlayID; + auto anchorOverlay = std::dynamic_pointer_cast(overlays.getOverlay(anchorOverlayID)); + if (anchorOverlay) { + std::pair keyboardLocation = calculateKeyboardPositionAndOrientation(); + anchorOverlay->setWorldPosition(keyboardLocation.first); + anchorOverlay->setWorldOrientation(keyboardLocation.second); + anchorOverlay->setVisible(raise); + + QVariantMap textDisplayProperties { + { "visible", raise } + }; + + overlays.editOverlay(_textDisplay.overlayID, textDisplayProperties); + } +} + +void Keyboard::scaleKeyboard(float sensorToWorldScale) { + Overlays& overlays = qApp->getOverlays(); + + glm::vec3 scaledDimensions = _anchor.originalDimensions * sensorToWorldScale; + auto volume3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(_anchor.overlayID)); + + if (volume3DOverlay) { + volume3DOverlay->setDimensions(scaledDimensions); + } + + for (auto& keyboardLayer: _keyboardLayers) { + for (auto& key: keyboardLayer) { + key.scaleKey(sensorToWorldScale); + } + } + + + + glm::vec3 scaledLocalPosition = _textDisplay.localPosition * sensorToWorldScale; + glm::vec3 textDisplayScaledDimensions = _textDisplay.dimensions * sensorToWorldScale; + + QVariantMap textDisplayProperties { + { "localPosition", vec3toVariant(scaledLocalPosition) }, + { "dimensions", vec3toVariant(textDisplayScaledDimensions) }, + { "lineHeight", (_textDisplay.lineHeight * sensorToWorldScale) } + }; + + overlays.editOverlay(_textDisplay.overlayID, textDisplayProperties); +} + +void Keyboard::startLayerSwitchTimer() { + if (_layerSwitchTimer) { + _layerSwitchTimer->start(LAYER_SWITCH_TIMEOUT_MS); + _layerSwitchTimer->setSingleShot(true); + } +} + +bool Keyboard::isLayerSwitchTimerFinished() { + if (_layerSwitchTimer) { + return (_layerSwitchTimer->remainingTime() <= 0); + } + return false; +} + +void Keyboard::raiseKeyboard(bool raise) const { + if (_keyboardLayers.empty()) { + return; + } + Overlays& overlays = qApp->getOverlays(); + for (const auto& key: _keyboardLayers[_layerIndex]) { + auto base3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(key.getID())); + if (base3DOverlay) { + base3DOverlay->setVisible(raise); + } + } +} + +bool Keyboard::isPassword() const { + return resultWithReadLock([&] { return _password; }); +} + +void Keyboard::setPassword(bool password) { + if (_password != password) { + withWriteLock([&] { + _password = password; + _typedCharacters.clear(); + }); + } + + updateTextDisplay(); +} + +void Keyboard::switchToLayer(int layerIndex) { + if (layerIndex >= 0 && layerIndex < (int)_keyboardLayers.size()) { + Overlays& overlays = qApp->getOverlays(); + + OverlayID currentAnchorOverlayID = _anchor.overlayID; + + glm::vec3 currentOverlayPosition; + glm::quat currentOverlayOrientation; + + auto currentAnchorOverlay = std::dynamic_pointer_cast(overlays.getOverlay(currentAnchorOverlayID)); + if (currentAnchorOverlay) { + currentOverlayPosition = currentAnchorOverlay->getWorldPosition(); + currentOverlayOrientation = currentAnchorOverlay->getWorldOrientation(); + } + + raiseKeyboardAnchor(false); + raiseKeyboard(false); + + setLayerIndex(layerIndex); + + raiseKeyboardAnchor(true); + raiseKeyboard(true); + + OverlayID newAnchorOverlayID = _anchor.overlayID; + auto newAnchorOverlay = std::dynamic_pointer_cast(overlays.getOverlay(newAnchorOverlayID)); + if (newAnchorOverlay) { + newAnchorOverlay->setWorldPosition(currentOverlayPosition); + newAnchorOverlay->setWorldOrientation(currentOverlayOrientation); + } + + startLayerSwitchTimer(); + } +} + +void Keyboard::handleTriggerBegin(const OverlayID& overlayID, const PointerEvent& event) { + if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished()) { + return; + } + + auto pointerID = event.getID(); + auto buttonType = event.getButton(); + + for (auto index = _keyboardLayers[_layerIndex].begin(); index != _keyboardLayers[_layerIndex].end(); index++) { + Key& key = *index; + if (key.getID() == overlayID && (pointerID == _leftHandStylus || pointerID == _rightHandStylus) && + buttonType == PointerEvent::PrimaryButton) { + + + if (key.timerFinished()) { + + auto handIndex = (pointerID == _leftHandStylus) ? controller::Hand::LEFT : controller::Hand::RIGHT; + auto userInputMapper = DependencyManager::get(); + userInputMapper->triggerHapticPulse(PULSE_STRENGTH, PULSE_DURATION, handIndex); + + Overlays& overlays = qApp->getOverlays(); + auto base3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(overlayID)); + + glm::vec3 keyWorldPosition; + if (base3DOverlay) { + keyWorldPosition = base3DOverlay->getWorldPosition(); + } + + AudioInjectorOptions audioOptions; + audioOptions.localOnly = true; + audioOptions.position = keyWorldPosition; + audioOptions.volume = 0.4f; + + AudioInjector::playSound(_keySound->getByteArray(), audioOptions); + + int scanCode = key.getScanCode(_capsEnabled); + QString keyString = key.getKeyString(_capsEnabled); + + auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); + + switch (key.getKeyType()) { + case Key::Type::CLOSE: + setRaised(false); + tablet->unfocus(); + return; + + case Key::Type::CAPS: + _capsEnabled = !_capsEnabled; + switchToLayer(key.getSwitchToLayerIndex()); + return; + case Key::Type::LAYER: + _capsEnabled = false; + switchToLayer(key.getSwitchToLayerIndex()); + return; + case Key::Type::BACKSPACE: + scanCode = Qt::Key_Backspace; + keyString = "\x08"; + _typedCharacters = _typedCharacters.left(_typedCharacters.length() -1); + updateTextDisplay(); + break; + case Key::Type::ENTER: + scanCode = Qt::Key_Return; + keyString = "\x0d"; + _typedCharacters.clear(); + updateTextDisplay(); + break; + case Key::Type::CHARACTER: + if (keyString != " ") { + _typedCharacters.push_back((_password ? "*" : keyString)); + } else { + _typedCharacters.clear(); + } + updateTextDisplay(); + break; + + default: + break; + } + + QKeyEvent* pressEvent = new QKeyEvent(QEvent::KeyPress, scanCode, Qt::NoModifier, keyString); + QKeyEvent* releaseEvent = new QKeyEvent(QEvent::KeyRelease, scanCode, Qt::NoModifier, keyString); + QCoreApplication::postEvent(QCoreApplication::instance(), pressEvent); + QCoreApplication::postEvent(QCoreApplication::instance(), releaseEvent); + + key.startTimer(KEY_PRESS_TIMEOUT_MS); + } + + break; + } + } +} + +void Keyboard::handleTriggerEnd(const OverlayID& overlayID, const PointerEvent& event) { + if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished()) { + return; + } + + auto pointerID = event.getID(); + + for (auto index = _keyboardLayers[_layerIndex].begin(); index != _keyboardLayers[_layerIndex].end(); index++) { + Key& key = *index; + + if (key.getID() == overlayID && (pointerID == _leftHandStylus || pointerID == _rightHandStylus)) { + Overlays& overlays = qApp->getOverlays(); + + auto base3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(overlayID)); + + if (base3DOverlay) { + base3DOverlay->setLocalPosition(key.getCurrentLocalPosition()); + } + + key.setIsPressed(false); + if (key.timerFinished()) { + key.startTimer(KEY_PRESS_TIMEOUT_MS); + } + break; + } + } + +} + +void Keyboard::handleTriggerContinue(const OverlayID& overlayID, const PointerEvent& event) { + if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished()) { + return; + } + + auto pointerID = event.getID(); + + for (auto index = _keyboardLayers[_layerIndex].begin(); index != _keyboardLayers[_layerIndex].end(); index++) { + Key& key = *index; + + if (key.getID() == overlayID && (pointerID == _leftHandStylus || pointerID == _rightHandStylus)) { + Overlays& overlays = qApp->getOverlays(); + + if (!key.isPressed()) { + auto pointerManager = DependencyManager::get(); + auto pickResult = pointerManager->getPrevPickResult(pointerID); + + auto base3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(overlayID)); + + if (base3DOverlay) { + auto pickResultVariant = pickResult->toVariantMap(); + auto stylusTipVariant = pickResultVariant["stylusTip"]; + auto stylusTipPositionVariant = stylusTipVariant.toMap()["position"]; + glm::vec3 stylusTipPosition = vec3FromVariant(stylusTipPositionVariant); + + glm::quat overlayOrientation = base3DOverlay->getWorldOrientation(); + glm::vec3 overlayPosition = base3DOverlay->getWorldPosition(); + + glm::mat4 overlayWorldMat = createMatFromQuatAndPos(overlayOrientation, overlayPosition); + glm::mat4 overlayWorldToLocalMat = glm::inverse(overlayWorldMat); + + glm::vec3 stylusTipInOverlaySpace = transformPoint(overlayWorldToLocalMat, stylusTipPosition); + + static const float PENATRATION_THRESHOLD = 0.025f; + if (stylusTipInOverlaySpace.z < PENATRATION_THRESHOLD) { + static const float Z_OFFSET = 0.002f; + glm::vec3 overlayYAxis = overlayOrientation * Z_AXIS; + glm::vec3 overlayYOffset = overlayYAxis * Z_OFFSET; + glm::vec3 localPosition = key.getCurrentLocalPosition() - overlayYOffset; + base3DOverlay->setLocalPosition(localPosition); + key.setIsPressed(true); + } + } + } + break; + } + } +} + +void Keyboard::disableStylus() { + auto pointerManager = DependencyManager::get(); + pointerManager->setRenderState(_leftHandStylus, "events off"); + pointerManager->disablePointer(_leftHandStylus); + pointerManager->setRenderState(_rightHandStylus, "events off"); + pointerManager->disablePointer(_rightHandStylus); +} + +void Keyboard::setLayerIndex(int layerIndex) { + if (layerIndex >= 0 && layerIndex < (int)_keyboardLayers.size()) { + _layerIndex = layerIndex; + } else { + _layerIndex = 0; + } +} + +void Keyboard::loadKeyboardFile(const QString& keyboardFile) { + if (keyboardFile.isEmpty()) { + return; + } + + auto request = DependencyManager::get()->createResourceRequest(this, keyboardFile); + + if (!request) { + qCWarning(interfaceapp) << "Could not create resource for Keyboard file" << keyboardFile; + } + + + connect(request, &ResourceRequest::finished, this, [=]() { + if (request->getResult() != ResourceRequest::Success) { + qCWarning(interfaceapp) << "Keyboard file failed to download"; + return; + } + + clearKeyboardKeys(); + Overlays& overlays = qApp->getOverlays(); + auto requestData = request->getData(); + + QVector includeItems; + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(requestData, &parseError); + + if (parseError.error != QJsonParseError::NoError) { + qCWarning(interfaceapp) << "Failed to parse keyboard json file - Error: " << parseError.errorString(); + return; + } + QJsonObject jsonObject = jsonDoc.object(); + QJsonArray layer = jsonObject["Layer1"].toArray(); + QJsonObject anchorObject = jsonObject["anchor"].toObject(); + bool useResourcePath = jsonObject["useResourcesPath"].toBool(); + QString resourcePath = PathUtils::resourcesUrl(); + + + if (anchorObject.isEmpty()) { + qCWarning(interfaceapp) << "No Anchor specified. Not creating keyboard"; + return; + } + + QVariantMap anchorProperties { + { "name", "KeyboardAnchor"}, + { "isSolid", true }, + { "visible", false }, + { "grabbable", true }, + { "ignoreRayIntersection", false }, + { "dimensions", anchorObject["dimensions"].toVariant() }, + { "position", anchorObject["position"].toVariant() }, + { "orientation", anchorObject["rotation"].toVariant() } + }; + + glm::vec3 dimensions = vec3FromVariant(anchorObject["dimensions"].toVariant()); + + Anchor anchor; + anchor.overlayID = overlays.addOverlay("cube", anchorProperties); + anchor.originalDimensions = dimensions; + _anchor = anchor; + + const QJsonArray& keyboardLayers = jsonObject["layers"].toArray(); + int keyboardLayerCount = keyboardLayers.size(); + _keyboardLayers.reserve(keyboardLayerCount); + + + for (int keyboardLayerIndex = 0; keyboardLayerIndex < keyboardLayerCount; keyboardLayerIndex++) { + const QJsonValue& keyboardLayer = keyboardLayers[keyboardLayerIndex].toArray(); + + std::vector keyboardLayerKeys; + foreach (const QJsonValue& keyboardKeyValue, keyboardLayer.toArray()) { + + QVariantMap textureMap; + if (!keyboardKeyValue["texture"].isNull()) { + textureMap = keyboardKeyValue["texture"].toObject().toVariantMap(); + + if (useResourcePath) { + for (auto iter = textureMap.begin(); iter != textureMap.end(); iter++) { + QString modifiedPath = resourcePath + iter.value().toString(); + textureMap[iter.key()] = modifiedPath; + } + } + } + + QString modelUrl = keyboardKeyValue["modelURL"].toString(); + QString url = (useResourcePath ? (resourcePath + modelUrl) : modelUrl); + + QVariantMap properties { + { "dimensions", keyboardKeyValue["dimensions"].toVariant() }, + { "position", keyboardKeyValue["position"].toVariant() }, + { "visible", false }, + { "isSolid", true }, + { "emissive", true }, + { "parentID", _anchor.overlayID }, + { "url", url }, + { "textures", textureMap }, + { "grabbable", false }, + { "localOrientation", keyboardKeyValue["localOrientation"].toVariant() } + }; + + OverlayID overlayID = overlays.addOverlay("model", properties); + + QString keyType = keyboardKeyValue["type"].toString(); + QString keyString = keyboardKeyValue["key"].toString(); + + Key key; + if (!keyType.isNull()) { + Key::Type type= Key::getKeyTypeFromString(keyType); + key.setKeyType(type); + + if (type == Key::Type::LAYER || type == Key::Type::CAPS) { + int switchToLayer = keyboardKeyValue["switchToLayer"].toInt(); + key.setSwitchToLayerIndex(switchToLayer); + } + } + key.setID(overlayID); + key.setKeyString(keyString); + key.saveDimensionsAndLocalPosition(); + + includeItems.append(key.getID()); + _itemsToIgnore.append(key.getID()); + keyboardLayerKeys.push_back(key); + } + + _keyboardLayers.push_back(keyboardLayerKeys); + + } + + TextDisplay textDisplay; + QJsonObject displayTextObject = jsonObject["textDisplay"].toObject(); + + QVariantMap displayTextProperties { + { "dimensions", displayTextObject["dimensions"].toVariant() }, + { "localPosition", displayTextObject["localPosition"].toVariant() }, + { "localOrientation", displayTextObject["localOrientation"].toVariant() }, + { "leftMargin", displayTextObject["leftMargin"].toVariant() }, + { "rightMargin", displayTextObject["rightMargin"].toVariant() }, + { "topMargin", displayTextObject["topMargin"].toVariant() }, + { "bottomMargin", displayTextObject["bottomMargin"].toVariant() }, + { "lineHeight", displayTextObject["lineHeight"].toVariant() }, + { "visible", false }, + { "emissive", true }, + { "grabbable", false }, + { "text", ""}, + { "parentID", _anchor.overlayID } + }; + + textDisplay.overlayID = overlays.addOverlay("text3d", displayTextProperties); + textDisplay.localPosition = vec3FromVariant(displayTextObject["localPosition"].toVariant()); + textDisplay.dimensions = vec3FromVariant(displayTextObject["dimensions"].toVariant()); + textDisplay.lineHeight = (float) displayTextObject["lineHeight"].toDouble(); + + _textDisplay = textDisplay; + + _ignoreItemsLock.withWriteLock([&] { + _itemsToIgnore.push_back(_textDisplay.overlayID); + _itemsToIgnore.push_back(_anchor.overlayID); + }); + _layerIndex = 0; + auto pointerManager = DependencyManager::get(); + pointerManager->setIncludeItems(_leftHandStylus, includeItems); + pointerManager->setIncludeItems(_rightHandStylus, includeItems); + }); + + request->send(); +} + +QVector Keyboard::getKeysID() { + return _ignoreItemsLock.resultWithReadLock>([&] { + return _itemsToIgnore; + }); +} + +void Keyboard::clearKeyboardKeys() { + Overlays& overlays = qApp->getOverlays(); + + for (const auto& keyboardLayer: _keyboardLayers) { + for (const Key& key: keyboardLayer) { + overlays.deleteOverlay(key.getID()); + } + } + + overlays.deleteOverlay(_anchor.overlayID); + overlays.deleteOverlay(_textDisplay.overlayID); + + _keyboardLayers.clear(); + + _ignoreItemsLock.withWriteLock([&] { + _itemsToIgnore.clear(); + }); +} + +void Keyboard::enableStylus() { + auto pointerManager = DependencyManager::get(); + pointerManager->setRenderState(_leftHandStylus, "events on"); + pointerManager->enablePointer(_leftHandStylus); + pointerManager->setRenderState(_rightHandStylus, "events on"); + pointerManager->enablePointer(_rightHandStylus); + +} diff --git a/interface/src/ui/Keyboard.h b/interface/src/ui/Keyboard.h new file mode 100644 index 0000000000..662a51c2da --- /dev/null +++ b/interface/src/ui/Keyboard.h @@ -0,0 +1,152 @@ +// +// Keyboard.h +// interface/src/scripting +// +// Created by Dante Ruiz on 2018-08-27. +// 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 +// + +#ifndef hifi_Keyboard_h +#define hifi_Keyboard_h + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "ui/overlays/Overlay.h" + +class PointerEvent; + + +class Key { +public: + Key() = default; + ~Key() = default; + + enum Type { + CHARACTER, + CAPS, + CLOSE, + LAYER, + BACKSPACE, + SPACE, + ENTER + }; + + + static Key::Type getKeyTypeFromString(const QString& keyTypeString); + + OverlayID getID() const { return _keyID; } + void setID(OverlayID overlayID) { _keyID = overlayID; } + + void startTimer(int time); + bool timerFinished(); + + void setKeyString(QString keyString) { _keyString = keyString; } + QString getKeyString(bool toUpper) const; + int getScanCode(bool toUpper) const; + + bool isPressed() const { return _pressed; } + void setIsPressed(bool pressed) { _pressed = pressed; } + + void setSwitchToLayerIndex(int layerIndex) { _switchToLayer = layerIndex; } + int getSwitchToLayerIndex() const { return _switchToLayer; } + + Type getKeyType() const { return _type; } + void setKeyType(Type type) { _type = type; } + + glm::vec3 getCurrentLocalPosition() const { return _currentLocalPosition; } + + void saveDimensionsAndLocalPosition(); + + void scaleKey(float sensorToWorldScale); +private: + Type _type { Type::CHARACTER }; + + int _switchToLayer { 0 }; + bool _pressed { false }; + + OverlayID _keyID; + QString _keyString; + + glm::vec3 _originalLocalPosition; + glm::vec3 _originalDimensions; + glm::vec3 _currentLocalPosition; + + std::shared_ptr _timer { std::make_shared() }; +}; + +class Keyboard : public Dependency, public QObject, public ReadWriteLockable { +public: + Keyboard(); + void createKeyboard(); + bool isRaised() const; + void setRaised(bool raised); + + bool isPassword() const; + void setPassword(bool password); + + void loadKeyboardFile(const QString& keyboardFile); + QVector getKeysID(); + +public slots: + void handleTriggerBegin(const OverlayID& overlayID, const PointerEvent& event); + void handleTriggerEnd(const OverlayID& overlayID, const PointerEvent& event); + void handleTriggerContinue(const OverlayID& overlayID, const PointerEvent& event); + void scaleKeyboard(float sensorToWorldScale); + +private: + struct Anchor { + OverlayID overlayID; + glm::vec3 originalDimensions; + }; + + struct TextDisplay { + float lineHeight; + OverlayID overlayID; + glm::vec3 localPosition; + glm::vec3 dimensions; + }; + + void raiseKeyboard(bool raise) const; + void raiseKeyboardAnchor(bool raise) const; + + void setLayerIndex(int layerIndex); + void enableStylus(); + void disableStylus(); + void clearKeyboardKeys(); + void switchToLayer(int layerIndex); + void updateTextDisplay(); + + void startLayerSwitchTimer(); + bool isLayerSwitchTimerFinished(); + + bool _raised { false }; + bool _password { false }; + bool _capsEnabled { false }; + int _layerIndex { 0 }; + unsigned int _leftHandStylus { 0 }; + unsigned int _rightHandStylus { 0 }; + SharedSoundPointer _keySound { nullptr }; + std::shared_ptr _layerSwitchTimer { std::make_shared() }; + + QString _typedCharacters; + TextDisplay _textDisplay; + Anchor _anchor; + + mutable ReadWriteLockable _ignoreItemsLock; + QVector _itemsToIgnore; + std::vector> _keyboardLayers; +}; + +#endif diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 190d9c3895..fd3ff69691 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -766,4 +766,4 @@ render::ItemKey ModelOverlay::getKey() { builder.withMetaCullGroup(); } return builder.build(); -} \ No newline at end of file +} diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index f4c98bb9e0..35228c6247 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -35,6 +35,7 @@ #include "RectangleOverlay.h" #include "Text3DOverlay.h" #include "Web3DOverlay.h" +#include "ui/Keyboard.h" #include #include @@ -868,8 +869,13 @@ void Overlays::mousePressPointerEvent(const OverlayID& overlayID, const PointerE QMetaObject::invokeMethod(thisOverlay.get(), "handlePointerEvent", Q_ARG(PointerEvent, event)); } - // emit to scripts - emit mousePressOnOverlay(overlayID, event); + + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeysID().contains(overlayID)) { + // emit to scripts + emit mousePressOnOverlay(overlayID, event); + } } bool Overlays::mouseDoublePressEvent(QMouseEvent* event) { @@ -902,8 +908,12 @@ void Overlays::hoverEnterPointerEvent(const OverlayID& overlayID, const PointerE QMetaObject::invokeMethod(thisOverlay.get(), "hoverEnterOverlay", Q_ARG(PointerEvent, event)); } - // emit to scripts - emit hoverEnterOverlay(overlayID, event); + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeysID().contains(overlayID)) { + // emit to scripts + emit hoverEnterOverlay(overlayID, event); + } } void Overlays::hoverOverPointerEvent(const OverlayID& overlayID, const PointerEvent& event) { @@ -917,8 +927,12 @@ void Overlays::hoverOverPointerEvent(const OverlayID& overlayID, const PointerEv QMetaObject::invokeMethod(thisOverlay.get(), "handlePointerEvent", Q_ARG(PointerEvent, event)); } - // emit to scripts - emit hoverOverOverlay(overlayID, event); + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeysID().contains(overlayID)) { + // emit to scripts + emit hoverOverOverlay(overlayID, event); + } } void Overlays::hoverLeavePointerEvent(const OverlayID& overlayID, const PointerEvent& event) { @@ -932,8 +946,12 @@ void Overlays::hoverLeavePointerEvent(const OverlayID& overlayID, const PointerE QMetaObject::invokeMethod(thisOverlay.get(), "hoverLeaveOverlay", Q_ARG(PointerEvent, event)); } - // emit to scripts - emit hoverLeaveOverlay(overlayID, event); + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeysID().contains(overlayID)) { + // emit to scripts + emit hoverLeaveOverlay(overlayID, event); + } } bool Overlays::mouseReleaseEvent(QMouseEvent* event) { @@ -962,8 +980,12 @@ void Overlays::mouseReleasePointerEvent(const OverlayID& overlayID, const Pointe QMetaObject::invokeMethod(thisOverlay.get(), "handlePointerEvent", Q_ARG(PointerEvent, event)); } - // emit to scripts - emit mouseReleaseOnOverlay(overlayID, event); + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeysID().contains(overlayID)) { + // emit to scripts + emit mouseReleaseOnOverlay(overlayID, event); + } } bool Overlays::mouseMoveEvent(QMouseEvent* event) { @@ -1014,8 +1036,13 @@ void Overlays::mouseMovePointerEvent(const OverlayID& overlayID, const PointerEv QMetaObject::invokeMethod(thisOverlay.get(), "handlePointerEvent", Q_ARG(PointerEvent, event)); } - // emit to scripts - emit mouseMoveOnOverlay(overlayID, event); + auto keyboard = DependencyManager::get(); + + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeysID().contains(overlayID)) { + // emit to scripts + emit mouseMoveOnOverlay(overlayID, event); + } } QVector Overlays::findOverlays(const glm::vec3& center, float radius) { @@ -1035,7 +1062,7 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { OverlayID thisID = i.key(); auto overlay = std::dynamic_pointer_cast(i.value()); // FIXME: this ignores overlays with ignorePickIntersection == true, which seems wrong - if (overlay && overlay->getVisible() && !overlay->getIgnorePickIntersection() && overlay->isLoaded()) { + if (overlay && overlay->getVisible() && overlay->isLoaded()) { // get AABox in frame of overlay glm::vec3 dimensions = overlay->getDimensions(); glm::vec3 low = dimensions * -0.5f; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index f13d25f22c..e7a0c5934e 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -41,6 +41,7 @@ #include "scripting/AssetMappingsScriptingInterface.h" #include "scripting/MenuScriptingInterface.h" #include "scripting/SettingsScriptingInterface.h" +#include "scripting/KeyboardScriptingInterface.h" #include #include #include @@ -273,6 +274,7 @@ void Web3DOverlay::setupQmlSurface(bool isTablet) { _webSurface->getSurfaceContext()->setContextProperty("HiFiAbout", AboutUtil::getInstance()); _webSurface->getSurfaceContext()->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); // Override min fps for tablet UI, for silky smooth scrolling setMaxFPS(90); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 6f285a9f0c..7da7a45e83 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1180,9 +1180,9 @@ int Model::getLastFreeJointIndex(int jointIndex) const { void Model::setTextures(const QVariantMap& textures) { if (isLoaded()) { - _pendingTextures.clear(); _needsFixupInScene = true; _renderGeometry->setTextures(textures); + _pendingTextures.clear(); } else { _pendingTextures = textures; } diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 1081f8c4e7..52d359ad0d 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -888,6 +888,12 @@ void TabletProxy::desktopWindowClosed() { gotoHomeScreen(); } +void TabletProxy::unfocus() { + if (_qmlOffscreenSurface) { + _qmlOffscreenSurface->lowerKeyboard(); + } +} + QQuickItem* TabletProxy::getQmlTablet() const { if (!_qmlTabletRoot) { diff --git a/libraries/ui/src/ui/TabletScriptingInterface.h b/libraries/ui/src/ui/TabletScriptingInterface.h index 2d37402d01..9821ad1263 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.h +++ b/libraries/ui/src/ui/TabletScriptingInterface.h @@ -232,6 +232,7 @@ public: const QString getName() const { return _name; } bool getToolbarMode() const { return _toolbarMode; } void setToolbarMode(bool toolbarMode); + void unfocus(); /**jsdoc * @function TabletProxy#gotoMenuScreen diff --git a/scripts/system/controllers/controllerModules/inEditMode.js b/scripts/system/controllers/controllerModules/inEditMode.js index 2b17f447a0..6adfa88fb2 100644 --- a/scripts/system/controllers/controllerModules/inEditMode.js +++ b/scripts/system/controllers/controllerModules/inEditMode.js @@ -175,6 +175,23 @@ Script.include("/~/system/libraries/utils.js"); return this.exitModule(); } } + + var stopRunning = false; + + 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); }; diff --git a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js index 763a0a0a27..9bddeb236a 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js @@ -46,6 +46,10 @@ Script.include("/~/system/libraries/utils.js"); 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; @@ -198,7 +202,7 @@ Script.include("/~/system/libraries/utils.js"); }; this.run = function (controllerData) { - if (controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0) { + if ((controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0) || !this.isGrabbedThingVisible()) { this.endNearParentingGrabOverlay(); this.robbed = false; return makeRunningValues(false, [], []); diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 8bfd776971..74c1e4baf0 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -726,9 +726,14 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { filterText = ""; } + var wasIsOpen = ui.isOpen; ui.isOpen = (onMarketplaceScreen || onCommerceScreen) && !onWalletScreen; ui.buttonActive(ui.isOpen); + if (wasIsOpen !== ui.isOpen && Keyboard.raised) { + Keyboard.raised = false; + } + if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { ContextOverlay.isInMarketplaceInspectionMode = true; } else { From f664421fe07ddb4603cccbf36ba862fc44141d59 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 31 Oct 2018 17:44:20 -0700 Subject: [PATCH 358/362] addressing code review feedback --- interface/src/Menu.cpp | 2 +- .../src/raypick/PointerScriptingInterface.cpp | 10 +- interface/src/raypick/RayPick.cpp | 6 +- interface/src/raypick/StylusPick.cpp | 4 +- interface/src/raypick/StylusPick.h | 3 - interface/src/raypick/StylusPointer.cpp | 4 +- interface/src/ui/Keyboard.cpp | 262 +++++++++--------- interface/src/ui/Keyboard.h | 4 +- interface/src/ui/overlays/Overlays.cpp | 7 +- libraries/shared/src/RegisteredMetaTypes.h | 10 +- 10 files changed, 160 insertions(+), 152 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 2ca997a1fc..1fc1e0c033 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -356,7 +356,7 @@ Menu::Menu() { qApp->setHmdTabletBecomesToolbarSetting(action->isChecked()); }); - addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::Use3DKeyboard, 0, false); + addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::Use3DKeyboard, 0, true); // Developer > Render >>> MenuWrapper* renderOptionsMenu = developerMenu->addMenu("Render"); diff --git a/interface/src/raypick/PointerScriptingInterface.cpp b/interface/src/raypick/PointerScriptingInterface.cpp index f0edb4d9ef..1893132917 100644 --- a/interface/src/raypick/PointerScriptingInterface.cpp +++ b/interface/src/raypick/PointerScriptingInterface.cpp @@ -56,8 +56,16 @@ unsigned int PointerScriptingInterface::createPointer(const PickQuery::PickType& * @property {boolean} [hover=false] If this pointer should generate hover events. * @property {boolean} [enabled=false] * @property {Vec3} [tipOffset] The specified offset of the from the joint index. - * @property {object} [model] Data to replace the default model url, positionOffset and rotationOffset. + * @property {Pointers.StylusPointerProperties.model} [model] Data to replace the default model url, positionOffset and rotationOffset. {@link Pointers.StylusPointerProperties.model} */ + /**jsdoc + * properties defining stylus pick model that can be included to {@link Pointers.StylusPointerProperties} + * @typedef {object} Pointers.StylusPointerProperties.model + * @property {string} [url] url to the model + * @property {Vec3} [dimensions] the dimensions of the model + * @property {Vec3} [positionOffset] the position offset of the model from the parent joint index + * @property {Vec3} [rotationOffset] the rotation offset of the model from the parent joint index + */ unsigned int PointerScriptingInterface::createStylus(const QVariant& properties) const { QVariantMap propertyMap = properties.toMap(); diff --git a/interface/src/raypick/RayPick.cpp b/interface/src/raypick/RayPick.cpp index b6adba4a12..a48d858504 100644 --- a/interface/src/raypick/RayPick.cpp +++ b/interface/src/raypick/RayPick.cpp @@ -10,7 +10,6 @@ #include "Application.h" #include "EntityScriptingInterface.h" #include "ui/overlays/Overlays.h" -#include "ui/Keyboard.h" #include "avatar/AvatarManager.h" #include "scripting/HMDScriptingInterface.h" #include "DependencyManager.h" @@ -41,12 +40,9 @@ PickResultPointer RayPick::getEntityIntersection(const PickRay& pick) { PickResultPointer RayPick::getOverlayIntersection(const PickRay& pick) { bool precisionPicking = !(getFilter().doesPickCoarse() || DependencyManager::get()->getForceCoarsePicking()); - auto keyboard = DependencyManager::get(); - QVector ignoreItems = keyboard->getKeysID(); - ignoreItems.append(getIgnoreItemsAs()); RayToOverlayIntersectionResult overlayRes = qApp->getOverlays().findRayIntersectionVector(pick, precisionPicking, - getIncludeItemsAs(), ignoreItems, !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable()); + getIncludeItemsAs(), getIgnoreItemsAs(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable()); if (overlayRes.intersects) { return std::make_shared(IntersectionType::OVERLAY, overlayRes.overlayID, overlayRes.distance, overlayRes.intersection, pick, overlayRes.surfaceNormal, overlayRes.extraInfo); } else { diff --git a/interface/src/raypick/StylusPick.cpp b/interface/src/raypick/StylusPick.cpp index 0416563272..0a76180be8 100644 --- a/interface/src/raypick/StylusPick.cpp +++ b/interface/src/raypick/StylusPick.cpp @@ -61,7 +61,7 @@ bool StylusPickResult::checkOrFilterAgainstMaxDistance(float maxDistance) { } StylusPick::StylusPick(Side side, const PickFilter& filter, float maxDistance, bool enabled, const glm::vec3& tipOffset) : - Pick(StylusTip(side), filter, maxDistance, enabled), _tipOffset(tipOffset) + Pick(StylusTip(side, tipOffset), filter, maxDistance, enabled) { } @@ -127,7 +127,7 @@ StylusTip StylusPick::getMathematicalPick() const { if (qApp->getPreferAvatarFingerOverStylus()) { result = getFingerWorldLocation(_mathPick.side); } else { - result = getControllerWorldLocation(_mathPick.side, _tipOffset); + result = getControllerWorldLocation(_mathPick.side, _mathPick.tipOffset); } return result; } diff --git a/interface/src/raypick/StylusPick.h b/interface/src/raypick/StylusPick.h index 3e0ee452e9..14821c0ce5 100644 --- a/interface/src/raypick/StylusPick.h +++ b/interface/src/raypick/StylusPick.h @@ -73,9 +73,6 @@ public: bool isMouse() const override { return false; } static float WEB_STYLUS_LENGTH; - -private: - glm::vec3 _tipOffset; }; #endif // hifi_StylusPick_h diff --git a/interface/src/raypick/StylusPointer.cpp b/interface/src/raypick/StylusPointer.cpp index caa3151cc5..5595c54b71 100644 --- a/interface/src/raypick/StylusPointer.cpp +++ b/interface/src/raypick/StylusPointer.cpp @@ -45,7 +45,7 @@ StylusPointer::~StylusPointer() { OverlayID StylusPointer::buildStylusOverlay(const QVariantMap& properties) { QVariantMap overlayProperties; - // TODO: make these configurable per pointe + QString modelUrl = DEFAULT_STYLUS_MODEL_URL; if (properties["model"].isValid()) { @@ -55,7 +55,7 @@ OverlayID StylusPointer::buildStylusOverlay(const QVariantMap& properties) { modelUrl = modelData["url"].toString(); } } - + // TODO: make these configurable per pointer overlayProperties["name"] = "stylus"; overlayProperties["url"] = modelUrl; overlayProperties["loadPriority"] = 10.0f; diff --git a/interface/src/ui/Keyboard.cpp b/interface/src/ui/Keyboard.cpp index 677d384a17..c00ea007f0 100644 --- a/interface/src/ui/Keyboard.cpp +++ b/interface/src/ui/Keyboard.cpp @@ -316,8 +316,8 @@ void Keyboard::scaleKeyboard(float sensorToWorldScale) { } for (auto& keyboardLayer: _keyboardLayers) { - for (auto& key: keyboardLayer) { - key.scaleKey(sensorToWorldScale); + for (auto iter = keyboardLayer.begin(); iter != keyboardLayer.end(); iter++) { + iter.value().scaleKey(sensorToWorldScale); } } @@ -354,8 +354,9 @@ void Keyboard::raiseKeyboard(bool raise) const { return; } Overlays& overlays = qApp->getOverlays(); - for (const auto& key: _keyboardLayers[_layerIndex]) { - auto base3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(key.getID())); + const auto& keyboardLayer = _keyboardLayers[_layerIndex]; + for (auto iter = keyboardLayer.begin(); iter != keyboardLayer.end(); iter++) { + auto base3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(iter.key())); if (base3DOverlay) { base3DOverlay->setVisible(raise); } @@ -419,87 +420,90 @@ void Keyboard::handleTriggerBegin(const OverlayID& overlayID, const PointerEvent auto pointerID = event.getID(); auto buttonType = event.getButton(); - for (auto index = _keyboardLayers[_layerIndex].begin(); index != _keyboardLayers[_layerIndex].end(); index++) { - Key& key = *index; - if (key.getID() == overlayID && (pointerID == _leftHandStylus || pointerID == _rightHandStylus) && - buttonType == PointerEvent::PrimaryButton) { + if ((pointerID != _leftHandStylus && pointerID != _rightHandStylus) || buttonType != PointerEvent::PrimaryButton) { + return; + } + auto& keyboardLayer = _keyboardLayers[_layerIndex]; + auto search = keyboardLayer.find(overlayID); - if (key.timerFinished()) { + if (search == keyboardLayer.end()) { + return; + } - auto handIndex = (pointerID == _leftHandStylus) ? controller::Hand::LEFT : controller::Hand::RIGHT; - auto userInputMapper = DependencyManager::get(); - userInputMapper->triggerHapticPulse(PULSE_STRENGTH, PULSE_DURATION, handIndex); + Key& key = search.value(); - Overlays& overlays = qApp->getOverlays(); - auto base3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(overlayID)); + if (key.timerFinished()) { - glm::vec3 keyWorldPosition; - if (base3DOverlay) { - keyWorldPosition = base3DOverlay->getWorldPosition(); - } + auto handIndex = (pointerID == _leftHandStylus) ? controller::Hand::LEFT : controller::Hand::RIGHT; + auto userInputMapper = DependencyManager::get(); + userInputMapper->triggerHapticPulse(PULSE_STRENGTH, PULSE_DURATION, handIndex); - AudioInjectorOptions audioOptions; - audioOptions.localOnly = true; - audioOptions.position = keyWorldPosition; - audioOptions.volume = 0.4f; + Overlays& overlays = qApp->getOverlays(); + auto base3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(overlayID)); - AudioInjector::playSound(_keySound->getByteArray(), audioOptions); - - int scanCode = key.getScanCode(_capsEnabled); - QString keyString = key.getKeyString(_capsEnabled); - - auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); - - switch (key.getKeyType()) { - case Key::Type::CLOSE: - setRaised(false); - tablet->unfocus(); - return; - - case Key::Type::CAPS: - _capsEnabled = !_capsEnabled; - switchToLayer(key.getSwitchToLayerIndex()); - return; - case Key::Type::LAYER: - _capsEnabled = false; - switchToLayer(key.getSwitchToLayerIndex()); - return; - case Key::Type::BACKSPACE: - scanCode = Qt::Key_Backspace; - keyString = "\x08"; - _typedCharacters = _typedCharacters.left(_typedCharacters.length() -1); - updateTextDisplay(); - break; - case Key::Type::ENTER: - scanCode = Qt::Key_Return; - keyString = "\x0d"; - _typedCharacters.clear(); - updateTextDisplay(); - break; - case Key::Type::CHARACTER: - if (keyString != " ") { - _typedCharacters.push_back((_password ? "*" : keyString)); - } else { - _typedCharacters.clear(); - } - updateTextDisplay(); - break; - - default: - break; - } - - QKeyEvent* pressEvent = new QKeyEvent(QEvent::KeyPress, scanCode, Qt::NoModifier, keyString); - QKeyEvent* releaseEvent = new QKeyEvent(QEvent::KeyRelease, scanCode, Qt::NoModifier, keyString); - QCoreApplication::postEvent(QCoreApplication::instance(), pressEvent); - QCoreApplication::postEvent(QCoreApplication::instance(), releaseEvent); - - key.startTimer(KEY_PRESS_TIMEOUT_MS); - } - - break; + glm::vec3 keyWorldPosition; + if (base3DOverlay) { + keyWorldPosition = base3DOverlay->getWorldPosition(); } + + AudioInjectorOptions audioOptions; + audioOptions.localOnly = true; + audioOptions.position = keyWorldPosition; + audioOptions.volume = 0.4f; + + AudioInjector::playSound(_keySound->getByteArray(), audioOptions); + + int scanCode = key.getScanCode(_capsEnabled); + QString keyString = key.getKeyString(_capsEnabled); + + auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); + + switch (key.getKeyType()) { + case Key::Type::CLOSE: + setRaised(false); + tablet->unfocus(); + return; + + case Key::Type::CAPS: + _capsEnabled = !_capsEnabled; + switchToLayer(key.getSwitchToLayerIndex()); + return; + case Key::Type::LAYER: + _capsEnabled = false; + switchToLayer(key.getSwitchToLayerIndex()); + return; + case Key::Type::BACKSPACE: + scanCode = Qt::Key_Backspace; + keyString = "\x08"; + _typedCharacters = _typedCharacters.left(_typedCharacters.length() -1); + updateTextDisplay(); + break; + case Key::Type::ENTER: + scanCode = Qt::Key_Return; + keyString = "\x0d"; + _typedCharacters.clear(); + updateTextDisplay(); + break; + case Key::Type::CHARACTER: + if (keyString != " ") { + _typedCharacters.push_back((_password ? "*" : keyString)); + } else { + _typedCharacters.clear(); + } + updateTextDisplay(); + break; + + default: + break; + } + + QKeyEvent* pressEvent = new QKeyEvent(QEvent::KeyPress, scanCode, Qt::NoModifier, keyString); + QKeyEvent* releaseEvent = new QKeyEvent(QEvent::KeyRelease, scanCode, Qt::NoModifier, keyString); + QCoreApplication::postEvent(QCoreApplication::instance(), pressEvent); + QCoreApplication::postEvent(QCoreApplication::instance(), releaseEvent); + + key.startTimer(KEY_PRESS_TIMEOUT_MS); } } @@ -509,27 +513,29 @@ void Keyboard::handleTriggerEnd(const OverlayID& overlayID, const PointerEvent& } auto pointerID = event.getID(); - - for (auto index = _keyboardLayers[_layerIndex].begin(); index != _keyboardLayers[_layerIndex].end(); index++) { - Key& key = *index; - - if (key.getID() == overlayID && (pointerID == _leftHandStylus || pointerID == _rightHandStylus)) { - Overlays& overlays = qApp->getOverlays(); - - auto base3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(overlayID)); - - if (base3DOverlay) { - base3DOverlay->setLocalPosition(key.getCurrentLocalPosition()); - } - - key.setIsPressed(false); - if (key.timerFinished()) { - key.startTimer(KEY_PRESS_TIMEOUT_MS); - } - break; - } + if (pointerID != _leftHandStylus && pointerID != _rightHandStylus) { + return; } + auto& keyboardLayer = _keyboardLayers[_layerIndex]; + auto search = keyboardLayer.find(overlayID); + + if (search == keyboardLayer.end()) { + return; + } + + Key& key = search.value();; + Overlays& overlays = qApp->getOverlays(); + auto base3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(overlayID)); + + if (base3DOverlay) { + base3DOverlay->setLocalPosition(key.getCurrentLocalPosition()); + } + + key.setIsPressed(false); + if (key.timerFinished()) { + key.startTimer(KEY_PRESS_TIMEOUT_MS); + } } void Keyboard::handleTriggerContinue(const OverlayID& overlayID, const PointerEvent& event) { @@ -539,44 +545,40 @@ void Keyboard::handleTriggerContinue(const OverlayID& overlayID, const PointerEv auto pointerID = event.getID(); - for (auto index = _keyboardLayers[_layerIndex].begin(); index != _keyboardLayers[_layerIndex].end(); index++) { - Key& key = *index; + if (pointerID != _leftHandStylus && pointerID != _rightHandStylus) { + return; + } - if (key.getID() == overlayID && (pointerID == _leftHandStylus || pointerID == _rightHandStylus)) { - Overlays& overlays = qApp->getOverlays(); + auto& keyboardLayer = _keyboardLayers[_layerIndex]; + auto search = keyboardLayer.find(overlayID); - if (!key.isPressed()) { - auto pointerManager = DependencyManager::get(); - auto pickResult = pointerManager->getPrevPickResult(pointerID); + if (search == keyboardLayer.end()) { + return; + } - auto base3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(overlayID)); + Key& key = search.value(); + Overlays& overlays = qApp->getOverlays(); - if (base3DOverlay) { - auto pickResultVariant = pickResult->toVariantMap(); - auto stylusTipVariant = pickResultVariant["stylusTip"]; - auto stylusTipPositionVariant = stylusTipVariant.toMap()["position"]; - glm::vec3 stylusTipPosition = vec3FromVariant(stylusTipPositionVariant); + if (!key.isPressed()) { + auto base3DOverlay = std::dynamic_pointer_cast(overlays.getOverlay(overlayID)); - glm::quat overlayOrientation = base3DOverlay->getWorldOrientation(); - glm::vec3 overlayPosition = base3DOverlay->getWorldPosition(); + if (base3DOverlay) { + auto pointerManager = DependencyManager::get(); + auto pickResult = pointerManager->getPrevPickResult(pointerID); + auto pickResultVariant = pickResult->toVariantMap(); - glm::mat4 overlayWorldMat = createMatFromQuatAndPos(overlayOrientation, overlayPosition); - glm::mat4 overlayWorldToLocalMat = glm::inverse(overlayWorldMat); + float distance = pickResultVariant["distance"].toFloat(); - glm::vec3 stylusTipInOverlaySpace = transformPoint(overlayWorldToLocalMat, stylusTipPosition); - - static const float PENATRATION_THRESHOLD = 0.025f; - if (stylusTipInOverlaySpace.z < PENATRATION_THRESHOLD) { - static const float Z_OFFSET = 0.002f; - glm::vec3 overlayYAxis = overlayOrientation * Z_AXIS; - glm::vec3 overlayYOffset = overlayYAxis * Z_OFFSET; - glm::vec3 localPosition = key.getCurrentLocalPosition() - overlayYOffset; - base3DOverlay->setLocalPosition(localPosition); - key.setIsPressed(true); - } - } + static const float PENATRATION_THRESHOLD = 0.025f; + if (distance < PENATRATION_THRESHOLD) { + static const float Z_OFFSET = 0.002f; + glm::quat overlayOrientation = base3DOverlay->getWorldOrientation(); + glm::vec3 overlayYAxis = overlayOrientation * Z_AXIS; + glm::vec3 overlayYOffset = overlayYAxis * Z_OFFSET; + glm::vec3 localPosition = key.getCurrentLocalPosition() - overlayYOffset; + base3DOverlay->setLocalPosition(localPosition); + key.setIsPressed(true); } - break; } } } @@ -666,7 +668,7 @@ void Keyboard::loadKeyboardFile(const QString& keyboardFile) { for (int keyboardLayerIndex = 0; keyboardLayerIndex < keyboardLayerCount; keyboardLayerIndex++) { const QJsonValue& keyboardLayer = keyboardLayers[keyboardLayerIndex].toArray(); - std::vector keyboardLayerKeys; + QHash keyboardLayerKeys; foreach (const QJsonValue& keyboardKeyValue, keyboardLayer.toArray()) { QVariantMap textureMap; @@ -718,7 +720,7 @@ void Keyboard::loadKeyboardFile(const QString& keyboardFile) { includeItems.append(key.getID()); _itemsToIgnore.append(key.getID()); - keyboardLayerKeys.push_back(key); + keyboardLayerKeys.insert(overlayID, key); } _keyboardLayers.push_back(keyboardLayerKeys); @@ -774,8 +776,8 @@ void Keyboard::clearKeyboardKeys() { Overlays& overlays = qApp->getOverlays(); for (const auto& keyboardLayer: _keyboardLayers) { - for (const Key& key: keyboardLayer) { - overlays.deleteOverlay(key.getID()); + for (auto iter = keyboardLayer.begin(); iter != keyboardLayer.end(); iter++) { + overlays.deleteOverlay(iter.key()); } } diff --git a/interface/src/ui/Keyboard.h b/interface/src/ui/Keyboard.h index 662a51c2da..2a29a12961 100644 --- a/interface/src/ui/Keyboard.h +++ b/interface/src/ui/Keyboard.h @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -43,7 +44,6 @@ public: ENTER }; - static Key::Type getKeyTypeFromString(const QString& keyTypeString); OverlayID getID() const { return _keyID; } @@ -146,7 +146,7 @@ private: mutable ReadWriteLockable _ignoreItemsLock; QVector _itemsToIgnore; - std::vector> _keyboardLayers; + std::vector> _keyboardLayers; }; #endif diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 35228c6247..7593e12e07 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -583,7 +583,7 @@ ParabolaToOverlayIntersectionResult Overlays::findParabolaIntersectionVector(con bool visibleOnly, bool collidableOnly) { float bestDistance = std::numeric_limits::max(); bool bestIsFront = false; - + const QVector keyboardKeysToDiscard = DependencyManager::get()->getKeysID(); QMutexLocker locker(&_mutex); ParabolaToOverlayIntersectionResult result; QMapIterator i(_overlaysWorld); @@ -593,7 +593,8 @@ ParabolaToOverlayIntersectionResult Overlays::findParabolaIntersectionVector(con auto thisOverlay = std::dynamic_pointer_cast(i.value()); if ((overlaysToDiscard.size() > 0 && overlaysToDiscard.contains(thisID)) || - (overlaysToInclude.size() > 0 && !overlaysToInclude.contains(thisID))) { + (overlaysToInclude.size() > 0 && !overlaysToInclude.contains(thisID)) || + (keyboardKeysToDiscard.size() > 0 && keyboardKeysToDiscard.contains(thisID))) { continue; } @@ -1061,7 +1062,7 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { i.next(); OverlayID thisID = i.key(); auto overlay = std::dynamic_pointer_cast(i.value()); - // FIXME: this ignores overlays with ignorePickIntersection == true, which seems wrong + if (overlay && overlay->getVisible() && overlay->isLoaded()) { // get AABox in frame of overlay glm::vec3 dimensions = overlay->getDimensions(); diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 64a874f63d..18c1339223 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -269,6 +269,7 @@ void pickRayFromScriptValue(const QScriptValue& object, PickRay& pickRay); * * @typedef {object} StylusTip * @property {number} side - The hand the tip is attached to: 0 for left, 1 for right. + * @property {Vec3} tipOffset - the position offset of the stylus tip. * @property {Vec3} position - The position of the stylus tip. * @property {Quat} orientation - The orientation of the stylus tip. * @property {Vec3} velocity - The velocity of the stylus tip. @@ -276,12 +277,14 @@ void pickRayFromScriptValue(const QScriptValue& object, PickRay& pickRay); class StylusTip : public MathPick { public: StylusTip() : position(NAN), velocity(NAN) {} - StylusTip(const bilateral::Side& side, const glm::vec3& position = Vectors::ZERO, const glm::quat& orientation = Quaternions::IDENTITY, const glm::vec3& velocity = Vectors::ZERO) : - side(side), position(position), orientation(orientation), velocity(velocity) {} + StylusTip(const bilateral::Side& side, const glm::vec3& tipOffset = Vectors::ZERO ,const glm::vec3& position = Vectors::ZERO, + const glm::quat& orientation = Quaternions::IDENTITY, const glm::vec3& velocity = Vectors::ZERO) : + side(side), tipOffset(tipOffset), position(position), orientation(orientation), velocity(velocity) {} StylusTip(const QVariantMap& pickVariant) : side(bilateral::Side(pickVariant["side"].toInt())), position(vec3FromVariant(pickVariant["position"])), orientation(quatFromVariant(pickVariant["orientation"])), velocity(vec3FromVariant(pickVariant["velocity"])) {} bilateral::Side side { bilateral::Side::Invalid }; + glm::vec3 tipOffset; glm::vec3 position; glm::quat orientation; glm::vec3 velocity; @@ -289,12 +292,13 @@ public: operator bool() const override { return side != bilateral::Side::Invalid; } bool operator==(const StylusTip& other) const { - return (side == other.side && position == other.position && orientation == other.orientation && velocity == other.velocity); + return (side == other.side && tipOffset == other.tipOffset && position == other.position && orientation == other.orientation && velocity == other.velocity); } QVariantMap toVariantMap() const override { QVariantMap stylusTip; stylusTip["side"] = (int)side; + stylusTip["tipOffset"] = vec3toVariant(tipOffset); stylusTip["position"] = vec3toVariant(position); stylusTip["orientation"] = quatToVariant(orientation); stylusTip["velocity"] = vec3toVariant(velocity); From 358aa547b17a6d6b578743d79c6cf571b5f389c3 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Mon, 5 Nov 2018 16:30:30 -0800 Subject: [PATCH 359/362] feedback changes --- interface/resources/config/keyboard.json | 4 +- .../resources/qml/controls/TabletWebView.qml | 4 + .../qml/dialogs/TabletLoginDialog.qml | 16 +++ .../qml/hifi/commerce/wallet/Wallet.qml | 2 +- interface/src/Application.cpp | 2 + .../src/raypick/PointerScriptingInterface.cpp | 4 +- interface/src/ui/Keyboard.cpp | 97 +++++++++++++++++-- interface/src/ui/Keyboard.h | 3 + interface/src/ui/PreferencesDialog.cpp | 1 - libraries/shared/src/RegisteredMetaTypes.h | 4 +- 10 files changed, 121 insertions(+), 16 deletions(-) diff --git a/interface/resources/config/keyboard.json b/interface/resources/config/keyboard.json index ba113da1f5..186a9c1084 100644 --- a/interface/resources/config/keyboard.json +++ b/interface/resources/config/keyboard.json @@ -31,8 +31,8 @@ "localOrientation": { "w": 0.000, "x": 0.000, - "y": 0.976, - "z": 0.216 + "y": 0.906, + "z": 0.423 }, "leftMargin": 0.0, "rightMargin": 0.0, diff --git a/interface/resources/qml/controls/TabletWebView.qml b/interface/resources/qml/controls/TabletWebView.qml index 0c5ca37e00..94f4c7978c 100644 --- a/interface/resources/qml/controls/TabletWebView.qml +++ b/interface/resources/qml/controls/TabletWebView.qml @@ -195,6 +195,10 @@ Item { keyboardEnabled = HMD.active; } + Component.onDestruction: { + keyboardRaised = false; + } + Keys.onPressed: { switch(event.key) { case Qt.Key_L: diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index 08aa903ae9..dad2bb91aa 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -95,6 +95,18 @@ TabletModalWindow { } } + Timer { + id: keyboardTimer + repeat: false + interval: 200 + + onTriggered: { + if (MenuInterface.isOptionChecked("Use 3D Keyboard")) { + KeyboardScriptingInterface.raised = true; + } + } + } + TabletModalFrame { id: mfRoot @@ -131,6 +143,10 @@ TabletModalWindow { loginKeyboard.raised = false; } + Component.onCompleted: { + keyboardTimer.start(); + } + Keyboard { id: loginKeyboard raised: root.keyboardEnabled && root.keyboardRaised diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 874c1c53b7..81fec4ace3 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -41,7 +41,7 @@ Rectangle { } Component.onDestruction: { - keyboard.raised = false; + KeyboardScriptingInterface.raised = false; } Connections { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ccefb3c772..afb2bde7f3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2644,6 +2644,7 @@ void Application::cleanupBeforeQuit() { // it accesses the PickManager to delete its associated Pick DependencyManager::destroy(); DependencyManager::destroy(); + DependencyManager::destroy(); DependencyManager::destroy(); qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; @@ -2929,6 +2930,7 @@ void Application::initializeRenderEngine() { // Now that OpenGL is initialized, we are sure we have a valid context and can create the various pipeline shaders with success. DependencyManager::get()->initializeShapePipelines(); + DependencyManager::get()->registerKeyboardHighlighting(); }); } diff --git a/interface/src/raypick/PointerScriptingInterface.cpp b/interface/src/raypick/PointerScriptingInterface.cpp index 1893132917..0009536479 100644 --- a/interface/src/raypick/PointerScriptingInterface.cpp +++ b/interface/src/raypick/PointerScriptingInterface.cpp @@ -56,14 +56,14 @@ unsigned int PointerScriptingInterface::createPointer(const PickQuery::PickType& * @property {boolean} [hover=false] If this pointer should generate hover events. * @property {boolean} [enabled=false] * @property {Vec3} [tipOffset] The specified offset of the from the joint index. - * @property {Pointers.StylusPointerProperties.model} [model] Data to replace the default model url, positionOffset and rotationOffset. {@link Pointers.StylusPointerProperties.model} + * @property {Pointers.StylusPointerProperties.model} [model] Data to replace the default model url, positionOffset and rotationOffset. */ /**jsdoc * properties defining stylus pick model that can be included to {@link Pointers.StylusPointerProperties} * @typedef {object} Pointers.StylusPointerProperties.model * @property {string} [url] url to the model * @property {Vec3} [dimensions] the dimensions of the model - * @property {Vec3} [positionOffset] the position offset of the model from the parent joint index + * @property {Vec3} [positionOffset] the position offset of the model from the stylus tip. * @property {Vec3} [rotationOffset] the rotation offset of the model from the parent joint index */ unsigned int PointerScriptingInterface::createStylus(const QVariant& properties) const { diff --git a/interface/src/ui/Keyboard.cpp b/interface/src/ui/Keyboard.cpp index c00ea007f0..773253f85c 100644 --- a/interface/src/ui/Keyboard.cpp +++ b/interface/src/ui/Keyboard.cpp @@ -44,6 +44,7 @@ #include "raypick/PickScriptingInterface.h" #include "scripting/HMDScriptingInterface.h" #include "scripting/WindowScriptingInterface.h" +#include "scripting/SelectionScriptingInterface.h" #include "DependencyManager.h" #include "raypick/StylusPointer.h" @@ -53,12 +54,12 @@ static const int LEFT_HAND_CONTROLLER_INDEX = 0; static const int RIGHT_HAND_CONTROLLER_INDEX = 1; -static const float MALLET_LENGTH = 0.4f; -static const float MALLET_TOUCH_Y_OFFSET = 0.105f; -static const float MALLET_Y_OFFSET = 0.35f; +static const float MALLET_LENGTH = 0.2f; +static const float MALLET_TOUCH_Y_OFFSET = 0.052f; +static const float MALLET_Y_OFFSET = 0.180f; static const glm::quat MALLET_ROTATION_OFFSET{0.70710678f, 0.0f, -0.70710678f, 0.0f}; -static const glm::vec3 MALLET_MODEL_DIMENSIONS{0.05f, MALLET_LENGTH, 0.05f}; +static const glm::vec3 MALLET_MODEL_DIMENSIONS{0.03f, MALLET_LENGTH, 0.03f}; static const glm::vec3 MALLET_POSITION_OFFSET{0.0f, -MALLET_Y_OFFSET / 2.0f, 0.0f}; static const glm::vec3 MALLET_TIP_OFFSET{0.0f, MALLET_LENGTH - MALLET_TOUCH_Y_OFFSET, 0.0f}; @@ -88,6 +89,28 @@ static const QString BACKSPACE_STRING = "backspace"; static const QString SPACE_STRING = "space"; static const QString ENTER_STRING = "enter"; +static const QString KEY_HOVER_HIGHLIGHT = "keyHoverHiglight"; +static const QString KEY_PRESSED_HIGHLIGHT = "keyPressesHighlight"; +static const QVariantMap KEY_HOVERING_STYLE { + { "isOutlineSmooth", true }, + { "outlineWidth", 3 }, + { "outlineUnoccludedColor", QVariantMap {{"red", 13}, {"green", 152}, {"blue", 186}}}, + { "outlineUnoccludedAlpha", 1.0 }, + { "outlineOccludedAlpha", 0.0 }, + { "fillUnoccludedAlpha", 0.0 }, + { "fillOccludedAlpha", 0.0 } +}; + +static const QVariantMap KEY_PRESSING_STYLE { + { "isOutlineSmooth", true }, + { "outlineWidth", 3 }, + { "fillUnoccludedColor", QVariantMap {{"red", 50}, {"green", 50}, {"blue", 50}}}, + { "outlineUnoccludedAlpha", 0.0 }, + { "outlineOccludedAlpha", 0.0 }, + { "fillUnoccludedAlpha", 0.6 }, + { "fillOccludedAlpha", 0.0 } +}; + std::pair calculateKeyboardPositionAndOrientation() { auto myAvatar = DependencyManager::get()->getMyAvatar(); auto hmd = DependencyManager::get(); @@ -201,10 +224,20 @@ Keyboard::Keyboard() { connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, this, &Keyboard::handleTriggerBegin, Qt::QueuedConnection); connect(pointerManager.data(), &PointerManager::triggerContinueOverlay, this, &Keyboard::handleTriggerContinue, Qt::QueuedConnection); connect(pointerManager.data(), &PointerManager::triggerEndOverlay, this, &Keyboard::handleTriggerEnd, Qt::QueuedConnection); + connect(pointerManager.data(), &PointerManager::hoverBeginOverlay, this, &Keyboard::handleHoverBegin, Qt::QueuedConnection); + connect(pointerManager.data(), &PointerManager::hoverEndOverlay, this, &Keyboard::handleHoverEnd, Qt::QueuedConnection); connect(myAvatar.get(), &MyAvatar::sensorToWorldScaleChanged, this, &Keyboard::scaleKeyboard, Qt::QueuedConnection); connect(windowScriptingInterface.data(), &WindowScriptingInterface::domainChanged, [&]() { setRaised(false); }); } +void Keyboard::registerKeyboardHighlighting() { + auto selection = DependencyManager::get(); + selection->enableListHighlight(KEY_HOVER_HIGHLIGHT, KEY_HOVERING_STYLE); + selection->enableListToScene(KEY_HOVER_HIGHLIGHT); + selection->enableListHighlight(KEY_PRESSED_HIGHLIGHT, KEY_PRESSING_STYLE); + selection->enableListToScene(KEY_PRESSED_HIGHLIGHT); +} + void Keyboard::createKeyboard() { auto pointerManager = DependencyManager::get(); @@ -450,7 +483,7 @@ void Keyboard::handleTriggerBegin(const OverlayID& overlayID, const PointerEvent AudioInjectorOptions audioOptions; audioOptions.localOnly = true; audioOptions.position = keyWorldPosition; - audioOptions.volume = 0.4f; + audioOptions.volume = 0.1f; AudioInjector::playSound(_keySound->getByteArray(), audioOptions); @@ -504,6 +537,8 @@ void Keyboard::handleTriggerBegin(const OverlayID& overlayID, const PointerEvent QCoreApplication::postEvent(QCoreApplication::instance(), releaseEvent); key.startTimer(KEY_PRESS_TIMEOUT_MS); + auto selection = DependencyManager::get(); + selection->addToSelectedItemsList(KEY_PRESSED_HIGHLIGHT, "overlay", overlayID); } } @@ -536,6 +571,9 @@ void Keyboard::handleTriggerEnd(const OverlayID& overlayID, const PointerEvent& if (key.timerFinished()) { key.startTimer(KEY_PRESS_TIMEOUT_MS); } + + auto selection = DependencyManager::get(); + selection->removeFromSelectedItemsList(KEY_PRESSED_HIGHLIGHT, "overlay", overlayID); } void Keyboard::handleTriggerContinue(const OverlayID& overlayID, const PointerEvent& event) { @@ -565,9 +603,8 @@ void Keyboard::handleTriggerContinue(const OverlayID& overlayID, const PointerEv if (base3DOverlay) { auto pointerManager = DependencyManager::get(); auto pickResult = pointerManager->getPrevPickResult(pointerID); - auto pickResultVariant = pickResult->toVariantMap(); - - float distance = pickResultVariant["distance"].toFloat(); + auto stylusPickResult = std::dynamic_pointer_cast(pickResult); + float distance = stylusPickResult->distance; static const float PENATRATION_THRESHOLD = 0.025f; if (distance < PENATRATION_THRESHOLD) { @@ -583,6 +620,50 @@ void Keyboard::handleTriggerContinue(const OverlayID& overlayID, const PointerEv } } +void Keyboard::handleHoverBegin(const OverlayID& overlayID, const PointerEvent& event) { + if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished()) { + return; + } + + auto pointerID = event.getID(); + + if (pointerID != _leftHandStylus && pointerID != _rightHandStylus) { + return; + } + + auto& keyboardLayer = _keyboardLayers[_layerIndex]; + auto search = keyboardLayer.find(overlayID); + + if (search == keyboardLayer.end()) { + return; + } + + auto selection = DependencyManager::get(); + selection->addToSelectedItemsList(KEY_HOVER_HIGHLIGHT, "overlay", overlayID); +} + +void Keyboard::handleHoverEnd(const OverlayID& overlayID, const PointerEvent& event) { + if (_keyboardLayers.empty() || !isLayerSwitchTimerFinished()) { + return; + } + + auto pointerID = event.getID(); + + if (pointerID != _leftHandStylus && pointerID != _rightHandStylus) { + return; + } + + auto& keyboardLayer = _keyboardLayers[_layerIndex]; + auto search = keyboardLayer.find(overlayID); + + if (search == keyboardLayer.end()) { + return; + } + + auto selection = DependencyManager::get(); + selection->removeFromSelectedItemsList(KEY_HOVER_HIGHLIGHT, "overlay", overlayID); +} + void Keyboard::disableStylus() { auto pointerManager = DependencyManager::get(); pointerManager->setRenderState(_leftHandStylus, "events off"); diff --git a/interface/src/ui/Keyboard.h b/interface/src/ui/Keyboard.h index 2a29a12961..18db38b2ae 100644 --- a/interface/src/ui/Keyboard.h +++ b/interface/src/ui/Keyboard.h @@ -90,6 +90,7 @@ class Keyboard : public Dependency, public QObject, public ReadWriteLockable { public: Keyboard(); void createKeyboard(); + void registerKeyboardHighlighting(); bool isRaised() const; void setRaised(bool raised); @@ -103,6 +104,8 @@ public slots: void handleTriggerBegin(const OverlayID& overlayID, const PointerEvent& event); void handleTriggerEnd(const OverlayID& overlayID, const PointerEvent& event); void handleTriggerContinue(const OverlayID& overlayID, const PointerEvent& event); + void handleHoverBegin(const OverlayID& overlayID, const PointerEvent& event); + void handleHoverEnd(const OverlayID& overlayID, const PointerEvent& event); void scaleKeyboard(float sensorToWorldScale); private: diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 34d80f50cf..d71ad9dc82 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -147,7 +147,6 @@ void setupPreferences() { preferences->addPreference(new CheckPreference(UI_CATEGORY, "Prefer Avatar Finger Over Stylus", getter, setter)); } */ - // Snapshots static const QString SNAPSHOTS { "Snapshots" }; { diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 18c1339223..ed637fe771 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -280,8 +280,8 @@ public: StylusTip(const bilateral::Side& side, const glm::vec3& tipOffset = Vectors::ZERO ,const glm::vec3& position = Vectors::ZERO, const glm::quat& orientation = Quaternions::IDENTITY, const glm::vec3& velocity = Vectors::ZERO) : side(side), tipOffset(tipOffset), position(position), orientation(orientation), velocity(velocity) {} - StylusTip(const QVariantMap& pickVariant) : side(bilateral::Side(pickVariant["side"].toInt())), position(vec3FromVariant(pickVariant["position"])), - orientation(quatFromVariant(pickVariant["orientation"])), velocity(vec3FromVariant(pickVariant["velocity"])) {} + StylusTip(const QVariantMap& pickVariant) : side(bilateral::Side(pickVariant["side"].toInt())), tipOffset(vec3FromVariant(pickVariant["tipOffset"])), + position(vec3FromVariant(pickVariant["position"])), orientation(quatFromVariant(pickVariant["orientation"])), velocity(vec3FromVariant(pickVariant["velocity"])) {} bilateral::Side side { bilateral::Side::Invalid }; glm::vec3 tipOffset; From 2a16ba57ac84b9e3988c6a122dddc57b5346413e Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 8 Nov 2018 15:24:57 -0800 Subject: [PATCH 360/362] fix image entity crash --- .../src/model-networking/ModelCache.cpp | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 8194361897..dd58b0e75e 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -631,14 +631,16 @@ NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textur _name = material.name.toStdString(); if (!material.albedoTexture.filename.isEmpty()) { auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); - _albedoTransform = material.albedoTexture.transform; - map->setTextureTransform(_albedoTransform); + if (map) { + _albedoTransform = material.albedoTexture.transform; + map->setTextureTransform(_albedoTransform); - if (!material.opacityTexture.filename.isEmpty()) { - if (material.albedoTexture.filename == material.opacityTexture.filename) { - // Best case scenario, just indicating that the albedo map contains transparency - // TODO: Different albedo/opacity maps are not currently supported - map->setUseAlphaChannel(true); + if (!material.opacityTexture.filename.isEmpty()) { + if (material.albedoTexture.filename == material.opacityTexture.filename) { + // Best case scenario, just indicating that the albedo map contains transparency + // TODO: Different albedo/opacity maps are not currently supported + map->setUseAlphaChannel(true); + } } } @@ -670,7 +672,9 @@ NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textur if (!material.occlusionTexture.filename.isEmpty()) { auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); - map->setTextureTransform(material.occlusionTexture.transform); + if (map) { + map->setTextureTransform(material.occlusionTexture.transform); + } setTextureMap(MapChannel::OCCLUSION_MAP, map); } @@ -686,10 +690,12 @@ NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textur if (!material.lightmapTexture.filename.isEmpty()) { auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); - _lightmapTransform = material.lightmapTexture.transform; - _lightmapParams = material.lightmapParams; - map->setTextureTransform(_lightmapTransform); - map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); + if (map) { + _lightmapTransform = material.lightmapTexture.transform; + _lightmapParams = material.lightmapParams; + map->setTextureTransform(_lightmapTransform); + map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); + } setTextureMap(MapChannel::LIGHTMAP_MAP, map); } } @@ -709,9 +715,11 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { if (!albedoName.isEmpty()) { auto url = textureMap.contains(albedoName) ? textureMap[albedoName].toUrl() : QUrl(); auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); - map->setTextureTransform(_albedoTransform); - // when reassigning the albedo texture we also check for the alpha channel used as opacity - map->setUseAlphaChannel(true); + if (map) { + map->setTextureTransform(_albedoTransform); + // when reassigning the albedo texture we also check for the alpha channel used as opacity + map->setUseAlphaChannel(true); + } setTextureMap(MapChannel::ALBEDO_MAP, map); } @@ -757,8 +765,10 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { if (!lightmapName.isEmpty()) { auto url = textureMap.contains(lightmapName) ? textureMap[lightmapName].toUrl() : QUrl(); auto map = fetchTextureMap(url, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); - map->setTextureTransform(_lightmapTransform); - map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); + if (map) { + map->setTextureTransform(_lightmapTransform); + map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); + } setTextureMap(MapChannel::LIGHTMAP_MAP, map); } } From 5d000495df53ce03224b9ea1efcd78c7e33bf967 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 8 Nov 2018 15:33:18 -0800 Subject: [PATCH 361/362] making requested changes --- .../src/model-networking/ModelCache.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index dd58b0e75e..c298cc5580 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -642,9 +642,9 @@ NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textur map->setUseAlphaChannel(true); } } - } - setTextureMap(MapChannel::ALBEDO_MAP, map); + setTextureMap(MapChannel::ALBEDO_MAP, map); + } } @@ -674,8 +674,8 @@ NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textur auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); if (map) { map->setTextureTransform(material.occlusionTexture.transform); + setTextureMap(MapChannel::OCCLUSION_MAP, map); } - setTextureMap(MapChannel::OCCLUSION_MAP, map); } if (!material.emissiveTexture.filename.isEmpty()) { @@ -695,8 +695,8 @@ NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textur _lightmapParams = material.lightmapParams; map->setTextureTransform(_lightmapTransform); map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); + setTextureMap(MapChannel::LIGHTMAP_MAP, map); } - setTextureMap(MapChannel::LIGHTMAP_MAP, map); } } @@ -719,8 +719,8 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { map->setTextureTransform(_albedoTransform); // when reassigning the albedo texture we also check for the alpha channel used as opacity map->setUseAlphaChannel(true); + setTextureMap(MapChannel::ALBEDO_MAP, map); } - setTextureMap(MapChannel::ALBEDO_MAP, map); } if (!normalName.isEmpty()) { @@ -768,8 +768,8 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { if (map) { map->setTextureTransform(_lightmapTransform); map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); + setTextureMap(MapChannel::LIGHTMAP_MAP, map); } - setTextureMap(MapChannel::LIGHTMAP_MAP, map); } } From f5b89ee7aa47770e45a2b7b387547d4500a0bdf4 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 8 Nov 2018 15:43:45 -0800 Subject: [PATCH 362/362] reverting previous change --- .../src/model-networking/ModelCache.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index c298cc5580..dd58b0e75e 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -642,9 +642,9 @@ NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textur map->setUseAlphaChannel(true); } } - - setTextureMap(MapChannel::ALBEDO_MAP, map); } + + setTextureMap(MapChannel::ALBEDO_MAP, map); } @@ -674,8 +674,8 @@ NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textur auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); if (map) { map->setTextureTransform(material.occlusionTexture.transform); - setTextureMap(MapChannel::OCCLUSION_MAP, map); } + setTextureMap(MapChannel::OCCLUSION_MAP, map); } if (!material.emissiveTexture.filename.isEmpty()) { @@ -695,8 +695,8 @@ NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textur _lightmapParams = material.lightmapParams; map->setTextureTransform(_lightmapTransform); map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); - setTextureMap(MapChannel::LIGHTMAP_MAP, map); } + setTextureMap(MapChannel::LIGHTMAP_MAP, map); } } @@ -719,8 +719,8 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { map->setTextureTransform(_albedoTransform); // when reassigning the albedo texture we also check for the alpha channel used as opacity map->setUseAlphaChannel(true); - setTextureMap(MapChannel::ALBEDO_MAP, map); } + setTextureMap(MapChannel::ALBEDO_MAP, map); } if (!normalName.isEmpty()) { @@ -768,8 +768,8 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { if (map) { map->setTextureTransform(_lightmapTransform); map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); - setTextureMap(MapChannel::LIGHTMAP_MAP, map); } + setTextureMap(MapChannel::LIGHTMAP_MAP, map); } }