/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2018 * * * * 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 #include #include #include #include #include #include #ifdef OPENSPACE_MODULE_GLOBEBROWSING_ENABLED #include #include #endif #include #include #include #include #ifdef WIN32 #pragma warning (push) #pragma warning (disable : 4310) // cast truncates constant value #endif // WIN32 #include #ifdef WIN32 #pragma warning (pop) #endif // WIN32 #include #include #include namespace { constexpr const char* _loggerCat = "TouchInteraction"; constexpr openspace::properties::Property::PropertyInfo OriginInfo = { "Origin", "Origin", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo UnitTestInfo = { "UnitTest", "Take a unit test saving the LM data into file", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo EventsInfo = { "TouchEvents", "True if we have a touch event", "", openspace::properties::Property::Visibility::Hidden }; constexpr openspace::properties::Property::PropertyInfo SetDefaultInfo = { "SetDefault", "Reset all properties to default", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo MaxTapTimeInfo = { "MaxTapTime", "Max tap delay (in ms) for double tap", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo DecelatesPerSecondInfo = { "DeceleratesPerSecond", "Number of times velocity is decelerated per second", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo TouchScreenSizeInfo = { "TouchScreenSize", "Touch Screen size in inches", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo TapZoomFactorInfo = { "TapZoomFactor", "Scaling distance travelled on tap", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo DirectManipulationInfo = { "DirectManipulationRadius", "Radius a planet has to have to activate direct-manipulation", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo RollThresholdInfo = { "RollThreshold", "Threshold for min angle for roll interpret", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo OrbitSpinningThreshold = { "OrbitThreshold", "Threshold to activate orbit spinning in direct-manipulation", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo SpinningSensitivityInfo = { "SpinningSensitivity", "Sensitivity of spinning in direct-manipulation", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo ZoomSensitivityExpInfo = { "ZoomSensitivityExp", "Sensitivity of exponential zooming in relation to distance from focus node", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo ZoomSensitivityPropInfo = { "ZoomSensitivityProp", "Sensitivity of zooming proportional to distance from focus node", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo ZoomSensitivityDistanceThresholdInfo = { "ZoomSensitivityDistanceThreshold", "Threshold of distance to target node for whether or not to use exponential " "zooming", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo ZoomBoundarySphereMultiplierInfo = { "ZoomBoundarySphereMultiplier", "Multiplies a node's boundary sphere by this in order to limit zoom & prevent " "surface collision", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo ConstantTimeDecaySecsInfo = { "ConstantTimeDecaySecs", "Time duration that a pitch/roll/zoom/pan should take to decay to zero (seconds)", "" }; constexpr openspace::properties::Property::PropertyInfo InputSensitivityInfo = { "InputSensitivity", "Threshold for interpreting input as still", "" }; constexpr openspace::properties::Property::PropertyInfo StationaryCentroidInfo = { "CentroidStationary", "Threshold for stationary centroid", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo PanModeInfo = { "PanMode", "Allow panning gesture", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo PanDeltaDistanceInfo = { "PanDeltaDistance", "Delta distance between fingers allowed for interpreting pan interaction", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo SlerpTimeInfo = { "SlerpTime", "Time to slerp in seconds to new orientation with new node picking", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo GuiButtonSizeInfo = { "GuiButtonSize", "GUI button size in pixels", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo FrictionInfo = { "Friction", "Friction for different interactions (orbit, zoom, roll, pan)", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo PickingRadiusInfo = { "PickingRadiusMinimum", "Minimum radius for picking in NDC coordinates", "" // @TODO Missing documentation }; } // namespace using namespace TUIO; namespace openspace { TouchInteraction::TouchInteraction() : properties::PropertyOwner({ "TouchInteraction" }) , _origin(OriginInfo) , _unitTest(UnitTestInfo, false) , _touchActive(EventsInfo, false) , _reset(SetDefaultInfo, false) , _maxTapTime(MaxTapTimeInfo, 300, 10, 1000) , _deceleratesPerSecond(DecelatesPerSecondInfo, 240, 60, 300) , _touchScreenSize(TouchScreenSizeInfo, 55.0f, 5.5f, 150.0f) , _tapZoomFactor(TapZoomFactorInfo, 0.2f, 0.f, 0.5f) , _nodeRadiusThreshold(DirectManipulationInfo, 0.2f, 0.0f, 1.0f) , _rollAngleThreshold(RollThresholdInfo, 0.025f, 0.f, 0.05f) , _orbitSpeedThreshold(OrbitSpinningThreshold, 0.005f, 0.f, 0.01f) , _spinSensitivity(SpinningSensitivityInfo, 0.25f, 0.f, 2.f) , _zoomSensitivityExponential(ZoomSensitivityExpInfo, 1.03f, 1.0f, 1.1f) , _zoomSensitivityProportionalDist(ZoomSensitivityPropInfo, 11.f, 5.f, 50.f) , _zoomSensitivityDistanceThreshold( ZoomSensitivityDistanceThresholdInfo, 0.05f, 0.01f, 0.25f ) , _zoomBoundarySphereMultiplier(ZoomBoundarySphereMultiplierInfo, 1.001f, 1.f, 1.01f) , _inputStillThreshold(InputSensitivityInfo, 0.0005f, 0.f, 0.001f) // used to void wrongly interpreted roll interactions , _centroidStillThreshold(StationaryCentroidInfo, 0.0018f, 0.f, 0.01f) , _panEnabled(PanModeInfo, false) , _interpretPan(PanDeltaDistanceInfo, 0.015f, 0.f, 0.1f) , _slerpTime(SlerpTimeInfo, 3.f, 0.1f, 5.f) , _guiButton( GuiButtonSizeInfo, glm::ivec2(32, 64), glm::ivec2(8, 16), glm::ivec2(128, 256) ) , _friction( FrictionInfo, glm::vec4(0.025f, 0.025f, 0.02f, 0.02f), glm::vec4(0.f), glm::vec4(0.2f) ) , _pickingRadiusMinimum( { "Picking Radius", "Minimum radius for picking in NDC coordinates", "" }, 0.1f, 0.f, 1.f ) , _ignoreGui( // @TODO Missing documentation { "Ignore GUI", "Disable GUI touch interaction", "" }, false ) , _vel{ glm::dvec2(0.0), 0.0, 0.0, glm::dvec2(0.0) } , _sensitivity{ glm::dvec2(0.08, 0.045), 12.0 /*4.0*/, 2.75, glm::dvec2(0.08, 0.045) } , _constTimeDecay_secs(ConstantTimeDecaySecsInfo, 1.75f, 0.1f, 4.0f) // calculated with two vectors with known diff in length, then // projDiffLength/diffLength. , _projectionScaleFactor(1.000004) , _currentRadius(1.0) , _slerpdT(1000) , _timeSlack(0.0) , _numOfTests(0) , _directTouchMode(false) , _wasPrevModeDirectTouch(false) , _tap(false) , _doubleTap(false) , _zoomOutTap(false) , _lmSuccess(true) , _guiON(false) #ifdef TOUCH_DEBUG_PROPERTIES , _debugProperties() #endif , _centroid(glm::dvec3(0.0)) { addProperty(_touchActive); addProperty(_unitTest); addProperty(_reset); addProperty(_maxTapTime); addProperty(_deceleratesPerSecond); addProperty(_touchScreenSize); addProperty(_tapZoomFactor); addProperty(_nodeRadiusThreshold); addProperty(_rollAngleThreshold); addProperty(_orbitSpeedThreshold); addProperty(_spinSensitivity); addProperty(_zoomSensitivityExponential); addProperty(_zoomSensitivityProportionalDist); addProperty(_zoomSensitivityDistanceThreshold); addProperty(_zoomBoundarySphereMultiplier); addProperty(_constTimeDecay_secs); addProperty(_inputStillThreshold); addProperty(_centroidStillThreshold); addProperty(_panEnabled); addProperty(_interpretPan); addProperty(_slerpTime); addProperty(_guiButton); addProperty(_friction); addProperty(_pickingRadiusMinimum); addProperty(_ignoreGui); #ifdef TOUCH_DEBUG_PROPERTIES addPropertySubOwner(_debugProperties); #endif _origin.onChange([this]() { SceneGraphNode* node = sceneGraphNode(_origin.value()); if (node) { setFocusNode(node); } else { LWARNING(fmt::format( "Could not find a node in scenegraph called '{}'", _origin.value() )); } }); levmarq_init(&_lmstat); _time.initSession(); } // Called each frame if there is any input void TouchInteraction::updateStateFromInput(const std::vector& list, std::vector& lastProcessed) { #ifdef TOUCH_DEBUG_PROPERTIES _debugProperties.nFingers = list.size(); #endif if (_tap) { // check for doubletap if (_time.getSessionTime().getTotalMilliseconds() < _maxTapTime) { _doubleTap = true; _tap = false; } _time.initSession(); } bool hasWebContent = webContent(list); //Code for lower-right corner double-tap to zoom-out glm::ivec2 res = global::windowDelegate.currentWindowSize(); glm::dvec2 pos = glm::vec2( list.at(0).getScreenX(res.x), list.at(0).getScreenY(res.y) ); const float bottomCornerSizeForZoomTap_fraction = 0.08f; int zoomTapThresholdX = static_cast( res.x * (1.0f - bottomCornerSizeForZoomTap_fraction) ); int zoomTapThresholdY = static_cast( res.y * (1.0f - bottomCornerSizeForZoomTap_fraction) ); bool isTapInLowerRightCorner = (std::abs(pos.x) > zoomTapThresholdX && std::abs(pos.y) > zoomTapThresholdY); if (_doubleTap && isTapInLowerRightCorner) { _zoomOutTap = true; _tap = false; _doubleTap = false; } if (!guiMode(list) && !hasWebContent) { bool isThisFrameTransitionBetweenTouchModes = (_wasPrevModeDirectTouch != _directTouchMode); if (isThisFrameTransitionBetweenTouchModes) { _vel.orbit = glm::dvec2(0.0, 0.0); _vel.zoom = 0.0; _vel.roll = 0.0; _vel.pan = glm::dvec2(0.0, 0.0); resetAfterInput(); /*if( _directTouchMode ) LINFO("Touch -> Direct-touch"); else LINFO("Direct-touch -> Touch");*/ } if (_directTouchMode && _selected.size() > 0 && list.size() == _selected.size()) { #ifdef TOUCH_DEBUG_PROPERTIES _debugProperties.interactionMode = "Direct"; #endif directControl(list); } if (_lmSuccess) { findSelectedNode(list); } if (!_directTouchMode) { #ifdef TOUCH_DEBUG_PROPERTIES _debugProperties.interactionMode = "Velocities"; #endif computeVelocities(list, lastProcessed); } _wasPrevModeDirectTouch = _directTouchMode; // evaluates if current frame is in directTouchMode (will be used next frame) _directTouchMode = (_currentRadius > _nodeRadiusThreshold && _selected.size() == list.size()); } } bool TouchInteraction::webContent(const std::vector& list) { glm::ivec2 res = global::windowDelegate.currentWindowSize(); glm::dvec2 pos = glm::vec2( list.at(0).getScreenX(res.x), list.at(0).getScreenY(res.y) ); WebBrowserModule& module = *(global::moduleEngine.module()); return module.eventHandler().hasContentCallback(pos.x, pos.y); } // Activates/Deactivates gui input mode (if active it voids all other interactions) bool TouchInteraction::guiMode(const std::vector& list) { if (_ignoreGui) { return false; } glm::ivec2 res = global::windowDelegate.currentWindowSize(); glm::dvec2 pos = glm::vec2( list.at(0).getScreenX(res.x), list.at(0).getScreenY(res.y) ); ImGUIModule& module = *(global::moduleEngine.module()); _guiON = module.gui.isEnabled(); if (_tap && list.size() == 1 && std::abs(pos.x) < _guiButton.value().x && std::abs(pos.y) < _guiButton.value().y) { // pressed invisible button _guiON = !_guiON; module.gui.setEnabled(_guiON); LINFO(fmt::format( "GUI mode is {}. Inside box by: ({}%, {}%)", _guiON ? "activated" : "deactivated", static_cast(100 * (pos.x / _guiButton.value().x)), static_cast(100 * (pos.y / _guiButton.value().y)) )); } else if (_guiON) { module.touchInput = { _guiON, pos, 1 }; // emulate touch input as a mouse } return _guiON; } // Sets _vel to update _camera according to direct-manipulation (L2 error) void TouchInteraction::directControl(const std::vector& list) { // Reset old velocities upon new interaction _vel.orbit = glm::dvec2(0.0, 0.0); _vel.zoom = 0.0; _vel.roll = 0.0; _vel.pan = glm::dvec2(0.0, 0.0); #ifdef TOUCH_DEBUG_PROPERTIES LINFO("DirectControl"); #endif // Returns the screen point s(xi,par) dependent the transform M(par) and object // point xi auto distToMinimize = [](double* par, int x, void* fdata, LMstat* lmstat) { FunctionData* ptr = reinterpret_cast(fdata); // Apply transform to camera and find the new screen point of the updated camera // state // { vec2 globalRot, zoom, roll, vec2 localRot } double q[6] = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; for (int i = 0; i < ptr->nDOF; ++i) { q[i] = par[i]; } using namespace glm; // Create variables from current state dvec3 camPos = ptr->camera->positionVec3(); dvec3 centerPos = ptr->node->worldPosition(); dvec3 directionToCenter = normalize(centerPos - camPos); dvec3 lookUp = ptr->camera->lookUpVectorWorldSpace(); dvec3 camDirection = ptr->camera->viewDirectionWorldSpace(); // Make a representation of the rotation quaternion with local and global // rotations dmat4 lookAtMat = lookAt( dvec3(0, 0, 0), directionToCenter, // To avoid problem with lookup in up direction normalize(camDirection + lookUp)); dquat globalCamRot = normalize(quat_cast(inverse(lookAtMat))); dquat localCamRot = inverse(globalCamRot) * ptr->camera->rotationQuaternion(); { // Roll dquat rollRot = angleAxis(q[3], dvec3(0.0, 0.0, 1.0)); localCamRot = localCamRot * rollRot; } { // Panning (local rotation) dvec3 eulerAngles(q[5], q[4], 0); dquat panRot = dquat(eulerAngles); localCamRot = localCamRot * panRot; } { // Orbit (global rotation) dvec3 eulerAngles(q[1], q[0], 0); dquat rotationDiffCamSpace = dquat(eulerAngles); dvec3 centerToCamera = camPos - centerPos; dquat rotationDiffWorldSpace = globalCamRot * rotationDiffCamSpace * inverse(globalCamRot); dvec3 rotationDiffVec3 = centerToCamera * rotationDiffWorldSpace - centerToCamera; camPos += rotationDiffVec3; centerToCamera = camPos - centerPos; directionToCenter = normalize(-centerToCamera); dvec3 lookUpWhenFacingCenter = globalCamRot * dvec3(ptr->camera->lookUpVectorCameraSpace()); lookAtMat = lookAt( dvec3(0, 0, 0), directionToCenter, lookUpWhenFacingCenter); globalCamRot = normalize(quat_cast(inverse(lookAtMat))); } { // Zooming camPos += directionToCenter * q[2]; } // Update the camera state Camera cam = *(ptr->camera); cam.setPositionVec3(camPos); cam.setRotation(globalCamRot * localCamRot); // we now have a new position and orientation of camera, project surfacePoint to // the new screen to get distance to minimize glm::dvec2 newScreenPoint = ptr->castToNDC( ptr->selectedPoints.at(x), cam, ptr->node ); lmstat->pos.push_back(newScreenPoint); return glm::length(ptr->screenPoints.at(x) - newScreenPoint); }; // Gradient of distToMinimize w.r.t par (using forward difference) auto gradient = [](double* g, double* par, int x, void* fdata, LMstat* lmstat) { FunctionData* ptr = reinterpret_cast(fdata); double h, lastG, f1, f0 = ptr->distToMinimize(par, x, fdata, lmstat); // scale value to find minimum step size h, dependant on planet size double scale = log10(ptr->node->boundingSphere()); std::vector dPar(ptr->nDOF, 0.0); dPar.assign(par, par + ptr->nDOF); for (int i = 0; i < ptr->nDOF; ++i) { // Initial values h = 1e-8; lastG = 1; dPar.at(i) += h; f1 = ptr->distToMinimize(dPar.data(), x, fdata, lmstat); dPar.at(i) = par[i]; // Iterative process to find the minimum step h that gives a good gradient for (int j = 0; j < 100; ++j) { if ((f1 - f0) != 0 && lastG == 0) { // found minimum step size h // scale up to get a good initial guess value h *= scale * scale * scale; // clamp min step size to a fraction of the incoming parameter if (i == 2) { double epsilon = 1e-3; // make sure incoming parameter is larger than 0 h = std::max(std::max(std::abs(dPar.at(i)), epsilon) * 0.001, h); } else if (ptr->nDOF == 2) { h = std::max(std::abs(dPar.at(i)) * 0.001, h); } // calculate f1 with good h for finite difference dPar.at(i) += h; f1 = ptr->distToMinimize(dPar.data(), x, fdata, lmstat); dPar.at(i) = par[i]; break; } else if ((f1 - f0) != 0 && lastG != 0) { // h too big h /= scale; } else if ((f1 - f0) == 0) { // h too small h *= scale; } lastG = f1 - f0; dPar.at(i) += h; f1 = ptr->distToMinimize(dPar.data(), x, fdata, lmstat); dPar.at(i) = par[i]; } g[i] = (f1 - f0) / h; } if (ptr->nDOF == 2) { // normalize on 1 finger case to allow for horizontal/vertical movement for (int i = 0; i < 2; ++i) { g[i] = g[i]/std::abs(g[i]); } } else if (ptr->nDOF == 6) { for (int i = 0; i < ptr->nDOF; ++i) { // lock to only pan and zoom on 3 finger case, no roll/orbit g[i] = (i == 2) ? g[i] : g[i] / std::abs(g[i]); } } }; // project back a 3D point in model view to clip space [-1,1] coordinates on the view // plane auto castToNDC = [](const glm::dvec3& vec, Camera& camera, SceneGraphNode* node) { glm::dvec3 posInCamSpace = glm::inverse(camera.rotationQuaternion()) * (node->rotationMatrix() * vec + (node->worldPosition() - camera.positionVec3())); glm::dvec4 clipspace = camera.projectionMatrix() * glm::dvec4(posInCamSpace, 1.0); return (glm::dvec2(clipspace) / clipspace.w); }; // only send in first three fingers (to make it easier for LMA to converge on 3+ // finger case with only zoom/pan) int nFingers = std::min(static_cast(list.size()), 3); int nDOF = std::min(nFingers * 2, 6); std::vector par(nDOF, 0.0); par.at(0) = _lastVel.orbit.x; // use _lastVel for orbit par.at(1) = _lastVel.orbit.y; // Parse input data to be used in the LM algorithm std::vector selectedPoints; std::vector screenPoints; for (int i = 0; i < nFingers; ++i) { const SelectedBody& sb = _selected.at(i); selectedPoints.push_back(sb.coordinates); std::vector::const_iterator c = std::find_if( list.begin(), list.end(), [&sb](const TuioCursor& c) { return c.getSessionID() == sb.id; } ); if (c != list.end()) { // normalized -1 to 1 coordinates on screen screenPoints.emplace_back(2 * (c->getX() - 0.5), -2 * (c->getY() - 0.5)); } else { global::moduleEngine.module()->touchInput = { true, glm::dvec2(0.0, 0.0), 1 }; resetAfterInput(); return; } } FunctionData fData = { selectedPoints, screenPoints, nDOF, castToNDC, distToMinimize, _camera, _selected.at(0).node, _lmstat, _currentRadius }; void* dataPtr = reinterpret_cast(&fData); // finds best transform values for the new camera state and stores them in par _lmSuccess = levmarq( nDOF, par.data(), static_cast(screenPoints.size()), nullptr, distToMinimize, gradient, dataPtr, &_lmstat ); if (_lmSuccess && !_unitTest) { // if good values were found set new camera state _vel.orbit = glm::dvec2(par.at(0), par.at(1)); if (nDOF > 2) { _vel.zoom = par.at(2); _vel.roll = par.at(3); if (_panEnabled && nDOF > 4) { _vel.roll = 0.0; _vel.pan = glm::dvec2(par.at(4), par.at(5)); } } step(1.0); // Reset velocities after setting new camera state _lastVel = _vel; _vel.orbit = glm::dvec2(0.0, 0.0); _vel.zoom = 0.0; _vel.roll = 0.0; _vel.pan = glm::dvec2(0.0, 0.0); } else { // prevents touch to infinitely be active (due to windows bridge case where event // doesnt get consumed sometimes when LMA fails to converge) global::moduleEngine.module()->touchInput = { true, glm::dvec2(0.0, 0.0), 1 }; resetAfterInput(); } } // Traces the touch input into the scene and finds the surface coordinates of touched // planets (if occuring) void TouchInteraction::findSelectedNode(const std::vector& list) { //trim list to only contain visible nodes that make sense std::string selectables[30] = { "Sun", "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", "Moon", "Titan", "Rhea", "Mimas", "Iapetus", "Enceladus", "Dione", "Io", "Ganymede", "Europa", "Callisto", "NewHorizons", "Styx", "Nix", "Kerberos", "Hydra", "Charon", "Tethys", "OsirisRex", "Bennu" }; std::vector selectableNodes; for (SceneGraphNode* node : global::renderEngine.scene()->allSceneGraphNodes()) { for (const std::string& name : selectables) { if (node->identifier() == name) { selectableNodes.push_back(node); } } } glm::dquat camToWorldSpace = _camera->rotationQuaternion(); glm::dvec3 camPos = _camera->positionVec3(); std::vector newSelected; struct PickingInfo { SceneGraphNode* node; double pickingDistanceNDC; double pickingDistanceWorld; }; std::vector pickingInfo; for (const TuioCursor& c : list) { double xCo = 2 * (c.getX() - 0.5); double yCo = -2 * (c.getY() - 0.5); // normalized -1 to 1 coordinates on screen // vec3(projectionmatrix * clipspace), divide with w? glm::dvec3 cursorInWorldSpace = camToWorldSpace * glm::dvec3(glm::inverse(_camera->projectionMatrix()) * glm::dvec4(xCo, yCo, -1.0, 1.0)); glm::dvec3 raytrace = glm::normalize(cursorInWorldSpace); long id = c.getSessionID(); for (SceneGraphNode* node : selectableNodes) { double boundingSphere = node->boundingSphere(); glm::dvec3 camToSelectable = node->worldPosition() - camPos; double dist = length(glm::cross(cursorInWorldSpace, camToSelectable)) / glm::length(cursorInWorldSpace) - boundingSphere; if (dist <= 0.0) { // finds intersection closest point between boundingsphere and line in // world coordinates, assumes line direction is normalized double d = glm::dot(raytrace, camToSelectable); double root = boundingSphere * boundingSphere - glm::dot(camToSelectable, camToSelectable) + d * d; if (root > 0) { // two intersection points (take the closest one) d -= sqrt(root); } glm::dvec3 intersectionPoint = camPos + d * raytrace; glm::dvec3 pointInModelView = glm::inverse(node->rotationMatrix()) * (intersectionPoint - node->worldPosition()); // Add id, node and surface coordinates to the selected list std::vector::iterator oldNode = std::find_if( newSelected.begin(), newSelected.end(), [id](SelectedBody s) { return s.id == id; } ); if (oldNode != newSelected.end()) { double oldNodeDist = glm::length( oldNode->node->worldPosition() - camPos ); if (glm::length(camToSelectable) < oldNodeDist) { // new node is closer, remove added node and add the new one // instead newSelected.pop_back(); newSelected.push_back({ id, node, pointInModelView }); } } else { newSelected.push_back({ id, node, pointInModelView }); } } // Compute locations in view space to perform the picking glm::dvec4 clip = glm::dmat4(_camera->projectionMatrix()) * _camera->combinedViewMatrix() * glm::vec4(node->worldPosition(), 1.0); glm::dvec2 ndc = clip / clip.w; // If the object is not in the screen, we dont want to consider it at all if (ndc.x >= -1.0 && ndc.x <= 1.0 && ndc.y >= -1.0 && ndc.y <= 1.0) { glm::dvec2 cursor = { xCo, yCo }; double ndcDist = glm::length(ndc - cursor); // We either want to select the object if it's bounding sphere as been // touched (checked by the first part of this loop above) or if the touch // point is within a minimum distance of the center if (dist <= 0.0 || (ndcDist <= _pickingRadiusMinimum)) { // If the user touched the planet directly, this is definitely the one // they are interested in => minimum distance if (dist <= 0.0) { #ifdef TOUCH_DEBUG_NODE_PICK_MESSAGES LINFOC( node->identifier(), "Picking candidate based on direct touch" ); #endif //#ifdef TOUCH_DEBUG_NODE_PICK_MESSAGES pickingInfo.push_back({ node, -std::numeric_limits::max(), -std::numeric_limits::max() }); } else { // The node was considered due to minimum picking distance radius #ifdef TOUCH_DEBUG_NODE_PICK_MESSAGES LINFOC( node->identifier(), "Picking candidate based on proximity" ); #endif //#ifdef TOUCH_DEBUG_NODE_PICK_MESSAGES pickingInfo.push_back({ node, ndcDist, dist }); } } } } } // After we are done with all of the nodes, we can sort the picking list and pick the // one that fits best (= is closest or was touched directly) std::sort( pickingInfo.begin(), pickingInfo.end(), [](const PickingInfo& lhs, const PickingInfo& rhs) { return lhs.pickingDistanceWorld < rhs.pickingDistanceWorld; } ); // If an item has been picked, it's in the first position of the vector now if (!pickingInfo.empty()) { _pickingSelected = pickingInfo.begin()->node; #ifdef TOUCH_DEBUG_NODE_PICK_MESSAGES LINFOC("Picking", "Picked node: " + _pickingSelected->identifier()); #endif //#ifdef TOUCH_DEBUG_NODE_PICK_MESSAGES } _selected = std::move(newSelected); } // Interprets the input gesture to a specific interaction int TouchInteraction::interpretInteraction(const std::vector& list, const std::vector& lastProcessed) { glm::dvec3 lastCentroid = _centroid; _centroid.x = std::accumulate( list.begin(), list.end(), 0.0, [](double x, const TuioCursor& c) { return x + c.getX(); } ) / list.size(); _centroid.y = std::accumulate( list.begin(), list.end(), 0.0, [](double y, const TuioCursor& c) { return y + c.getY(); } ) / list.size(); // see if the distance between fingers changed - used in pan interpretation double dist = 0; double lastDist = 0; TuioCursor cursor = list.at(0); for (const TuioCursor& c : list) { dist += glm::length( glm::dvec2(c.getX(), c.getY()) - glm::dvec2(cursor.getX(), cursor.getY()) ); cursor = c; } TuioPoint point = lastProcessed.at(0).second; for (const Point& p : lastProcessed) { lastDist += glm::length(glm::dvec2(p.second.getX(), p.second.getY()) - glm::dvec2(point.getX(), point.getY())); point = p.second; } // find the slowest moving finger - used in roll interpretation double minDiff = 1000; long id = 0; for (const TuioCursor& c : list) { auto it = std::find_if( lastProcessed.begin(), lastProcessed.end(), [&c](const Point& p) { return p.first == c.getSessionID(); }); if (it == lastProcessed.end()) { continue; } TuioPoint itPoint = it->second; double diff = c.getX() - itPoint.getX() + c.getY() - itPoint.getY(); if (!c.isMoving()) { diff = minDiff = 0.0; id = c.getSessionID(); } else if (std::abs(diff) < std::abs(minDiff)) { minDiff = diff; id = c.getSessionID(); } } // find if all fingers angles are high - used in roll interpretation double rollOn = std::accumulate( list.begin(), list.end(), 0.0, [&](double diff, const TuioCursor& c) { TuioPoint point = std::find_if( lastProcessed.begin(), lastProcessed.end(), [&c](const Point& p) { return p.first == c.getSessionID(); } )->second; double res = 0.0; float lastAngle = point.getAngle( static_cast(_centroid.x), static_cast(_centroid.y) ); float currentAngle = c.getAngle( static_cast(_centroid.x), static_cast(_centroid.y) ); if (lastAngle > currentAngle + 1.5 * M_PI) { res = currentAngle + (2 * M_PI - lastAngle); } else if (currentAngle > lastAngle + 1.5 * M_PI) { res = (2 * M_PI - currentAngle) + lastAngle; } else { res = currentAngle - lastAngle; } if (std::abs(res) < _rollAngleThreshold) { return 1000.0; } else { return (diff + res); } } ); double normalizedCentroidDistance = glm::distance( _centroid, lastCentroid ) / list.size(); #ifdef TOUCH_DEBUG_PROPERTIES _debugProperties.normalizedCentroidDistance = normalizedCentroidDistance; _debugProperties.rollOn = rollOn; _debugProperties.minDiff = minDiff; #endif if (_zoomOutTap) { return ZOOM_OUT; } else if (_doubleTap) { return PICK; } else if (list.size() == 1) { return ROT; } else { float avgDistance = static_cast( std::abs(dist - lastDist) / list.at(0).getMotionSpeed() ); // if average distance between 3 fingers are constant we have panning if (_panEnabled && (avgDistance < _interpretPan && list.size() == 3)) { return PAN; } // we have roll if one finger is still, or the total roll angles around the // centroid is over _rollAngleThreshold (_centroidStillThreshold is used to void // misinterpretations) else if (std::abs(minDiff) < _inputStillThreshold || (std::abs(rollOn) < 100.0 && normalizedCentroidDistance < _centroidStillThreshold)) { return ROLL; } else { return PINCH; } } } // Calculate how much interpreted interaction (_vel) should change the camera state void TouchInteraction::computeVelocities(const std::vector& list, const std::vector& lastProcessed) { const TuioCursor& cursor = list.at(0); const int action = interpretInteraction(list, lastProcessed); const SceneGraphNode* anchor = global::navigationHandler.orbitalNavigator().anchorNode(); #ifdef TOUCH_DEBUG_PROPERTIES const std::map interactionNames = { { ROT, "Rotation" }, { PINCH, "Pinch" }, { PAN, "Pan" }, { ROLL, "Roll" }, { PICK, "Pick" } }; _debugProperties.interpretedInteraction = interactionNames.at(action); if (pinchConsecCt > 0 && action != PINCH) { if (pinchConsecCt > 3) { LDEBUG(fmt::format( "PINCH gesture ended with {} drag distance and {} counts", pinchConsecZoomFactor, pinchConsecCt )); } pinchConsecCt = 0; pinchConsecZoomFactor = 0.0; } #endif switch (action) { case ROT: { // add rotation velocity _vel.orbit += glm::dvec2(cursor.getXSpeed() * _sensitivity.orbit.x, cursor.getYSpeed() * _sensitivity.orbit.y); double orbitVelocityAvg = glm::distance(_vel.orbit.x, _vel.orbit.y); _constTimeDecayCoeff.orbit = computeConstTimeDecayCoefficient(orbitVelocityAvg); break; } case PINCH: { // add zooming velocity - dependant on distance difference between contact // points this/last frame double distance = std::accumulate( list.begin(), list.end(), 0.0, [&](double d, const TuioCursor& c) { return d + c.getDistance( static_cast(_centroid.x), static_cast(_centroid.y) ); } ) / list.size(); double lastDistance = std::accumulate( lastProcessed.begin(), lastProcessed.end(), 0.0f, [&](float d, const Point& p) { return d + p.second.getDistance( static_cast(_centroid.x), static_cast(_centroid.y) ); } ) / lastProcessed.size(); glm::dvec3 camPos = _camera->positionVec3(); glm::dvec3 centerPos = anchor->worldPosition(); glm::dvec3 currDistanceToFocusNode = camPos - centerPos; double distanceFromFocusSurface = length(currDistanceToFocusNode) - anchor->boundingSphere(); double zoomFactor = (distance - lastDistance); #ifdef TOUCH_DEBUG_PROPERTIES pinchConsecCt++; pinchConsecZoomFactor += zoomFactor; #endif _constTimeDecayCoeff.zoom = computeConstTimeDecayCoefficient(_vel.zoom); if (distanceFromFocusSurface > 0.1) { double ratioOfDistanceToNodeVsSurface = length(currDistanceToFocusNode) / distanceFromFocusSurface; if (ratioOfDistanceToNodeVsSurface > _zoomSensitivityDistanceThreshold) { zoomFactor *= pow( std::abs(distanceFromFocusSurface), static_cast(_zoomSensitivityExponential) ); } } else { zoomFactor = 1.0; } _vel.zoom = zoomFactor * _zoomSensitivityProportionalDist * std::max(_touchScreenSize.value() * 0.1, 1.0); break; } case ROLL: { // add global roll rotation velocity double rollFactor = std::accumulate( list.begin(), list.end(), 0.0, [&](double diff, const TuioCursor& c) { TuioPoint point = std::find_if( lastProcessed.begin(), lastProcessed.end(), [&c](const Point& p) { return p.first == c.getSessionID(); } )->second; double res = diff; double lastAngle = point.getAngle( static_cast(_centroid.x), static_cast(_centroid.y) ); double currentAngle = c.getAngle( static_cast(_centroid.x), static_cast(_centroid.y) ); // if's used to set angles 359 + 1 = 0 and 0 - 1 = 359 if (lastAngle > currentAngle + 1.5 * M_PI) { res += currentAngle + (2 * M_PI - lastAngle); } else if (currentAngle > lastAngle + 1.5 * M_PI) { res += (2 * M_PI - currentAngle) + lastAngle; } else { res += currentAngle - lastAngle; } return res; } ) / list.size(); _vel.roll += -rollFactor * _sensitivity.roll; _constTimeDecayCoeff.roll = computeConstTimeDecayCoefficient(_vel.roll); break; } case PAN: { // add local rotation velocity _vel.pan += glm::dvec2(cursor.getXSpeed() * _sensitivity.pan.x, cursor.getYSpeed() * _sensitivity.pan.y); double panVelocityAvg = glm::distance(_vel.pan.x, _vel.pan.y); _constTimeDecayCoeff.pan = computeConstTimeDecayCoefficient(panVelocityAvg); break; } case PICK: { // pick something in the scene as focus node if (_pickingSelected) { setFocusNode(_pickingSelected); // rotate camera to look at new focus, using slerp quat glm::dvec3 camToFocus = _pickingSelected->worldPosition() - _camera->positionVec3(); glm::dvec3 forward = glm::normalize(_camera->viewDirectionWorldSpace()); double angle = glm::angle(forward, camToFocus); glm::dvec3 axis = glm::normalize(glm::cross(forward, camToFocus)); _toSlerp.x = axis.x * sin(angle / 2.0); _toSlerp.y = axis.y * sin(angle / 2.0); _toSlerp.z = axis.z * sin(angle / 2.0); _toSlerp.w = cos(angle / 2.0); _slerpdT = 0.0; } else { // zooms in to current if PICK interpret happened but only space was // selected _vel.zoom = computeTapZoomDistance(0.3); _constTimeDecayCoeff.zoom = computeConstTimeDecayCoefficient(_vel.zoom); } break; } case ZOOM_OUT: { // zooms out from current if triple tap occurred _vel.zoom = computeTapZoomDistance(-1.0); _constTimeDecayCoeff.zoom = computeConstTimeDecayCoefficient(_vel.zoom); } } } double TouchInteraction::computeConstTimeDecayCoefficient(double velocity) { const double postDecayVelocityTarget = 1e-6; double stepsToDecay = _constTimeDecay_secs / _frameTimeAvg.averageFrameTime(); if (stepsToDecay > 0.0 && std::abs(velocity) > postDecayVelocityTarget) { return std::pow(postDecayVelocityTarget / std::abs(velocity), 1.0 / stepsToDecay); } else { return 1.0; } } double TouchInteraction::computeTapZoomDistance(double zoomGain) { const SceneGraphNode* anchor = global::navigationHandler.orbitalNavigator().anchorNode(); double dist = glm::distance( _camera->positionVec3(), global::navigationHandler.orbitalNavigator().anchorNode()->worldPosition() ); dist -= anchor->boundingSphere(); double newVelocity = dist * _tapZoomFactor; newVelocity *= std::max(_touchScreenSize.value() * 0.1, 1.0); newVelocity *= _zoomSensitivityProportionalDist * zoomGain; return newVelocity; } // Main update call, calculates the new orientation and position for the camera depending // on _vel and dt. Called every frame void TouchInteraction::step(double dt) { using namespace glm; const SceneGraphNode* anchor = global::navigationHandler.orbitalNavigator().anchorNode(); // since functions cant be called directly (TouchInteraction not a subclass of // InteractionMode) setFocusNode(global::navigationHandler.orbitalNavigator().anchorNode()); if (anchor && _camera) { // Create variables from current state dvec3 camPos = _camera->positionVec3(); dvec3 centerPos = anchor->worldPosition(); dvec3 directionToCenter = normalize(centerPos - camPos); dvec3 centerToCamera = camPos - centerPos; dvec3 lookUp = _camera->lookUpVectorWorldSpace(); dvec3 camDirection = _camera->viewDirectionWorldSpace(); // Make a representation of the rotation quaternion with local and global // rotations // To avoid problem with lookup in up direction dmat4 lookAtMat = lookAt( dvec3(0, 0, 0), directionToCenter, normalize(camDirection + lookUp) ); dquat globalCamRot = normalize(quat_cast(inverse(lookAtMat))); dquat localCamRot = inverse(globalCamRot) * _camera->rotationQuaternion(); double boundingSphere = anchor->boundingSphere(); dvec3 centerToBoundingSphere; double distance = std::max(length(centerToCamera) - boundingSphere, 0.0); _currentRadius = boundingSphere / std::max(distance * _projectionScaleFactor, 1.0); { // Roll dquat camRollRot = angleAxis(_vel.roll * dt, dvec3(0.0, 0.0, 1.0)); localCamRot = localCamRot * camRollRot; } { // Panning (local rotation) dvec3 eulerAngles(_vel.pan.y * dt, _vel.pan.x * dt, 0); dquat rotationDiff = dquat(eulerAngles); localCamRot = localCamRot * rotationDiff; // if we have chosen a new focus node if (_slerpdT < _slerpTime) { _slerpdT += 0.1*dt; localCamRot = slerp(localCamRot, _toSlerp, _slerpdT / _slerpTime); } } { // Orbit (global rotation) dvec3 eulerAngles(_vel.orbit.y*dt, _vel.orbit.x*dt, 0); dquat rotationDiffCamSpace = dquat(eulerAngles); dquat rotationDiffWorldSpace = globalCamRot * rotationDiffCamSpace * inverse(globalCamRot); dvec3 rotationDiffVec3 = centerToCamera * rotationDiffWorldSpace - centerToCamera; camPos += rotationDiffVec3; dvec3 centerToCam = camPos - centerPos; directionToCenter = normalize(-centerToCam); dvec3 lookUpWhenFacingCenter = globalCamRot * dvec3(_camera->lookUpVectorCameraSpace()); dmat4 lookAtMatrix = lookAt( dvec3(0, 0, 0), directionToCenter, lookUpWhenFacingCenter); globalCamRot = normalize(quat_cast(inverse(lookAtMatrix))); } { // Zooming centerToBoundingSphere = -directionToCenter * boundingSphere; centerToCamera = camPos - centerPos; double planetBoundaryRadius = length(centerToBoundingSphere); planetBoundaryRadius *= _zoomBoundarySphereMultiplier; double distToSurface = length(centerToCamera - planetBoundaryRadius); WebGuiModule& module = *(global::moduleEngine.module()); //Apply the velocity to update camera position glm::dvec3 velocityIncr = directionToCenter * _vel.zoom * dt; bool isDeltaLessThanDistToSurface = (length(_vel.zoom * dt) < distToSurface); bool isNewPosOutsidePlanetRadius = (length(centerToCamera + velocityIncr) > planetBoundaryRadius); if (isDeltaLessThanDistToSurface && isNewPosOutsidePlanetRadius) { camPos += velocityIncr; } else { #ifdef TOUCH_DEBUG_PROPERTIES LINFO("Zero the zoom velocity close to surface."); #endif _vel.zoom = 0.0; } } decelerate(dt); // Update the camera state _camera->setPositionVec3(camPos); _camera->setRotation(globalCamRot * localCamRot); #ifdef TOUCH_DEBUG_PROPERTIES //Show velocity status every N frames if (++stepVelUpdate >= 60) { stepVelUpdate = 0; LINFO(fmt::format( "DistToFocusNode {} stepZoomVelUpdate {}", length(centerToCamera), _vel.zoom )); } #endif _tap = false; _doubleTap = false; _zoomOutTap = false; if (_reset) { resetToDefault(); } } } void TouchInteraction::unitTest() { if (_unitTest) { _lmstat.verbose = true; // set _selected pos and new pos (on screen) std::vector lastFrame = { { TuioCursor(0, 10, 0.45f, 0.4f) }, // session id, cursor id, x, y { TuioCursor(1, 11, 0.55f, 0.6f) } }; std::vector currFrame = { { TuioCursor(0, 10, 0.2f, 0.6f) }, // (-0.6,-0.2) { TuioCursor(1, 11, 0.8f, 0.4f) } // (0.6, 0.2) }; // call update findSelectedNode(lastFrame); directControl(currFrame); // save lmstats.data into a file and clear it char buffer[32]; snprintf(buffer, sizeof(char) * 32, "lmdata%i.csv", _numOfTests); _numOfTests++; std::ofstream file(buffer); file << _lmstat.data; // clear everything _selected.clear(); _pickingSelected = nullptr; _vel.orbit = glm::dvec2(0.0, 0.0); _vel.zoom = 0.0; _vel.roll = 0.0; _vel.pan = glm::dvec2(0.0, 0.0); _lastVel = _vel; _unitTest = false; // could be the camera copy in func } } // Decelerate velocities, called a set number of times per second to dereference it from // frame time // Example: // Assume: frequency = 0.01, dt = 0.05 (200 fps), _timeSlack = 0.0001 // times = floor((0.05 + 0.0001) / 0.01) = 5 // _timeSlack = 0.0501 % 0.01 = 0.01 void TouchInteraction::decelerate(double dt) { _frameTimeAvg.updateWithNewFrame(dt); double expectedFrameTime = _frameTimeAvg.averageFrameTime(); // Number of times velocities should decelerate, depending on chosen frequency and // time slack over from last frame int times = static_cast((dt + _timeSlack) / expectedFrameTime); // Save the new time slack for the next frame _timeSlack = fmod((dt + _timeSlack), expectedFrameTime) * expectedFrameTime; //Ensure the number of times to apply the decay coefficient is valid times = std::min(times, 1); _vel.orbit *= computeDecayCoeffFromFrametime(_constTimeDecayCoeff.orbit, times); _vel.roll *= computeDecayCoeffFromFrametime(_constTimeDecayCoeff.roll, times); _vel.pan *= computeDecayCoeffFromFrametime(_constTimeDecayCoeff.pan, times); _vel.zoom *= computeDecayCoeffFromFrametime(_constTimeDecayCoeff.zoom, times); } double TouchInteraction::computeDecayCoeffFromFrametime(double coeff, int times) { if (coeff > 0.00001) { return std::pow(coeff, times); } else { return 0.0; } } // Called if all fingers are off the screen void TouchInteraction::resetAfterInput() { #ifdef TOUCH_DEBUG_PROPERTIES _debugProperties.nFingers = 0; _debugProperties.interactionMode = "None"; #endif if (_directTouchMode && !_selected.empty() && _lmSuccess) { double spinDelta = _spinSensitivity / global::windowDelegate.averageDeltaTime(); if (glm::length(_lastVel.orbit) > _orbitSpeedThreshold) { // allow node to start "spinning" after direct-manipulation finger is let go _vel.orbit = _lastVel.orbit * spinDelta; } } // Reset emulated mouse values ImGUIModule& module = *(global::moduleEngine.module()); if (_guiON) { bool activeLastFrame = module.touchInput.action; module.touchInput.active = false; if (activeLastFrame) { module.touchInput.active = true; module.touchInput.action = 0; } } else { module.touchInput.active = false; module.touchInput.action = 0; } _lmSuccess = true; // Ensure that _guiON is consistent with properties in OnScreenGUI and _guiON = module.gui.isEnabled(); // Reset variables _lastVel.orbit = glm::dvec2(0.0, 0.0); _lastVel.zoom = 0.0; _lastVel.roll = 0.0; _lastVel.pan = glm::dvec2(0.0, 0.0); _selected.clear(); _pickingSelected = nullptr; } // Reset all property values to default void TouchInteraction::resetToDefault() { _unitTest.set(false); _reset.set(false); _maxTapTime.set(300); _deceleratesPerSecond.set(240); _touchScreenSize.set(55.0f); _tapZoomFactor.set(0.2f); _nodeRadiusThreshold.set(0.2f); _rollAngleThreshold.set(0.025f); _orbitSpeedThreshold.set(0.005f); _spinSensitivity.set(1.0f); _zoomSensitivityExponential.set(1.025f); _inputStillThreshold.set(0.0005f); _centroidStillThreshold.set(0.0018f); _interpretPan.set(0.015f); _slerpTime.set(3.0f); _guiButton.set(glm::ivec2(32, 64)); _friction.set(glm::vec4(0.025, 0.025, 0.02, 0.02)); } void TouchInteraction::tap() { _tap = true; } void TouchInteraction::touchActive(bool active) { _touchActive = active; } // Get & Setters Camera* TouchInteraction::getCamera() { return _camera; } const SceneGraphNode* TouchInteraction::getFocusNode() { return global::navigationHandler.orbitalNavigator().anchorNode(); } void TouchInteraction::setCamera(Camera* camera) { _camera = camera; } void TouchInteraction::setFocusNode(const SceneGraphNode* focusNode) { global::navigationHandler.orbitalNavigator().setAnchorNode(focusNode->identifier()); } void FrameTimeAverage::updateWithNewFrame(double sample) { if (sample > 0.0005) { _samples[_index++] = sample; if (_index >= TotalSamples) { _index = 0; } if (_nSamples < TotalSamples) { _nSamples++; } } } double FrameTimeAverage::averageFrameTime() const { double ft; if (_nSamples == 0) ft = 1.0 / 60.0; //Just guess at 60fps if no data is available yet else ft = std::accumulate(_samples, _samples + _nSamples, 0.0) / (double)(_nSamples); return ft; } #ifdef TOUCH_DEBUG_PROPERTIES TouchInteraction::DebugProperties::DebugProperties() : properties::PropertyOwner({ "TouchDebugProperties" }) , interactionMode( { "interactionMode", "Current interaction mode", "" }, "Unknown" ) , nFingers( {"nFingers", "Number of fingers", ""}, 0, 0, 20 ) , interpretedInteraction( { "interpretedInteraction", "Interpreted interaction", "" }, "Unknown" ) , normalizedCentroidDistance( { "normalizedCentroidDistance", "Normalized Centroid Distance", "" }, 0.f, 0.f, 0.01f ) , minDiff( { "minDiff", "Movement of slowest moving finger", "" }, 0.f, 0.f, 100.f ) , rollOn( { "rollOn", "Roll On", "" }, 0.f, 0.f, 100.f ) { addProperty(interactionMode); addProperty(nFingers); addProperty(interpretedInteraction); addProperty(normalizedCentroidDistance); addProperty(minDiff); addProperty(rollOn); } #endif } // openspace namespace