//-----------------------------------------------------------------------
//
//
// Copyright 2018 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
//-----------------------------------------------------------------------
// Modifications copyright © 2020 Unity Technologies ApS
#if AR_FOUNDATION_PRESENT || PACKAGE_DOCS_GENERATION
using System;
using System.Collections.Generic;
using Unity.XR.CoreUtils;
namespace UnityEngine.XR.Interaction.Toolkit.AR
{
///
/// A Gesture Recognizer processes touch input to determine if a gesture should start
/// and fires an event when the gesture is started.
///
/// The actual gesture.
///
/// To determine when a gesture is finished/updated, listen to events on the
/// object.
///
public abstract partial class GestureRecognizer where T : Gesture
{
///
/// Calls the methods in its invocation list when a gesture is started.
/// To receive an event when the gesture is finished/updated, listen to
/// events on the object.
///
public event Action onGestureStarted;
///
/// The
/// that will be used by gestures (such as to get the [Camera](xref:UnityEngine.Camera)
/// or to transform from Session space).
///
public XROrigin xrOrigin { get; set; }
///
/// Gets or sets layer mask used for limiting ray cast targets.
///
public LayerMask raycastMask { get; set;} = -1;
///
/// Gets or sets type of interaction with trigger colliders via ray cast.
///
///
/// When set to , the value of Queries Hit Triggers ()
/// in Edit > Project Settings > Physics will be used.
///
public QueryTriggerInteraction raycastTriggerInteraction { get; set;} = QueryTriggerInteraction.Ignore;
///
/// List of current active gestures.
///
///
/// Gestures must be added or removed using and
/// rather than by directly modifying this list. This list should be treated as read-only.
///
///
///
protected List gestures { get; } = new List();
readonly List m_PostponedGesturesToRemove = new List();
readonly List m_DeadGesturePool = new List();
bool m_IsUpdatingGestures;
///
/// Instantiate and update all gestures.
///
public void Update()
{
// Instantiate gestures based on touch input.
// Just because a gesture was created, doesn't mean that it is started.
// For example, a DragGesture is created when the user touches down,
// but doesn't actually start until the touch has moved beyond a threshold.
TryCreateGestures();
// Update all gestures
m_IsUpdatingGestures = true;
foreach (var gesture in gestures)
{
gesture.Update();
}
m_IsUpdatingGestures = false;
// Gestures that finished can now be removed. Removals may have been postponed
// while the gestures list was being iterated.
if (m_PostponedGesturesToRemove.Count > 0)
{
foreach (var gesture in m_PostponedGesturesToRemove)
{
gestures.Remove(gesture);
m_DeadGesturePool.Add(gesture);
}
m_PostponedGesturesToRemove.Clear();
}
}
///
/// Try to recognize and create gestures.
///
protected abstract void TryCreateGestures();
///
/// Add the given to be managed
/// so that it can be updated during .
///
/// The gesture to add.
///
protected void AddGesture(T gesture)
{
// Should not attempt to add to Gestures list while iterating that list
if (m_IsUpdatingGestures)
{
Debug.LogError($"Cannot add {typeof(T).Name} while updating Gestures." +
$" It should be done during {nameof(TryCreateGestures)} instead.");
return;
}
gesture.onStart += OnStart;
gesture.onFinished += OnFinished;
gestures.Add(gesture);
}
///
/// Remove the given from being managed.
/// After being removed, it will no longer be updated during .
///
/// The gesture to remove.
///
protected void RemoveGesture(T gesture)
{
// Should not attempt to remove from Gestures list while iterating that list
if (m_IsUpdatingGestures)
{
m_PostponedGesturesToRemove.Add(gesture);
}
else
{
gestures.Remove(gesture);
m_DeadGesturePool.Add(gesture);
}
}
///
/// Helper function for creating or re-initializing one-finger gestures when a touch begins.
///
/// Function to be executed to create the gesture if no dead gesture was available to re-initialize.
/// Function to be executed to re-initialize the gesture if a dead gesture was available to re-initialize.
protected void TryCreateOneFingerGestureOnTouchBegan(
Func createGestureFunction,
Action reinitializeGestureFunction)
{
TryCreateOneFingerGestureOnTouchBegan(
TouchConverterClosureHelper.GetFunc(createGestureFunction),
TouchActionConverterClosureHelper.GetAction(reinitializeGestureFunction));
}
///
/// Helper function for creating or reinitializing one-finger gestures when a touch begins.
///
/// Function to be executed to create the gesture if no dead gesture was available to re-initialize.
/// Function to be executed to re-initialize the gesture if a dead gesture was available to re-initialize.
protected void TryCreateOneFingerGestureOnTouchBegan(
Func createGestureFunction,
Action reinitializeGestureFunction)
{
TryCreateOneFingerGestureOnTouchBegan(
TouchConverterClosureHelper.GetFunc(createGestureFunction),
TouchActionConverterClosureHelper.GetAction(reinitializeGestureFunction));
}
void TryCreateOneFingerGestureOnTouchBegan(
Func createGestureFunction,
Action reinitializeGestureFunction)
{
foreach (var touch in GestureTouchesUtility.touches)
{
if (touch.isPhaseBegan &&
!GestureTouchesUtility.IsFingerIdRetained(touch.fingerId) &&
!GestureTouchesUtility.IsTouchOffScreenEdge(touch))
{
T gesture;
if (m_DeadGesturePool.Count > 0)
{
gesture = m_DeadGesturePool[m_DeadGesturePool.Count - 1];
m_DeadGesturePool.RemoveAt(m_DeadGesturePool.Count - 1);
reinitializeGestureFunction(gesture, touch);
}
else
{
gesture = createGestureFunction(touch);
}
AddGesture(gesture);
}
}
}
///
/// Helper function for creating or re-initializing two-finger gestures when a touch begins.
///
/// Function to be executed to create the gesture if no dead gesture was available to re-initialize.
/// Function to be executed to re-initialize the gesture if a dead gesture was available to re-initialize.
protected void TryCreateTwoFingerGestureOnTouchBegan(
Func createGestureFunction,
Action reinitializeGestureFunction)
{
TryCreateTwoFingerGestureOnTouchBegan(
TouchConverterClosureHelper.GetFunc(createGestureFunction),
TouchActionConverterClosureHelper.GetAction(reinitializeGestureFunction));
}
///
/// Helper function for creating two-finger gestures when a touch begins.
///
/// Function to be executed to create the gesture if no dead gesture was available to re-initialize.
/// Function to be executed to re-initialize the gesture if a dead gesture was available to re-initialize.
protected void TryCreateTwoFingerGestureOnTouchBegan(
Func createGestureFunction,
Action reinitializeGestureFunction)
{
TryCreateTwoFingerGestureOnTouchBegan(
TouchConverterClosureHelper.GetFunc(createGestureFunction),
TouchActionConverterClosureHelper.GetAction(reinitializeGestureFunction));
}
void TryCreateTwoFingerGestureOnTouchBegan(
Func createGestureFunction,
Action reinitializeGestureFunction)
{
var touches = GestureTouchesUtility.touches;
if (touches.Count < 2)
return;
for (var i = 0; i < touches.Count; ++i)
{
TryCreateGestureTwoFingerGestureOnTouchBeganForTouchIndex(i, touches, createGestureFunction, reinitializeGestureFunction);
}
}
void TryCreateGestureTwoFingerGestureOnTouchBeganForTouchIndex(
int touchIndex,
IReadOnlyList touches,
Func createGestureFunction,
Action reinitializeGestureFunction)
{
var touch = touches[touchIndex];
if (!touch.isPhaseBegan ||
GestureTouchesUtility.IsFingerIdRetained(touch.fingerId) ||
GestureTouchesUtility.IsTouchOffScreenEdge(touch))
{
return;
}
for (var i = 0; i < touches.Count; i++)
{
if (i == touchIndex)
continue;
var otherTouch = touches[i];
// Prevents the same two touches from creating two gestures if both touches began on
// the same frame.
if (i < touchIndex && otherTouch.isPhaseBegan)
{
continue;
}
if (GestureTouchesUtility.IsFingerIdRetained(otherTouch.fingerId) ||
GestureTouchesUtility.IsTouchOffScreenEdge(otherTouch))
{
continue;
}
T gesture;
if (m_DeadGesturePool.Count > 0)
{
gesture = m_DeadGesturePool[m_DeadGesturePool.Count - 1];
m_DeadGesturePool.RemoveAt(m_DeadGesturePool.Count - 1);
reinitializeGestureFunction(gesture, touch, otherTouch);
}
else
{
gesture = createGestureFunction(touch, otherTouch);
}
AddGesture(gesture);
}
}
void OnStart(T gesture)
{
onGestureStarted?.Invoke(gesture);
}
void OnFinished(T gesture)
{
RemoveGesture(gesture);
}
///
/// Helper class to preallocate delegates to avoid GC Alloc that would happen
/// when passing the lambda to the methods which create one or two finger gestures.
///
static class TouchConverterClosureHelper
{
// One Touch to Gesture input argument Func
static Func s_CreateGestureFromOneTouchFunction;
static Func s_CreateGestureFromOneEnhancedTouchFunction;
// Two Touch to Gesture input argument Func
static Func s_CreateGestureFromTwoTouchFunction;
static Func s_CreateGestureFromTwoEnhancedTouchFunction;
// Preallocate delegates to avoid GC Alloc
static readonly Func s_ConvertUsingOneTouch = ConvertUsingOneTouch;
static readonly Func s_ConvertUsingOneEnhancedTouch = ConvertUsingOneEnhancedTouch;
static readonly Func s_ConvertUsingTwoTouch = ConvertUsingTwoTouch;
static readonly Func s_ConvertUsingTwoEnhancedTouch = ConvertUsingTwoEnhancedTouch;
public static Func GetFunc(Func createGestureFunction)
{
s_CreateGestureFromOneTouchFunction = createGestureFunction;
return s_ConvertUsingOneTouch;
}
public static Func GetFunc(Func createGestureFunction)
{
s_CreateGestureFromOneEnhancedTouchFunction = createGestureFunction;
return s_ConvertUsingOneEnhancedTouch;
}
public static Func GetFunc(Func createGestureFunction)
{
s_CreateGestureFromTwoTouchFunction = createGestureFunction;
return s_ConvertUsingTwoTouch;
}
public static Func GetFunc(Func createGestureFunction)
{
s_CreateGestureFromTwoEnhancedTouchFunction = createGestureFunction;
return s_ConvertUsingTwoEnhancedTouch;
}
static T ConvertUsingOneTouch(CommonTouch touch) =>
s_CreateGestureFromOneTouchFunction(touch.GetTouch());
static T ConvertUsingOneEnhancedTouch(CommonTouch touch) =>
s_CreateGestureFromOneEnhancedTouchFunction(touch.GetEnhancedTouch());
static T ConvertUsingTwoTouch(CommonTouch touch, CommonTouch otherTouch) =>
s_CreateGestureFromTwoTouchFunction(touch.GetTouch(), otherTouch.GetTouch());
static T ConvertUsingTwoEnhancedTouch(CommonTouch touch, CommonTouch otherTouch) =>
s_CreateGestureFromTwoEnhancedTouchFunction(touch.GetEnhancedTouch(), otherTouch.GetEnhancedTouch());
}
///
/// Helper class to preallocate delegates to avoid GC Alloc that would happen
/// when passing the lambda to the methods which reinitialize one or two finger gestures.
///
static class TouchActionConverterClosureHelper
{
// One Touch to Gesture input argument Func
static Action s_ReinitializeGestureFromOneTouchFunction;
static Action s_ReinitializeGestureFromOneEnhancedTouchFunction;
// Two Touch to Gesture input argument Func
static Action s_ReinitializeGestureFromTwoTouchFunction;
static Action s_ReinitializeGestureFromTwoEnhancedTouchFunction;
// Preallocate delegates to avoid GC Alloc
static readonly Action s_ConvertUsingOneTouch = ConvertUsingOneTouch;
static readonly Action s_ConvertUsingOneEnhancedTouch = ConvertUsingOneEnhancedTouch;
static readonly Action s_ConvertUsingTwoTouch = ConvertUsingTwoTouch;
static readonly Action s_ConvertUsingTwoEnhancedTouch = ConvertUsingTwoEnhancedTouch;
public static Action GetAction(Action reinitializeGestureFunction)
{
s_ReinitializeGestureFromOneTouchFunction = reinitializeGestureFunction;
return s_ConvertUsingOneTouch;
}
public static Action GetAction(Action reinitializeGestureFunction)
{
s_ReinitializeGestureFromOneEnhancedTouchFunction = reinitializeGestureFunction;
return s_ConvertUsingOneEnhancedTouch;
}
public static Action GetAction(Action reinitializeGestureFunction)
{
s_ReinitializeGestureFromTwoTouchFunction = reinitializeGestureFunction;
return s_ConvertUsingTwoTouch;
}
public static Action GetAction(Action reinitializeGestureFunction)
{
s_ReinitializeGestureFromTwoEnhancedTouchFunction = reinitializeGestureFunction;
return s_ConvertUsingTwoEnhancedTouch;
}
static void ConvertUsingOneTouch(T gesture, CommonTouch touch) =>
s_ReinitializeGestureFromOneTouchFunction(gesture, touch.GetTouch());
static void ConvertUsingOneEnhancedTouch(T gesture, CommonTouch touch) =>
s_ReinitializeGestureFromOneEnhancedTouchFunction(gesture, touch.GetEnhancedTouch());
static void ConvertUsingTwoTouch(T gesture, CommonTouch touch, CommonTouch otherTouch) =>
s_ReinitializeGestureFromTwoTouchFunction(gesture, touch.GetTouch(), otherTouch.GetTouch());
static void ConvertUsingTwoEnhancedTouch(T gesture, CommonTouch touch, CommonTouch otherTouch) =>
s_ReinitializeGestureFromTwoEnhancedTouchFunction(gesture, touch.GetEnhancedTouch(), otherTouch.GetEnhancedTouch());
}
}
}
#endif