/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2024 * * * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * * software and associated documentation files (the "Software"), to deal in the Software * * without restriction, including without limitation the rights to use, copy, modify, * * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to the following * * conditions: * * * * The above copyright notice and this permission notice shall be included in all copies * * or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include using namespace TUIO; namespace { constexpr std::string_view _loggerCat = "TouchModule"; constexpr openspace::properties::Property::PropertyInfo EnableTouchInfo = { "EnableTouchInteraction", "Enable Touch Interaction", "Use this property to turn on/off touch input navigation in the 3D scene. " "Disabling will reset all current touch inputs to the navigation.", openspace::properties::Property::Visibility::User }; constexpr openspace::properties::Property::PropertyInfo EventsInfo = { "DetectedTouchEvent", "Detected Touch Event", "True when there is an active touch event", openspace::properties::Property::Visibility::Hidden }; constexpr openspace::properties::Property::PropertyInfo DefaultDirectTouchRenderableTypesInfo = { "DefaultDirectTouchRenderableTypes", "Default Direct Touch Renderable Types", "A list of renderable types that will automatically use the \'direct " "manipulation\' scheme when interacted with, keeping the finger on a static " "position on the interaction sphere of the object when touching. Good for " "relatively spherical objects.", openspace::properties::Property::Visibility::AdvancedUser }; } // namespace openspace namespace openspace { TouchModule::TouchModule() : OpenSpaceModule("Touch") , _touchIsEnabled(EnableTouchInfo, true) , _hasActiveTouchEvent(EventsInfo, false) , _defaultDirectTouchRenderableTypes(DefaultDirectTouchRenderableTypesInfo) { addPropertySubOwner(_touch); addPropertySubOwner(_markers); addProperty(_touchIsEnabled); _touchIsEnabled.onChange([this]() { _touch.resetAfterInput(); _lastTouchInputs.clear(); }); _hasActiveTouchEvent.setReadOnly(true); addProperty(_hasActiveTouchEvent); _defaultDirectTouchRenderableTypes.onChange([this]() { _sortedDefaultRenderableTypes.clear(); for (const std::string& s : _defaultDirectTouchRenderableTypes.value()) { ghoul::TemplateFactory* fRenderable = FactoryManager::ref().factory(); if (!fRenderable->hasClass(s)) { LWARNING(std::format( "In property 'DefaultDirectTouchRenderableTypes': '{}' is not a " "registered renderable type. Ignoring", s )); continue; } _sortedDefaultRenderableTypes.insert(s); } }); addProperty(_defaultDirectTouchRenderableTypes); } TouchModule::~TouchModule() { // intentionally left empty } bool TouchModule::isDefaultDirectTouchType(std::string_view renderableType) const { return _sortedDefaultRenderableTypes.find(std::string(renderableType)) != _sortedDefaultRenderableTypes.end(); } void TouchModule::internalInitialize(const ghoul::Dictionary&){ _ear.reset(new TuioEar()); global::callback::initializeGL->push_back([this]() { LDEBUG("Initializing TouchMarker OpenGL"); _markers.initialize(); #ifdef WIN32 // We currently only support one window of touch input internally // so here we grab the first window-handle and use it. void* nativeWindowHandle = global::windowDelegate->getNativeWindowHandle(0); if (nativeWindowHandle) { _win32TouchHook = std::make_unique(nativeWindowHandle); } #endif }); global::callback::deinitializeGL->push_back([this]() { LDEBUG("Deinitialize TouchMarker OpenGL"); _markers.deinitialize(); }); // These are handled in UI thread, which (as of 20th dec 2019) is in main/rendering // thread so we don't need a mutex here global::callback::touchDetected->push_back( [this](TouchInput i) { addTouchInput(i); return true; } ); global::callback::touchUpdated->push_back( [this](TouchInput i) { updateOrAddTouchInput(i); return true; } ); global::callback::touchExit->push_back( std::bind(&TouchModule::removeTouchInput, this, std::placeholders::_1) ); global::callback::preSync->push_back([this]() { if (!_touchIsEnabled) { return; } OpenSpaceEngine::Mode mode = global::openSpaceEngine->currentMode(); if (mode == OpenSpaceEngine::Mode::CameraPath || mode == OpenSpaceEngine::Mode::SessionRecordingPlayback) { // Reset everything, to avoid problems once we process inputs again _lastTouchInputs.clear(); _touch.resetAfterInput(); clearInputs(); return; } _touch.setCamera(global::navigationHandler->camera()); bool gotNewInput = processNewInput(); if (gotNewInput && global::windowDelegate->isMaster()) { _touch.updateStateFromInput(_touchPoints, _lastTouchInputs); } else if (_touchPoints.empty()) { _touch.resetAfterInput(); } // Update last processed touch inputs _lastTouchInputs.clear(); for (const TouchInputHolder& points : _touchPoints) { _lastTouchInputs.emplace_back(points.latestInput()); } // Calculate the new camera state for this frame _touch.step(global::windowDelegate->deltaTime()); clearInputs(); }); global::callback::render->push_back([this]() { _markers.render(_touchPoints); }); } bool TouchModule::processNewInput() { // Get new input from listener std::vector earInputs = _ear->takeInput(); std::vector earRemovals = _ear->takeRemovals(); for (const TouchInput& input : earInputs) { updateOrAddTouchInput(input); } for (const TouchInput& removal : earRemovals) { removeTouchInput(removal); } bool touchHappened = !_touchPoints.empty(); _hasActiveTouchEvent = touchHappened; // Set touch property to active (to void mouse input, mainly for mtdev bridges) if (touchHappened) { global::interactionMonitor->markInteraction(); } // Erase old input ids that no longer exist _lastTouchInputs.erase( std::remove_if( _lastTouchInputs.begin(), _lastTouchInputs.end(), [this](const TouchInput& input) { return !std::any_of( _touchPoints.cbegin(), _touchPoints.cend(), [&input](const TouchInputHolder& holder) { return holder.holdsInput(input); } ); } ), _lastTouchInputs.end() ); if (_tap) { _touch.tap(); _tap = false; return true; } // Return true if we got new input if (_touchPoints.size() == _lastTouchInputs.size() && !_touchPoints.empty()) { // @TODO (emmbr26, 2023-02-03) Looks to me like this code will always return // true? That's a bit weird and should probably be investigated bool newInput = true; // Go through list and check if the last registrered time is newer than the // last processed touch inputs (last frame) std::for_each( _lastTouchInputs.begin(), _lastTouchInputs.end(), [this, &newInput](TouchInput& input) { std::vector::iterator holder = std::find_if( _touchPoints.begin(), _touchPoints.end(), [&input](const TouchInputHolder& inputHolder) { return inputHolder.holdsInput(input); } ); if (!holder->isMoving()) { newInput = true; } } ); return newInput; } else { return false; } } void TouchModule::clearInputs() { for (const TouchInput& input : _deferredRemovals) { for (TouchInputHolder& inputHolder : _touchPoints) { if (inputHolder.holdsInput(input)) { inputHolder = std::move(_touchPoints.back()); _touchPoints.pop_back(); break; } } } _deferredRemovals.clear(); } void TouchModule::addTouchInput(TouchInput input) { _touchPoints.emplace_back(input); } void TouchModule::updateOrAddTouchInput(TouchInput input) { for (TouchInputHolder& inputHolder : _touchPoints) { if (inputHolder.holdsInput(input)) { inputHolder.tryAddInput(input); return; } } _touchPoints.emplace_back(input); } void TouchModule::removeTouchInput(TouchInput input) { _deferredRemovals.emplace_back(input); // Check for "tap" gesture: for (TouchInputHolder& inputHolder : _touchPoints) { if (inputHolder.holdsInput(input)) { inputHolder.tryAddInput(input); const double totalTime = inputHolder.gestureTime(); const float totalDistance = inputHolder.gestureDistance(); // Magic values taken from tuioear.cpp: const bool isWithinTapTime = totalTime < 0.18; const bool wasStationary = totalDistance < 0.0004f; if (isWithinTapTime && wasStationary && _touchPoints.size() == 1 && _deferredRemovals.size() == 1) { _tap = true; } return; } } } } // namespace openspace