mirror of https://github.com/mosra/magnum.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
361 lines
15 KiB
361 lines
15 KiB
#ifndef Magnum_Platform_Gesture_h |
|
#define Magnum_Platform_Gesture_h |
|
/* |
|
This file is part of Magnum. |
|
|
|
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, |
|
2020, 2021, 2022, 2023, 2024, 2025 |
|
Vladimír Vondruš <mosra@centrum.cz> |
|
|
|
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. |
|
*/ |
|
|
|
/** @file |
|
* @brief Class @ref Magnum::Platform::TwoFingerGesture |
|
* @m_since_latest |
|
*/ |
|
|
|
#include "Magnum/Magnum.h" |
|
#include "Magnum/Math/Functions.h" /* Math::sqrt() */ |
|
#include "Magnum/Math/Complex.h" |
|
|
|
namespace Magnum { namespace Platform { |
|
|
|
/** |
|
@brief Two-finger gesture recognition |
|
@m_since_latest |
|
|
|
Tracks position of a primary finger and an arbitrary secondary finger based on |
|
pointer events passed to @ref pressEvent(), @ref releaseEvent() and |
|
@ref moveEvent(). Once two fingers are pressed, the instance is contextually |
|
convertible to @cpp true @ce, and @ref position(), @ref direction(), |
|
@ref relativeTranslation(), @ref relativeRotation() and @ref relativeScaling() |
|
contain gesture properties. Example usage: |
|
|
|
@snippet Platform.cpp TwoFingerGesture |
|
|
|
The interface is designed primarily for @ref Sdl2Application "*Application" |
|
subclasses and their @relativeref{Sdl2Application,PointerEvent} and |
|
@relativeref{Sdl2Application,PointerMoveEvent} instances, but works also with |
|
any other types that provide appropriate |
|
@relativeref{Sdl2Application::PointerEvent,source()}, |
|
@relativeref{Sdl2Application::PointerEvent,isPrimary()}, |
|
@relativeref{Sdl2Application::PointerEvent,id()} and @relativeref{Sdl2Application::PointerEvent,position()} members. |
|
|
|
@experimental |
|
*/ |
|
class TwoFingerGesture { |
|
public: |
|
/** |
|
* @brief Handle a press event |
|
* |
|
* Accepts a pointer event instance such as the one coming from |
|
* @ref Platform::Sdl2Application::pointerPressEvent(). If the event |
|
* comes from a primary finger, replaces the internal state with it, |
|
* waiting for the secondary finger press to happen. If the event comes |
|
* from a secondary finger, it's used only a primary finger is known, |
|
* there's no known secondary finger ID yet. or the ID matches the |
|
* known secondary finger ID. Events that don't come from a touch |
|
* source are ignored. Returns @cpp true @ce if the event was used, |
|
* @cpp false @ce if not. |
|
* |
|
* The function doesn't modify the event in any way. If needed, it's up |
|
* to the caller to call |
|
* @relativeref{Sdl2Application::PointerEvent,setAccepted()}. |
|
*/ |
|
template<class PointerEvent> bool pressEvent(const PointerEvent& event); |
|
|
|
/** |
|
* @brief Handle a release event |
|
* |
|
* Accepts a pointer event instance such as the one coming from |
|
* @ref Platform::Sdl2Application::pointerReleaseEvent(). If the |
|
* release comes from a primary finger whose ID is known, resets the |
|
* state for both the primary and secondary touch, waiting for a |
|
* primary finger press to happen again. Otherwise, if the release |
|
* comes from a secondary finger whose ID is known. resets just the |
|
* secondary finger state, waiting for a different secondary finger |
|
* press to happen. Events that don't come from a touch source are |
|
* ignored. Returns @cpp true @ce if the event was used, @cpp false @ce |
|
* if not. |
|
* |
|
* The function doesn't modify the event in any way. If needed, it's up |
|
* to the caller to call |
|
* @relativeref{Sdl2Application::PointerEvent,setAccepted()}. |
|
*/ |
|
template<class PointerEvent> bool releaseEvent(const PointerEvent& event); |
|
|
|
/** |
|
* @brief Handle a move event |
|
* |
|
* Accepts a pointer move event instance such as the one coming from |
|
* @ref Platform::Sdl2Application::pointerMoveEvent(). If the move |
|
* comes from a primary finger whose ID is known or from a secondary |
|
* finger whose ID is known, updates given finger state. Events that |
|
* don't come from a touch source are ignored. Returns @cpp true @ce if |
|
* the event was used, @cpp false @ce if not. |
|
* |
|
* The function doesn't modify the event in any way. If needed, it's up |
|
* to the caller to call |
|
* @relativeref{Sdl2Application::PointerEvent,setAccepted()}. |
|
*/ |
|
template<class PointerMoveEvent> bool moveEvent(const PointerMoveEvent& event); |
|
|
|
/** |
|
* @brief Count of known pressed fingers |
|
* |
|
* Is @cpp 0 @ce if @ref pressEvent() wasn't called yet or |
|
* @ref releaseEvent() happened for the primary finger, @cpp 1 @ce |
|
* if only the primary finger is pressed or a secondary finger was |
|
* released and @cpp 2 @ce if both the primary and a secondary finger |
|
* is currently pressed. |
|
* @see @ref isGesture() |
|
*/ |
|
UnsignedInt fingerCount() const { |
|
return (_primaryTouchId != NoTouchId) + |
|
(_secondaryTouchId != NoTouchId); |
|
} |
|
|
|
/** |
|
* @brief Whether the internal state represents a two-finger gesture |
|
* |
|
* Returns @cpp true @ce if both the primary and a secondary finger |
|
* are pressed, @cpp false @ce otherwise. Same as @ref operator bool(). |
|
*/ |
|
bool isGesture() const { |
|
return fingerCount() == 2; |
|
} |
|
|
|
/** |
|
* @brief Whether the internal state represents a two-finger gesture |
|
* |
|
* Returns @cpp true @ce if both the primary and a secondary finger |
|
* are pressed, @cpp false @ce otherwise. Same as @ref operator bool(). |
|
*/ |
|
explicit operator bool() const { |
|
return fingerCount() == 2; |
|
} |
|
|
|
/** |
|
* @brief Centroid between the two known pressed finger positions |
|
* |
|
* If only one or no fingers are pressed --- i.e., @ref isGesture() is |
|
* @cpp false @ce --- returns a NaN vector. |
|
* @see @ref direction(), @ref relativeTranslation(), |
|
* @ref Math::isNan() |
|
*/ |
|
Vector2 position() const { |
|
return (_primaryTouchPosition + _secondaryTouchPosition)*0.5f; |
|
} |
|
|
|
/** |
|
* @brief Direction from the center to the primary finger position |
|
* |
|
* Negate the return value to get direction from the center to the |
|
* secondary finger. If only one or no fingers are pressed --- i.e., |
|
* @ref isGesture() is @cpp false @ce --- returns a NaN vector. |
|
* @see @ref position(), @ref relativeRotation(), |
|
* @ref relativeScaling(), @ref Math::isNan() |
|
*/ |
|
Vector2 direction() const { |
|
return (_primaryTouchPosition - _secondaryTouchPosition)*0.5f; |
|
} |
|
|
|
/** |
|
* @brief Translation of the centroid relative to the previous finger positions |
|
* |
|
* If there was no movement since the press, returns a zero vector. If |
|
* only one or no fingers are pressed --- i.e., @ref isGesture() is |
|
* @cpp false @ce --- returns a NaN vector. |
|
* @see @ref position(), @ref relativeRotation(), |
|
* @ref relativeScaling(), @ref Math::isNan() |
|
*/ |
|
Vector2 relativeTranslation() const { |
|
return (_primaryTouchPosition - _primaryPreviousTouchPosition + |
|
_secondaryTouchPosition - _secondaryPreviousTouchPosition)*0.5f; |
|
} |
|
|
|
/** |
|
* @brief Rotation relative to the previous finger positions |
|
* |
|
* Note that given the event coordinates are in a Y down coordinate, |
|
* positive rotation angle is clockwise. If there was no movement since |
|
* the press, returns an identity rotation. If only one or no fingers |
|
* are pressed --- i.e., @ref isGesture() is @cpp false @ce --- returns |
|
* a complex NaN. |
|
* |
|
* The function returns a @relativeref{Math,Complex} instead of an |
|
* angle as the angle would likely be converted back to a rotation |
|
* representation anyway. Use @ref Complex::toMatrix(), |
|
* @ref Complex::transformVector() or @ref Complex::angle() if a |
|
* different representation is needed. |
|
* @see @ref direction(), @ref relativeTranslation(), |
|
* @ref relativeScaling(), @ref Math::isNan() |
|
*/ |
|
Complex relativeRotation() const { |
|
/* prev * rot = cur |
|
prev^-1 * prev * rot = prev^-1 * cur |
|
rot = prev^-1 * cur */ |
|
return Complex{(_primaryPreviousTouchPosition - _secondaryPreviousTouchPosition).normalized()}.inverted()*Complex{(_primaryTouchPosition - _secondaryTouchPosition).normalized()}; |
|
} |
|
|
|
/** |
|
* @brief Scaling relative to the previous finger positions |
|
* |
|
* The returned value is always positive. Values less than |
|
* @cpp 1.0f @ce are when the points are getting closer, values larger |
|
* than @cpp 1.0f @ce are when the points are getting further apart. If |
|
* there was no movement since the press, returns @cpp 1.0f @ce. If |
|
* only one or no fingers are pressed --- i.e., @ref isGesture() is |
|
* @cpp false @ce --- returns a NaN. |
|
* @see @ref direction(), @ref relativeTranslation(), |
|
* @ref relativeRotation(), @ref Math::isNan() |
|
*/ |
|
Float relativeScaling() const { |
|
return Math::sqrt( |
|
(_secondaryTouchPosition - _primaryTouchPosition).dot()/ |
|
(_secondaryPreviousTouchPosition - _primaryPreviousTouchPosition).dot()); |
|
} |
|
|
|
private: |
|
struct Touch { |
|
Long id; |
|
Vector2 position; |
|
}; |
|
|
|
/* ~Long{} is -1, which may collide with actual pointer IDs (SDL uses |
|
it to denote a mouse, for example) */ |
|
constexpr static Long NoTouchId = 1ll << 63; |
|
|
|
Long _primaryTouchId = NoTouchId; |
|
Vector2 _primaryTouchPosition{Constants::nan()}, |
|
_primaryPreviousTouchPosition{Constants::nan()}; |
|
|
|
Long _secondaryTouchId = NoTouchId; |
|
Vector2 _secondaryTouchPosition{Constants::nan()}, |
|
_secondaryPreviousTouchPosition{Constants::nan()}; |
|
}; |
|
|
|
namespace Implementation { |
|
|
|
/* Not all Application::PointerEventSource enums have a Touch member (e.g. |
|
GlfwApplication doesn't), this makes it return false for such types, instead |
|
of failing to compile */ |
|
template<class PointerEventSource> constexpr bool isTouchPointerEventSource(PointerEventSource p, decltype(PointerEventSource::Touch)* = nullptr) { |
|
return p == PointerEventSource::Touch; |
|
} |
|
constexpr bool isTouchPointerEventSource(...) { return false; } |
|
|
|
} |
|
|
|
template<class PointerEvent> bool TwoFingerGesture::pressEvent(const PointerEvent& event) { |
|
/* Filter away non-touch sources. Other than that just assume it's a finger |
|
or something equivalent, capable of multi-touch -- i.e., don't even |
|
check pointers(). */ |
|
if(!Implementation::isTouchPointerEventSource(event.source())) |
|
return false; |
|
|
|
/* If this is the primary finger, unconditionally replace the primary touch |
|
with it, and reset everything else */ |
|
if(event.isPrimary()) { |
|
_primaryTouchId = event.id(); |
|
_primaryTouchPosition = event.position(); |
|
_primaryPreviousTouchPosition = event.position(); |
|
_secondaryTouchId = NoTouchId; |
|
_secondaryTouchPosition = Vector2{Constants::nan()}; |
|
_secondaryPreviousTouchPosition = Vector2{Constants::nan()}; |
|
return true; |
|
} |
|
|
|
/* If this is a secondary finger and a primary finger is already known, |
|
remember it either if it has a matching ID or there's no recorded second |
|
touch yet */ |
|
if(_primaryTouchId != NoTouchId && (_secondaryTouchId == NoTouchId || event.id() == _secondaryTouchId)) { |
|
_secondaryTouchId = event.id(); |
|
_secondaryTouchPosition = event.position(); |
|
_secondaryPreviousTouchPosition = event.position(); |
|
return true; |
|
} |
|
|
|
/* If this is a secondary finger and a primary finger is not known yet, or |
|
if this is a secondary finger with a different ID, do nothing */ |
|
return false; |
|
} |
|
|
|
template<class PointerEvent> bool TwoFingerGesture::releaseEvent(const PointerEvent& event) { |
|
/* Filter away non-touch sources. Other than that just assume it's a finger |
|
or something equivalent, capable of multi-touch -- i.e., don't even |
|
check pointers(). */ |
|
if(!Implementation::isTouchPointerEventSource(event.source())) |
|
return false; |
|
|
|
/* If the primary finger is lifted, reset everything and wait for the next |
|
time a primary finger is pressed again */ |
|
if(event.isPrimary() && event.id() == _primaryTouchId) { |
|
_primaryTouchId = NoTouchId; |
|
_primaryTouchPosition = Vector2{Constants::nan()}; |
|
_primaryPreviousTouchPosition = Vector2{Constants::nan()}; |
|
_secondaryTouchId = NoTouchId; |
|
_secondaryTouchPosition = Vector2{Constants::nan()}; |
|
_secondaryPreviousTouchPosition = Vector2{Constants::nan()}; |
|
return true; |
|
} |
|
|
|
/* If this is a secondary finger, reset just that one, and wait for another |
|
secondary finger press to take up its place */ |
|
if(!event.isPrimary() && event.id() == _secondaryTouchId) { |
|
_secondaryTouchId = NoTouchId; |
|
_secondaryTouchPosition = Vector2{Constants::nan()}; |
|
_secondaryPreviousTouchPosition = Vector2{Constants::nan()}; |
|
return true; |
|
} |
|
|
|
/* If the IDs don't match or their primary/secondary state doesn't match, |
|
do nothing */ |
|
return false; |
|
} |
|
|
|
template<class PointerMoveEvent> bool TwoFingerGesture::moveEvent(const PointerMoveEvent& event) { |
|
/* Filter away non-touch sources. Other than that just assume it's a finger |
|
or something equivalent, capable of multi-touch -- i.e., don't even |
|
check pointers(). */ |
|
if(!Implementation::isTouchPointerEventSource(event.source())) |
|
return false; |
|
|
|
/* If the event matches any of the recorded IDs, update the corresponding |
|
values */ |
|
if(event.isPrimary() && event.id() == _primaryTouchId) { |
|
_primaryPreviousTouchPosition = _primaryTouchPosition; |
|
_primaryTouchPosition = event.position(); |
|
return true; |
|
} |
|
if(!event.isPrimary() && event.id() == _secondaryTouchId) { |
|
_secondaryPreviousTouchPosition = _secondaryTouchPosition; |
|
_secondaryTouchPosition = event.position(); |
|
return true; |
|
} |
|
|
|
/* If the IDs don't match or their primary/secondary state doesn't match, |
|
do nothing */ |
|
return false; |
|
} |
|
|
|
}} |
|
|
|
#endif
|
|
|