Files
OpenSpace/modules/touch/touchmodule.cpp
2025-10-27 14:20:29 +01:00

345 lines
13 KiB
C++

/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2025 *
* *
* 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 <modules/touch/touchmodule.h>
#include <modules/touch/include/tuioear.h>
#include <modules/touch/include/win32_touch.h>
#include <openspace/engine/globals.h>
#include <openspace/engine/globalscallbacks.h>
#include <openspace/engine/openspaceengine.h>
#include <openspace/engine/windowdelegate.h>
#include <openspace/interaction/interactionmonitor.h>
#include <openspace/navigation/navigationhandler.h>
#include <openspace/util/factorymanager.h>
#include <ghoul/logging/logmanager.h>
using namespace TUIO;
namespace {
constexpr std::string_view _loggerCat = "TouchModule";
constexpr openspace::properties::Property::PropertyInfo TuioPortInfo = {
"TuioPort",
"TUIO Port",
"TUIO UDP port, by default 3333. The port cannot be changed after startup.",
openspace::properties::Property::Visibility::AdvancedUser
};
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
};
struct [[codegen::Dictionary(TouchModule)]] Parameters {
// [[codegen::verbatim(TuioPortInfo.description)]]
std::optional<int> tuioPort [[codegen::inrange(1, 65535)]];
};
#include "touchmodule_codegen.cpp"
} // namespace
namespace openspace {
TouchModule::TouchModule()
: OpenSpaceModule("Touch")
, _tuioPort(TuioPortInfo, 3333, 1, 65535)
, _touchIsEnabled(EnableTouchInfo, true)
, _hasActiveTouchEvent(EventsInfo, false)
, _defaultDirectTouchRenderableTypes(DefaultDirectTouchRenderableTypesInfo)
{
addPropertySubOwner(_touch);
addPropertySubOwner(_markers);
_tuioPort.setReadOnly(true);
addProperty(_tuioPort);
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<Renderable>* fRenderable =
FactoryManager::ref().factory<Renderable>();
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& dict) {
const Parameters p = codegen::bake<Parameters>(dict);
_tuioPort = p.tuioPort.value_or(_tuioPort);
_ear = std::make_unique<TuioEar>(_tuioPort);
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<Win32TouchHook>(nativeWindowHandle);
}
#endif // WIN32
});
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(std::move(i));
return true;
}
);
global::callback::touchUpdated->push_back(
[this](TouchInput i) {
updateOrAddTouchInput(std::move(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<TouchInput> earInputs = _ear->takeInputs();
std::vector<TouchInput> 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<TouchInputHolder>::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