diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index a549df5fb3..da60a07367 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -109,7 +109,7 @@ private: QHash _outgoingScriptAudioSequenceNumbers; AudioGate _audioGate; - bool _audioGateOpen { false }; + bool _audioGateOpen { true }; bool _isNoiseGateEnabled { false }; CodecPluginPointer _codec; diff --git a/cmake/macros/GenerateInstallers.cmake b/cmake/macros/GenerateInstallers.cmake index aa681e27b4..6c131168d5 100644 --- a/cmake/macros/GenerateInstallers.cmake +++ b/cmake/macros/GenerateInstallers.cmake @@ -27,21 +27,12 @@ macro(GENERATE_INSTALLERS) endif () set(CPACK_PACKAGE_INSTALL_DIRECTORY ${_DISPLAY_NAME}) + if (WIN32) - # include CMake module that will install compiler system libraries - # so that we have msvcr120 and msvcp120 installed with targets - set(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION ${INTERFACE_INSTALL_DIR}) - - # as long as we're including sixense plugin with installer - # we need re-distributables for VS 2011 as well - # this should be removed if/when sixense support is pulled - set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS - "${EXTERNALS_BINARY_DIR}/sixense/project/src/sixense/samples/win64/msvcr100.dll" - "${EXTERNALS_BINARY_DIR}/sixense/project/src/sixense/samples/win64/msvcp100.dll" - ) - + # Do not install the Visual Studio C runtime libraries. The installer will do this automatically + set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) + include(InstallRequiredSystemLibraries) - set(CPACK_NSIS_MUI_ICON "${HF_CMAKE_DIR}/installer/installer.ico") # install and reference the Add/Remove icon @@ -93,3 +84,4 @@ macro(GENERATE_INSTALLERS) include(CPack) endmacro() + diff --git a/cmake/macros/InstallBesideConsole.cmake b/cmake/macros/InstallBesideConsole.cmake index d5777fff12..3c991acf86 100644 --- a/cmake/macros/InstallBesideConsole.cmake +++ b/cmake/macros/InstallBesideConsole.cmake @@ -22,9 +22,12 @@ macro(install_beside_console) else () # setup install of executable and things copied by fixup/windeployqt install( - FILES "$/" + DIRECTORY "$/" DESTINATION ${COMPONENT_INSTALL_DIR} COMPONENT ${SERVER_COMPONENT} + PATTERN "*.pdb" EXCLUDE + PATTERN "*.lib" EXCLUDE + PATTERN "*.exp" EXCLUDE ) # on windows for PR and production builds, sign the executable diff --git a/cmake/templates/FixupBundlePostBuild.cmake.in b/cmake/templates/FixupBundlePostBuild.cmake.in index 57d1fd787f..d4726884c2 100644 --- a/cmake/templates/FixupBundlePostBuild.cmake.in +++ b/cmake/templates/FixupBundlePostBuild.cmake.in @@ -11,34 +11,28 @@ include(BundleUtilities) -# replace copy_resolved_item_into_bundle -# -# The official version of copy_resolved_item_into_bundle will print out a "warning:" when -# the resolved item matches the resolved embedded item. This not not really an issue that -# should rise to the level of a "warning" so we replace this message with a "status:" -# -function(copy_resolved_item_into_bundle resolved_item resolved_embedded_item) - if (WIN32) - # ignore case on Windows - string(TOLOWER "${resolved_item}" resolved_item_compare) - string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare) - else() - set(resolved_item_compare "${resolved_item}") - set(resolved_embedded_item_compare "${resolved_embedded_item}") +function(gp_resolved_file_type_override resolved_file type_var) + if( file MATCHES ".*VCRUNTIME140.*" ) + set(type "system" PARENT_SCOPE) endif() - - if ("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}") - # this is our only change from the original version - message(STATUS "status: resolved_item == resolved_embedded_item - not copying...") - else() - #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy ${resolved_item} ${resolved_embedded_item}") - execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}") - if(UNIX AND NOT APPLE) - file(RPATH_REMOVE FILE "${resolved_embedded_item}") - endif() + if( file MATCHES ".*concrt140.*" ) + set(type "system" PARENT_SCOPE) + endif() + if( file MATCHES ".*msvcp140.*" ) + set(type "system" PARENT_SCOPE) + endif() + if( file MATCHES ".*vcruntime140.*" ) + set(type "system" PARENT_SCOPE) + endif() + if( file MATCHES ".*api-ms-win-crt-conio.*" ) + set(type "system" PARENT_SCOPE) + endif() + if( file MATCHES ".*api-ms-win-core-winrt.*" ) + set(type "system" PARENT_SCOPE) endif() endfunction() + message(STATUS "FIXUP_LIBS for fixup_bundle called for bundle ${BUNDLE_EXECUTABLE} are @FIXUP_LIBS@") message(STATUS "Scanning for plugins from ${BUNDLE_PLUGIN_DIR}") @@ -52,3 +46,4 @@ endif() file(GLOB EXTRA_PLUGINS "${BUNDLE_PLUGIN_DIR}/*.${PLUGIN_EXTENSION}") fixup_bundle("${BUNDLE_EXECUTABLE}" "${EXTRA_PLUGINS}" "@FIXUP_LIBS@") + diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 5417220ef1..5af51ff8c9 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -853,6 +853,8 @@ Section "-Core installation" ; Rename the incorrectly cased Raleway font Rename "$INSTDIR\resources\qml\styles-uit\RalewaySemibold.qml" "$INSTDIR\resources\qml\styles-uit\RalewaySemiBold.qml" + ExecWait "$INSTDIR\vcredist_x64.exe /install /q /norestart" + ; Remove the Old Interface directory and vcredist_x64.exe (from installs prior to Server Console) RMDir /r "$INSTDIR\Interface" Delete "$INSTDIR\vcredist_x64.exe" diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index c97975e50a..81c8a44baf 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -309,9 +309,12 @@ else (APPLE) # setup install of executable and things copied by fixup/windeployqt install( - FILES "$/" + DIRECTORY "$/" DESTINATION ${INTERFACE_INSTALL_DIR} COMPONENT ${CLIENT_COMPONENT} + PATTERN "*.pdb" EXCLUDE + PATTERN "*.lib" EXCLUDE + PATTERN "*.exp" EXCLUDE ) set(SCRIPTS_INSTALL_DIR "${INTERFACE_INSTALL_DIR}") diff --git a/interface/resources/controllers/keyboardMouse.json b/interface/resources/controllers/keyboardMouse.json index c384817ff6..f377e02e5f 100644 --- a/interface/resources/controllers/keyboardMouse.json +++ b/interface/resources/controllers/keyboardMouse.json @@ -15,7 +15,7 @@ { "comment" : "Mouse turn need to be small continuous increments", "from": { "makeAxis" : [ [ "Keyboard.MouseMoveLeft" ], - [ "Keyboard.MouseMoveRight" ] + [ "Keyboard.MouseMoveRight" ] ] }, "when": [ "Application.InHMD", "Application.SnapTurn", "Keyboard.RightMouseButton" ], @@ -31,8 +31,8 @@ { "comment" : "Touchpad turn need to be small continuous increments, but without the RMB constraint", "from": { "makeAxis" : [ [ "Keyboard.TouchpadLeft" ], - [ "Keyboard.TouchpadRight" ] - ] + [ "Keyboard.TouchpadRight" ] + ] }, "when": [ "Application.InHMD", "Application.SnapTurn" ], "to": "Actions.StepYaw", diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 166f1a6869..0d5c095585 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -109,6 +109,23 @@ { "from": "Standard.Head", "to": "Actions.Head" }, { "from": "Standard.LeftArm", "to": "Actions.LeftArm" }, - { "from": "Standard.RightArm", "to": "Actions.RightArm" } + { "from": "Standard.RightArm", "to": "Actions.RightArm" }, + + { "from": "Standard.TrackedObject00", "to" : "Actions.TrackedObject00" }, + { "from": "Standard.TrackedObject01", "to" : "Actions.TrackedObject01" }, + { "from": "Standard.TrackedObject02", "to" : "Actions.TrackedObject02" }, + { "from": "Standard.TrackedObject03", "to" : "Actions.TrackedObject03" }, + { "from": "Standard.TrackedObject04", "to" : "Actions.TrackedObject04" }, + { "from": "Standard.TrackedObject05", "to" : "Actions.TrackedObject05" }, + { "from": "Standard.TrackedObject06", "to" : "Actions.TrackedObject06" }, + { "from": "Standard.TrackedObject07", "to" : "Actions.TrackedObject07" }, + { "from": "Standard.TrackedObject08", "to" : "Actions.TrackedObject08" }, + { "from": "Standard.TrackedObject09", "to" : "Actions.TrackedObject09" }, + { "from": "Standard.TrackedObject10", "to" : "Actions.TrackedObject10" }, + { "from": "Standard.TrackedObject11", "to" : "Actions.TrackedObject11" }, + { "from": "Standard.TrackedObject12", "to" : "Actions.TrackedObject12" }, + { "from": "Standard.TrackedObject13", "to" : "Actions.TrackedObject13" }, + { "from": "Standard.TrackedObject14", "to" : "Actions.TrackedObject14" }, + { "from": "Standard.TrackedObject15", "to" : "Actions.TrackedObject15" } ] } diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 73ab5cb2ae..02fc09c815 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -77,6 +77,23 @@ { "from": "Vive.Head", "to" : "Standard.Head"}, { "from": "Vive.RightArm", "to" : "Standard.RightArm" }, - { "from": "Vive.LeftArm", "to" : "Standard.LeftArm" } + { "from": "Vive.LeftArm", "to" : "Standard.LeftArm" }, + + { "from": "Vive.TrackedObject00", "to" : "Standard.TrackedObject00" }, + { "from": "Vive.TrackedObject01", "to" : "Standard.TrackedObject01" }, + { "from": "Vive.TrackedObject02", "to" : "Standard.TrackedObject02" }, + { "from": "Vive.TrackedObject03", "to" : "Standard.TrackedObject03" }, + { "from": "Vive.TrackedObject04", "to" : "Standard.TrackedObject04" }, + { "from": "Vive.TrackedObject05", "to" : "Standard.TrackedObject05" }, + { "from": "Vive.TrackedObject06", "to" : "Standard.TrackedObject06" }, + { "from": "Vive.TrackedObject07", "to" : "Standard.TrackedObject07" }, + { "from": "Vive.TrackedObject08", "to" : "Standard.TrackedObject08" }, + { "from": "Vive.TrackedObject09", "to" : "Standard.TrackedObject09" }, + { "from": "Vive.TrackedObject10", "to" : "Standard.TrackedObject10" }, + { "from": "Vive.TrackedObject11", "to" : "Standard.TrackedObject11" }, + { "from": "Vive.TrackedObject12", "to" : "Standard.TrackedObject12" }, + { "from": "Vive.TrackedObject13", "to" : "Standard.TrackedObject13" }, + { "from": "Vive.TrackedObject14", "to" : "Standard.TrackedObject14" }, + { "from": "Vive.TrackedObject15", "to" : "Standard.TrackedObject15" } ] } diff --git a/interface/resources/images/inspect-icon.png b/interface/resources/images/inspect-icon.png new file mode 100644 index 0000000000..9259de23e9 Binary files /dev/null and b/interface/resources/images/inspect-icon.png differ diff --git a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml index 2fd33e9cbc..9076cd6c48 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml @@ -114,6 +114,7 @@ Item { } function clearMenus() { + topMenu = null d.clear() } diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index faa4013bce..c7df6ac64f 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -202,5 +202,11 @@ Item { width: 480 height: 706 - function setShown(value) {} + function setShown(value) { + if (value === true) { + HMD.openTablet() + } else { + HMD.closeTablet() + } + } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ceded99f40..549e5338a0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -70,7 +70,7 @@ #include #include #include -#include +#include "ui/overlays/ContextOverlayInterface.h" #include #include #include @@ -595,7 +595,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -1324,12 +1324,16 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Keyboard focus handling for Web overlays. auto overlays = &(qApp->getOverlays()); - connect(overlays, &Overlays::mousePressOnOverlay, [=](OverlayID overlayID, const PointerEvent& event) { - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - setKeyboardFocusOverlay(overlayID); + connect(overlays, &Overlays::mousePressOnOverlay, [=](const OverlayID& overlayID, const PointerEvent& event) { + auto thisOverlay = std::dynamic_pointer_cast(overlays->getOverlay(overlayID)); + // Only Web overlays can have keyboard focus. + if (thisOverlay) { + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + setKeyboardFocusOverlay(overlayID); + } }); - connect(overlays, &Overlays::overlayDeleted, [=](OverlayID overlayID) { + connect(overlays, &Overlays::overlayDeleted, [=](const OverlayID& overlayID) { if (overlayID == _keyboardFocusedOverlay.get()) { setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID); } @@ -1344,6 +1348,21 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); }); + connect(overlays, + SIGNAL(mousePressOnOverlay(const OverlayID&, const PointerEvent&)), + DependencyManager::get().data(), + SLOT(contextOverlays_mousePressOnOverlay(const OverlayID&, const PointerEvent&))); + + connect(overlays, + SIGNAL(hoverEnterOverlay(const OverlayID&, const PointerEvent&)), + DependencyManager::get().data(), + SLOT(contextOverlays_hoverEnterOverlay(const OverlayID&, const PointerEvent&))); + + connect(overlays, + SIGNAL(hoverLeaveOverlay(const OverlayID&, const PointerEvent&)), + DependencyManager::get().data(), + SLOT(contextOverlays_hoverLeaveOverlay(const OverlayID&, const PointerEvent&))); + // Add periodic checks to send user activity data static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; static int NEARBY_AVATAR_RADIUS_METERS = 10; @@ -1470,7 +1489,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["atp_mapping_requests"] = atpMappingRequests; properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; - + QJsonObject bytesDownloaded; bytesDownloaded["atp"] = statTracker->getStat(STAT_ATP_RESOURCE_TOTAL_BYTES).toInt(); bytesDownloaded["http"] = statTracker->getStat(STAT_HTTP_RESOURCE_TOTAL_BYTES).toInt(); @@ -2131,7 +2150,7 @@ void Application::initializeUi() { surfaceContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); - surfaceContext->setContextProperty("HoverOverlay", DependencyManager::get().data()); + surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); @@ -2237,7 +2256,7 @@ void Application::paintGL() { QMutexLocker viewLocker(&_viewMutex); _viewFrustum.calculate(); } - renderArgs = RenderArgs(_gpuContext, getEntities(), lodManager->getOctreeSizeScale(), + renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); { @@ -2753,7 +2772,12 @@ bool Application::importSVOFromURL(const QString& urlString) { return true; } -bool _renderRequested { false }; +void Application::onPresent(quint32 frameCount) { + if (shouldPaint()) { + postEvent(this, new QEvent(static_cast(Idle)), Qt::HighEventPriority); + postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); + } +} bool Application::event(QEvent* event) { if (!Menu::getInstance()) { @@ -2769,23 +2793,9 @@ bool Application::event(QEvent* event) { // Explicit idle keeps the idle running at a lower interval, but without any rendering // see (windowMinimizedChanged) case Event::Idle: - { - float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed(); - _lastTimeUpdated.start(); - idle(nsecsElapsed); - } - return true; - - case Event::Present: - if (!_renderRequested) { - float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed(); - if (shouldPaint(nsecsElapsed)) { - _renderRequested = true; - _lastTimeUpdated.start(); - idle(nsecsElapsed); - postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); - } - } + idle(); + // Clear the event queue of pending idle calls + removePostedEvents(this, Idle); return true; case Event::Paint: @@ -2793,9 +2803,8 @@ bool Application::event(QEvent* event) { // or AvatarInputs will mysteriously move to the bottom-right AvatarInputs::getInstance()->update(); paintGL(); - // wait for the next present event before starting idle / paint again - removePostedEvents(this, Present); - _renderRequested = false; + // Clear the event queue of pending paint calls + removePostedEvents(this, Paint); return true; default: @@ -3614,7 +3623,7 @@ bool Application::acceptSnapshot(const QString& urlString) { static uint32_t _renderedFrameIndex { INVALID_FRAME }; -bool Application::shouldPaint(float nsecsElapsed) { +bool Application::shouldPaint() { if (_aboutToQuit) { return false; } @@ -3634,11 +3643,9 @@ bool Application::shouldPaint(float nsecsElapsed) { (float)paintDelaySamples / paintDelayUsecs << "us"; } #endif - - float msecondsSinceLastUpdate = nsecsElapsed / NSECS_PER_USEC / USECS_PER_MSEC; - + // Throttle if requested - if (displayPlugin->isThrottled() && (msecondsSinceLastUpdate < THROTTLED_SIM_FRAME_PERIOD_MS)) { + if (displayPlugin->isThrottled() && (_lastTimeUpdated.elapsed() < THROTTLED_SIM_FRAME_PERIOD_MS)) { return false; } @@ -3855,7 +3862,7 @@ void setupCpuMonitorThread() { #endif -void Application::idle(float nsecsElapsed) { +void Application::idle() { PerformanceTimer perfTimer("idle"); // Update the deadlock watchdog @@ -3912,7 +3919,8 @@ void Application::idle(float nsecsElapsed) { steamClient->runCallbacks(); } - float secondsSinceLastUpdate = nsecsElapsed / NSECS_PER_MSEC / MSECS_PER_SECOND; + float secondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_MSEC / MSECS_PER_SECOND; + _lastTimeUpdated.start(); // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. if (_keyboardDeviceHasFocus && offscreenUi && offscreenUi->getWindow()->activeFocusItem() != offscreenUi->getRootItem()) { @@ -4146,6 +4154,7 @@ void Application::loadSettings() { //DependencyManager::get()->setAutomaticLODAdjust(false); Menu::getInstance()->loadSettings(); + // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. auto pluginManager = PluginManager::getInstance(); auto plugins = pluginManager->getPreferredDisplayPlugins(); @@ -4159,24 +4168,44 @@ void Application::loadSettings() { break; } } - } else { + } + + Setting::Handle firstRun { Settings::firstRun, true }; + bool isFirstPerson = false; + if (firstRun.get()) { // If this is our first run, and no preferred devices were set, default to // an HMD device if available. - Setting::Handle firstRun { Settings::firstRun, true }; - if (firstRun.get()) { - auto displayPlugins = pluginManager->getDisplayPlugins(); - for (auto& plugin : displayPlugins) { - if (plugin->isHmd()) { - if (auto action = menu->getActionForOption(plugin->getName())) { - action->setChecked(true); - action->trigger(); - break; - } + auto displayPlugins = pluginManager->getDisplayPlugins(); + for (auto& plugin : displayPlugins) { + if (plugin->isHmd()) { + if (auto action = menu->getActionForOption(plugin->getName())) { + action->setChecked(true); + action->trigger(); + break; } } } + + isFirstPerson = (qApp->isHMDMode()); + } else { + // if this is not the first run, the camera will be initialized differently depending on user settings + + if (qApp->isHMDMode()) { + // if the HMD is active, use first-person camera, unless the appropriate setting is checked + isFirstPerson = menu->isOptionChecked(MenuOption::FirstPersonHMD); + } else { + // if HMD is not active, only use first person if the menu option is checked + isFirstPerson = menu->isOptionChecked(MenuOption::FirstPerson); + } } + // finish initializing the camera, based on everything we checked above. Third person camera will be used if no settings + // dictated that we should be in first person + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, isFirstPerson); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !isFirstPerson); + _myCamera.setMode((isFirstPerson) ? CAMERA_MODE_FIRST_PERSON : CAMERA_MODE_THIRD_PERSON); + cameraMenuChanged(); + auto inputs = pluginManager->getInputPlugins(); for (auto plugin : inputs) { if (!plugin->isActive()) { @@ -4225,7 +4254,6 @@ void Application::init() { DependencyManager::get()->init(); DependencyManager::get()->init(); - _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); _timerStart.start(); _lastTimeUpdated.start(); @@ -5397,6 +5425,17 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se } renderArgs->_debugFlags = renderDebugFlags; //ViveControllerManager::getInstance().updateRendering(renderArgs, _main3DScene, transaction); + + RenderArgs::OutlineFlags renderOutlineFlags = RenderArgs::RENDER_OUTLINE_NONE; + auto contextOverlayInterface = DependencyManager::get(); + if (contextOverlayInterface->getEnabled()) { + if (DependencyManager::get()->getIsInMarketplaceInspectionMode()) { + renderOutlineFlags = RenderArgs::RENDER_OUTLINE_MARKETPLACE_MODE; + } else { + renderOutlineFlags = RenderArgs::RENDER_OUTLINE_WIREFRAMES; + } + } + renderArgs->_outlineFlags = renderOutlineFlags; } } @@ -5860,7 +5899,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri auto entityScriptServerLog = DependencyManager::get(); scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data()); scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); - scriptEngine->registerGlobalObject("HoverOverlay", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("ContextOverlay", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine, OverlayIDtoScriptValue, OverlayIDfromScriptValue); @@ -7056,6 +7095,7 @@ void Application::updateDisplayMode() { auto oldDisplayPlugin = _displayPlugin; if (_displayPlugin) { + disconnect(_displayPluginPresentConnection); _displayPlugin->deactivate(); } @@ -7096,6 +7136,7 @@ void Application::updateDisplayMode() { _offscreenContext->makeCurrent(); getApplicationCompositor().setDisplayPlugin(newDisplayPlugin); _displayPlugin = newDisplayPlugin; + _displayPluginPresentConnection = connect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent); offscreenUi->getDesktop()->setProperty("repositionLocked", wasRepositionLocked); } diff --git a/interface/src/Application.h b/interface/src/Application.h index 123aa85e2e..300bd4ac02 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -129,8 +129,7 @@ public: virtual DisplayPluginPointer getActiveDisplayPlugin() const override; enum Event { - Present = DisplayPlugin::Present, - Paint, + Paint = QEvent::User + 1, Idle, Lambda }; @@ -409,6 +408,7 @@ private slots: void clearDomainOctreeDetails(); void clearDomainAvatars(); void onAboutToQuit(); + void onPresent(quint32 frameCount); void resettingDomain(); @@ -455,8 +455,8 @@ private: void cleanupBeforeQuit(); - bool shouldPaint(float nsecsElapsed); - void idle(float nsecsElapsed); + bool shouldPaint(); + void idle(); void update(float deltaTime); // Various helper functions called during update() @@ -518,6 +518,7 @@ private: OffscreenGLCanvas* _offscreenContext { nullptr }; DisplayPluginPointer _displayPlugin; + QMetaObject::Connection _displayPluginPresentConnection; mutable std::mutex _displayPluginLock; InputPluginList _activeInputPlugins; diff --git a/interface/src/scripting/AudioDevices.cpp b/interface/src/scripting/AudioDevices.cpp index d02f4d8fcf..f2e6dbf4d7 100644 --- a/interface/src/scripting/AudioDevices.cpp +++ b/interface/src/scripting/AudioDevices.cpp @@ -12,6 +12,7 @@ #include #include +#include #include "AudioDevices.h" diff --git a/interface/src/ui/JSConsole.cpp b/interface/src/ui/JSConsole.cpp index a62fb2270b..8c1ff87d13 100644 --- a/interface/src/ui/JSConsole.cpp +++ b/interface/src/ui/JSConsole.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "JSConsole.h" + #include #include #include @@ -20,7 +22,6 @@ #include #include "Application.h" -#include "JSConsole.h" #include "ScriptHighlighting.h" const int NO_CURRENT_HISTORY_COMMAND = -1; @@ -60,14 +61,12 @@ void _writeLines(const QString& filename, const QList& lines) { QTextStream(&file) << json; } -JSConsole::JSConsole(QWidget* parent, ScriptEngine* scriptEngine) : +JSConsole::JSConsole(QWidget* parent, const QSharedPointer& scriptEngine) : QWidget(parent), _ui(new Ui::Console), _currentCommandInHistory(NO_CURRENT_HISTORY_COMMAND), _savedHistoryFilename(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + HISTORY_FILENAME), - _commandHistory(_readLines(_savedHistoryFilename)), - _ownScriptEngine(scriptEngine == NULL), - _scriptEngine(NULL) { + _commandHistory(_readLines(_savedHistoryFilename)) { _ui->setupUi(this); _ui->promptTextEdit->setLineWrapMode(QTextEdit::NoWrap); _ui->promptTextEdit->setWordWrapMode(QTextOption::NoWrap); @@ -90,36 +89,37 @@ JSConsole::JSConsole(QWidget* parent, ScriptEngine* scriptEngine) : } JSConsole::~JSConsole() { - disconnect(_scriptEngine, SIGNAL(printedMessage(const QString&)), this, SLOT(handlePrint(const QString&))); - disconnect(_scriptEngine, SIGNAL(errorMessage(const QString&)), this, SLOT(handleError(const QString&))); - if (_ownScriptEngine) { - _scriptEngine->deleteLater(); + if (_scriptEngine) { + disconnect(_scriptEngine.data(), SIGNAL(printedMessage(const QString&)), this, SLOT(handlePrint(const QString&))); + disconnect(_scriptEngine.data(), SIGNAL(errorMessage(const QString&)), this, SLOT(handleError(const QString&))); + _scriptEngine.reset(); } delete _ui; } -void JSConsole::setScriptEngine(ScriptEngine* scriptEngine) { +void JSConsole::setScriptEngine(const QSharedPointer& scriptEngine) { if (_scriptEngine == scriptEngine && scriptEngine != NULL) { return; } if (_scriptEngine != NULL) { - disconnect(_scriptEngine, &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); - disconnect(_scriptEngine, &ScriptEngine::infoMessage, this, &JSConsole::handleInfo); - disconnect(_scriptEngine, &ScriptEngine::warningMessage, this, &JSConsole::handleWarning); - disconnect(_scriptEngine, &ScriptEngine::errorMessage, this, &JSConsole::handleError); - if (_ownScriptEngine) { - _scriptEngine->deleteLater(); - } + disconnect(_scriptEngine.data(), &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); + disconnect(_scriptEngine.data(), &ScriptEngine::infoMessage, this, &JSConsole::handleInfo); + disconnect(_scriptEngine.data(), &ScriptEngine::warningMessage, this, &JSConsole::handleWarning); + disconnect(_scriptEngine.data(), &ScriptEngine::errorMessage, this, &JSConsole::handleError); + _scriptEngine.reset(); } // if scriptEngine is NULL then create one and keep track of it using _ownScriptEngine - _ownScriptEngine = (scriptEngine == NULL); - _scriptEngine = _ownScriptEngine ? DependencyManager::get()->loadScript(_consoleFileName, false) : scriptEngine; + if (scriptEngine.isNull()) { + _scriptEngine = QSharedPointer(DependencyManager::get()->loadScript(_consoleFileName, false), &QObject::deleteLater); + } else { + _scriptEngine = scriptEngine; + } - connect(_scriptEngine, &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); - connect(_scriptEngine, &ScriptEngine::infoMessage, this, &JSConsole::handleInfo); - connect(_scriptEngine, &ScriptEngine::warningMessage, this, &JSConsole::handleWarning); - connect(_scriptEngine, &ScriptEngine::errorMessage, this, &JSConsole::handleError); + connect(_scriptEngine.data(), &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); + connect(_scriptEngine.data(), &ScriptEngine::infoMessage, this, &JSConsole::handleInfo); + connect(_scriptEngine.data(), &ScriptEngine::warningMessage, this, &JSConsole::handleWarning); + connect(_scriptEngine.data(), &ScriptEngine::errorMessage, this, &JSConsole::handleError); } void JSConsole::executeCommand(const QString& command) { @@ -135,19 +135,22 @@ void JSConsole::executeCommand(const QString& command) { appendMessage(">", "" + command.toHtmlEscaped() + ""); - QFuture future = QtConcurrent::run(this, &JSConsole::executeCommandInWatcher, command); + QWeakPointer weakScriptEngine = _scriptEngine; + auto consoleFileName = _consoleFileName; + QFuture future = QtConcurrent::run([weakScriptEngine, consoleFileName, command]()->QScriptValue{ + QScriptValue result; + auto scriptEngine = weakScriptEngine.lock(); + if (scriptEngine) { + BLOCKING_INVOKE_METHOD(scriptEngine.data(), "evaluate", + Q_RETURN_ARG(QScriptValue, result), + Q_ARG(const QString&, command), + Q_ARG(const QString&, consoleFileName)); + } + return result; + }); _executeWatcher.setFuture(future); } -QScriptValue JSConsole::executeCommandInWatcher(const QString& command) { - QScriptValue result; - BLOCKING_INVOKE_METHOD(_scriptEngine, "evaluate", - Q_RETURN_ARG(QScriptValue, result), - Q_ARG(const QString&, command), - Q_ARG(const QString&, _consoleFileName)); - return result; -} - void JSConsole::commandFinished() { QScriptValue result = _executeWatcher.result(); diff --git a/interface/src/ui/JSConsole.h b/interface/src/ui/JSConsole.h index 59280f65aa..5010b5b9ca 100644 --- a/interface/src/ui/JSConsole.h +++ b/interface/src/ui/JSConsole.h @@ -17,6 +17,7 @@ #include #include #include +#include #include "ui_console.h" #include "ScriptEngine.h" @@ -29,10 +30,10 @@ const int CONSOLE_HEIGHT = 200; class JSConsole : public QWidget { Q_OBJECT public: - JSConsole(QWidget* parent, ScriptEngine* scriptEngine = NULL); + JSConsole(QWidget* parent, const QSharedPointer& scriptEngine = QSharedPointer()); ~JSConsole(); - void setScriptEngine(ScriptEngine* scriptEngine = NULL); + void setScriptEngine(const QSharedPointer& scriptEngine = QSharedPointer()); void clear(); public slots: @@ -58,17 +59,14 @@ private: void setToNextCommandInHistory(); void setToPreviousCommandInHistory(); void resetCurrentCommandHistory(); - QScriptValue executeCommandInWatcher(const QString& command); QFutureWatcher _executeWatcher; Ui::Console* _ui; int _currentCommandInHistory; QString _savedHistoryFilename; QList _commandHistory; - // Keeps track if the script engine is created inside the JSConsole - bool _ownScriptEngine; QString _rootCommand; - ScriptEngine* _scriptEngine; + QSharedPointer _scriptEngine; static const QString _consoleFileName; }; diff --git a/interface/src/ui/ResourceImageItem.cpp b/interface/src/ui/ResourceImageItem.cpp index 7b9592fa4c..5b7c1896fe 100644 --- a/interface/src/ui/ResourceImageItem.cpp +++ b/interface/src/ui/ResourceImageItem.cpp @@ -8,7 +8,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -//#include "Application.h" #include "ResourceImageItem.h" #include @@ -16,6 +15,8 @@ #include #include +#include + ResourceImageItem::ResourceImageItem() : QQuickFramebufferObject() { auto textureCache = DependencyManager::get(); connect(textureCache.data(), SIGNAL(spectatorCameraFramebufferReset()), this, SLOT(update())); diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 70767b007d..3c00be8358 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -13,8 +13,9 @@ #include #include #include +#include -#include +#include #include "SnapshotAnimated.h" QTimer* SnapshotAnimated::snapshotAnimatedTimer = NULL; diff --git a/interface/src/ui/TestingDialog.cpp b/interface/src/ui/TestingDialog.cpp index f55eb63a5b..bba14cd5d7 100644 --- a/interface/src/ui/TestingDialog.cpp +++ b/interface/src/ui/TestingDialog.cpp @@ -24,12 +24,12 @@ TestingDialog::TestingDialog(QWidget* parent) : _console->setFixedHeight(TESTING_CONSOLE_HEIGHT); auto _engines = DependencyManager::get(); - _engine = _engines->loadScript(qApp->applicationDirPath() + testRunnerRelativePath); + _engine.reset(_engines->loadScript(qApp->applicationDirPath() + testRunnerRelativePath)); _console->setScriptEngine(_engine); - connect(_engine, &ScriptEngine::finished, this, &TestingDialog::onTestingFinished); + connect(_engine.data(), &ScriptEngine::finished, this, &TestingDialog::onTestingFinished); } void TestingDialog::onTestingFinished(const QString& scriptPath) { - _engine = nullptr; - _console->setScriptEngine(nullptr); + _engine.reset(); + _console->setScriptEngine(); } diff --git a/interface/src/ui/TestingDialog.h b/interface/src/ui/TestingDialog.h index b90b8f2e99..055e43eaf7 100644 --- a/interface/src/ui/TestingDialog.h +++ b/interface/src/ui/TestingDialog.h @@ -29,7 +29,7 @@ public: private: std::unique_ptr _console; - ScriptEngine* _engine; + QSharedPointer _engine; }; #endif diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp new file mode 100644 index 0000000000..e406d139d0 --- /dev/null +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -0,0 +1,271 @@ +// +// ContextOverlayInterface.cpp +// interface/src/ui/overlays +// +// Created by Zach Fox on 2017-07-14. +// 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 "ContextOverlayInterface.h" +#include "Application.h" + +#include + +static const float CONTEXT_OVERLAY_TABLET_OFFSET = 30.0f; // Degrees +static const float CONTEXT_OVERLAY_TABLET_ORIENTATION = 210.0f; // Degrees +static const float CONTEXT_OVERLAY_TABLET_DISTANCE = 0.65F; // Meters +ContextOverlayInterface::ContextOverlayInterface() { + // "context_overlay" debug log category disabled by default. + // Create your own "qtlogging.ini" file and set your "QT_LOGGING_CONF" environment variable + // if you'd like to enable/disable certain categories. + // More details: http://doc.qt.io/qt-5/qloggingcategory.html#configuring-categories + QLoggingCategory::setFilterRules(QStringLiteral("hifi.context_overlay.debug=false")); + + _entityScriptingInterface = DependencyManager::get(); + _hmdScriptingInterface = DependencyManager::get(); + _tabletScriptingInterface = DependencyManager::get(); + + _entityPropertyFlags += PROP_POSITION; + _entityPropertyFlags += PROP_ROTATION; + _entityPropertyFlags += PROP_MARKETPLACE_ID; + _entityPropertyFlags += PROP_DIMENSIONS; + _entityPropertyFlags += PROP_REGISTRATION_POINT; + + // initially, set _enabled to match the switch. Later we enable/disable via the getter/setters + // if we are in edit or pal (for instance). Note this is temporary, as we expect to enable this all + // the time after getting edge highlighting, etc... + _enabled = _settingSwitch.get(); + + auto entityTreeRenderer = DependencyManager::get().data(); + connect(entityTreeRenderer, SIGNAL(mousePressOnEntity(const EntityItemID&, const PointerEvent&)), this, SLOT(createOrDestroyContextOverlay(const EntityItemID&, const PointerEvent&))); + connect(entityTreeRenderer, SIGNAL(hoverEnterEntity(const EntityItemID&, const PointerEvent&)), this, SLOT(contextOverlays_hoverEnterEntity(const EntityItemID&, const PointerEvent&))); + connect(entityTreeRenderer, SIGNAL(hoverLeaveEntity(const EntityItemID&, const PointerEvent&)), this, SLOT(contextOverlays_hoverLeaveEntity(const EntityItemID&, const PointerEvent&))); + connect(_tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"), &TabletProxy::tabletShownChanged, this, [&]() { + if (_contextOverlayJustClicked && _hmdScriptingInterface->isMounted()) { + QUuid tabletFrameID = _hmdScriptingInterface->getCurrentTabletFrameID(); + auto myAvatar = DependencyManager::get()->getMyAvatar(); + glm::quat cameraOrientation = qApp->getCamera().getOrientation(); + QVariantMap props; + props.insert("position", vec3toVariant(myAvatar->getEyePosition() + glm::quat(glm::radians(glm::vec3(0.0f, CONTEXT_OVERLAY_TABLET_OFFSET, 0.0f))) * (CONTEXT_OVERLAY_TABLET_DISTANCE * (cameraOrientation * Vectors::FRONT)))); + props.insert("orientation", quatToVariant(cameraOrientation * glm::quat(glm::radians(glm::vec3(0.0f, CONTEXT_OVERLAY_TABLET_ORIENTATION, 0.0f))))); + qApp->getOverlays().editOverlay(tabletFrameID, props); + _contextOverlayJustClicked = false; + } + }); +} + +static const uint32_t LEFT_HAND_HW_ID = 1; +static const xColor CONTEXT_OVERLAY_COLOR = { 255, 255, 255 }; +static const float CONTEXT_OVERLAY_INSIDE_DISTANCE = 1.0f; // in meters +static const float CONTEXT_OVERLAY_CLOSE_DISTANCE = 1.5f; // in meters +static const float CONTEXT_OVERLAY_CLOSE_SIZE = 0.12f; // in meters, same x and y dims +static const float CONTEXT_OVERLAY_FAR_SIZE = 0.08f; // in meters, same x and y dims +static const float CONTEXT_OVERLAY_CLOSE_OFFSET_ANGLE = 20.0f; +static const float CONTEXT_OVERLAY_UNHOVERED_ALPHA = 0.85f; +static const float CONTEXT_OVERLAY_HOVERED_ALPHA = 1.0f; +static const float CONTEXT_OVERLAY_UNHOVERED_PULSEMIN = 0.6f; +static const float CONTEXT_OVERLAY_UNHOVERED_PULSEMAX = 1.0f; +static const float CONTEXT_OVERLAY_UNHOVERED_PULSEPERIOD = 1.0f; +static const float CONTEXT_OVERLAY_UNHOVERED_COLORPULSE = 1.0f; +static const float CONTEXT_OVERLAY_FAR_OFFSET = 0.1f; + +void ContextOverlayInterface::setEnabled(bool enabled) { + // only enable/disable if the setting in 'on'. If it is 'off', + // make sure _enabled is always false. + if (_settingSwitch.get()) { + _enabled = enabled; + } else { + _enabled = false; + } +} + +bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { + if (_enabled && event.getButton() == PointerEvent::SecondaryButton) { + if (contextOverlayFilterPassed(entityItemID)) { + qCDebug(context_overlay) << "Creating Context Overlay on top of entity with ID: " << entityItemID; + + // Add all necessary variables to the stack + EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(entityItemID, _entityPropertyFlags); + glm::vec3 cameraPosition = qApp->getCamera().getPosition(); + float distanceFromCameraToEntity = glm::distance(entityProperties.getPosition(), cameraPosition); + glm::vec3 entityDimensions = entityProperties.getDimensions(); + glm::vec3 entityPosition = entityProperties.getPosition(); + glm::vec3 contextOverlayPosition = entityProperties.getPosition(); + glm::vec2 contextOverlayDimensions; + + // Update the position of the overlay if the registration point of the entity + // isn't default + if (entityProperties.getRegistrationPoint() != glm::vec3(0.5f)) { + glm::vec3 adjustPos = entityProperties.getRegistrationPoint() - glm::vec3(0.5f); + entityPosition = entityPosition - (entityProperties.getRotation() * (adjustPos * entityProperties.getDimensions())); + } + + enableEntityHighlight(entityItemID); + + AABox boundingBox = AABox(entityPosition - (entityDimensions / 2.0f), entityDimensions * 2.0f); + + // Update the cached Entity Marketplace ID + _entityMarketplaceID = entityProperties.getMarketplaceID(); + + + if (!_currentEntityWithContextOverlay.isNull() && _currentEntityWithContextOverlay != entityItemID) { + disableEntityHighlight(_currentEntityWithContextOverlay); + } + + // Update the cached "Current Entity with Context Overlay" variable + setCurrentEntityWithContextOverlay(entityItemID); + + // Here, we determine the position and dimensions of the Context Overlay. + if (boundingBox.contains(cameraPosition)) { + // If the camera is inside the box... + // ...position the Context Overlay 1 meter in front of the camera. + contextOverlayPosition = cameraPosition + CONTEXT_OVERLAY_INSIDE_DISTANCE * (qApp->getCamera().getOrientation() * Vectors::FRONT); + contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_CLOSE_SIZE, CONTEXT_OVERLAY_CLOSE_SIZE) * glm::distance(contextOverlayPosition, cameraPosition); + } else if (distanceFromCameraToEntity < CONTEXT_OVERLAY_CLOSE_DISTANCE) { + // Else if the entity is too close to the camera... + // ...rotate the Context Overlay to the right of the entity. + // This makes it easy to inspect things you're holding. + float offsetAngle = -CONTEXT_OVERLAY_CLOSE_OFFSET_ANGLE; + if (event.getID() == LEFT_HAND_HW_ID) { + offsetAngle *= -1; + } + contextOverlayPosition = (glm::quat(glm::radians(glm::vec3(0.0f, offsetAngle, 0.0f))) * (entityPosition - cameraPosition)) + cameraPosition; + contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_CLOSE_SIZE, CONTEXT_OVERLAY_CLOSE_SIZE) * glm::distance(contextOverlayPosition, cameraPosition); + } else { + // Else, place the Context Overlay some offset away from the entity's bounding + // box in the direction of the camera. + glm::vec3 direction = glm::normalize(entityPosition - cameraPosition); + float distance; + BoxFace face; + glm::vec3 normal; + boundingBox.findRayIntersection(cameraPosition, direction, distance, face, normal); + contextOverlayPosition = (cameraPosition + direction * distance) - direction * CONTEXT_OVERLAY_FAR_OFFSET; + contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_FAR_SIZE, CONTEXT_OVERLAY_FAR_SIZE) * glm::distance(contextOverlayPosition, cameraPosition); + } + + // Finally, setup and draw the Context Overlay + if (_contextOverlayID == UNKNOWN_OVERLAY_ID || !qApp->getOverlays().isAddedOverlay(_contextOverlayID)) { + _contextOverlay = std::make_shared(); + _contextOverlay->setAlpha(CONTEXT_OVERLAY_UNHOVERED_ALPHA); + _contextOverlay->setPulseMin(CONTEXT_OVERLAY_UNHOVERED_PULSEMIN); + _contextOverlay->setPulseMax(CONTEXT_OVERLAY_UNHOVERED_PULSEMAX); + _contextOverlay->setColorPulse(CONTEXT_OVERLAY_UNHOVERED_COLORPULSE); + _contextOverlay->setIgnoreRayIntersection(false); + _contextOverlay->setDrawInFront(true); + _contextOverlay->setURL(PathUtils::resourcesPath() + "images/inspect-icon.png"); + _contextOverlay->setIsFacingAvatar(true); + _contextOverlayID = qApp->getOverlays().addOverlay(_contextOverlay); + } + _contextOverlay->setPosition(contextOverlayPosition); + _contextOverlay->setDimensions(contextOverlayDimensions); + _contextOverlay->setRotation(entityProperties.getRotation()); + _contextOverlay->setVisible(true); + + return true; + } + } else { + if (!_currentEntityWithContextOverlay.isNull()) { + return destroyContextOverlay(_currentEntityWithContextOverlay, event); + } + return false; + } + return false; +} + +bool ContextOverlayInterface::contextOverlayFilterPassed(const EntityItemID& entityItemID) { + EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(entityItemID, _entityPropertyFlags); + return (entityProperties.getMarketplaceID().length() != 0); +} + +bool ContextOverlayInterface::destroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { + if (_contextOverlayID != UNKNOWN_OVERLAY_ID) { + qCDebug(context_overlay) << "Destroying Context Overlay on top of entity with ID: " << entityItemID; + disableEntityHighlight(entityItemID); + setCurrentEntityWithContextOverlay(QUuid()); + _entityMarketplaceID.clear(); + // Destroy the Context Overlay + qApp->getOverlays().deleteOverlay(_contextOverlayID); + _contextOverlay = NULL; + _contextOverlayID = UNKNOWN_OVERLAY_ID; + return true; + } + return false; +} + +bool ContextOverlayInterface::destroyContextOverlay(const EntityItemID& entityItemID) { + return ContextOverlayInterface::destroyContextOverlay(entityItemID, PointerEvent()); +} + +void ContextOverlayInterface::contextOverlays_mousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { + if (overlayID == _contextOverlayID && event.getButton() == PointerEvent::PrimaryButton) { + qCDebug(context_overlay) << "Clicked Context Overlay. Entity ID:" << _currentEntityWithContextOverlay << "Overlay ID:" << overlayID; + openMarketplace(); + destroyContextOverlay(_currentEntityWithContextOverlay, PointerEvent()); + _contextOverlayJustClicked = true; + } +} + +void ContextOverlayInterface::contextOverlays_hoverEnterOverlay(const OverlayID& overlayID, const PointerEvent& event) { + if (_contextOverlayID != UNKNOWN_OVERLAY_ID && _contextOverlay) { + qCDebug(context_overlay) << "Started hovering over Context Overlay. Overlay ID:" << overlayID; + _contextOverlay->setColor(CONTEXT_OVERLAY_COLOR); + _contextOverlay->setColorPulse(0.0f); // pulse off + _contextOverlay->setPulsePeriod(0.0f); // pulse off + _contextOverlay->setAlpha(CONTEXT_OVERLAY_HOVERED_ALPHA); + } +} + +void ContextOverlayInterface::contextOverlays_hoverLeaveOverlay(const OverlayID& overlayID, const PointerEvent& event) { + if (_contextOverlayID != UNKNOWN_OVERLAY_ID && _contextOverlay) { + qCDebug(context_overlay) << "Stopped hovering over Context Overlay. Overlay ID:" << overlayID; + _contextOverlay->setColor(CONTEXT_OVERLAY_COLOR); + _contextOverlay->setColorPulse(CONTEXT_OVERLAY_UNHOVERED_COLORPULSE); + _contextOverlay->setPulsePeriod(CONTEXT_OVERLAY_UNHOVERED_PULSEPERIOD); + _contextOverlay->setAlpha(CONTEXT_OVERLAY_UNHOVERED_ALPHA); + } +} + +void ContextOverlayInterface::contextOverlays_hoverEnterEntity(const EntityItemID& entityID, const PointerEvent& event) { + if (contextOverlayFilterPassed(entityID)) { + enableEntityHighlight(entityID); + } +} + +void ContextOverlayInterface::contextOverlays_hoverLeaveEntity(const EntityItemID& entityID, const PointerEvent& event) { + if (_currentEntityWithContextOverlay != entityID) { + disableEntityHighlight(entityID); + } +} + +static const QString MARKETPLACE_BASE_URL = "https://metaverse.highfidelity.com/marketplace/items/"; + +void ContextOverlayInterface::openMarketplace() { + // lets open the tablet and go to the current item in + // the marketplace (if the current entity has a + // marketplaceID) + if (!_currentEntityWithContextOverlay.isNull() && _entityMarketplaceID.length() > 0) { + auto tablet = dynamic_cast(_tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + // construct the url to the marketplace item + QString url = MARKETPLACE_BASE_URL + _entityMarketplaceID; + tablet->gotoWebScreen(url); + _hmdScriptingInterface->openTablet(); + _isInMarketplaceInspectionMode = true; + } +} + +void ContextOverlayInterface::enableEntityHighlight(const EntityItemID& entityItemID) { + if (!qApp->getEntities()->getTree()->findEntityByEntityItemID(entityItemID)->getShouldHighlight()) { + qCDebug(context_overlay) << "Setting 'shouldHighlight' to 'true' for Entity ID:" << entityItemID; + qApp->getEntities()->getTree()->findEntityByEntityItemID(entityItemID)->setShouldHighlight(true); + } +} + +void ContextOverlayInterface::disableEntityHighlight(const EntityItemID& entityItemID) { + if (qApp->getEntities()->getTree()->findEntityByEntityItemID(entityItemID)->getShouldHighlight()) { + qCDebug(context_overlay) << "Setting 'shouldHighlight' to 'false' for Entity ID:" << entityItemID; + qApp->getEntities()->getTree()->findEntityByEntityItemID(entityItemID)->setShouldHighlight(false); + } +} diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h new file mode 100644 index 0000000000..ba9cb68575 --- /dev/null +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -0,0 +1,85 @@ +// +// ContextOverlayInterface.h +// interface/src/ui/overlays +// +// Created by Zach Fox on 2017-07-14. +// 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 +// + +#pragma once +#ifndef hifi_ContextOverlayInterface_h +#define hifi_ContextOverlayInterface_h + +#include +#include + +#include +#include +#include +#include "avatar/AvatarManager.h" + +#include "EntityScriptingInterface.h" +#include "ui/overlays/Image3DOverlay.h" +#include "ui/overlays/Overlays.h" +#include "scripting/HMDScriptingInterface.h" + +#include "EntityTree.h" +#include "ContextOverlayLogging.h" + +/**jsdoc +* @namespace ContextOverlay +*/ +class ContextOverlayInterface : public QObject, public Dependency { + Q_OBJECT + + Q_PROPERTY(QUuid entityWithContextOverlay READ getCurrentEntityWithContextOverlay WRITE setCurrentEntityWithContextOverlay) + Q_PROPERTY(bool enabled READ getEnabled WRITE setEnabled) + Q_PROPERTY(bool isInMarketplaceInspectionMode READ getIsInMarketplaceInspectionMode WRITE setIsInMarketplaceInspectionMode) + QSharedPointer _entityScriptingInterface; + EntityPropertyFlags _entityPropertyFlags; + QSharedPointer _hmdScriptingInterface; + QSharedPointer _tabletScriptingInterface; + OverlayID _contextOverlayID { UNKNOWN_OVERLAY_ID }; + std::shared_ptr _contextOverlay { nullptr }; +public: + ContextOverlayInterface(); + + Q_INVOKABLE QUuid getCurrentEntityWithContextOverlay() { return _currentEntityWithContextOverlay; } + void setCurrentEntityWithContextOverlay(const QUuid& entityID) { _currentEntityWithContextOverlay = entityID; } + void setEnabled(bool enabled); + bool getEnabled() { return _enabled; } + bool getIsInMarketplaceInspectionMode() { return _isInMarketplaceInspectionMode; } + void setIsInMarketplaceInspectionMode(bool mode) { _isInMarketplaceInspectionMode = mode; } + +public slots: + bool createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event); + bool destroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event); + bool destroyContextOverlay(const EntityItemID& entityItemID); + void contextOverlays_mousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event); + void contextOverlays_hoverEnterOverlay(const OverlayID& overlayID, const PointerEvent& event); + void contextOverlays_hoverLeaveOverlay(const OverlayID& overlayID, const PointerEvent& event); + void contextOverlays_hoverEnterEntity(const EntityItemID& entityID, const PointerEvent& event); + void contextOverlays_hoverLeaveEntity(const EntityItemID& entityID, const PointerEvent& event); + bool contextOverlayFilterPassed(const EntityItemID& entityItemID); + +private: + bool _verboseLogging { true }; + bool _enabled { true }; + QUuid _currentEntityWithContextOverlay{}; + QString _entityMarketplaceID; + bool _contextOverlayJustClicked { false }; + + bool _isInMarketplaceInspectionMode { false }; + + Setting::Handle _settingSwitch { "inspectionMode", false }; + + void openMarketplace(); + void enableEntityHighlight(const EntityItemID& entityItemID); + void disableEntityHighlight(const EntityItemID& entityItemID); + +}; + +#endif // hifi_ContextOverlayInterface_h diff --git a/libraries/entities/src/HoverOverlayLogging.cpp b/interface/src/ui/overlays/ContextOverlayLogging.cpp similarity index 60% rename from libraries/entities/src/HoverOverlayLogging.cpp rename to interface/src/ui/overlays/ContextOverlayLogging.cpp index 99a2dff782..c2c3fb7734 100644 --- a/libraries/entities/src/HoverOverlayLogging.cpp +++ b/interface/src/ui/overlays/ContextOverlayLogging.cpp @@ -1,6 +1,6 @@ // -// HoverOverlayLogging.cpp -// libraries/entities/src +// ContextOverlayLogging.cpp +// interface/src/ui/overlays // // Created by Zach Fox on 2017-07-17 // Copyright 2017 High Fidelity, Inc. @@ -9,6 +9,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "HoverOverlayLogging.h" +#include "ContextOverlayLogging.h" -Q_LOGGING_CATEGORY(hover_overlay, "hifi.hover_overlay") +Q_LOGGING_CATEGORY(context_overlay, "hifi.context_overlay") diff --git a/libraries/entities/src/HoverOverlayLogging.h b/interface/src/ui/overlays/ContextOverlayLogging.h similarity index 56% rename from libraries/entities/src/HoverOverlayLogging.h rename to interface/src/ui/overlays/ContextOverlayLogging.h index f0a024bba9..182ebc1425 100644 --- a/libraries/entities/src/HoverOverlayLogging.h +++ b/interface/src/ui/overlays/ContextOverlayLogging.h @@ -1,6 +1,6 @@ // -// HoverOverlayLogging.h -// libraries/entities/src +// ContextOverlayLogging.h +// interface/src/ui/overlays // // Created by Zach Fox on 2017-07-17 // Copyright 2017 High Fidelity, Inc. @@ -10,11 +10,11 @@ // #pragma once -#ifndef hifi_HoverOverlayLogging_h -#define hifi_HoverOverlayLogging_h +#ifndef hifi_ContextOverlayLogging_h +#define hifi_ContextOverlayLogging_h #include -Q_DECLARE_LOGGING_CATEGORY(hover_overlay) +Q_DECLARE_LOGGING_CATEGORY(context_overlay) -#endif // hifi_HoverOverlayLogging_h +#endif // hifi_ContextOverlayLogging_h diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index b650da3522..675dff7e93 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -20,7 +20,7 @@ Overlay::Overlay() : _renderItemID(render::Item::INVALID_ITEM_ID), _isLoaded(true), _alpha(DEFAULT_ALPHA), - _pulse(0.0f), + _pulse(1.0f), _pulseMax(0.0f), _pulseMin(0.0f), _pulsePeriod(1.0f), diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 7fb4722c7d..b6c7dcbd46 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -116,7 +116,7 @@ void Overlays::renderHUD(RenderArgs* renderArgs) { QMutexLocker locker(&_mutex); foreach(Overlay::Pointer thisOverlay, _overlaysHUD) { - + // Reset all batch pipeline settings between overlay geometryCache->useSimpleDrawPipeline(batch); batch.setResourceTexture(0, textureCache->getWhiteTexture()); // FIXME - do we really need to do this?? @@ -136,7 +136,7 @@ void Overlays::enable() { _enabled = true; } -// Note, can't be invoked by scripts, but can be called by the InterfaceParentFinder +// Note, can't be invoked by scripts, but can be called by the InterfaceParentFinder // class on packet processing threads Overlay::Pointer Overlays::getOverlay(OverlayID id) const { QMutexLocker locker(&_mutex); @@ -244,8 +244,8 @@ OverlayID Overlays::cloneOverlay(OverlayID id) { bool Overlays::editOverlay(OverlayID id, const QVariant& properties) { if (QThread::currentThread() != thread()) { - // NOTE editOverlay can be called very frequently in scripts and can't afford to - // block waiting on the main thread. Additionally, no script actually + // NOTE editOverlay can be called very frequently in scripts and can't afford to + // block waiting on the main thread. Additionally, no script actually // examines the return value and does something useful with it, so use a non-blocking // invoke and just always return true QMetaObject::invokeMethod(this, "editOverlay", Q_ARG(OverlayID, id), Q_ARG(QVariant, properties)); @@ -705,27 +705,27 @@ bool Overlays::isAddedOverlay(OverlayID id) { return _overlaysHUD.contains(id) || _overlaysWorld.contains(id); } -void Overlays::sendMousePressOnOverlay(OverlayID overlayID, const PointerEvent& event) { +void Overlays::sendMousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { QMetaObject::invokeMethod(this, "mousePressOnOverlay", Q_ARG(OverlayID, overlayID), Q_ARG(PointerEvent, event)); } -void Overlays::sendMouseReleaseOnOverlay(OverlayID overlayID, const PointerEvent& event) { +void Overlays::sendMouseReleaseOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { QMetaObject::invokeMethod(this, "mouseReleaseOnOverlay", Q_ARG(OverlayID, overlayID), Q_ARG(PointerEvent, event)); } -void Overlays::sendMouseMoveOnOverlay(OverlayID overlayID, const PointerEvent& event) { +void Overlays::sendMouseMoveOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { QMetaObject::invokeMethod(this, "mouseMoveOnOverlay", Q_ARG(OverlayID, overlayID), Q_ARG(PointerEvent, event)); } -void Overlays::sendHoverEnterOverlay(OverlayID id, PointerEvent event) { +void Overlays::sendHoverEnterOverlay(const OverlayID& id, const PointerEvent& event) { QMetaObject::invokeMethod(this, "hoverEnterOverlay", Q_ARG(OverlayID, id), Q_ARG(PointerEvent, event)); } -void Overlays::sendHoverOverOverlay(OverlayID id, PointerEvent event) { +void Overlays::sendHoverOverOverlay(const OverlayID& id, const PointerEvent& event) { QMetaObject::invokeMethod(this, "hoverOverOverlay", Q_ARG(OverlayID, id), Q_ARG(PointerEvent, event)); } -void Overlays::sendHoverLeaveOverlay(OverlayID id, PointerEvent event) { +void Overlays::sendHoverLeaveOverlay(const OverlayID& id, const PointerEvent& event) { QMetaObject::invokeMethod(this, "hoverLeaveOverlay", Q_ARG(OverlayID, id), Q_ARG(PointerEvent, event)); } @@ -775,7 +775,7 @@ float Overlays::height() { static const uint32_t MOUSE_POINTER_ID = 0; -static glm::vec2 projectOntoOverlayXYPlane(glm::vec3 position, glm::quat rotation, glm::vec2 dimensions, const PickRay& pickRay, +static glm::vec2 projectOntoOverlayXYPlane(glm::vec3 position, glm::quat rotation, glm::vec2 dimensions, const PickRay& pickRay, const RayToOverlayIntersectionResult& rayPickResult) { // Project the intersection point onto the local xy plane of the overlay. @@ -818,15 +818,20 @@ static PointerEvent::Button toPointerButton(const QMouseEvent& event) { } } -PointerEvent Overlays::calculatePointerEvent(Overlay::Pointer overlay, PickRay ray, +PointerEvent Overlays::calculateOverlayPointerEvent(OverlayID overlayID, PickRay ray, RayToOverlayIntersectionResult rayPickResult, QMouseEvent* event, PointerEvent::EventType eventType) { + auto overlay = std::dynamic_pointer_cast(getOverlay(overlayID)); + if (getOverlayType(overlayID) == "web3d") { + overlay = std::dynamic_pointer_cast(getOverlay(overlayID)); + } + if (!overlay) { + return PointerEvent(); + } + glm::vec3 position = overlay->getPosition(); + glm::quat rotation = overlay->getRotation(); + glm::vec2 dimensions = overlay->getSize(); - auto thisOverlay = std::dynamic_pointer_cast(overlay); - - auto position = thisOverlay->getPosition(); - auto rotation = thisOverlay->getRotation(); - auto dimensions = thisOverlay->getSize(); glm::vec2 pos2D = projectOntoOverlayXYPlane(position, rotation, dimensions, ray, rayPickResult); @@ -874,13 +879,9 @@ bool Overlays::mousePressEvent(QMouseEvent* event) { if (rayPickResult.intersects) { _currentClickingOnOverlayID = rayPickResult.overlayID; - // Only Web overlays can have focus. - auto thisOverlay = std::dynamic_pointer_cast(getOverlay(_currentClickingOnOverlayID)); - if (thisOverlay) { - auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Press); - emit mousePressOnOverlay(_currentClickingOnOverlayID, pointerEvent); - return true; - } + PointerEvent pointerEvent = calculateOverlayPointerEvent(_currentClickingOnOverlayID, ray, rayPickResult, event, PointerEvent::Press); + emit mousePressOnOverlay(_currentClickingOnOverlayID, pointerEvent); + return true; } emit mousePressOffOverlay(); return false; @@ -894,13 +895,9 @@ bool Overlays::mouseDoublePressEvent(QMouseEvent* event) { if (rayPickResult.intersects) { _currentClickingOnOverlayID = rayPickResult.overlayID; - // Only Web overlays can have focus. - auto thisOverlay = std::dynamic_pointer_cast(getOverlay(_currentClickingOnOverlayID)); - if (thisOverlay) { - auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Press); - emit mouseDoublePressOnOverlay(_currentClickingOnOverlayID, pointerEvent); - return true; - } + auto pointerEvent = calculateOverlayPointerEvent(_currentClickingOnOverlayID, ray, rayPickResult, event, PointerEvent::Press); + emit mouseDoublePressOnOverlay(_currentClickingOnOverlayID, pointerEvent); + return true; } emit mouseDoublePressOffOverlay(); return false; @@ -912,13 +909,8 @@ bool Overlays::mouseReleaseEvent(QMouseEvent* event) { PickRay ray = qApp->computePickRay(event->x(), event->y()); RayToOverlayIntersectionResult rayPickResult = findRayIntersectionForMouseEvent(ray); if (rayPickResult.intersects) { - - // Only Web overlays can have focus. - auto thisOverlay = std::dynamic_pointer_cast(getOverlay(rayPickResult.overlayID)); - if (thisOverlay) { - auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Release); - emit mouseReleaseOnOverlay(rayPickResult.overlayID, pointerEvent); - } + auto pointerEvent = calculateOverlayPointerEvent(rayPickResult.overlayID, ray, rayPickResult, event, PointerEvent::Release); + emit mouseReleaseOnOverlay(rayPickResult.overlayID, pointerEvent); } _currentClickingOnOverlayID = UNKNOWN_OVERLAY_ID; @@ -931,40 +923,29 @@ bool Overlays::mouseMoveEvent(QMouseEvent* event) { PickRay ray = qApp->computePickRay(event->x(), event->y()); RayToOverlayIntersectionResult rayPickResult = findRayIntersectionForMouseEvent(ray); if (rayPickResult.intersects) { + auto pointerEvent = calculateOverlayPointerEvent(rayPickResult.overlayID, ray, rayPickResult, event, PointerEvent::Move); + emit mouseMoveOnOverlay(rayPickResult.overlayID, pointerEvent); - // Only Web overlays can have focus. - auto thisOverlay = std::dynamic_pointer_cast(getOverlay(rayPickResult.overlayID)); - if (thisOverlay) { - auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Move); - emit mouseMoveOnOverlay(rayPickResult.overlayID, pointerEvent); - - // If previously hovering over a different overlay then leave hover on that overlay. - if (_currentHoverOverOverlayID != UNKNOWN_OVERLAY_ID && rayPickResult.overlayID != _currentHoverOverOverlayID) { - auto thisOverlay = std::dynamic_pointer_cast(getOverlay(_currentHoverOverOverlayID)); - if (thisOverlay) { - auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Move); - emit hoverLeaveOverlay(_currentHoverOverOverlayID, pointerEvent); - } - } - - // If hovering over a new overlay then enter hover on that overlay. - if (rayPickResult.overlayID != _currentHoverOverOverlayID) { - emit hoverEnterOverlay(rayPickResult.overlayID, pointerEvent); - } - - // Hover over current overlay. - emit hoverOverOverlay(rayPickResult.overlayID, pointerEvent); - - _currentHoverOverOverlayID = rayPickResult.overlayID; + // If previously hovering over a different overlay then leave hover on that overlay. + if (_currentHoverOverOverlayID != UNKNOWN_OVERLAY_ID && rayPickResult.overlayID != _currentHoverOverOverlayID) { + auto pointerEvent = calculateOverlayPointerEvent(_currentHoverOverOverlayID, ray, rayPickResult, event, PointerEvent::Move); + emit hoverLeaveOverlay(_currentHoverOverOverlayID, pointerEvent); } + + // If hovering over a new overlay then enter hover on that overlay. + if (rayPickResult.overlayID != _currentHoverOverOverlayID) { + emit hoverEnterOverlay(rayPickResult.overlayID, pointerEvent); + } + + // Hover over current overlay. + emit hoverOverOverlay(rayPickResult.overlayID, pointerEvent); + + _currentHoverOverOverlayID = rayPickResult.overlayID; } else { // If previously hovering an overlay then leave hover. if (_currentHoverOverOverlayID != UNKNOWN_OVERLAY_ID) { - auto thisOverlay = std::dynamic_pointer_cast(getOverlay(_currentHoverOverOverlayID)); - if (thisOverlay) { - auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Move); - emit hoverLeaveOverlay(_currentHoverOverOverlayID, pointerEvent); - } + auto pointerEvent = calculateOverlayPointerEvent(_currentHoverOverOverlayID, ray, rayPickResult, event, PointerEvent::Move); + emit hoverLeaveOverlay(_currentHoverOverOverlayID, pointerEvent); _currentHoverOverOverlayID = UNKNOWN_OVERLAY_ID; } diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index d21bc974a8..a915acb06a 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -131,7 +131,7 @@ public slots: OverlayID cloneOverlay(OverlayID id); /**jsdoc - * Edit an overlay's properties. + * Edit an overlay's properties. * * @function Overlays.editOverlay * @param {Overlays.OverlayID} overlayID The ID of the overlay to edit. @@ -288,13 +288,13 @@ public slots: #endif - void sendMousePressOnOverlay(OverlayID overlayID, const PointerEvent& event); - void sendMouseReleaseOnOverlay(OverlayID overlayID, const PointerEvent& event); - void sendMouseMoveOnOverlay(OverlayID overlayID, const PointerEvent& event); + void sendMousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event); + void sendMouseReleaseOnOverlay(const OverlayID& overlayID, const PointerEvent& event); + void sendMouseMoveOnOverlay(const OverlayID& overlayID, const PointerEvent& event); - void sendHoverEnterOverlay(OverlayID id, PointerEvent event); - void sendHoverOverOverlay(OverlayID id, PointerEvent event); - void sendHoverLeaveOverlay(OverlayID id, PointerEvent event); + void sendHoverEnterOverlay(const OverlayID& id, const PointerEvent& event); + void sendHoverOverOverlay(const OverlayID& id, const PointerEvent& event); + void sendHoverLeaveOverlay(const OverlayID& id, const PointerEvent& event); OverlayID getKeyboardFocusOverlay(); void setKeyboardFocusOverlay(OverlayID id); @@ -337,7 +337,7 @@ private: #endif bool _enabled = true; - PointerEvent calculatePointerEvent(Overlay::Pointer overlay, PickRay ray, RayToOverlayIntersectionResult rayPickResult, + PointerEvent calculateOverlayPointerEvent(OverlayID overlayID, PickRay ray, RayToOverlayIntersectionResult rayPickResult, QMouseEvent* event, PointerEvent::EventType eventType); OverlayID _currentClickingOnOverlayID { UNKNOWN_OVERLAY_ID }; diff --git a/interface/src/ui/overlays/Planar3DOverlay.h b/interface/src/ui/overlays/Planar3DOverlay.h index 9c502ab75e..8127d4ebb9 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.h +++ b/interface/src/ui/overlays/Planar3DOverlay.h @@ -21,6 +21,7 @@ public: Planar3DOverlay(const Planar3DOverlay* planar3DOverlay); virtual AABox getBounds() const override; + virtual glm::vec2 getSize() const { return _dimensions; }; glm::vec2 getDimensions() const { return _dimensions; } void setDimensions(float value) { _dimensions = glm::vec2(value); } diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 1c80aa244c..d61370151c 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -597,7 +597,7 @@ void Web3DOverlay::setScriptURL(const QString& scriptURL) { } } -glm::vec2 Web3DOverlay::getSize() { +glm::vec2 Web3DOverlay::getSize() const { return _resolution / _dpi * INCHES_TO_METERS * getDimensions(); }; diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 61dc7a1749..c08499cdf4 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -50,7 +50,7 @@ public: void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; - glm::vec2 getSize(); + glm::vec2 getSize() const override; virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) override; diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 31e36671c7..1c26b1bb08 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -364,7 +364,7 @@ private: AudioIOStats _stats; AudioGate* _audioGate { nullptr }; - bool _audioGateOpen { false }; + bool _audioGateOpen { true }; AudioPositionGetter _positionGetter; AudioOrientationGetter _orientationGetter; diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index b3c0ed3f05..d8dd7f5e35 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -101,6 +101,23 @@ namespace controller { makePosePair(Action::RIGHT_HAND_PINKY3, "RightHandPinky3"), makePosePair(Action::RIGHT_HAND_PINKY4, "RightHandPinky4"), + makePosePair(Action::TRACKED_OBJECT_00, "TrackedObject00"), + makePosePair(Action::TRACKED_OBJECT_01, "TrackedObject01"), + makePosePair(Action::TRACKED_OBJECT_02, "TrackedObject02"), + makePosePair(Action::TRACKED_OBJECT_03, "TrackedObject03"), + makePosePair(Action::TRACKED_OBJECT_04, "TrackedObject04"), + makePosePair(Action::TRACKED_OBJECT_05, "TrackedObject05"), + makePosePair(Action::TRACKED_OBJECT_06, "TrackedObject06"), + makePosePair(Action::TRACKED_OBJECT_07, "TrackedObject07"), + makePosePair(Action::TRACKED_OBJECT_08, "TrackedObject08"), + makePosePair(Action::TRACKED_OBJECT_09, "TrackedObject09"), + makePosePair(Action::TRACKED_OBJECT_10, "TrackedObject10"), + makePosePair(Action::TRACKED_OBJECT_11, "TrackedObject11"), + makePosePair(Action::TRACKED_OBJECT_12, "TrackedObject12"), + makePosePair(Action::TRACKED_OBJECT_13, "TrackedObject13"), + makePosePair(Action::TRACKED_OBJECT_14, "TrackedObject14"), + makePosePair(Action::TRACKED_OBJECT_15, "TrackedObject15"), + makeButtonPair(Action::LEFT_HAND_CLICK, "LeftHandClick"), makeButtonPair(Action::RIGHT_HAND_CLICK, "RightHandClick"), diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index ec4800c9aa..6319b5746e 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -146,6 +146,23 @@ enum class Action { RIGHT_HAND_PINKY3, RIGHT_HAND_PINKY4, + TRACKED_OBJECT_00, + TRACKED_OBJECT_01, + TRACKED_OBJECT_02, + TRACKED_OBJECT_03, + TRACKED_OBJECT_04, + TRACKED_OBJECT_05, + TRACKED_OBJECT_06, + TRACKED_OBJECT_07, + TRACKED_OBJECT_08, + TRACKED_OBJECT_09, + TRACKED_OBJECT_10, + TRACKED_OBJECT_11, + TRACKED_OBJECT_12, + TRACKED_OBJECT_13, + TRACKED_OBJECT_14, + TRACKED_OBJECT_15, + NUM_ACTIONS, }; diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index 40b87bc6b2..ed729867df 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -166,6 +166,23 @@ Input::NamedVector StandardController::getAvailableInputs() const { makePair(DD, "Down"), makePair(DL, "Left"), makePair(DR, "Right"), + + makePair(TRACKED_OBJECT_00, "TrackedObject00"), + makePair(TRACKED_OBJECT_01, "TrackedObject01"), + makePair(TRACKED_OBJECT_02, "TrackedObject02"), + makePair(TRACKED_OBJECT_03, "TrackedObject03"), + makePair(TRACKED_OBJECT_04, "TrackedObject04"), + makePair(TRACKED_OBJECT_05, "TrackedObject05"), + makePair(TRACKED_OBJECT_06, "TrackedObject06"), + makePair(TRACKED_OBJECT_07, "TrackedObject07"), + makePair(TRACKED_OBJECT_08, "TrackedObject08"), + makePair(TRACKED_OBJECT_09, "TrackedObject09"), + makePair(TRACKED_OBJECT_10, "TrackedObject10"), + makePair(TRACKED_OBJECT_11, "TrackedObject11"), + makePair(TRACKED_OBJECT_12, "TrackedObject12"), + makePair(TRACKED_OBJECT_13, "TrackedObject13"), + makePair(TRACKED_OBJECT_14, "TrackedObject14"), + makePair(TRACKED_OBJECT_15, "TrackedObject15") }; return availableInputs; } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 987d3118f7..d55352d31e 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -26,7 +26,6 @@ #include #include #include -#include #include "RenderableEntityItem.h" @@ -453,8 +452,6 @@ RayToEntityIntersectionResult EntityTreeRenderer::findRayIntersectionWorker(cons void EntityTreeRenderer::connectSignalsToSlots(EntityScriptingInterface* entityScriptingInterface) { - auto hoverOverlayInterface = DependencyManager::get().data(); - connect(this, &EntityTreeRenderer::mousePressOnEntity, entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity); connect(this, &EntityTreeRenderer::mouseMoveOnEntity, entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity); connect(this, &EntityTreeRenderer::mouseReleaseOnEntity, entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity); @@ -464,12 +461,8 @@ void EntityTreeRenderer::connectSignalsToSlots(EntityScriptingInterface* entityS connect(this, &EntityTreeRenderer::clickReleaseOnEntity, entityScriptingInterface, &EntityScriptingInterface::clickReleaseOnEntity); connect(this, &EntityTreeRenderer::hoverEnterEntity, entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity); - connect(this, SIGNAL(hoverEnterEntity(const EntityItemID&, const PointerEvent&)), hoverOverlayInterface, SLOT(createHoverOverlay(const EntityItemID&, const PointerEvent&))); - connect(this, &EntityTreeRenderer::hoverOverEntity, entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity); - connect(this, &EntityTreeRenderer::hoverLeaveEntity, entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity); - connect(this, SIGNAL(hoverLeaveEntity(const EntityItemID&, const PointerEvent&)), hoverOverlayInterface, SLOT(destroyHoverOverlay(const EntityItemID&, const PointerEvent&))); connect(this, &EntityTreeRenderer::enterEntity, entityScriptingInterface, &EntityScriptingInterface::enterEntity); connect(this, &EntityTreeRenderer::leaveEntity, entityScriptingInterface, &EntityScriptingInterface::leaveEntity); @@ -682,7 +675,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { PickRay ray = _viewState->computePickRay(event->x(), event->y()); - bool precisionPicking = false; // for mouse moves we do not do precision picking + bool precisionPicking = true; // for mouse moves we do precision picking RayToEntityIntersectionResult rayPickResult = findRayIntersectionWorker(ray, Octree::TryLock, precisionPicking); if (rayPickResult.intersects) { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 9884debcce..911fdf4184 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -372,6 +372,21 @@ void RenderableModelEntityItem::render(RenderArgs* args) { _model->updateRenderItems(); } + // this simple logic should say we set showingEntityHighlight to true whenever we are in marketplace mode and we have a marketplace id, or + // whenever we are not set to none and shouldHighlight is true. + bool showingEntityHighlight = ((bool)(args->_outlineFlags & (int)RenderArgs::RENDER_OUTLINE_MARKETPLACE_MODE) && getMarketplaceID().length() != 0) || + (args->_outlineFlags != RenderArgs::RENDER_OUTLINE_NONE && getShouldHighlight()); + if (showingEntityHighlight) { + static glm::vec4 yellowColor(1.0f, 1.0f, 0.0f, 1.0f); + gpu::Batch& batch = *args->_batch; + bool success; + auto shapeTransform = getTransformToCenter(success); + if (success) { + batch.setModelTransform(shapeTransform); // we want to include the scale as well + DependencyManager::get()->renderWireCubeInstance(args, batch, yellowColor); + } + } + if (!hasModel() || (_model && _model->didVisualGeometryRequestFail())) { static glm::vec4 greenColor(0.0f, 1.0f, 0.0f, 1.0f); gpu::Batch& batch = *args->_batch; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 9a898b4071..ba8e0c18e7 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -69,7 +69,7 @@ RenderableWebEntityItem::~RenderableWebEntityItem() { } } -bool RenderableWebEntityItem::buildWebSurface(QSharedPointer renderer) { +bool RenderableWebEntityItem::buildWebSurface() { if (_currentWebCount >= MAX_CONCURRENT_WEB_VIEWS) { qWarning() << "Too many concurrent web views to create new view"; return false; @@ -132,6 +132,8 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer handlePointerEvent(event); } }; + + auto renderer = DependencyManager::get(); _mousePressConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::mousePressOnEntity, forwardPointerEvent); _mouseReleaseConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::mouseReleaseOnEntity, forwardPointerEvent); _mouseMoveConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::mouseMoveOnEntity, forwardPointerEvent); @@ -185,8 +187,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { #endif if (!_webSurface) { - auto renderer = qSharedPointerCast(args->_renderData); - if (!buildWebSurface(renderer)) { + if (!buildWebSurface()) { return; } _fadeStartTime = usecTimestampNow(); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 0f5d307766..a2318081b6 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -57,7 +57,7 @@ public: virtual QObject* getRootItem() override; private: - bool buildWebSurface(QSharedPointer renderer); + bool buildWebSurface(); void destroyWebSurface(); glm::vec2 getWindowSize() const; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 422488f86f..c4136a0430 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -133,6 +133,7 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_LOCKED; requestedProperties += PROP_USER_DATA; requestedProperties += PROP_MARKETPLACE_ID; + requestedProperties += PROP_SHOULD_HIGHLIGHT; requestedProperties += PROP_NAME; requestedProperties += PROP_HREF; requestedProperties += PROP_DESCRIPTION; @@ -278,6 +279,7 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_LOCKED, getLocked()); APPEND_ENTITY_PROPERTY(PROP_USER_DATA, getUserData()); APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, getMarketplaceID()); + APPEND_ENTITY_PROPERTY(PROP_SHOULD_HIGHLIGHT, getShouldHighlight()); APPEND_ENTITY_PROPERTY(PROP_NAME, getName()); APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, getCollisionSoundURL()); APPEND_ENTITY_PROPERTY(PROP_HREF, getHref()); @@ -829,6 +831,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef READ_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, QString, setMarketplaceID); } + if (args.bitstreamVersion >= VERSION_ENTITIES_HAS_SHOULD_HIGHLIGHT) { + READ_ENTITY_PROPERTY(PROP_SHOULD_HIGHLIGHT, bool, setShouldHighlight); + } + READ_ENTITY_PROPERTY(PROP_NAME, QString, setName); READ_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); READ_ENTITY_PROPERTY(PROP_HREF, QString, setHref); @@ -2803,6 +2809,20 @@ void EntityItem::setMarketplaceID(const QString& value) { }); } +bool EntityItem::getShouldHighlight() const { + bool result; + withReadLock([&] { + result = _shouldHighlight; + }); + return result; +} + +void EntityItem::setShouldHighlight(const bool value) { + withWriteLock([&] { + _shouldHighlight = value; + }); +} + uint32_t EntityItem::getDirtyFlags() const { uint32_t result; withReadLock([&] { diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 92c83651aa..36ac6ba1cc 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -316,6 +316,9 @@ public: QString getMarketplaceID() const; void setMarketplaceID(const QString& value); + bool getShouldHighlight() const; + void setShouldHighlight(const bool value); + // TODO: get rid of users of getRadius()... float getRadius() const; @@ -532,6 +535,7 @@ protected: QString _userData; SimulationOwner _simulationOwner; QString _marketplaceID; + bool _shouldHighlight { false }; QString _name; QString _href; //Hyperlink href QString _description; //Hyperlink description diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 20b541f563..1bd75f78d4 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -289,6 +289,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_RADIUS_START, radiusStart); CHECK_PROPERTY_CHANGE(PROP_RADIUS_FINISH, radiusFinish); CHECK_PROPERTY_CHANGE(PROP_MARKETPLACE_ID, marketplaceID); + CHECK_PROPERTY_CHANGE(PROP_SHOULD_HIGHLIGHT, shouldHighlight); CHECK_PROPERTY_CHANGE(PROP_NAME, name); CHECK_PROPERTY_CHANGE(PROP_BACKGROUND_MODE, backgroundMode); CHECK_PROPERTY_CHANGE(PROP_SOURCE_URL, sourceUrl); @@ -406,6 +407,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCKED, locked); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USER_DATA, userData); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MARKETPLACE_ID, marketplaceID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SHOULD_HIGHLIGHT, shouldHighlight); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NAME, name); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLLISION_SOUND_URL, collisionSoundURL); @@ -982,6 +984,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_RADIUS_START, RadiusStart, radiusStart, float); ADD_PROPERTY_TO_MAP(PROP_RADIUS_FINISH, RadiusFinish, radiusFinish, float); ADD_PROPERTY_TO_MAP(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString); + ADD_PROPERTY_TO_MAP(PROP_SHOULD_HIGHLIGHT, ShouldHighlight, shouldHighlight, bool); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_COLOR, KeyLightColor, keyLightColor, xColor); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_INTENSITY, KeyLightIntensity, keyLightIntensity, float); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_AMBIENT_INTENSITY, KeyLightAmbientIntensity, keyLightAmbientIntensity, float); @@ -1334,6 +1337,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_SHAPE, properties.getShape()); } APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, properties.getMarketplaceID()); + APPEND_ENTITY_PROPERTY(PROP_SHOULD_HIGHLIGHT, properties.getShouldHighlight()); APPEND_ENTITY_PROPERTY(PROP_NAME, properties.getName()); APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, properties.getCollisionSoundURL()); APPEND_ENTITY_PROPERTY(PROP_ACTION_DATA, properties.getActionData()); @@ -1632,6 +1636,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int } READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_MARKETPLACE_ID, QString, setMarketplaceID); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHOULD_HIGHLIGHT, bool, setShouldHighlight); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_NAME, QString, setName); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ACTION_DATA, QByteArray, setActionData); @@ -1746,6 +1751,7 @@ void EntityItemProperties::markAllChanged() { //_alphaFinishChanged = true; _marketplaceIDChanged = true; + _shouldHighlightChanged = true; _keyLight.markAllChanged(); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 90afd39fbf..8c636addb5 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -171,6 +171,7 @@ public: DEFINE_PROPERTY(PROP_RADIUS_FINISH, RadiusFinish, radiusFinish, float, ParticleEffectEntityItem::DEFAULT_RADIUS_FINISH); DEFINE_PROPERTY(PROP_EMITTER_SHOULD_TRAIL, EmitterShouldTrail, emitterShouldTrail, bool, ParticleEffectEntityItem::DEFAULT_EMITTER_SHOULD_TRAIL); DEFINE_PROPERTY_REF(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString, ENTITY_ITEM_DEFAULT_MARKETPLACE_ID); + DEFINE_PROPERTY_REF(PROP_SHOULD_HIGHLIGHT, ShouldHighlight, shouldHighlight, bool, ENTITY_ITEM_DEFAULT_SHOULD_HIGHLIGHT); DEFINE_PROPERTY_GROUP(KeyLight, keyLight, KeyLightPropertyGroup); DEFINE_PROPERTY_REF(PROP_VOXEL_VOLUME_SIZE, VoxelVolumeSize, voxelVolumeSize, glm::vec3, PolyVoxEntityItem::DEFAULT_VOXEL_VOLUME_SIZE); DEFINE_PROPERTY_REF(PROP_VOXEL_DATA, VoxelData, voxelData, QByteArray, PolyVoxEntityItem::DEFAULT_VOXEL_DATA); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index d52c5d9aab..43d0e33ba6 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -27,6 +27,7 @@ const glm::vec3 ENTITY_ITEM_HALF_VEC3 = glm::vec3(0.5f); const bool ENTITY_ITEM_DEFAULT_LOCKED = false; const QString ENTITY_ITEM_DEFAULT_USER_DATA = QString(""); const QString ENTITY_ITEM_DEFAULT_MARKETPLACE_ID = QString(""); +const bool ENTITY_ITEM_DEFAULT_SHOULD_HIGHLIGHT = false; const QUuid ENTITY_ITEM_DEFAULT_SIMULATOR_ID = QUuid(); const float ENTITY_ITEM_DEFAULT_ALPHA = 1.0f; diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index b3cfc143c2..9600d0d4fe 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -78,6 +78,7 @@ enum EntityPropertyList { PROP_COMPOUND_SHAPE_URL, // used by Model + zones entities PROP_MARKETPLACE_ID, // all entities + PROP_SHOULD_HIGHLIGHT, // all entities PROP_ACCELERATION, // all entities PROP_SIMULATION_OWNER, // formerly known as PROP_SIMULATOR_ID PROP_NAME, // all entities diff --git a/libraries/entities/src/HoverOverlayInterface.cpp b/libraries/entities/src/HoverOverlayInterface.cpp deleted file mode 100644 index dcfde41e39..0000000000 --- a/libraries/entities/src/HoverOverlayInterface.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// -// HoverOverlayInterface.cpp -// libraries/entities/src -// -// Created by Zach Fox on 2017-07-14. -// 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 "HoverOverlayInterface.h" - -HoverOverlayInterface::HoverOverlayInterface() { - // "hover_overlay" debug log category disabled by default. - // Create your own "qtlogging.ini" file and set your "QT_LOGGING_CONF" environment variable - // if you'd like to enable/disable certain categories. - // More details: http://doc.qt.io/qt-5/qloggingcategory.html#configuring-categories - QLoggingCategory::setFilterRules(QStringLiteral("hifi.hover_overlay.debug=false")); -} - -void HoverOverlayInterface::createHoverOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { - qCDebug(hover_overlay) << "Creating Hover Overlay on top of entity with ID: " << entityItemID; - setCurrentHoveredEntity(entityItemID); -} - -void HoverOverlayInterface::createHoverOverlay(const EntityItemID& entityItemID) { - HoverOverlayInterface::createHoverOverlay(entityItemID, PointerEvent()); -} - -void HoverOverlayInterface::destroyHoverOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { - qCDebug(hover_overlay) << "Destroying Hover Overlay on top of entity with ID: " << entityItemID; - setCurrentHoveredEntity(QUuid()); -} - -void HoverOverlayInterface::destroyHoverOverlay(const EntityItemID& entityItemID) { - HoverOverlayInterface::destroyHoverOverlay(entityItemID, PointerEvent()); -} diff --git a/libraries/entities/src/HoverOverlayInterface.h b/libraries/entities/src/HoverOverlayInterface.h deleted file mode 100644 index a39faab819..0000000000 --- a/libraries/entities/src/HoverOverlayInterface.h +++ /dev/null @@ -1,49 +0,0 @@ -// -// HoverOverlayInterface.h -// libraries/entities/src -// -// Created by Zach Fox on 2017-07-14. -// 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 -// - -#pragma once -#ifndef hifi_HoverOverlayInterface_h -#define hifi_HoverOverlayInterface_h - -#include -#include - -#include -#include - -#include "EntityTree.h" -#include "HoverOverlayLogging.h" - -/**jsdoc -* @namespace HoverOverlay -*/ -class HoverOverlayInterface : public QObject, public Dependency { - Q_OBJECT - - Q_PROPERTY(QUuid currentHoveredEntity READ getCurrentHoveredEntity WRITE setCurrentHoveredEntity) -public: - HoverOverlayInterface(); - - Q_INVOKABLE QUuid getCurrentHoveredEntity() { return _currentHoveredEntity; } - void setCurrentHoveredEntity(const QUuid& entityID) { _currentHoveredEntity = entityID; } - -public slots: - void createHoverOverlay(const EntityItemID& entityItemID, const PointerEvent& event); - void createHoverOverlay(const EntityItemID& entityItemID); - void destroyHoverOverlay(const EntityItemID& entityItemID, const PointerEvent& event); - void destroyHoverOverlay(const EntityItemID& entityItemID); - -private: - bool _verboseLogging { true }; - QUuid _currentHoveredEntity{}; -}; - -#endif // hifi_HoverOverlayInterface_h diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index b5a2fc6b3c..e9ec6d8910 100755 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -236,6 +236,7 @@ controller::Input::NamedVector KeyboardMouseDevice::InputDevice::getAvailableInp availableInputs.append(Input::NamedPair(makeInput(Qt::Key_Shift), "Shift")); availableInputs.append(Input::NamedPair(makeInput(Qt::Key_PageUp), QKeySequence(Qt::Key_PageUp).toString())); availableInputs.append(Input::NamedPair(makeInput(Qt::Key_PageDown), QKeySequence(Qt::Key_PageDown).toString())); + availableInputs.append(Input::NamedPair(makeInput(Qt::Key_Tab), QKeySequence(Qt::Key_Tab).toString())); availableInputs.append(Input::NamedPair(makeInput(Qt::LeftButton), "LeftMouseButton")); availableInputs.append(Input::NamedPair(makeInput(Qt::MiddleButton), "MiddleMouseButton")); diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 240697d890..d2500196d9 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -62,7 +62,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return VERSION_ENTITIES_BULLET_DYNAMICS; + return VERSION_ENTITIES_HAS_SHOULD_HIGHLIGHT; case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::JSONFilterWithFamilyTree); case PacketType::AvatarIdentity: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 848bfd97cf..cb3db791b4 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -218,6 +218,7 @@ const PacketVersion VERSION_ENTITIES_PHYSICS_PACKET = 67; const PacketVersion VERSION_ENTITIES_ZONE_FILTERS = 68; const PacketVersion VERSION_ENTITIES_HINGE_CONSTRAINT = 69; const PacketVersion VERSION_ENTITIES_BULLET_DYNAMICS = 70; +const PacketVersion VERSION_ENTITIES_HAS_SHOULD_HIGHLIGHT = 71; enum class EntityQueryPacketVersion: PacketVersion { JSONFilter = 18, diff --git a/libraries/plugins/src/plugins/DisplayPlugin.cpp b/libraries/plugins/src/plugins/DisplayPlugin.cpp index 747c72c08e..20c72159c4 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.cpp +++ b/libraries/plugins/src/plugins/DisplayPlugin.cpp @@ -18,6 +18,19 @@ void DisplayPlugin::incrementPresentCount() { ++_presentedFrameIndex; - // Alert the app that it needs to paint a new presentation frame - qApp->postEvent(qApp, new QEvent(static_cast(Present)), Qt::HighEventPriority); + { + QMutexLocker locker(&_presentMutex); + _presentCondition.wakeAll(); + } + + emit presented(_presentedFrameIndex); } + +void DisplayPlugin::waitForPresent() { + QMutexLocker locker(&_presentMutex); + while (isActive()) { + if (_presentCondition.wait(&_presentMutex, MSECS_PER_SECOND)) { + break; + } + } +} \ No newline at end of file diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 481a2609fc..9e18ee534d 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include @@ -134,10 +136,6 @@ class DisplayPlugin : public Plugin, public HmdDisplay { Q_OBJECT using Parent = Plugin; public: - enum Event { - Present = QEvent::User + 1 - }; - virtual int getRequiredThreadCount() const { return 0; } virtual bool isHmd() const { return false; } virtual int getHmdScreen() const { return -1; } @@ -221,12 +219,15 @@ public: virtual void cycleDebugOutput() {} + void waitForPresent(); + static const QString& MENU_PATH(); signals: void recommendedFramebufferSizeChanged(const QSize& size); void resetSensorsRequested(); + void presented(quint32 frame); protected: void incrementPresentCount(); @@ -234,6 +235,8 @@ protected: gpu::ContextPointer _gpuContext; private: + QMutex _presentMutex; + QWaitCondition _presentCondition; std::atomic _presentedFrameIndex; mutable std::mutex _paintDelayMutex; QElapsedTimer _paintDelayTimer; diff --git a/libraries/render/src/render/Args.h b/libraries/render/src/render/Args.h index 449a3ac22b..6a91081c95 100644 --- a/libraries/render/src/render/Args.h +++ b/libraries/render/src/render/Args.h @@ -63,6 +63,12 @@ namespace render { public: enum RenderMode { DEFAULT_RENDER_MODE, SHADOW_RENDER_MODE, DIFFUSE_RENDER_MODE, NORMAL_RENDER_MODE, MIRROR_RENDER_MODE, SECONDARY_CAMERA_RENDER_MODE }; enum DisplayMode { MONO, STEREO_MONITOR, STEREO_HMD }; + enum OutlineFlags { + RENDER_OUTLINE_NONE = 0, + RENDER_OUTLINE_WIREFRAMES = 1, + RENDER_OUTLINE_MARKETPLACE_MODE = 2, + RENDER_OUTLINE_SHADER = 4 + }; enum DebugFlags { RENDER_DEBUG_NONE = 0, RENDER_DEBUG_HULLS = 1 @@ -71,7 +77,6 @@ namespace render { Args() {} Args(const gpu::ContextPointer& context, - QSharedPointer renderData = QSharedPointer(nullptr), float sizeScale = 1.0f, int boundaryLevelAdjust = 0, RenderMode renderMode = DEFAULT_RENDER_MODE, @@ -79,7 +84,6 @@ namespace render { DebugFlags debugFlags = RENDER_DEBUG_NONE, gpu::Batch* batch = nullptr) : _context(context), - _renderData(renderData), _sizeScale(sizeScale), _boundaryLevelAdjust(boundaryLevelAdjust), _renderMode(renderMode), @@ -104,7 +108,6 @@ namespace render { std::shared_ptr _context; std::shared_ptr _blitFramebuffer; std::shared_ptr _shapePipeline; - QSharedPointer _renderData; std::stack _viewFrustums; glm::ivec4 _viewport { 0.0f, 0.0f, 1.0f, 1.0f }; glm::vec3 _boomOffset { 0.0f, 0.0f, 1.0f }; @@ -112,6 +115,7 @@ namespace render { int _boundaryLevelAdjust { 0 }; RenderMode _renderMode { DEFAULT_RENDER_MODE }; DisplayMode _displayMode { MONO }; + OutlineFlags _outlineFlags{ RENDER_OUTLINE_NONE }; DebugFlags _debugFlags { RENDER_DEBUG_NONE }; gpu::Batch* _batch = nullptr; diff --git a/libraries/shared/src/ThreadHelpers.cpp b/libraries/shared/src/ThreadHelpers.cpp index 8f3d16a577..654b8e0252 100644 --- a/libraries/shared/src/ThreadHelpers.cpp +++ b/libraries/shared/src/ThreadHelpers.cpp @@ -10,29 +10,71 @@ #include +// Support for viewing the thread name in the debugger. +// Note, Qt actually does this for you but only in debug builds +// Code from https://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx +// and matches logic in `qt_set_thread_name` in qthread_win.cpp +#ifdef Q_OS_WIN +#include +#pragma pack(push,8) +struct THREADNAME_INFO { + DWORD dwType; // Must be 0x1000. + LPCSTR szName; // Pointer to name (in user addr space). + DWORD dwThreadID; // Thread ID (-1=caller thread). + DWORD dwFlags; // Reserved for future use, must be zero. +}; +#pragma pack(pop) +#endif + +void setThreadName(const std::string& name) { +#ifdef Q_OS_WIN + static const DWORD MS_VC_EXCEPTION = 0x406D1388; + THREADNAME_INFO info{ 0x1000, name.c_str(), (DWORD)-1, 0 }; + __try { + RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info); + } __except (EXCEPTION_EXECUTE_HANDLER) { } +#endif +} + +void moveToNewNamedThread(QObject* object, const QString& name, std::function preStartCallback, std::function startCallback, QThread::Priority priority) { + Q_ASSERT(QThread::currentThread() == object->thread()); + + // Create the target thread + QThread* thread = new QThread(); + thread->setObjectName(name); + + // Execute any additional work to do before the thread starts like moving members to the target thread. + // This is required as QObject::moveToThread isn't virutal, so we can't override it on objects that contain + // an OpenGLContext and ensure that the context moves to the target thread as well. + preStartCallback(thread); + + // Link the in-thread initialization code + QObject::connect(thread, &QThread::started, [name, startCallback] { + if (!name.isEmpty()) { + // Make it easy to spot our thread processes inside the debugger + setThreadName("Hifi_" + name.toStdString()); + } + startCallback(); + }); + + // Make sure the thread will be destroyed and cleaned up. The assumption here is that the incoming object + // will be destroyed and the thread will quit when that occurs. + QObject::connect(object, &QObject::destroyed, thread, &QThread::quit); + // When the thread itself stops running, it should also be deleted. + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + + // put the object on the thread + object->moveToThread(thread); + thread->start(); + if (priority != QThread::InheritPriority) { + thread->setPriority(priority); + } +} void moveToNewNamedThread(QObject* object, const QString& name, std::function startCallback, QThread::Priority priority) { - Q_ASSERT(QThread::currentThread() == object->thread()); - // setup a thread for the NodeList and its PacketReceiver - QThread* thread = new QThread(); - thread->setObjectName(name); - - QString tempName = name; - QObject::connect(thread, &QThread::started, [startCallback] { - startCallback(); - }); - // Make sure the thread will be destroyed and cleaned up - QObject::connect(object, &QObject::destroyed, thread, &QThread::quit); - QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); - - // put the object on the thread - object->moveToThread(thread); - thread->start(); - if (priority != QThread::InheritPriority) { - thread->setPriority(priority); - } + moveToNewNamedThread(object, name, [](QThread*){}, startCallback, priority); } void moveToNewNamedThread(QObject* object, const QString& name, QThread::Priority priority) { - moveToNewNamedThread(object, name, [] {}, priority); + moveToNewNamedThread(object, name, [](QThread*){}, []{}, priority); } diff --git a/libraries/shared/src/ThreadHelpers.h b/libraries/shared/src/ThreadHelpers.h index 6e024f787a..d236344dc5 100644 --- a/libraries/shared/src/ThreadHelpers.h +++ b/libraries/shared/src/ThreadHelpers.h @@ -32,8 +32,17 @@ void withLock(QMutex& lock, F function) { function(); } -void moveToNewNamedThread(QObject* object, const QString& name, std::function startCallback, QThread::Priority priority = QThread::InheritPriority); -void moveToNewNamedThread(QObject* object, const QString& name, QThread::Priority priority = QThread::InheritPriority); +void moveToNewNamedThread(QObject* object, const QString& name, + std::function preStartCallback, + std::function startCallback, + QThread::Priority priority = QThread::InheritPriority); + +void moveToNewNamedThread(QObject* object, const QString& name, + std::function startCallback, + QThread::Priority priority = QThread::InheritPriority); + +void moveToNewNamedThread(QObject* object, const QString& name, + QThread::Priority priority = QThread::InheritPriority); class ConditionalGuard { public: diff --git a/libraries/shared/src/shared/QtHelpers.cpp b/libraries/shared/src/shared/QtHelpers.cpp index 1ce1c3e07c..3e8c6d57ed 100644 --- a/libraries/shared/src/shared/QtHelpers.cpp +++ b/libraries/shared/src/shared/QtHelpers.cpp @@ -11,11 +11,24 @@ #include #include #include +#include +#include "../Profile.h" Q_LOGGING_CATEGORY(thread_safety, "hifi.thread_safety") namespace hifi { namespace qt { +static QHash threadHash; +static QReadWriteLock threadHashLock; + +void addBlockingForbiddenThread(const QString& name, QThread* thread) { + if (!thread) { + thread = QThread::currentThread(); + } + QWriteLocker locker(&threadHashLock); + threadHash[thread] = name; +} + bool blockingInvokeMethod( const char* function, QObject *obj, const char *member, @@ -30,9 +43,23 @@ bool blockingInvokeMethod( QGenericArgument val7, QGenericArgument val8, QGenericArgument val9) { - if (QThread::currentThread() == qApp->thread()) { + auto currentThread = QThread::currentThread(); + if (currentThread == qApp->thread()) { qCWarning(thread_safety) << "BlockingQueuedConnection invoked on main thread from " << function; + return QMetaObject::invokeMethod(obj, member, + Qt::BlockingQueuedConnection, ret, val0, val1, val2, val3, val4, val5, val6, val7, val8, val9); + } + + { + QReadLocker locker(&threadHashLock); + for (const auto& thread : threadHash.keys()) { + if (currentThread == thread) { + qCWarning(thread_safety) << "BlockingQueuedConnection invoked on forbidden thread " << threadHash[thread]; + } + } } + + PROFILE_RANGE(app, function); return QMetaObject::invokeMethod(obj, member, Qt::BlockingQueuedConnection, ret, val0, val1, val2, val3, val4, val5, val6, val7, val8, val9); } diff --git a/libraries/shared/src/shared/QtHelpers.h b/libraries/shared/src/shared/QtHelpers.h index 5da65a378f..2133119324 100644 --- a/libraries/shared/src/shared/QtHelpers.h +++ b/libraries/shared/src/shared/QtHelpers.h @@ -14,6 +14,7 @@ namespace hifi { namespace qt { +void addBlockingForbiddenThread(const QString& name, QThread* thread = nullptr); bool blockingInvokeMethod( const char* function, diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 4ace49927d..0fd32b42e6 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -187,6 +187,7 @@ TabletProxy::TabletProxy(QObject* parent, const QString& name) : QObject(parent) if (QThread::currentThread() != qApp->thread()) { qCWarning(uiLogging) << "Creating tablet proxy on wrong thread " << _name; } + connect(this, &TabletProxy::tabletShownChanged, this, &TabletProxy::onTabletShown); } TabletProxy::~TabletProxy() { @@ -194,6 +195,7 @@ TabletProxy::~TabletProxy() { if (QThread::currentThread() != thread()) { qCWarning(uiLogging) << "Destroying tablet proxy on wrong thread" << _name; } + disconnect(this, &TabletProxy::tabletShownChanged, this, &TabletProxy::onTabletShown); } void TabletProxy::setToolbarMode(bool toolbarMode) { @@ -208,12 +210,13 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { _toolbarMode = toolbarMode; + auto offscreenUi = DependencyManager::get(); + if (toolbarMode) { removeButtonsFromHomeScreen(); addButtonsToToolbar(); // create new desktop window - auto offscreenUi = DependencyManager::get(); auto tabletRootWindow = new TabletRootWindow(); tabletRootWindow->initQml(QVariantMap()); auto quickItem = tabletRootWindow->asQuickItem(); @@ -234,7 +237,11 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { } else { loadHomeScreen(true); } - + //check if running scripts window opened and save it for reopen in Tablet + if (offscreenUi->isVisible("RunningScripts")) { + offscreenUi->hide("RunningScripts"); + _showRunningScripts = true; + } // destroy desktop window if (_desktopWindow) { _desktopWindow->deleteLater(); @@ -316,6 +323,13 @@ void TabletProxy::emitWebEvent(const QVariant& msg) { emit webEventReceived(msg); } +void TabletProxy::onTabletShown() { + if (_tabletShown && _showRunningScripts) { + _showRunningScripts = false; + pushOntoStack("../../hifi/dialogs/TabletRunningScripts.qml"); + } +} + bool TabletProxy::isPathLoaded(const QVariant& path) { if (QThread::currentThread() != thread()) { bool result = false; @@ -365,9 +379,16 @@ void TabletProxy::setQmlTabletRoot(OffscreenQmlSurface* qmlOffscreenSurface) { }); if (_initialScreen) { - pushOntoStack(_initialPath); + if (!_showRunningScripts) { + pushOntoStack(_initialPath); + } _initialScreen = false; } + + if (_showRunningScripts) { + //show Tablet. Make sure, setShown implemented in TabletRoot.qml + QMetaObject::invokeMethod(_qmlTabletRoot, "setShown", Q_ARG(const QVariant&, QVariant(true))); + } } else { removeButtonsFromHomeScreen(); _state = State::Uninitialized; @@ -509,7 +530,7 @@ bool TabletProxy::pushOntoStack(const QVariant& path) { qCDebug(uiLogging) << "tablet cannot push QML because _qmlTabletRoot or _desktopWindow is null"; } - return root; + return (root != nullptr); } void TabletProxy::popFromStack() { diff --git a/libraries/ui/src/ui/TabletScriptingInterface.h b/libraries/ui/src/ui/TabletScriptingInterface.h index e61398585e..af38cb9351 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.h +++ b/libraries/ui/src/ui/TabletScriptingInterface.h @@ -232,6 +232,7 @@ protected slots: void addButtonsToHomeScreen(); void desktopWindowClosed(); void emitWebEvent(const QVariant& msg); + void onTabletShown(); protected: void removeButtonsFromHomeScreen(); void loadHomeScreen(bool forceOntoHomeScreen); @@ -252,6 +253,7 @@ protected: enum class State { Uninitialized, Home, Web, Menu, QML }; State _state { State::Uninitialized }; bool _landscape { false }; + bool _showRunningScripts { false }; }; Q_DECLARE_METATYPE(TabletProxy*); diff --git a/plugins/hifiSixense/CMakeLists.txt b/plugins/hifiSixense/CMakeLists.txt index 14676217db..58aeb6c88f 100644 --- a/plugins/hifiSixense/CMakeLists.txt +++ b/plugins/hifiSixense/CMakeLists.txt @@ -6,9 +6,11 @@ # See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html # -if (NOT ANDROID) - set(TARGET_NAME hifiSixense) - setup_hifi_plugin(Script Qml Widgets) - link_hifi_libraries(shared controllers ui plugins ui-plugins input-plugins) - target_sixense() -endif () +# FIXME if we want to re-enable this, we need to fix the mechanism for installing the +# dependency dlls `msvcr100` and `msvcp100` WITHOUT using CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS +#if (NOT ANDROID) +# set(TARGET_NAME hifiSixense) +# setup_hifi_plugin(Script Qml Widgets) +# link_hifi_libraries(shared controllers ui plugins ui-plugins input-plugins) +# target_sixense() +#endif () diff --git a/scripts/developer/tests/puck-attach.js b/scripts/developer/tests/puck-attach.js index 2d0a2e6d02..04d5db5710 100644 --- a/scripts/developer/tests/puck-attach.js +++ b/scripts/developer/tests/puck-attach.js @@ -1,21 +1,13 @@ // // 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. This panel contains the following fields. -// -// * Tracked Object - A drop down list of all the available pucks found. If no pucks are found this list will only have a single NONE entry. -// Closing and re-opening the app will refresh this list. -// * Model URL - A model url of the model you wish to be attached to the specified puck. -// * Position X, Y, Z - used to apply an offset between the puck and the attached model. -// * Rot X, Y, Z - used to apply euler angle offsets, in degrees, between the puck and the attached model. -// * Create Attachment - when this button is pressed a new Entity will be created at the specified puck's location. -// If a puck atttachment already exists, it will be destroyed before the new entity is created. -// * Destroy Attachmetn - destroies the entity attached to the puck. +// Click this app to bring up the puck attachment panel. // /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ @@ -25,7 +17,7 @@ Script.include("/~/system/libraries/Xform.js"); (function() { // BEGIN LOCAL_SCOPE var TABLET_BUTTON_NAME = "PUCKTACH"; -var HTML_URL = "https://s3.amazonaws.com/hifi-public/tony/html/puck-attach.html"; +var TABLET_APP_URL = "https://hifi-content.s3.amazonaws.com/seefo/production/puck-attach/puck-attach.html"; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var tabletButton = tablet.addButton({ @@ -34,32 +26,13 @@ var tabletButton = tablet.addButton({ activeIcon: "https://s3.amazonaws.com/hifi-public/tony/icons/puck-a.svg" }); -tabletButton.clicked.connect(function () { - if (shown) { - tablet.gotoHomeScreen(); - } else { - tablet.gotoWebScreen(HTML_URL); - } -}); - var shown = false; -var attachedEntity; -var attachedObj; - function onScreenChanged(type, url) { - if (type === "Web" && url === HTML_URL) { + if (type === "Web" && url === TABLET_APP_URL) { tabletButton.editProperties({isActive: true}); if (!shown) { // hook up to event bridge tablet.webEventReceived.connect(onWebEventReceived); - - Script.setTimeout(function () { - // send available tracked objects to the html running in the tablet. - var availableTrackedObjects = getAvailableTrackedObjects(); - tablet.emitScriptEvent(JSON.stringify(availableTrackedObjects)); - - // print("PUCK-ATTACH: availableTrackedObjects = " + JSON.stringify(availableTrackedObjects)); - }, 1000); // wait 1 sec before sending.. } shown = true; } else { @@ -71,104 +44,264 @@ function onScreenChanged(type, url) { 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 NUM_TRACKED_OBJECTS = 16; var i; for (i = 0; i < NUM_TRACKED_OBJECTS; i++) { var key = indexToTrackedObjectName(i); - var pose = Controller.getPoseValue(Controller.Hardware.Vive[key]); + var pose = Controller.getPoseValue(Controller.Standard[key]); if (pose && pose.valid) { available.push(i); } } return available; } - -function attach(obj) { - attachedEntity = Entities.addEntity({ - type: "Model", - name: "puck-attach-entity", - modelURL: obj.modelurl - }); - attachedObj = obj; - var localPos = {x: Number(obj.posx), y: Number(obj.posy), z: Number(obj.posz)}; - var localRot = Quat.fromVec3Degrees({x: Number(obj.rotx), y: Number(obj.roty), z: Number(obj.rotz)}); - attachedObj.localXform = new Xform(localRot, localPos); - var key = indexToTrackedObjectName(Number(attachedObj.puckno)); - attachedObj.key = key; - - // print("PUCK-ATTACH: attachedObj = " + JSON.stringify(attachedObj)); - - Script.update.connect(update); - update(); +function sendAvailableTrackedObjects() { + tablet.emitScriptEvent(JSON.stringify({ + pucks: getAvailableTrackedObjects(), + selectedPuck: ((lastPuck === undefined) ? -1 : lastPuck.name) + })); } -function remove() { - if (attachedEntity) { - Script.update.disconnect(update); - Entities.deleteEntity(attachedEntity); - attachedEntity = undefined; +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 = "http://content.highfidelity.com/seefo/production/puck-attach/vive_tracker_puck.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; + +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); } - attachedObj = undefined; } +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); -function pad(num, size) { - var tempString = "000000000" + num; - return tempString.substr(tempString.length - size); -} + var foundEntity; + var leastDistance = Number.MAX_VALUE; -function update() { - if (attachedEntity && attachedObj && Controller.Hardware.Vive) { - var pose = Controller.getPoseValue(Controller.Hardware.Vive[attachedObj.key]); - var avatarXform = new Xform(MyAvatar.orientation, MyAvatar.position); - var puckXform = new Xform(pose.rotation, pose.translation); - var finalXform = Xform.mul(avatarXform, Xform.mul(puckXform, attachedObj.localXform)); - if (pose && pose.valid) { - Entities.editEntity(attachedEntity, { - position: finalXform.pos, - rotation: finalXform.rot - }); - } else { - if (pose) { - print("PUCK-ATTACH: WARNING: invalid pose for " + attachedObj.key); - } else { - print("PUCK-ATTACH: WARNING: could not find key " + attachedObj.key); + 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 + Script.setTimeout(function() { + // delete the puck + Entities.deleteEntity(puckEntityID); + }, 100); +} +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 { + + try { obj = JSON.parse(msg); - } catch (err) { - return; + } catch (err) { + return; } - if (obj.cmd === "attach") { - remove(); - attach(obj); - } else if (obj.cmd === "detach") { - remove(); + + switch (obj.cmd) { + case "ready": + sendAvailableTrackedObjects(); + break; + case "create": + createPuck(obj); + break; + case "finalize": + finalizePuck(obj.puckno); + break; + case "destroy": + destroyPuck(obj.puckno); + break; } } +Script.update.connect(updatePucks); Script.scriptEnding.connect(function () { - remove(); tablet.removeButton(tabletButton); + destroyPucks(); if (shown) { tablet.webEventReceived.disconnect(onWebEventReceived); tablet.gotoHomeScreen(); } tablet.screenChanged.disconnect(onScreenChanged); }); - -}()); // END LOCAL_SCOPE +tabletButton.clicked.connect(function () { + if (shown) { + tablet.gotoHomeScreen(); + } else { + tablet.gotoWebScreen(TABLET_APP_URL); + } +}); +}()); // END LOCAL_SCOPE \ No newline at end of file diff --git a/scripts/developer/tests/viveTrackedObjects.js b/scripts/developer/tests/viveTrackedObjects.js index 4155afb82b..1d60f658d9 100644 --- a/scripts/developer/tests/viveTrackedObjects.js +++ b/scripts/developer/tests/viveTrackedObjects.js @@ -23,7 +23,7 @@ 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.Hardware.Vive[key]); + var pose = Controller.getPoseValue(Controller.Standard[key]); if (pose.valid) { DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, BLUE); } else { diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index a9a96c8e3c..708c4e0235 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -187,7 +187,10 @@ var DEFAULT_GRABBABLE_DATA = { var USE_BLACKLIST = true; var blacklist = []; -var entitiesWithHoverOverlays = []; +var hoveredEntityID = false; +var contextOverlayTimer = false; +var entityWithContextOverlay = false; +var contextualHand = -1; var FORBIDDEN_GRAB_NAMES = ["Grab Debug Entity", "grab pointer"]; var FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"]; @@ -224,6 +227,7 @@ CONTROLLER_STATE_MACHINE[STATE_OFF] = { CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = { name: "searching", enterMethod: "searchEnter", + exitMethod: "searchExit", updateMethod: "search" }; CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { @@ -351,7 +355,9 @@ function projectOntoXYPlane(worldPos, position, rotation, dimensions, registrati function projectOntoEntityXYPlane(entityID, worldPos) { var props = entityPropertiesCache.getProps(entityID); - return projectOntoXYPlane(worldPos, props.position, props.rotation, props.dimensions, props.registrationPoint); + if (props) { + return projectOntoXYPlane(worldPos, props.position, props.rotation, props.dimensions, props.registrationPoint); + } } function projectOntoOverlayXYPlane(overlayID, worldPos) { @@ -369,7 +375,9 @@ function projectOntoOverlayXYPlane(overlayID, worldPos) { dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); } else { dimensions = Overlays.getProperty(overlayID, "dimensions"); - dimensions.z = 0.01; // overlay dimensions are 2D, not 3D. + if (dimensions.z) { + dimensions.z = 0.01; // overlay dimensions are 2D, not 3D. + } } return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT); @@ -2191,6 +2199,14 @@ function MyController(hand) { } }; + this.searchExit = function () { + contextualHand = -1; + if (hoveredEntityID) { + Entities.sendHoverLeaveEntity(hoveredEntityID, pointerEvent); + } + hoveredEntityID = false; + }; + this.search = function(deltaTime, timestamp) { var _this = this; var name; @@ -2220,13 +2236,63 @@ function MyController(hand) { entityPropertiesCache.addEntity(rayPickInfo.entityID); } - if (rayPickInfo.entityID && entitiesWithHoverOverlays.indexOf(rayPickInfo.entityID) == -1) { - entitiesWithHoverOverlays.forEach(function (element) { - HoverOverlay.destroyHoverOverlay(element); - }); - entitiesWithHoverOverlays = []; - HoverOverlay.createHoverOverlay(rayPickInfo.entityID); - entitiesWithHoverOverlays.push(rayPickInfo.entityID); + pointerEvent = { + type: "Move", + id: this.hand + 1, // 0 is reserved for hardware mouse + pos2D: projectOntoEntityXYPlane(rayPickInfo.entityID, rayPickInfo.intersection), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.normal, + direction: rayPickInfo.searchRay.direction, + button: "None" + }; + if (rayPickInfo.entityID) { + if (hoveredEntityID !== rayPickInfo.entityID) { + if (contextOverlayTimer) { + Script.clearTimeout(contextOverlayTimer); + contextOverlayTimer = false; + } + if (hoveredEntityID) { + Entities.sendHoverLeaveEntity(hoveredEntityID, pointerEvent); + } + hoveredEntityID = rayPickInfo.entityID; + Entities.sendHoverEnterEntity(hoveredEntityID, pointerEvent); + } + + // If we already have a context overlay, we don't want to move it to + // another entity while we're searching. + if (!entityWithContextOverlay && !contextOverlayTimer) { + contextOverlayTimer = Script.setTimeout(function () { + if (rayPickInfo.entityID === hoveredEntityID && + !entityWithContextOverlay && + contextualHand !== -1 && + contextOverlayTimer) { + var pointerEvent = { + type: "Move", + id: contextualHand + 1, // 0 is reserved for hardware mouse + pos2D: projectOntoEntityXYPlane(rayPickInfo.entityID, rayPickInfo.intersection), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.normal, + direction: rayPickInfo.searchRay.direction, + button: "Secondary" + }; + if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.entityID, pointerEvent)) { + entityWithContextOverlay = rayPickInfo.entityID; + hoveredEntityID = false; + } + } + contextOverlayTimer = false; + }, 500); + contextualHand = this.hand; + } + } else { + if (hoveredEntityID) { + Entities.sendHoverLeaveEntity(hoveredEntityID, pointerEvent); + hoveredEntityID = false; + } + if (contextOverlayTimer) { + Script.clearTimeout(contextOverlayTimer); + contextOverlayTimer = false; + } } var candidateHotSpotEntities = Entities.findEntities(handPosition, MAX_EQUIP_HOTSPOT_RADIUS); @@ -2433,8 +2499,11 @@ function MyController(hand) { button: "None" }; - this.hoverEntity = entity; - Entities.sendHoverEnterEntity(entity, pointerEvent); + if (this.hoverEntity !== entity) { + Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); + this.hoverEntity = entity; + Entities.sendHoverEnterEntity(this.hoverEntity, pointerEvent); + } } // send mouse events for button highlights and tooltips. @@ -2483,25 +2552,25 @@ function MyController(hand) { var pointerEvent; if (rayPickInfo.overlayID) { var overlay = rayPickInfo.overlayID; - if (Overlays.getProperty(overlay, "type") != "web3d") { - return false; - } - if (Overlays.keyboardFocusOverlay != overlay) { + if ((Overlays.getProperty(overlay, "type") === "web3d") && Overlays.keyboardFocusOverlay != overlay) { Entities.keyboardFocusEntity = null; Overlays.keyboardFocusOverlay = overlay; + } - pointerEvent = { - type: "Move", - id: HARDWARE_MOUSE_ID, - pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; + pointerEvent = { + type: "Move", + id: HARDWARE_MOUSE_ID, + pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.normal, + direction: rayPickInfo.searchRay.direction, + button: "None" + }; + if (this.hoverOverlay !== overlay) { + Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent); this.hoverOverlay = overlay; - Overlays.sendHoverEnterOverlay(overlay, pointerEvent); + Overlays.sendHoverEnterOverlay(this.hoverOverlay, pointerEvent); } // Send mouse events for button highlights and tooltips. @@ -3477,6 +3546,15 @@ function MyController(hand) { var existingSearchDistance = this.searchSphereDistance; this.release(); + if (hoveredEntityID) { + Entities.sendHoverLeaveEntity(hoveredEntityID, pointerEvent); + hoveredEntityID = false; + } + if (entityWithContextOverlay) { + ContextOverlay.destroyContextOverlay(entityWithContextOverlay); + entityWithContextOverlay = false; + } + if (isInEditMode()) { this.searchSphereDistance = existingSearchDistance; } @@ -3597,7 +3675,7 @@ function MyController(hand) { }; this.overlayLaserTouchingEnter = function () { - // Test for intersection between controller laser and Web overlay plane. + // Test for intersection between controller laser and overlay plane. var controllerLocation = getControllerWorldLocation(this.handToController(), true); var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation); if (intersectInfo) { @@ -3791,11 +3869,6 @@ function MyController(hand) { this.release = function() { this.turnOffVisualizations(); - entitiesWithHoverOverlays.forEach(function (element) { - HoverOverlay.destroyHoverOverlay(element); - }); - entitiesWithHoverOverlays = []; - if (this.grabbedThingID !== null) { Messages.sendMessage('Hifi-Teleport-Ignore-Remove', this.grabbedThingID); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 4ce8fe5943..69a0dc30ac 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -394,7 +394,6 @@ var toolBar = (function () { function initialize() { Script.scriptEnding.connect(cleanup); - Window.domainChanged.connect(function () { that.setActive(false); that.clearEntityList(); @@ -622,6 +621,7 @@ var toolBar = (function () { }; that.setActive = function (active) { + ContextOverlay.enabled = !active; Settings.setValue(EDIT_SETTING, active); if (active) { Controller.captureEntityClickEvents(); @@ -2194,6 +2194,7 @@ var PopupMenu = function () { }; function cleanup() { + ContextOverlay.enabled = true; for (var i = 0; i < overlays.length; i++) { Overlays.deleteOverlay(overlays[i]); } diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index 3598b461a4..c9c3dbe493 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -43,17 +43,20 @@ var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); // Independent and Entity mode make people sick; disable them in hmd. var desktopOnlyViews = ['Independent Mode', 'Entity Mode']; +var switchToVR = "ENTER VR"; +var switchToDesktop = "EXIT VR"; + function onHmdChanged(isHmd) { HMD.closeTablet(); if (isHmd) { button.editProperties({ icon: "icons/tablet-icons/switch-desk-i.svg", - text: "DESKTOP" + text: switchToDesktop }); } else { button.editProperties({ icon: "icons/tablet-icons/switch-vr-i.svg", - text: "VR" + text: switchToVR }); } desktopOnlyViews.forEach(function (view) { @@ -70,7 +73,7 @@ function onClicked() { if (headset) { button = tablet.addButton({ icon: HMD.active ? "icons/tablet-icons/switch-desk-i.svg" : "icons/tablet-icons/switch-vr-i.svg", - text: HMD.active ? "DESKTOP" : "VR", + text: HMD.active ? switchToDesktop : switchToVR, sortOrder: 2 }); onHmdChanged(HMD.active); diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 3be8143830..7b25589e92 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -11,135 +11,140 @@ /* global Tablet, Script, HMD, UserActivityLogger, Entities */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ -(function() { // BEGIN LOCAL_SCOPE +(function () { // BEGIN LOCAL_SCOPE -Script.include("../libraries/WebTablet.js"); + Script.include("../libraries/WebTablet.js"); -var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; -var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. -var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); -var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); + var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; + var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. + var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); + var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); -var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; -// var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; + var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; + // var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; -// Event bridge messages. -var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; -var CLARA_IO_STATUS = "CLARA.IO STATUS"; -var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; -var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; -var GOTO_DIRECTORY = "GOTO_DIRECTORY"; -var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; -var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; -var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; + // Event bridge messages. + var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; + var CLARA_IO_STATUS = "CLARA.IO STATUS"; + var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; + var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; + var GOTO_DIRECTORY = "GOTO_DIRECTORY"; + var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; + var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; + var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; -var CLARA_DOWNLOAD_TITLE = "Preparing Download"; -var messageBox = null; -var isDownloadBeingCancelled = false; + var CLARA_DOWNLOAD_TITLE = "Preparing Download"; + var messageBox = null; + var isDownloadBeingCancelled = false; -var CANCEL_BUTTON = 4194304; // QMessageBox::Cancel -var NO_BUTTON = 0; // QMessageBox::NoButton + var CANCEL_BUTTON = 4194304; // QMessageBox::Cancel + var NO_BUTTON = 0; // QMessageBox::NoButton -var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server."; + var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server."; -function onMessageBoxClosed(id, button) { - if (id === messageBox && button === CANCEL_BUTTON) { - isDownloadBeingCancelled = true; - messageBox = null; - tablet.emitScriptEvent(CLARA_IO_CANCEL_DOWNLOAD); + function onMessageBoxClosed(id, button) { + if (id === messageBox && button === CANCEL_BUTTON) { + isDownloadBeingCancelled = true; + messageBox = null; + tablet.emitScriptEvent(CLARA_IO_CANCEL_DOWNLOAD); + } } -} -Window.messageBoxClosed.connect(onMessageBoxClosed); + Window.messageBoxClosed.connect(onMessageBoxClosed); -var onMarketplaceScreen = false; + var onMarketplaceScreen = false; -function showMarketplace() { - UserActivityLogger.openedMarketplace(); - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); - tablet.webEventReceived.connect(function (message) { + function showMarketplace() { + UserActivityLogger.openedMarketplace(); + tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + tablet.webEventReceived.connect(function (message) { - if (message === GOTO_DIRECTORY) { - tablet.gotoWebScreen(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); - } + if (message === GOTO_DIRECTORY) { + tablet.gotoWebScreen(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); + } - if (message === QUERY_CAN_WRITE_ASSETS) { - tablet.emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets()); - } + if (message === QUERY_CAN_WRITE_ASSETS) { + tablet.emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets()); + } - if (message === WARN_USER_NO_PERMISSIONS) { - Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); - } + if (message === WARN_USER_NO_PERMISSIONS) { + Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); + } - if (message.slice(0, CLARA_IO_STATUS.length) === CLARA_IO_STATUS) { - if (isDownloadBeingCancelled) { + if (message.slice(0, CLARA_IO_STATUS.length) === CLARA_IO_STATUS) { + if (isDownloadBeingCancelled) { + return; + } + + var text = message.slice(CLARA_IO_STATUS.length); + if (messageBox === null) { + messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); + } else { + Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); + } return; } - var text = message.slice(CLARA_IO_STATUS.length); - if (messageBox === null) { - messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); - } else { - Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); + if (message.slice(0, CLARA_IO_DOWNLOAD.length) === CLARA_IO_DOWNLOAD) { + if (messageBox !== null) { + Window.closeMessageBox(messageBox); + messageBox = null; + } + return; } - return; - } - if (message.slice(0, CLARA_IO_DOWNLOAD.length) === CLARA_IO_DOWNLOAD) { - if (messageBox !== null) { - Window.closeMessageBox(messageBox); - messageBox = null; + if (message === CLARA_IO_CANCELLED_DOWNLOAD) { + isDownloadBeingCancelled = false; } - return; - } + }); + } - if (message === CLARA_IO_CANCELLED_DOWNLOAD) { - isDownloadBeingCancelled = false; - } + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var marketplaceButton = tablet.addButton({ + icon: "icons/tablet-icons/market-i.svg", + activeIcon: "icons/tablet-icons/market-a.svg", + text: "MARKET", + sortOrder: 9 }); -} -var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); -var marketplaceButton = tablet.addButton({ - icon: "icons/tablet-icons/market-i.svg", - activeIcon: "icons/tablet-icons/market-a.svg", - text: "MARKET", - sortOrder: 9 -}); - -function onCanWriteAssetsChanged() { - var message = CAN_WRITE_ASSETS + " " + Entities.canWriteAssets(); - tablet.emitScriptEvent(message); -} - -function onClick() { - if (onMarketplaceScreen) { - // for toolbar-mode: go back to home screen, this will close the window. - tablet.gotoHomeScreen(); - } else { - var entity = HMD.tabletID; - Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); - showMarketplace(); + function onCanWriteAssetsChanged() { + var message = CAN_WRITE_ASSETS + " " + Entities.canWriteAssets(); + tablet.emitScriptEvent(message); } -} -function onScreenChanged(type, url) { - onMarketplaceScreen = type === "Web" && url === MARKETPLACE_URL_INITIAL - // for toolbar mode: change button to active when window is first openend, false otherwise. - marketplaceButton.editProperties({isActive: onMarketplaceScreen}); -} - -marketplaceButton.clicked.connect(onClick); -tablet.screenChanged.connect(onScreenChanged); -Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); - -Script.scriptEnding.connect(function () { - if (onMarketplaceScreen) { - tablet.gotoHomeScreen(); + function onClick() { + if (onMarketplaceScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + var entity = HMD.tabletID; + Entities.editEntity(entity, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); + showMarketplace(); + } } - tablet.removeButton(marketplaceButton); - tablet.screenChanged.disconnect(onScreenChanged); - Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); -}); + + function onScreenChanged(type, url) { + onMarketplaceScreen = type === "Web" && url === MARKETPLACE_URL_INITIAL + // for toolbar mode: change button to active when window is first openend, false otherwise. + marketplaceButton.editProperties({ isActive: onMarketplaceScreen }); + if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { + ContextOverlay.isInMarketplaceInspectionMode = true; + } else { + ContextOverlay.isInMarketplaceInspectionMode = false; + } + } + + marketplaceButton.clicked.connect(onClick); + tablet.screenChanged.connect(onScreenChanged); + Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); + + Script.scriptEnding.connect(function () { + if (onMarketplaceScreen) { + tablet.gotoHomeScreen(); + } + tablet.removeButton(marketplaceButton); + tablet.screenChanged.disconnect(onScreenChanged); + Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); + }); }()); // END LOCAL_SCOPE diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 8ea22192fc..2a8a89ae7d 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -710,6 +710,7 @@ function off() { Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); isWired = false; + ContextOverlay.enabled = true } if (audioTimer) { Script.clearInterval(audioTimer); @@ -722,6 +723,7 @@ function off() { function tabletVisibilityChanged() { if (!tablet.tabletShown) { + ContextOverlay.enabled = true; tablet.gotoHomeScreen(); } } @@ -732,7 +734,9 @@ function onTabletButtonClicked() { if (onPalScreen) { // for toolbar-mode: go back to home screen, this will close the window. tablet.gotoHomeScreen(); + ContextOverlay.enabled = true; } else { + ContextOverlay.enabled = false; tablet.loadQMLSource(PAL_QML_SOURCE); tablet.tabletShownChanged.connect(tabletVisibilityChanged); Users.requestsDomainListData = true; @@ -863,6 +867,7 @@ function avatarDisconnected(nodeID) { function clearLocalQMLDataAndClosePAL() { sendToQml({ method: 'clearLocalQMLData' }); if (onPalScreen) { + ContextOverlay.enabled = true; tablet.gotoHomeScreen(); } } diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index 9188f39a2e..257a56bf09 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -104,7 +104,6 @@ function showTabletUI() { checkTablet() - gTablet.tabletShown = true; if (!tabletRezzed || !tabletIsValid()) { closeTabletUI(); @@ -123,6 +122,7 @@ Overlays.editOverlay(HMD.tabletScreenID, { visible: true }); Overlays.editOverlay(HMD.tabletScreenID, { maxFPS: 90 }); } + gTablet.tabletShown = true; } function hideTabletUI() { diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index ce47a896aa..dbb315a9ae 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -681,7 +681,7 @@ private: _renderCount = _renderThread._presentCount.load(); update(); - RenderArgs renderArgs(_renderThread._gpuContext, _octree, DEFAULT_OCTREE_SIZE_SCALE, + RenderArgs renderArgs(_renderThread._gpuContext, DEFAULT_OCTREE_SIZE_SCALE, 0, RenderArgs::DEFAULT_RENDER_MODE, RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/app-doppleganger-attachments.js b/unpublishedScripts/marketplace/doppleganger-attachments/app-doppleganger-attachments.js index 4617cf47b6..c09ad602f8 100644 --- a/unpublishedScripts/marketplace/doppleganger-attachments/app-doppleganger-attachments.js +++ b/unpublishedScripts/marketplace/doppleganger-attachments/app-doppleganger-attachments.js @@ -9,21 +9,44 @@ // 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 commonjs */ +/* global DriveKeys, require:true, console */ +/* eslint-disable comma-dangle */ -var require = Script.require; +// decomment next line for automatic require cache-busting +// var require = function require(id) { return Script.require(id + '?'+new Date().getTime().toString(36)); }; +if (typeof require !== 'function') { + require = Script.require; +} +var VERSION = '0.0.0'; var WANT_DEBUG = false; +var DEBUG_MOVE_AS_YOU_MOVE = false; +var ROTATE_AS_YOU_MOVE = false; + +log(VERSION); var DopplegangerClass = require('./doppleganger.js'), DopplegangerAttachments = require('./doppleganger-attachments.js'), - modelHelper = require('./model-helper.js').modelHelper; + DebugControls = require('./doppleganger-debug.js'), + modelHelper = require('./model-helper.js').modelHelper, + utils = require('./utils.js'); + +// eslint-disable-next-line camelcase +var isWebpack = typeof __webpack_require__ === 'function'; + +var buttonConfig = utils.assign({ + text: 'MIRROR', +}, !isWebpack ? { + icon: Script.resolvePath('./doppleganger-i.svg'), + activeIcon: Script.resolvePath('./doppleganger-a.svg'), +} : { + icon: require('./doppleganger-i.svg.json'), + activeIcon: require('./doppleganger-a.svg.json'), +}); var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'), - button = tablet.addButton({ - icon: Script.resolvePath('./doppleganger-i.svg'), - activeIcon: Script.resolvePath('./doppleganger-a.svg'), - text: 'MIRROR' - }); + button = tablet.addButton(buttonConfig); Script.scriptEnding.connect(function() { tablet.removeButton(button); @@ -65,6 +88,59 @@ var doppleganger = new DopplegangerClass({ } } +// add support for "move as you move" +{ + var movementKeys = 'W,A,S,D,Up,Down,Right,Left'.split(','); + var controllerKeys = 'LX,LY,RY'.split(','); + var translationKeys = Object.keys(DriveKeys).filter(function(p) { + return /translate/i.test(p); + }); + var startingPosition; + + // returns an array of any active driving keys (eg: ['W', 'TRANSLATE_Z']) + function currentDrivers() { + return [].concat( + movementKeys.map(function(key) { + return Controller.getValue(Controller.Hardware.Keyboard[key]) && key; + }) + ).concat( + controllerKeys.map(function(key) { + return Controller.getValue(Controller.Standard[key]) !== 0.0 && key; + }) + ).concat( + translationKeys.map(function(key) { + return MyAvatar.getRawDriveKey(DriveKeys[key]) !== 0.0 && key; + }) + ).filter(Boolean); + } + + doppleganger.jointsUpdated.connect(function(objectID) { + var drivers = currentDrivers(), + isDriving = drivers.length > 0; + if (isDriving) { + if (startingPosition) { + debugPrint('resetting startingPosition since drivers == ', drivers.join('|')); + startingPosition = null; + } + } else if (HMD.active || DEBUG_MOVE_AS_YOU_MOVE) { + startingPosition = startingPosition || MyAvatar.position; + var movement = Vec3.subtract(MyAvatar.position, startingPosition); + startingPosition = MyAvatar.position; + // Vec3.length(movement) > 0.0001 && Vec3.print('+avatarMovement', movement); + + // "mirror" the relative translation vector + movement.x *= -1; + movement.z *= -1; + var props = {}; + props.position = doppleganger.position = Vec3.sum(doppleganger.position, movement); + if (ROTATE_AS_YOU_MOVE) { + props.rotation = doppleganger.orientation = MyAvatar.orientation; + } + modelHelper.editObject(doppleganger.objectID, props); + } + }); +} + // hide the doppleganger if this client script is unloaded Script.scriptEnding.connect(doppleganger, 'stop'); @@ -103,15 +179,21 @@ doppleganger.modelLoaded.connect(function(error, result) { // add debug indicators, but only if the user has configured the settings value if (Settings.getValue('debug.doppleganger', false)) { - WANT_DEBUG = true; - DopplegangerClass.addDebugControls(doppleganger); + WANT_DEBUG = WANT_DEBUG || true; + DopplegangerClass.WANT_DEBUG = WANT_DEBUG; + DopplegangerAttachments.WANT_DEBUG = WANT_DEBUG; + new DebugControls(doppleganger); +} + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('app-doppleganger | ' + [].slice.call(arguments).join(' ')); } function debugPrint() { - if (WANT_DEBUG) { - print('app-doppleganger | ' + [].slice.call(arguments).join(' ')); - } + WANT_DEBUG && log.apply(this, arguments); } + // ---------------------------------------------------------------------------- UserActivityLogger.logAction('doppleganger_app_load'); diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/dist/app-doppleganger-marketplace.js b/unpublishedScripts/marketplace/doppleganger-attachments/dist/app-doppleganger-marketplace.js new file mode 100644 index 0000000000..c2cf2a2353 --- /dev/null +++ b/unpublishedScripts/marketplace/doppleganger-attachments/dist/app-doppleganger-marketplace.js @@ -0,0 +1,1500 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 3); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports) { + +/* eslint-env commonjs */ +/* global console */ + +module.exports = { + version: '0.0.1', + bind: bind, + signal: signal, + assign: assign, + assert: assert +}; + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('utils | ' + [].slice.call(arguments).join(' ')); +} +log(module.exports.version); + +// @function - bind a function to a `this` context +// @param {Object} - the `this` context +// @param {Function|String} - function or method name +// @param {value} varargs... - optional curry-right arguments (passed to method after any explicit arguments) +function bind(thiz, method, varargs) { + method = thiz[method] || method; + varargs = [].slice.call(arguments, 2); + return function() { + var args = [].slice.call(arguments).concat(varargs); + return method.apply(thiz, args); + }; +} + +// @function - Qt signal polyfill +function signal(template) { + var callbacks = []; + return Object.defineProperties(function() { + var args = [].slice.call(arguments); + callbacks.forEach(function(obj) { + obj.handler.apply(obj.scope, args); + }); + }, { + connect: { value: function(scope, handler) { + var callback = {scope: scope, handler: scope[handler] || handler || scope}; + if (!callback.handler || !callback.handler.apply) { + throw new Error('invalid arguments to connect:' + [template, scope, handler]); + } + callbacks.push({scope: scope, handler: scope[handler] || handler || scope}); + }}, + disconnect: { value: function(scope, handler) { + var match = {scope: scope, handler: scope[handler] || handler || scope}; + callbacks = callbacks.filter(function(obj) { + return !(obj.scope === match.scope && obj.handler === match.handler); + }); + }} + }); +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill +/* eslint-disable */ +function assign(target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; +} +/* eslint-enable */ +// //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill + +// examples: +// assert(function assertion() { return (conditions === true) }, 'assertion failed!') +// var neededValue = assert(idString, 'idString not specified!'); +// assert(false, 'unexpected state'); +function assert(truthy, message) { + message = message || 'Assertion Failed:'; + + if (typeof truthy === 'function' && truthy.name === 'assertion') { + // extract function body to display with the assertion message + var assertion = (truthy+'').replace(/[\r\n]/g, ' ') + .replace(/^[^{]+\{|\}$|^\s*|\s*$/g, '').trim() + .replace(/^return /,'').replace(/\s[\r\n\t\s]+/g, ' '); + message += ' ' + JSON.stringify(assertion); + try { + truthy = truthy(); + } catch (e) { + message += '(exception: ' + e +')'; + } + } + if (!truthy) { + message += ' ('+truthy+')'; + throw new Error(message); + } + return truthy; +} + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +// model-helper.js +// +// Created by Timothy Dedischew on 06/01/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 +// + +/* eslint-env commonjs */ +/* global console */ +// @module model-helper +// +// This module provides ModelReadyWatcher (a helper class for knowing when a model becomes usable inworld) and +// also initial plumbing helpers to eliminate unnecessary API differences when working with Model Overlays and +// Model Entities at a high-level from scripting. + +var utils = __webpack_require__(0), + assert = utils.assert; + +module.exports = { + version: '0.0.1', + ModelReadyWatcher: ModelReadyWatcher +}; + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('model-helper | ' + [].slice.call(arguments).join(' ')); +} +log(module.exports.version); + +var _objectDeleted = utils.signal(function objectDeleted(objectID){}); +// proxy for _objectDeleted that only binds deletion tracking if script actually connects to the unified signal +var objectDeleted = utils.assign(function objectDeleted(objectID){}, { + connect: function() { + Overlays.overlayDeleted.connect(_objectDeleted); + // Entities.deletingEntity.connect(objectDeleted); + Script.scriptEnding.connect(function() { + Overlays.overlayDeleted.disconnect(_objectDeleted); + // Entities.deletingEntity.disconnect(objectDeleted); + }); + // hereafter _objectDeleted.connect will be used instead + this.connect = utils.bind(_objectDeleted, 'connect'); + return this.connect.apply(this, arguments); + }, + disconnect: utils.bind(_objectDeleted, 'disconnect') +}); + +var modelHelper = module.exports.modelHelper = { + // Entity <-> Overlay property translations + _entityFromOverlay: { + modelURL: function url() { + return this.url; + }, + dimensions: function dimensions() { + return Vec3.multiply(this.scale, this.naturalDimensions); + } + }, + _overlayFromEntity: { + url: function modelURL() { + return this.modelURL; + }, + scale: function scale() { + return this.dimensions && this.naturalDimensions && { + x: this.dimensions.x / this.naturalDimensions.x, + y: this.dimensions.y / this.naturalDimensions.y, + z: this.dimensions.z / this.naturalDimensions.z + }; + } + }, + objectDeleted: objectDeleted, + type: function(objectID) { + // TODO: support Model Entities (by detecting type from objectID, which is already possible) + return !Uuid.isNull(objectID) ? 'overlay' : null; + }, + addObject: function(properties) { + var type = 'overlay'; // this.resolveType(properties) + switch (type) { + case 'overlay': return Overlays.addOverlay(properties.type, this.toOverlayProps(properties)); + } + return false; + }, + editObject: function(objectID, properties) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.editOverlay(objectID, this.toOverlayProps(properties)); + } + return false; + }, + deleteObject: function(objectID) { + return this.type(objectID) === 'overlay' && Overlays.deleteOverlay(objectID); + }, + getProperty: function(objectID, propertyName) { + return this.type(objectID) === 'overlay' && Overlays.getProperty(objectID, propertyName); + }, + getProperties: function(objectID, filter) { + switch (this.type(objectID)) { + case 'overlay': + filter = Array.isArray(filter) ? filter : [ + 'position', 'rotation', 'localPosition', 'localRotation', 'parentID', + 'parentJointIndex', 'scale', 'name', 'visible', 'type', 'url', + 'dimensions', 'naturalDimensions', 'grabbable' + ]; + var properties = filter.reduce(function(out, propertyName) { + out[propertyName] = Overlays.getProperty(objectID, propertyName); + return out; + }, {}); + return this.toEntityProps(properties); + } + return null; + }, + // adapt Entity conventions to Overlay (eg: { modelURL: ... } -> { url: ... }) + toOverlayProps: function(properties) { + var result = {}; + for (var from in this._overlayFromEntity) { + var adapter = this._overlayFromEntity[from]; + result[from] = adapter.call(properties, from, adapter.name); + } + return utils.assign(result, properties); + }, + // adapt Overlay conventions to Entity (eg: { url: ... } -> { modelURL: ... }) + toEntityProps: function(properties) { + var result = {}; + for (var from in this._entityToOverlay) { + var adapter = this._entityToOverlay[from]; + result[from] = adapter.call(properties, from, adapter.name); + } + return utils.assign(result, properties); + }, + editObjects: function(updatedObjects) { + var objectIDs = Object.keys(updatedObjects), + type = this.type(objectIDs[0]); + switch (type) { + case 'overlay': + var translated = {}; + for (var objectID in updatedObjects) { + translated[objectID] = this.toOverlayProps(updatedObjects[objectID]); + } + return Overlays.editOverlays(translated); + } + return false; + }, + getJointIndex: function(objectID, name) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointNames').indexOf(name); + } + return -1; + }, + getJointNames: function(objectID) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointNames'); + } + return []; + }, + // @function - derives mirrored joint names from a list of regular joint names + // @param {Array} - list of joint names to mirror + // @return {Array} - list of mirrored joint names (note: entries for non-mirrored joints will be `undefined`) + deriveMirroredJointNames: function(jointNames) { + return jointNames.map(function(name, i) { + if (/Left/.test(name)) { + return name.replace('Left', 'Right'); + } + if (/Right/.test(name)) { + return name.replace('Right', 'Left'); + } + return undefined; + }); + }, + getJointPosition: function(objectID, index) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointPositions')[index]; + } + return Vec3.ZERO; + }, + getJointPositions: function(objectID) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointPositions'); + } + return []; + }, + getJointOrientation: function(objectID, index) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointOrientations')[index]; + } + return Quat.normalize({}); + }, + getJointOrientations: function(objectID) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointOrientations'); + } + }, + getJointTranslation: function(objectID, index) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointTranslations')[index]; + } + return Vec3.ZERO; + }, + getJointTranslations: function(objectID) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointTranslations'); + } + return []; + }, + getJointRotation: function(objectID, index) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointRotations')[index]; + } + return Quat.normalize({}); + }, + getJointRotations: function(objectID) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointRotations'); + } + return []; + } +}; // modelHelper + + +// @property {PreconditionFunction} - indicates when the model's jointNames have become available +ModelReadyWatcher.JOINTS = function(state) { + return Array.isArray(state.jointNames); +}; +// @property {PreconditionFunction} - indicates when a model's naturalDimensions have become available +ModelReadyWatcher.DIMENSIONS = function(state) { + return Vec3.length(state.naturalDimensions) > 0; +}; +// @property {PreconditionFunction} - indicates when both a model's naturalDimensions and jointNames have become available +ModelReadyWatcher.JOINTS_AND_DIMENSIONS = function(state) { + // eslint-disable-next-line new-cap + return ModelReadyWatcher.JOINTS(state) && ModelReadyWatcher.DIMENSIONS(state); +}; +// @property {int} - interval used for continually rechecking model readiness, until ready or a timeout occurs +ModelReadyWatcher.RECHECK_MS = 50; +// @property {int} - default wait time before considering a model unready-able. +ModelReadyWatcher.DEFAULT_TIMEOUT_SECS = 10; + +// @private @class - waits for model to become usable inworld and tracks errors/timeouts +// @param [Object} options -- key/value config options: +// @param {ModelResource} options.resource - a ModelCache prefetched resource to observe for determining load state +// @param {Uuid} options.objectID - an inworld object to observe for determining readiness states +// @param {Function} [options.precondition=ModelReadyWatcher.JOINTS] - the precondition used to determine if the model is usable +// @param {int} [options.maxWaitSeconds=10] - max seconds to wait for the model to become usable, after which a timeout error is emitted +// @return {ModelReadyWatcher} +function ModelReadyWatcher(options) { + options = utils.assign({ + precondition: ModelReadyWatcher.JOINTS, + maxWaitSeconds: ModelReadyWatcher.DEFAULT_TIMEOUT_SECS + }, options); + + assert(!Uuid.isNull(options.objectID), 'Error: invalid options.objectID'); + assert(options.resource && 'state' in options.resource, 'Error: invalid options.resource'); + assert(typeof options.precondition === 'function', 'Error: invalid options.precondition'); + + utils.assign(this, { + resource: options.resource, + objectID: options.objectID, + precondition: options.precondition, + + // @signal - emitted when the model becomes ready, or with the error that prevented it + modelReady: utils.signal(function modelReady(error, result){}), + + // @public + ready: false, // tracks readiness state + jointNames: null, // populated with detected jointNames + naturalDimensions: null, // populated with detected naturalDimensions + + _startTime: new Date, + _watchdogTimer: Script.setTimeout(utils.bind(this, function() { + this._watchdogTimer = null; + }), options.maxWaitSeconds * 1000), + _interval: Script.setInterval(utils.bind(this, '_waitUntilReady'), ModelReadyWatcher.RECHECK_MS) + }); + + this.modelReady.connect(this, function(error, result) { + this.ready = !error && result; + }); +} + +ModelReadyWatcher.prototype = { + contructor: ModelReadyWatcher, + // @public method -- cancels monitoring and (if model was not yet ready) emits an error + cancel: function() { + return this._stop() && !this.ready && this.modelReady('cancelled', null); + }, + // stop pending timers + _stop: function() { + var stopped = 0; + if (this._watchdogTimer) { + Script.clearTimeout(this._watchdogTimer); + this._watchdogTimer = null; + stopped++; + } + if (this._interval) { + Script.clearInterval(this._interval); + this._interval = null; + stopped++; + } + return stopped; + }, + // the monitoring thread func + _waitUntilReady: function() { + var error = null, result = null; + if (!this._watchdogTimer) { + error = this.precondition.name || 'timeout'; + } else if (this.resource.state === Resource.State.FAILED) { + error = 'prefetch_failed'; + } else if (this.resource.state === Resource.State.FINISHED) { + // in theory there will always be at least one "joint name" that represents the main submesh + var names = modelHelper.getJointNames(this.objectID); + if (Array.isArray(names) && names.length) { + this.jointNames = names; + } + var props = modelHelper.getProperties(this.objectID, ['naturalDimensions']); + if (props && props.naturalDimensions && Vec3.length(props.naturalDimensions)) { + this.naturalDimensions = props.naturalDimensions; + } + var state = { + resource: this.resource, + objectID: this.objectID, + waitTime: (new Date - this._startTime) / 1000, + jointNames: this.jointNames, + naturalDimensions: this.naturalDimensions + }; + if (this.precondition(state)) { + result = state; + } + } + if (error || result !== null) { + this._stop(); + this.modelReady(error, result); + } + } +}; // ModelReadyWatcher.prototype + + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +// doppleganger.js +// +// Created by Timothy Dedischew on 04/21/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 +// + +/* eslint-env commonjs */ +/* global console */ +// @module doppleganger +// +// This module contains the `Doppleganger` class implementation for creating an inspectable replica of +// an Avatar (as a model directly in front of and facing them). Joint positions and rotations are copied +// over in an update thread, so that the model automatically mirrors the Avatar's joint movements. +// An Avatar can then for example walk around "themselves" and examine from the back, etc. +// +// This should be helpful for inspecting your own look and debugging avatars, etc. +// +// The doppleganger is created as an overlay so that others do not see it -- and this also allows for the +// highest possible update rate when keeping joint data in sync. + +module.exports = Doppleganger; + +Doppleganger.version = '0.0.1a'; +log(Doppleganger.version); + +var _modelHelper = __webpack_require__(1), + modelHelper = _modelHelper.modelHelper, + ModelReadyWatcher = _modelHelper.ModelReadyWatcher, + utils = __webpack_require__(0); + +// @property {bool} - toggle verbose debug logging on/off +Doppleganger.WANT_DEBUG = false; + +// @property {bool} - when set true, Script.update will be used instead of setInterval for syncing joint data +Doppleganger.USE_SCRIPT_UPDATE = false; + +// @property {int} - the frame rate to target when using setInterval for joint updates +Doppleganger.TARGET_FPS = 60; + +// @class Doppleganger - Creates a new instance of a Doppleganger. +// @param {Avatar} [options.avatar=MyAvatar] - Avatar used to retrieve position and joint data. +// @param {bool} [options.mirrored=true] - Apply "symmetric mirroring" of Left/Right joints. +// @param {bool} [options.autoUpdate=true] - Automatically sync joint data. +function Doppleganger(options) { + this.options = options = options || {}; + this.avatar = options.avatar || MyAvatar; + this.mirrored = 'mirrored' in options ? options.mirrored : true; + this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true; + + // @public + this.active = false; // whether doppleganger is currently being displayed/updated + this.objectID = null; // current doppleganger's Overlay or Entity id + this.frame = 0; // current joint update frame + + // @signal - emitted when .active state changes + this.activeChanged = utils.signal(function(active, reason) {}); + // @signal - emitted once model is either loaded or errors out + this.modelLoaded = utils.signal(function(error, result){}); + // @signal - emitted each time the model's joint data has been synchronized + this.jointsUpdated = utils.signal(function(objectID){}); +} + +Doppleganger.prototype = { + // @public @method - toggles doppleganger on/off + toggle: function() { + if (this.active) { + debugPrint('toggling off'); + this.stop(); + } else { + debugPrint('toggling on'); + this.start(); + } + return this.active; + }, + + // @public @method - synchronize the joint data between Avatar / doppleganger + update: function() { + this.frame++; + try { + if (!this.objectID) { + throw new Error('!this.objectID'); + } + + if (this.avatar.skeletonModelURL !== this.skeletonModelURL) { + return this.stop('avatar_changed'); + } + + var rotations = this.avatar.getJointRotations(); + var translations = this.avatar.getJointTranslations(); + var size = rotations.length; + + // note: this mismatch can happen when the avatar's model is actively changing + if (size !== translations.length || + (this.jointStateCount && size !== this.jointStateCount)) { + debugPrint('mismatched joint counts (avatar model likely changed)', size, translations.length, this.jointStateCount); + this.stop('avatar_changed_joints'); + return; + } + this.jointStateCount = size; + + if (this.mirrored) { + var mirroredIndexes = this.mirroredIndexes; + var outRotations = new Array(size); + var outTranslations = new Array(size); + for (var i=0; i < size; i++) { + var index = mirroredIndexes[i]; + if (index < 0 || index === false) { + index = i; + } + var rot = rotations[index]; + var trans = translations[index]; + trans.x *= -1; + rot.y *= -1; + rot.z *= -1; + outRotations[i] = rot; + outTranslations[i] = trans; + } + rotations = outRotations; + translations = outTranslations; + } + var jointUpdates = { + jointRotations: rotations, + jointTranslations: translations + }; + modelHelper.editObject(this.objectID, jointUpdates); + this.jointsUpdated(this.objectID, jointUpdates); + } catch (e) { + log('.update error: '+ e, index, e.stack); + this.stop('update_error'); + throw e; + } + }, + + // @public @method - show the doppleganger (and start the update thread, if options.autoUpdate was specified). + // @param {vec3} [options.position=(in front of avatar)] - starting position + // @param {quat} [options.orientation=avatar.orientation] - starting orientation + start: function(options) { + options = utils.assign(this.options, options); + if (this.objectID) { + log('start() called but object model already exists', this.objectID); + return; + } + var avatar = this.avatar; + if (!avatar.jointNames.length) { + return this.stop('joints_unavailable'); + } + + this.frame = 0; + var localPosition = Vec3.multiply(2, Quat.getForward(avatar.orientation)); + this.position = options.position || Vec3.sum(avatar.position, localPosition); + this.orientation = options.orientation || avatar.orientation; + this.skeletonModelURL = avatar.skeletonModelURL; + this.scale = avatar.scale || 1.0; + this.jointStateCount = 0; + this.jointNames = avatar.jointNames; + this.type = options.type || 'overlay'; + this.mirroredNames = modelHelper.deriveMirroredJointNames(this.jointNames); + this.mirroredIndexes = this.mirroredNames.map(function(name) { + return name ? avatar.getJointIndex(name) : false; + }); + + this.objectID = modelHelper.addObject({ + type: this.type === 'overlay' ? 'model' : 'Model', + modelURL: this.skeletonModelURL, + position: this.position, + rotation: this.orientation, + scale: this.scale + }); + Script.scriptEnding.connect(this, function() { + modelHelper.deleteObject(this.objectID); + }); + debugPrint('doppleganger created; objectID =', this.objectID); + + // trigger clean up (and stop updates) if the object gets deleted + this.onObjectDeleted = function(uuid) { + if (uuid === this.objectID) { + log('onObjectDeleted', uuid); + this.stop('object_deleted'); + } + }; + modelHelper.objectDeleted.connect(this, 'onObjectDeleted'); + + if ('onLoadComplete' in avatar) { + // stop the current doppleganger if Avatar loads a different model URL + this.onLoadComplete = function() { + if (avatar.skeletonModelURL !== this.skeletonModelURL) { + this.stop('avatar_changed_load'); + } + }; + avatar.onLoadComplete.connect(this, 'onLoadComplete'); + } + + this.onModelLoaded = function(error, result) { + if (error) { + return this.stop(error); + } + debugPrint('model ('+modelHelper.type(this.objectID)+')' + ' is ready; # joints == ' + result.jointNames.length); + var naturalDimensions = this.naturalDimensions = modelHelper.getProperties(this.objectID, ['naturalDimensions']).naturalDimensions; + debugPrint('naturalDimensions:', JSON.stringify(naturalDimensions)); + var props = { visible: true }; + if (naturalDimensions) { + props.dimensions = this.dimensions = Vec3.multiply(this.scale, naturalDimensions); + } + debugPrint('scaledDimensions:', this.scale, JSON.stringify(props.dimensions)); + modelHelper.editObject(this.objectID, props); + if (!options.position) { + this.syncVerticalPosition(); + } + if (this.autoUpdate) { + this._createUpdateThread(); + } + }; + + this._resource = ModelCache.prefetch(this.skeletonModelURL); + this._modelReadier = new ModelReadyWatcher({ + resource: this._resource, + objectID: this.objectID + }); + this._modelReadier.modelReady.connect(this, 'onModelLoaded'); + this.activeChanged(this.active = true, 'start'); + }, + + // @public @method - hide the doppleganger + // @param {String} [reason=stop] - the reason stop was called + stop: function(reason) { + reason = reason || 'stop'; + if (this.onUpdate) { + Script.update.disconnect(this, 'onUpdate'); + delete this.onUpdate; + } + if (this._interval) { + Script.clearInterval(this._interval); + this._interval = undefined; + } + if (this.onObjectDeleted) { + modelHelper.objectDeleted.disconnect(this, 'onObjectDeleted'); + delete this.onObjectDeleted; + } + if (this.onLoadComplete) { + this.avatar.onLoadComplete.disconnect(this, 'onLoadComplete'); + delete this.onLoadComplete; + } + if (this.onModelLoaded) { + this._modelReadier && this._modelReadier.modelReady.disconnect(this, 'onModelLoaded'); + this._modelReadier = this.onModelLoaded = undefined; + } + if (this.objectID) { + modelHelper.deleteObject(this.objectID); + this.objectID = undefined; + } + if (this.active) { + this.activeChanged(this.active = false, reason); + } else if (reason) { + debugPrint('already stopped so not triggering another activeChanged; latest reason was:', reason); + } + }, + // @public @method - Reposition the doppleganger so it sees "eye to eye" with the Avatar. + // @param {String} [byJointName=Hips] - the reference joint used to align the Doppleganger and Avatar + syncVerticalPosition: function(byJointName) { + byJointName = byJointName || 'Hips'; + var positions = modelHelper.getJointPositions(this.objectID), + properties = modelHelper.getProperties(this.objectID), + dopplePosition = properties.position, + doppleJointIndex = modelHelper.getJointIndex(this.objectID, byJointName),// names.indexOf(byJointName), + doppleJointPosition = positions[doppleJointIndex]; + + debugPrint('........... doppleJointPosition', JSON.stringify({ + byJointName: byJointName, + dopplePosition: dopplePosition, + doppleJointIndex: doppleJointIndex, + doppleJointPosition: doppleJointPosition, + properties: properties.type, + positions: positions[0] + },0,2)); + var avatarPosition = this.avatar.position, + avatarJointIndex = this.avatar.getJointIndex(byJointName), + avatarJointPosition = this.avatar.getJointPosition(avatarJointIndex); + + var offset = (avatarJointPosition.y - doppleJointPosition.y); + debugPrint('adjusting for offset', offset); + if (properties.type === 'model') { + dopplePosition.y = avatarPosition.y + offset; + } else { + dopplePosition.y = avatarPosition.y - offset; + } + + this.position = dopplePosition; + modelHelper.editObject(this.objectID, { position: this.position }); + }, + + // @private @method - creates the update thread to synchronize joint data + _createUpdateThread: function() { + if (Doppleganger.USE_SCRIPT_UPDATE) { + debugPrint('creating Script.update thread'); + this.onUpdate = this.update; + Script.update.connect(this, 'onUpdate'); + } else { + debugPrint('creating Script.setInterval thread @ ~', Doppleganger.TARGET_FPS +'fps'); + var timeout = 1000 / Doppleganger.TARGET_FPS; + this._interval = Script.setInterval(utils.bind(this, 'update'), timeout); + } + } + +}; + +// @function - debug logging +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('doppleganger | ' + [].slice.call(arguments).join(' ')); +} + +function debugPrint() { + Doppleganger.WANT_DEBUG && log.apply(this, arguments); +} + + +/***/ }), +/* 3 */ +/***/ (function(module, exports, __webpack_require__) { + +// doppleganger-app.js +// +// Created by Timothy Dedischew on 04/21/2017. +// Copyright 2017 High Fidelity, Inc. +// +// This Client script creates an instance of a Doppleganger that can be toggled on/off via tablet button. +// (for more info see doppleganger.js) +// +// 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 commonjs */ +/* global DriveKeys, require:true, console */ +/* eslint-disable comma-dangle */ + +// decomment next line for automatic require cache-busting +// var require = function require(id) { return Script.require(id + '?'+new Date().getTime().toString(36)); }; +if (false) { + require = Script.require; +} + +var VERSION = '0.0.0'; +var WANT_DEBUG = false; +var DEBUG_MOVE_AS_YOU_MOVE = false; +var ROTATE_AS_YOU_MOVE = false; + +log(VERSION); + +var DopplegangerClass = __webpack_require__(2), + DopplegangerAttachments = __webpack_require__(4), + DebugControls = __webpack_require__(5), + modelHelper = __webpack_require__(1).modelHelper, + utils = __webpack_require__(0); + +// eslint-disable-next-line camelcase +var isWebpack = typeof __webpack_require__ === 'function'; + +var buttonConfig = utils.assign({ + text: 'MIRROR', +}, !isWebpack ? { + icon: Script.resolvePath('./doppleganger-i.svg'), + activeIcon: Script.resolvePath('./doppleganger-a.svg'), +} : { + icon: __webpack_require__(6), + activeIcon: __webpack_require__(7), +}); + +var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'), + button = tablet.addButton(buttonConfig); + +Script.scriptEnding.connect(function() { + tablet.removeButton(button); + button = null; +}); + +var doppleganger = new DopplegangerClass({ + avatar: MyAvatar, + mirrored: false, + autoUpdate: true, + type: 'overlay' +}); + +// add support for displaying regular (non-soft) attachments on the doppleganger +{ + var RECHECK_ATTACHMENT_MS = 1000; + var dopplegangerAttachments = new DopplegangerAttachments(doppleganger), + attachmentInterval = null, + lastHash = dopplegangerAttachments.getAttachmentsHash(); + + // monitor for attachment changes, but only when the doppleganger is active + doppleganger.activeChanged.connect(function(active, reason) { + if (attachmentInterval) { + Script.clearInterval(attachmentInterval); + } + if (active) { + attachmentInterval = Script.setInterval(checkAttachmentsHash, RECHECK_ATTACHMENT_MS); + } else { + attachmentInterval = null; + } + }); + function checkAttachmentsHash() { + var currentHash = dopplegangerAttachments.getAttachmentsHash(); + if (currentHash !== lastHash) { + lastHash = currentHash; + debugPrint('app-doppleganger | detect attachment change'); + dopplegangerAttachments.refreshAttachments(); + } + } +} + +// add support for "move as you move" +{ + var movementKeys = 'W,A,S,D,Up,Down,Right,Left'.split(','); + var controllerKeys = 'LX,LY,RY'.split(','); + var translationKeys = Object.keys(DriveKeys).filter(function(p) { + return /translate/i.test(p); + }); + var startingPosition; + + // returns an array of any active driving keys (eg: ['W', 'TRANSLATE_Z']) + function currentDrivers() { + return [].concat( + movementKeys.map(function(key) { + return Controller.getValue(Controller.Hardware.Keyboard[key]) && key; + }) + ).concat( + controllerKeys.map(function(key) { + return Controller.getValue(Controller.Standard[key]) !== 0.0 && key; + }) + ).concat( + translationKeys.map(function(key) { + return MyAvatar.getRawDriveKey(DriveKeys[key]) !== 0.0 && key; + }) + ).filter(Boolean); + } + + doppleganger.jointsUpdated.connect(function(objectID) { + var drivers = currentDrivers(), + isDriving = drivers.length > 0; + if (isDriving) { + if (startingPosition) { + debugPrint('resetting startingPosition since drivers == ', drivers.join('|')); + startingPosition = null; + } + } else if (HMD.active || DEBUG_MOVE_AS_YOU_MOVE) { + startingPosition = startingPosition || MyAvatar.position; + var movement = Vec3.subtract(MyAvatar.position, startingPosition); + startingPosition = MyAvatar.position; + // Vec3.length(movement) > 0.0001 && Vec3.print('+avatarMovement', movement); + + // "mirror" the relative translation vector + movement.x *= -1; + movement.z *= -1; + var props = {}; + props.position = doppleganger.position = Vec3.sum(doppleganger.position, movement); + if (ROTATE_AS_YOU_MOVE) { + props.rotation = doppleganger.orientation = MyAvatar.orientation; + } + modelHelper.editObject(doppleganger.objectID, props); + } + }); +} + +// hide the doppleganger if this client script is unloaded +Script.scriptEnding.connect(doppleganger, 'stop'); + +// hide the doppleganger if the user switches domains (which might place them arbitrarily far away in world space) +function onDomainChanged() { + if (doppleganger.active) { + doppleganger.stop('domain_changed'); + } +} +Window.domainChanged.connect(onDomainChanged); +Window.domainConnectionRefused.connect(onDomainChanged); +Script.scriptEnding.connect(function() { + Window.domainChanged.disconnect(onDomainChanged); + Window.domainConnectionRefused.disconnect(onDomainChanged); +}); + +// toggle on/off via tablet button +button.clicked.connect(doppleganger, 'toggle'); + +// highlight tablet button based on current doppleganger state +doppleganger.activeChanged.connect(function(active, reason) { + if (button) { + button.editProperties({ isActive: active }); + debugPrint('doppleganger.activeChanged', active, reason); + } +}); + +// alert the user if there was an error applying their skeletonModelURL +doppleganger.modelLoaded.connect(function(error, result) { + if (doppleganger.active && error) { + Window.alert('doppleganger | ' + error + '\n' + doppleganger.skeletonModelURL); + } +}); + +// ---------------------------------------------------------------------------- + +// add debug indicators, but only if the user has configured the settings value +if (Settings.getValue('debug.doppleganger', false)) { + WANT_DEBUG = WANT_DEBUG || true; + DopplegangerClass.WANT_DEBUG = WANT_DEBUG; + DopplegangerAttachments.WANT_DEBUG = WANT_DEBUG; + new DebugControls(doppleganger); +} + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('app-doppleganger | ' + [].slice.call(arguments).join(' ')); +} + +function debugPrint() { + WANT_DEBUG && log.apply(this, arguments); +} + +// ---------------------------------------------------------------------------- + +UserActivityLogger.logAction('doppleganger_app_load'); +doppleganger.activeChanged.connect(function(active, reason) { + if (active) { + UserActivityLogger.logAction('doppleganger_enable'); + } else { + if (reason === 'stop') { + // user intentionally toggled the doppleganger + UserActivityLogger.logAction('doppleganger_disable'); + } else { + debugPrint('doppleganger stopped:', reason); + UserActivityLogger.logAction('doppleganger_autodisable', { reason: reason }); + } + } +}); +dopplegangerAttachments.attachmentsUpdated.connect(function(attachments) { + UserActivityLogger.logAction('doppleganger_attachments', { count: attachments.length }); +}); + + + +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* eslint-env commonjs */ +/* eslint-disable comma-dangle */ +/* global console */ + +// var require = function(id) { return Script.require(id + '?'+new Date().getTime().toString(36)); } +module.exports = DopplegangerAttachments; + +DopplegangerAttachments.version = '0.0.1b'; +DopplegangerAttachments.WANT_DEBUG = false; + +var _modelHelper = __webpack_require__(1), + modelHelper = _modelHelper.modelHelper, + ModelReadyWatcher = _modelHelper.ModelReadyWatcher, + utils = __webpack_require__(0); + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('doppleganger-attachments | ' + [].slice.call(arguments).join(' ')); +} +log(DopplegangerAttachments.version); + +function debugPrint() { + DopplegangerAttachments.WANT_DEBUG && log.apply(this, arguments); +} + +function DopplegangerAttachments(doppleganger, options) { + utils.assign(this, { + _options: options, + doppleganger: doppleganger, + attachments: undefined, + manualJointSync: true, + attachmentsUpdated: utils.signal(function attachmentsUpdated(currentAttachments, previousAttachments){}), + }); + this._initialize(); + debugPrint('DopplegangerAttachments...', JSON.stringify(options)); +} +DopplegangerAttachments.prototype = { + // "hash" the current attachments (so that changes can be detected) + getAttachmentsHash: function() { + return JSON.stringify(this.doppleganger.avatar.getAttachmentsVariant()); + }, + // create a pure Object copy of the current native attachments variant + _cloneAttachmentsVariant: function() { + return JSON.parse(JSON.stringify(this.doppleganger.avatar.getAttachmentsVariant())); + }, + // fetch and resolve attachments to include jointIndex and other relevant $metadata + _getResolvedAttachments: function() { + var attachments = this._cloneAttachmentsVariant(), + objectID = this.doppleganger.objectID; + function toString() { + return '[attachment #' + this.$index + ' ' + this.$path + ' @ ' + this.jointName + '{' + this.$jointIndex + '}]'; + } + return attachments.map(function(attachment, i) { + var jointIndex = modelHelper.getJointIndex(objectID, attachment.jointName), + path = (attachment.modelUrl+'').split(/[?#]/)[0].split('/').slice(-3).join('/'); + return Object.defineProperties(attachment, { + $hash: { value: JSON.stringify(attachment) }, + $index: { value: i }, + $jointIndex: { value: jointIndex }, + $path: { value: path }, + toString: { value: toString }, + }); + }); + }, + // compare before / after attachment sets to determine which ones need to be (re)created + refreshAttachments: function() { + if (!this.doppleganger.objectID) { + return log('refreshAttachments -- canceling; !this.doppleganger.objectID'); + } + var before = this.attachments || [], + beforeIndex = before.reduce(function(out, att, index) { + out[att.$hash] = index; return out; + }, {}); + var after = this._getResolvedAttachments(), + afterIndex = after.reduce(function(out, att, index) { + out[att.$hash] = index; return out; + }, {}); + + Object.keys(beforeIndex).concat(Object.keys(afterIndex)).forEach(function(hash) { + if (hash in beforeIndex && hash in afterIndex) { + // print('reusing previous attachment', hash); + after[afterIndex[hash]] = before[beforeIndex[hash]]; + } else if (!(hash in afterIndex)) { + var attachment = before[beforeIndex[hash]]; + attachment.properties && attachment.properties.objectID && + modelHelper.deleteObject(attachment.properties.objectID); + delete attachment.properties; + } + }); + this.attachments = after; + this._createAttachmentObjects(); + this.attachmentsUpdated(after, before); + }, + _createAttachmentObjects: function() { + try { + var attachments = this.attachments, + parentID = this.doppleganger.objectID, + jointNames = this.doppleganger.jointNames, + properties = modelHelper.getProperties(this.doppleganger.objectID), + modelType = properties && properties.type; + utils.assert(modelType === 'model' || modelType === 'Model', 'unrecognized doppleganger modelType:' + modelType); + debugPrint('DopplegangerAttachments..._createAttachmentObjects', JSON.stringify({ + modelType: modelType, + attachments: attachments.length, + parentID: parentID, + jointNames: jointNames.join(' | '), + },0,2)); + return attachments.map(utils.bind(this, function(attachment, i) { + var objectType = modelHelper.type(attachment.properties && attachment.properties.objectID); + if (objectType === 'overlay') { + debugPrint('skipping already-provisioned attachment object', objectType, attachment.properties && attachment.properties.name); + return attachment; + } + var jointIndex = attachment.$jointIndex, // jointNames.indexOf(attachment.jointName), + scale = this.doppleganger.avatar.scale * (attachment.scale||1.0); + + attachment.properties = utils.assign({ + name: attachment.toString(), + type: modelType, + modelURL: attachment.modelUrl, + scale: scale, + dimensions: modelHelper.type(parentID) === 'entity' ? + Vec3.multiply(attachment.scale||1.0, Vec3.ONE) : undefined, + visible: false, + collisionless: true, + dynamic: false, + shapeType: 'none', + lifetime: 60, + }, !this.manualJointSync && { + parentID: parentID, + parentJointIndex: jointIndex, + localPosition: attachment.translation, + localRotation: Quat.fromVec3Degrees(attachment.rotation), + }); + var objectID = attachment.properties.objectID = modelHelper.addObject(attachment.properties); + utils.assert(!Uuid.isNull(objectID), 'could not create attachment: ' + [objectID, JSON.stringify(attachment.properties,0,2)]); + attachment._resource = ModelCache.prefetch(attachment.properties.modelURL); + attachment._modelReadier = new ModelReadyWatcher({ + resource: attachment._resource, + objectID: objectID, + }); + this.doppleganger.activeChanged.connect(attachment._modelReadier, '_stop'); + + attachment._modelReadier.modelReady.connect(this, function(err, result) { + if (err) { + log('>>>>> modelReady ERROR <<<<<: ' + err, attachment.properties.modelURL); + modelHelper.deleteObject(objectID); + return objectID = null; + } + debugPrint('attachment model ('+modelHelper.type(result.objectID)+') is ready; # joints ==', + result.jointNames && result.jointNames.length, JSON.stringify(result.naturalDimensions), result.objectID); + var properties = modelHelper.getProperties(result.objectID), + naturalDimensions = attachment.properties.naturalDimensions = properties.naturalDimensions || result.naturalDimensions; + modelHelper.editObject(objectID, { + dimensions: naturalDimensions ? Vec3.multiply(attachment.scale, naturalDimensions) : undefined, + localRotation: Quat.normalize({}), + localPosition: Vec3.ZERO, + }); + this.onJointsUpdated(parentID); // trigger once to initialize position/rotation + // give time for model overlay to "settle", then make it visible + Script.setTimeout(utils.bind(this, function() { + modelHelper.editObject(objectID, { + visible: true, + }); + attachment._loaded = true; + }), 100); + }); + return attachment; + })); + } catch (e) { + log('_createAttachmentObjects ERROR:', e.stack || e, e.fileName, e.lineNumber); + } + }, + + _removeAttachmentObjects: function() { + if (this.attachments) { + this.attachments.forEach(function(attachment) { + if (attachment.properties) { + if (attachment.properties.objectID) { + modelHelper.deleteObject(attachment.properties.objectID); + } + delete attachment.properties.objectID; + } + }); + delete this.attachments; + } + }, + + onJointsUpdated: function onJointsUpdated(objectID, jointUpdates) { + var jointOrientations = modelHelper.getJointOrientations(objectID), + jointPositions = modelHelper.getJointPositions(objectID), + parentID = objectID, + avatarScale = this.doppleganger.scale, + manualJointSync = this.manualJointSync; + + if (!this.attachments) { + this.refreshAttachments(); + } + var updatedObjects = this.attachments.reduce(function(updates, attachment, i) { + if (!attachment.properties || !attachment._loaded) { + return updates; + } + var objectID = attachment.properties.objectID, + jointIndex = attachment.$jointIndex, + jointOrientation = jointOrientations[jointIndex], + jointPosition = jointPositions[jointIndex]; + + var translation = Vec3.multiply(avatarScale, attachment.translation), + rotation = Quat.fromVec3Degrees(attachment.rotation); + + var localPosition = Vec3.multiplyQbyV(jointOrientation, translation), + localRotation = rotation; + + updates[objectID] = manualJointSync ? { + visible: true, + position: Vec3.sum(jointPosition, localPosition), + rotation: Quat.multiply(jointOrientation, localRotation), + scale: avatarScale * attachment.scale, + } : { + visible: true, + parentID: parentID, + parentJointIndex: jointIndex, + localRotation: localRotation, + localPosition: localPosition, + scale: attachment.scale, + }; + return updates; + }, {}); + modelHelper.editObjects(updatedObjects); + }, + + _initialize: function() { + var doppleganger = this.doppleganger; + if ('$attachmentControls' in doppleganger) { + throw new Error('only one set of attachment controls can be added per doppleganger'); + } + doppleganger.$attachmentControls = this; + doppleganger.activeChanged.connect(this, function(active) { + if (active) { + doppleganger.jointsUpdated.connect(this, 'onJointsUpdated'); + } else { + doppleganger.jointsUpdated.disconnect(this, 'onJointsUpdated'); + this._removeAttachmentObjects(); + } + }); + + Script.scriptEnding.connect(this, '_removeAttachmentObjects'); + }, +}; + + +/***/ }), +/* 5 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +// -- ADVANCED DEBUGGING -- +// @function - Add debug joint indicators / extra debugging info. +// @param {Doppleganger} - existing Doppleganger instance to add controls to +// +// @note: +// * rightclick toggles mirror mode on/off +// * shift-rightclick toggles the debug indicators on/off +// * clicking on an indicator displays the joint name and mirrored joint name in the debug log. +// +// Example use: +// var doppleganger = new DopplegangerClass(); +// DopplegangerClass.addDebugControls(doppleganger); + + +/* eslint-env commonjs */ +/* eslint-disable comma-dangle */ +/* global console */ + +var DopplegangerClass = __webpack_require__(2), + modelHelper = __webpack_require__(1).modelHelper, + utils = __webpack_require__(0); + +module.exports = DebugControls; +// mixin addDebugControls to DopplegangerClass for backwards-compatibility +DopplegangerClass.addDebugControls = function(doppleganger) { + new DebugControls(doppleganger); + return doppleganger; +}; + +DebugControls.version = '0.0.0'; +DebugControls.COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; +DebugControls.COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('doppleganger-debug | ' + [].slice.call(arguments).join(' ')); +} + +function DebugControls(doppleganger) { + this.enableIndicators = true; + this.selectedJointName = null; + this.debugOverlayIDs = undefined; + this.jointSelected = utils.signal(function(result) {}); + this.doppleganger = doppleganger; + this._initialize(); +} +DebugControls.prototype = { + start: function() { + if (!this.onMousePressEvent) { + this.onMousePressEvent = this._onMousePressEvent; + Controller.mousePressEvent.connect(this, 'onMousePressEvent'); + this.doppleganger.jointsUpdated.connect(this, 'onJointsUpdated'); + } + }, + + stop: function() { + this.removeIndicators(); + if (this.onMousePressEvent) { + this.doppleganger.jointsUpdated.disconnect(this, 'onJointsUpdated'); + Controller.mousePressEvent.disconnect(this, 'onMousePressEvent'); + delete this.onMousePressEvent; + } + }, + + createIndicators: function(jointNames) { + this.jointNames = jointNames; + return jointNames.map(function(name, i) { + return Overlays.addOverlay('shape', { + shape: 'Icosahedron', + scale: 0.1, + solid: false, + alpha: 0.5 + }); + }); + }, + + removeIndicators: function() { + if (this.debugOverlayIDs) { + this.debugOverlayIDs.forEach(Overlays.deleteOverlay); + this.debugOverlayIDs = undefined; + } + }, + + onJointsUpdated: function(overlayID) { + if (!this.enableIndicators) { + return; + } + var jointNames = Overlays.getProperty(overlayID, 'jointNames'), + jointOrientations = Overlays.getProperty(overlayID, 'jointOrientations'), + jointPositions = Overlays.getProperty(overlayID, 'jointPositions'), + selectedIndex = jointNames.indexOf(this.selectedJointName); + + if (!this.debugOverlayIDs) { + this.debugOverlayIDs = this.createIndicators(jointNames); + } + + // batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API) + var updatedOverlays = this.debugOverlayIDs.reduce(function(updates, id, i) { + updates[id] = { + position: jointPositions[i], + rotation: jointOrientations[i], + color: i === selectedIndex ? DebugControls.COLOR_SELECTED : DebugControls.COLOR_DEFAULT, + solid: i === selectedIndex + }; + return updates; + }, {}); + Overlays.editOverlays(updatedOverlays); + }, + + _onMousePressEvent: function(evt) { + if (evt.isLeftButton) { + if (!this.enableIndicators || !this.debugOverlayIDs) { + return; + } + var ray = Camera.computePickRay(evt.x, evt.y), + hit = Overlays.findRayIntersection(ray, true, this.debugOverlayIDs); + + hit.jointIndex = this.debugOverlayIDs.indexOf(hit.overlayID); + hit.jointName = this.jointNames[hit.jointIndex]; + this.jointSelected(hit); + } else if (evt.isRightButton) { + if (evt.isShifted) { + this.enableIndicators = !this.enableIndicators; + if (!this.enableIndicators) { + this.removeIndicators(); + } + } else { + this.doppleganger.mirrored = !this.doppleganger.mirrored; + } + } + }, + + _initialize: function() { + if ('$debugControls' in this.doppleganger) { + throw new Error('only one set of debug controls can be added per doppleganger'); + } + this.doppleganger.$debugControls = this; + + this.doppleganger.activeChanged.connect(this, function(active) { + if (active) { + this.start(); + } else { + this.stop(); + } + }); + + this.jointSelected.connect(this, function(hit) { + this.selectedJointName = hit.jointName; + if (hit.jointIndex < 0) { + return; + } + hit.mirroredJointName = modelHelper.deriveMirroredJointNames([hit.jointName])[0]; + log('selected joint:', JSON.stringify(hit, 0, 2)); + }); + + Script.scriptEnding.connect(this, 'removeIndicators'); + }, +}; // DebugControls.prototype + + +/***/ }), +/* 6 */ +/***/ (function(module, exports) { + +module.exports = "data:image/svg+xml;xml,\n\n\nimage/svg+xml\n\t.st0{fill:#FFFFFF;}\n"; + +/***/ }), +/* 7 */ +/***/ (function(module, exports) { + +module.exports = "data:image/svg+xml;xml,\n\n\nimage/svg+xml\n\t.st0{fill:#FFFFFF;}\n"; + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-attachments.js b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-attachments.js index a3b3873c2d..7526f56511 100644 --- a/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-attachments.js +++ b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-attachments.js @@ -1,10 +1,12 @@ "use strict"; /* eslint-env commonjs */ /* eslint-disable comma-dangle */ +/* global console */ +// var require = function(id) { return Script.require(id + '?'+new Date().getTime().toString(36)); } module.exports = DopplegangerAttachments; -DopplegangerAttachments.version = '0.0.0'; +DopplegangerAttachments.version = '0.0.1b'; DopplegangerAttachments.WANT_DEBUG = false; var _modelHelper = require('./model-helper.js'), @@ -13,8 +15,10 @@ var _modelHelper = require('./model-helper.js'), utils = require('./utils.js'); function log() { - print('doppleganger-attachments | ' + [].slice.call(arguments).join(' ')); + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('doppleganger-attachments | ' + [].slice.call(arguments).join(' ')); } +log(DopplegangerAttachments.version); function debugPrint() { DopplegangerAttachments.WANT_DEBUG && log.apply(this, arguments); @@ -61,6 +65,9 @@ DopplegangerAttachments.prototype = { }, // compare before / after attachment sets to determine which ones need to be (re)created refreshAttachments: function() { + if (!this.doppleganger.objectID) { + return log('refreshAttachments -- canceling; !this.doppleganger.objectID'); + } var before = this.attachments || [], beforeIndex = before.reduce(function(out, att, index) { out[att.$hash] = index; return out; @@ -90,18 +97,19 @@ DopplegangerAttachments.prototype = { var attachments = this.attachments, parentID = this.doppleganger.objectID, jointNames = this.doppleganger.jointNames, - properties = modelHelper.getProperties(this.doppleganger.objectID); - + properties = modelHelper.getProperties(this.doppleganger.objectID), + modelType = properties && properties.type; + utils.assert(modelType === 'model' || modelType === 'Model', 'unrecognized doppleganger modelType:' + modelType); debugPrint('DopplegangerAttachments..._createAttachmentObjects', JSON.stringify({ - type: properties.type, + modelType: modelType, attachments: attachments.length, parentID: parentID, jointNames: jointNames.join(' | '), },0,2)); return attachments.map(utils.bind(this, function(attachment, i) { - var type = modelHelper.type(attachment.properties && attachment.properties.objectID); - if (type === 'overlay') { - debugPrint('skipping already-provisioned attachment object', type, attachment.properties && attachment.properties.name); + var objectType = modelHelper.type(attachment.properties && attachment.properties.objectID); + if (objectType === 'overlay') { + debugPrint('skipping already-provisioned attachment object', objectType, attachment.properties && attachment.properties.name); return attachment; } var jointIndex = attachment.$jointIndex, // jointNames.indexOf(attachment.jointName), @@ -109,7 +117,7 @@ DopplegangerAttachments.prototype = { attachment.properties = utils.assign({ name: attachment.toString(), - type: properties.type, + type: modelType, modelURL: attachment.modelUrl, scale: scale, dimensions: modelHelper.type(parentID) === 'entity' ? @@ -119,14 +127,16 @@ DopplegangerAttachments.prototype = { dynamic: false, shapeType: 'none', lifetime: 60, - grabbable: true, }, !this.manualJointSync && { parentID: parentID, parentJointIndex: jointIndex, + localPosition: attachment.translation, + localRotation: Quat.fromVec3Degrees(attachment.rotation), }); var objectID = attachment.properties.objectID = modelHelper.addObject(attachment.properties); + utils.assert(!Uuid.isNull(objectID), 'could not create attachment: ' + [objectID, JSON.stringify(attachment.properties,0,2)]); attachment._resource = ModelCache.prefetch(attachment.properties.modelURL); - attachment._modelReadier = new ModelReadyWatcher( { + attachment._modelReadier = new ModelReadyWatcher({ resource: attachment._resource, objectID: objectID, }); @@ -134,21 +144,23 @@ DopplegangerAttachments.prototype = { attachment._modelReadier.modelReady.connect(this, function(err, result) { if (err) { - log('>>>>> modelReady ERROR <<<<<: ' + err, attachment.modelUrl); + log('>>>>> modelReady ERROR <<<<<: ' + err, attachment.properties.modelURL); modelHelper.deleteObject(objectID); return objectID = null; } debugPrint('attachment model ('+modelHelper.type(result.objectID)+') is ready; # joints ==', - result.jointNames && result.jointNames.length, result.naturalDimensions, result.objectID); + result.jointNames && result.jointNames.length, JSON.stringify(result.naturalDimensions), result.objectID); var properties = modelHelper.getProperties(result.objectID), - naturalDimensions = attachment.properties.naturalDimensions = properties.naturalDimensions; - modelHelper.editObject(result.objectID, { + naturalDimensions = attachment.properties.naturalDimensions = properties.naturalDimensions || result.naturalDimensions; + modelHelper.editObject(objectID, { dimensions: naturalDimensions ? Vec3.multiply(attachment.scale, naturalDimensions) : undefined, + localRotation: Quat.normalize({}), + localPosition: Vec3.ZERO, }); this.onJointsUpdated(parentID); // trigger once to initialize position/rotation // give time for model overlay to "settle", then make it visible Script.setTimeout(utils.bind(this, function() { - modelHelper.editObject(result.objectID, { + modelHelper.editObject(objectID, { visible: true, }); attachment._loaded = true; @@ -175,7 +187,7 @@ DopplegangerAttachments.prototype = { } }, - onJointsUpdated: function onJointsUpdated(objectID) { + onJointsUpdated: function onJointsUpdated(objectID, jointUpdates) { var jointOrientations = modelHelper.getJointOrientations(objectID), jointPositions = modelHelper.getJointPositions(objectID), parentID = objectID, @@ -195,8 +207,9 @@ DopplegangerAttachments.prototype = { jointPosition = jointPositions[jointIndex]; var translation = Vec3.multiply(avatarScale, attachment.translation), - rotation = Quat.fromVec3Degrees(attachment.rotation), - localPosition = Vec3.multiplyQbyV(jointOrientation, translation), + rotation = Quat.fromVec3Degrees(attachment.rotation); + + var localPosition = Vec3.multiplyQbyV(jointOrientation, translation), localRotation = rotation; updates[objectID] = manualJointSync ? { @@ -212,7 +225,6 @@ DopplegangerAttachments.prototype = { localPosition: localPosition, scale: attachment.scale, }; - onJointsUpdated[objectID] = updates[objectID]; return updates; }, {}); modelHelper.editObjects(updatedObjects); @@ -221,7 +233,7 @@ DopplegangerAttachments.prototype = { _initialize: function() { var doppleganger = this.doppleganger; if ('$attachmentControls' in doppleganger) { - throw new Error('only one set of debug controls can be added per doppleganger'); + throw new Error('only one set of attachment controls can be added per doppleganger'); } doppleganger.$attachmentControls = this; doppleganger.activeChanged.connect(this, function(active) { diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-debug.js b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-debug.js new file mode 100644 index 0000000000..b0a83117fb --- /dev/null +++ b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-debug.js @@ -0,0 +1,158 @@ +// -- ADVANCED DEBUGGING -- +// @function - Add debug joint indicators / extra debugging info. +// @param {Doppleganger} - existing Doppleganger instance to add controls to +// +// @note: +// * rightclick toggles mirror mode on/off +// * shift-rightclick toggles the debug indicators on/off +// * clicking on an indicator displays the joint name and mirrored joint name in the debug log. +// +// Example use: +// var doppleganger = new DopplegangerClass(); +// DopplegangerClass.addDebugControls(doppleganger); + +"use strict"; +/* eslint-env commonjs */ +/* eslint-disable comma-dangle */ +/* global console */ + +var DopplegangerClass = require('./doppleganger.js'), + modelHelper = require('./model-helper.js').modelHelper, + utils = require('./utils.js'); + +module.exports = DebugControls; +// mixin addDebugControls to DopplegangerClass for backwards-compatibility +DopplegangerClass.addDebugControls = function(doppleganger) { + new DebugControls(doppleganger); + return doppleganger; +}; + +DebugControls.version = '0.0.0'; +DebugControls.COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; +DebugControls.COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('doppleganger-debug | ' + [].slice.call(arguments).join(' ')); +} + +function DebugControls(doppleganger) { + this.enableIndicators = true; + this.selectedJointName = null; + this.debugOverlayIDs = undefined; + this.jointSelected = utils.signal(function(result) {}); + this.doppleganger = doppleganger; + this._initialize(); +} +DebugControls.prototype = { + start: function() { + if (!this.onMousePressEvent) { + this.onMousePressEvent = this._onMousePressEvent; + Controller.mousePressEvent.connect(this, 'onMousePressEvent'); + this.doppleganger.jointsUpdated.connect(this, 'onJointsUpdated'); + } + }, + + stop: function() { + this.removeIndicators(); + if (this.onMousePressEvent) { + this.doppleganger.jointsUpdated.disconnect(this, 'onJointsUpdated'); + Controller.mousePressEvent.disconnect(this, 'onMousePressEvent'); + delete this.onMousePressEvent; + } + }, + + createIndicators: function(jointNames) { + this.jointNames = jointNames; + return jointNames.map(function(name, i) { + return Overlays.addOverlay('shape', { + shape: 'Icosahedron', + scale: 0.1, + solid: false, + alpha: 0.5 + }); + }); + }, + + removeIndicators: function() { + if (this.debugOverlayIDs) { + this.debugOverlayIDs.forEach(Overlays.deleteOverlay); + this.debugOverlayIDs = undefined; + } + }, + + onJointsUpdated: function(overlayID) { + if (!this.enableIndicators) { + return; + } + var jointNames = Overlays.getProperty(overlayID, 'jointNames'), + jointOrientations = Overlays.getProperty(overlayID, 'jointOrientations'), + jointPositions = Overlays.getProperty(overlayID, 'jointPositions'), + selectedIndex = jointNames.indexOf(this.selectedJointName); + + if (!this.debugOverlayIDs) { + this.debugOverlayIDs = this.createIndicators(jointNames); + } + + // batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API) + var updatedOverlays = this.debugOverlayIDs.reduce(function(updates, id, i) { + updates[id] = { + position: jointPositions[i], + rotation: jointOrientations[i], + color: i === selectedIndex ? DebugControls.COLOR_SELECTED : DebugControls.COLOR_DEFAULT, + solid: i === selectedIndex + }; + return updates; + }, {}); + Overlays.editOverlays(updatedOverlays); + }, + + _onMousePressEvent: function(evt) { + if (evt.isLeftButton) { + if (!this.enableIndicators || !this.debugOverlayIDs) { + return; + } + var ray = Camera.computePickRay(evt.x, evt.y), + hit = Overlays.findRayIntersection(ray, true, this.debugOverlayIDs); + + hit.jointIndex = this.debugOverlayIDs.indexOf(hit.overlayID); + hit.jointName = this.jointNames[hit.jointIndex]; + this.jointSelected(hit); + } else if (evt.isRightButton) { + if (evt.isShifted) { + this.enableIndicators = !this.enableIndicators; + if (!this.enableIndicators) { + this.removeIndicators(); + } + } else { + this.doppleganger.mirrored = !this.doppleganger.mirrored; + } + } + }, + + _initialize: function() { + if ('$debugControls' in this.doppleganger) { + throw new Error('only one set of debug controls can be added per doppleganger'); + } + this.doppleganger.$debugControls = this; + + this.doppleganger.activeChanged.connect(this, function(active) { + if (active) { + this.start(); + } else { + this.stop(); + } + }); + + this.jointSelected.connect(this, function(hit) { + this.selectedJointName = hit.jointName; + if (hit.jointIndex < 0) { + return; + } + hit.mirroredJointName = modelHelper.deriveMirroredJointNames([hit.jointName])[0]; + log('selected joint:', JSON.stringify(hit, 0, 2)); + }); + + Script.scriptEnding.connect(this, 'removeIndicators'); + }, +}; // DebugControls.prototype diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger.js b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger.js index bebd36df45..190a8aa69e 100644 --- a/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger.js +++ b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger.js @@ -10,6 +10,7 @@ // /* eslint-env commonjs */ +/* global console */ // @module doppleganger // // This module contains the `Doppleganger` class implementation for creating an inspectable replica of @@ -24,9 +25,13 @@ module.exports = Doppleganger; +Doppleganger.version = '0.0.1a'; +log(Doppleganger.version); + var _modelHelper = require('./model-helper.js'), modelHelper = _modelHelper.modelHelper, - ModelReadyWatcher = _modelHelper.ModelReadyWatcher; + ModelReadyWatcher = _modelHelper.ModelReadyWatcher, + utils = require('./utils.js'); // @property {bool} - toggle verbose debug logging on/off Doppleganger.WANT_DEBUG = false; @@ -48,16 +53,16 @@ function Doppleganger(options) { this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true; // @public - this.active = false; // whether doppleganger is currently being displayed/updated + this.active = false; // whether doppleganger is currently being displayed/updated this.objectID = null; // current doppleganger's Overlay or Entity id - this.frame = 0; // current joint update frame + this.frame = 0; // current joint update frame // @signal - emitted when .active state changes - this.activeChanged = signal(function(active, reason) {}); + this.activeChanged = utils.signal(function(active, reason) {}); // @signal - emitted once model is either loaded or errors out - this.modelLoaded = signal(function(error, result){}); + this.modelLoaded = utils.signal(function(error, result){}); // @signal - emitted each time the model's joint data has been synchronized - this.jointsUpdated = signal(function(objectID){}); + this.jointsUpdated = utils.signal(function(objectID){}); } Doppleganger.prototype = { @@ -118,11 +123,12 @@ Doppleganger.prototype = { rotations = outRotations; translations = outTranslations; } - modelHelper.editObject(this.objectID, { + var jointUpdates = { jointRotations: rotations, jointTranslations: translations - }); - this.jointsUpdated(this.objectID); + }; + modelHelper.editObject(this.objectID, jointUpdates); + this.jointsUpdated(this.objectID, jointUpdates); } catch (e) { log('.update error: '+ e, index, e.stack); this.stop('update_error'); @@ -134,7 +140,7 @@ Doppleganger.prototype = { // @param {vec3} [options.position=(in front of avatar)] - starting position // @param {quat} [options.orientation=avatar.orientation] - starting orientation start: function(options) { - options = assign(this.options, options); + options = utils.assign(this.options, options); if (this.objectID) { log('start() called but object model already exists', this.objectID); return; @@ -194,11 +200,11 @@ Doppleganger.prototype = { return this.stop(error); } debugPrint('model ('+modelHelper.type(this.objectID)+')' + ' is ready; # joints == ' + result.jointNames.length); - var naturalDimensions = modelHelper.getProperties(this.objectID, ['naturalDimensions']).naturalDimensions; + var naturalDimensions = this.naturalDimensions = modelHelper.getProperties(this.objectID, ['naturalDimensions']).naturalDimensions; debugPrint('naturalDimensions:', JSON.stringify(naturalDimensions)); var props = { visible: true }; if (naturalDimensions) { - props.dimensions = Vec3.multiply(this.scale, naturalDimensions); + props.dimensions = this.dimensions = Vec3.multiply(this.scale, naturalDimensions); } debugPrint('scaledDimensions:', this.scale, JSON.stringify(props.dimensions)); modelHelper.editObject(this.objectID, props); @@ -296,220 +302,18 @@ Doppleganger.prototype = { } else { debugPrint('creating Script.setInterval thread @ ~', Doppleganger.TARGET_FPS +'fps'); var timeout = 1000 / Doppleganger.TARGET_FPS; - this._interval = Script.setInterval(bind(this, 'update'), timeout); + this._interval = Script.setInterval(utils.bind(this, 'update'), timeout); } } }; -// @function - bind a function to a `this` context -// @param {Object} - the `this` context -// @param {Function|String} - function or method name -function bind(thiz, method) { - method = thiz[method] || method; - return function() { - return method.apply(thiz, arguments); - }; -} - -// @function - Qt signal polyfill -function signal(template) { - var callbacks = []; - return Object.defineProperties(function() { - var args = [].slice.call(arguments); - callbacks.forEach(function(obj) { - obj.handler.apply(obj.scope, args); - }); - }, { - connect: { value: function(scope, handler) { - var callback = {scope: scope, handler: scope[handler] || handler || scope}; - if (!callback.handler || !callback.handler.apply) { - throw new Error('invalid arguments to connect:' + [template, scope, handler]); - } - callbacks.push({scope: scope, handler: scope[handler] || handler || scope}); - }}, - disconnect: { value: function(scope, handler) { - var match = {scope: scope, handler: scope[handler] || handler || scope}; - callbacks = callbacks.filter(function(obj) { - return !(obj.scope === match.scope && obj.handler === match.handler); - }); - }} - }); -} - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill -/* eslint-disable */ -function assign(target, varArgs) { // .length of function is 2 - 'use strict'; - if (target == null) { // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - var to = Object(target); - - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; - - if (nextSource != null) { // Skip over if undefined or null - for (var nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - return to; -} -/* eslint-enable */ -// //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill - // @function - debug logging function log() { - print('doppleganger | ' + [].slice.call(arguments).join(' ')); + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('doppleganger | ' + [].slice.call(arguments).join(' ')); } function debugPrint() { Doppleganger.WANT_DEBUG && log.apply(this, arguments); } - -// -- ADVANCED DEBUGGING -- -// @function - Add debug joint indicators / extra debugging info. -// @param {Doppleganger} - existing Doppleganger instance to add controls to -// -// @note: -// * rightclick toggles mirror mode on/off -// * shift-rightclick toggles the debug indicators on/off -// * clicking on an indicator displays the joint name and mirrored joint name in the debug log. -// -// Example use: -// var doppleganger = new Doppleganger(); -// Doppleganger.addDebugControls(doppleganger); -Doppleganger.addDebugControls = function(doppleganger) { - DebugControls.COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; - DebugControls.COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; - - function DebugControls() { - this.enableIndicators = true; - this.selectedJointName = null; - this.debugOverlayIDs = undefined; - this.jointSelected = signal(function(result) {}); - } - DebugControls.prototype = { - start: function() { - if (!this.onMousePressEvent) { - this.onMousePressEvent = this._onMousePressEvent; - Controller.mousePressEvent.connect(this, 'onMousePressEvent'); - } - }, - - stop: function() { - this.removeIndicators(); - if (this.onMousePressEvent) { - Controller.mousePressEvent.disconnect(this, 'onMousePressEvent'); - delete this.onMousePressEvent; - } - }, - - createIndicators: function(jointNames) { - this.jointNames = jointNames; - return jointNames.map(function(name, i) { - return Overlays.addOverlay('shape', { - shape: 'Icosahedron', - scale: 0.1, - solid: false, - alpha: 0.5 - }); - }); - }, - - removeIndicators: function() { - if (this.debugOverlayIDs) { - this.debugOverlayIDs.forEach(Overlays.deleteOverlay); - this.debugOverlayIDs = undefined; - } - }, - - onJointsUpdated: function(overlayID) { - if (!this.enableIndicators) { - return; - } - var jointNames = Overlays.getProperty(overlayID, 'jointNames'), - jointOrientations = Overlays.getProperty(overlayID, 'jointOrientations'), - jointPositions = Overlays.getProperty(overlayID, 'jointPositions'), - selectedIndex = jointNames.indexOf(this.selectedJointName); - - if (!this.debugOverlayIDs) { - this.debugOverlayIDs = this.createIndicators(jointNames); - } - - // batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API) - var updatedOverlays = this.debugOverlayIDs.reduce(function(updates, id, i) { - updates[id] = { - position: jointPositions[i], - rotation: jointOrientations[i], - color: i === selectedIndex ? DebugControls.COLOR_SELECTED : DebugControls.COLOR_DEFAULT, - solid: i === selectedIndex - }; - return updates; - }, {}); - Overlays.editOverlays(updatedOverlays); - }, - - _onMousePressEvent: function(evt) { - if (!evt.isLeftButton || !this.enableIndicators || !this.debugOverlayIDs) { - return; - } - var ray = Camera.computePickRay(evt.x, evt.y), - hit = Overlays.findRayIntersection(ray, true, this.debugOverlayIDs); - - hit.jointIndex = this.debugOverlayIDs.indexOf(hit.overlayID); - hit.jointName = this.jointNames[hit.jointIndex]; - this.jointSelected(hit); - } - }; - - if ('$debugControls' in doppleganger) { - throw new Error('only one set of debug controls can be added per doppleganger'); - } - var debugControls = new DebugControls(); - doppleganger.$debugControls = debugControls; - - function onMousePressEvent(evt) { - if (evt.isRightButton) { - if (evt.isShifted) { - debugControls.enableIndicators = !debugControls.enableIndicators; - if (!debugControls.enableIndicators) { - debugControls.removeIndicators(); - } - } else { - doppleganger.mirrored = !doppleganger.mirrored; - } - } - } - - doppleganger.activeChanged.connect(function(active) { - if (active) { - debugControls.start(); - doppleganger.jointsUpdated.connect(debugControls, 'onJointsUpdated'); - Controller.mousePressEvent.connect(onMousePressEvent); - } else { - Controller.mousePressEvent.disconnect(onMousePressEvent); - doppleganger.jointsUpdated.disconnect(debugControls, 'onJointsUpdated'); - debugControls.stop(); - } - }); - - debugControls.jointSelected.connect(function(hit) { - debugControls.selectedJointName = hit.jointName; - if (hit.jointIndex < 0) { - return; - } - hit.mirroredJointName = modelHelper.deriveMirroredJointNames([hit.jointName])[0]; - log('selected joint:', JSON.stringify(hit, 0, 2)); - }); - - Script.scriptEnding.connect(debugControls, 'removeIndicators'); - - return doppleganger; -}; diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/makefile b/unpublishedScripts/marketplace/doppleganger-attachments/makefile new file mode 100644 index 0000000000..eaf02dbed9 --- /dev/null +++ b/unpublishedScripts/marketplace/doppleganger-attachments/makefile @@ -0,0 +1,11 @@ +all: + @echo "make dist" + +dist: doppleganger-a.svg.json doppleganger-i.svg.json dist/app-doppleganger-marketplace.js + @echo "OK" + +%.svg.json: %.svg + cat $< | jq -sR '"data:image/svg+xml;xml,"+.' > $@ + +dist/app-doppleganger-marketplace.js: *.js + ./node_modules/.bin/webpack --verbose app-doppleganger-attachments.js $@ diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/model-helper.js b/unpublishedScripts/marketplace/doppleganger-attachments/model-helper.js index 2dda2c12ec..8edaf50cb7 100644 --- a/unpublishedScripts/marketplace/doppleganger-attachments/model-helper.js +++ b/unpublishedScripts/marketplace/doppleganger-attachments/model-helper.js @@ -8,6 +8,7 @@ // /* eslint-env commonjs */ +/* global console */ // @module model-helper // // This module provides ModelReadyWatcher (a helper class for knowing when a model becomes usable inworld) and @@ -18,10 +19,16 @@ var utils = require('./utils.js'), assert = utils.assert; module.exports = { - version: '0.0.0', + version: '0.0.1', ModelReadyWatcher: ModelReadyWatcher }; +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('model-helper | ' + [].slice.call(arguments).join(' ')); +} +log(module.exports.version); + var _objectDeleted = utils.signal(function objectDeleted(objectID){}); // proxy for _objectDeleted that only binds deletion tracking if script actually connects to the unified signal var objectDeleted = utils.assign(function objectDeleted(objectID){}, { diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/package.json b/unpublishedScripts/marketplace/doppleganger-attachments/package.json new file mode 100644 index 0000000000..f18136fc1b --- /dev/null +++ b/unpublishedScripts/marketplace/doppleganger-attachments/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "webpack": "^3.0.0" + } +} diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/readme.md b/unpublishedScripts/marketplace/doppleganger-attachments/readme.md new file mode 100644 index 0000000000..d0ebf2d6e1 --- /dev/null +++ b/unpublishedScripts/marketplace/doppleganger-attachments/readme.md @@ -0,0 +1,4 @@ +note: to rebuild webpack version: +* install `jq` https://stedolan.github.io/jq (used to encode the icon.svg's as Data URI JSON strings) +* `npm install` +* `make dist` diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/utils.js b/unpublishedScripts/marketplace/doppleganger-attachments/utils.js index 76c6e1ef7f..b34af1e632 100644 --- a/unpublishedScripts/marketplace/doppleganger-attachments/utils.js +++ b/unpublishedScripts/marketplace/doppleganger-attachments/utils.js @@ -1,12 +1,20 @@ /* eslint-env commonjs */ +/* global console */ module.exports = { + version: '0.0.1', bind: bind, signal: signal, assign: assign, assert: assert }; +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('utils | ' + [].slice.call(arguments).join(' ')); +} +log(module.exports.version); + // @function - bind a function to a `this` context // @param {Object} - the `this` context // @param {Function|String} - function or method name