//----------------------------------------------------------------------- // // // 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