// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using System; using System.Collections.Generic; using UnityEngine; #if UNITY_WSA && !UNITY_2017_2_OR_NEWER using UnityEngine.VR.WSA.Input; #endif namespace HoloToolkit.Unity.InputModule { /// <summary> /// Input source for raw interactions sources information, which gives finer details about current source state and position /// than the standard GestureRecognizer. /// This mostly allows users to access the source up/down and detected/lost events, /// which are not communicated as part of standard Windows gestures. /// </summary> /// <remarks> /// This input source only triggers SourceUp/SourceDown and SourceDetected/SourceLost. /// Everything else is handled by InteractionInputSource. /// </remarks> [Obsolete("Will be removed in a future release")] public class RawInteractionInputSource : BaseInputSource { /// <summary> /// Data for an interaction source. /// </summary> private class SourceData { public SourceData(uint sourceId) { SourceId = sourceId; HasPosition = false; SourcePosition = Vector3.zero; IsSourceDown = false; IsSourceDownPending = false; SourceStateChanged = false; SourceStateUpdateTimer = -1; } public readonly uint SourceId; public bool HasPosition; public Vector3 SourcePosition; public bool IsSourceDown; public bool IsSourceDownPending; public bool SourceStateChanged; public float SourceStateUpdateTimer; } /// <summary> /// Delay before a source press is considered. /// This mitigates fake source taps that can sometimes be detected while the input source is moving. /// </summary> private const float SourcePressDelay = 0.07f; [Tooltip("Use unscaled time. This is useful for games that have a pause mechanism or otherwise adjust the game timescale.")] public bool UseUnscaledTime = true; /// <summary> /// Dictionary linking each source ID to its data. /// </summary> private readonly Dictionary<uint, SourceData> sourceIdToData = new Dictionary<uint, SourceData>(4); private readonly List<uint> pendingSourceIdDeletes = new List<uint>(); // HashSets used to be able to quickly update the sources data when they become visible / not visible private readonly HashSet<uint> currentSources = new HashSet<uint>(); private readonly HashSet<uint> newSources = new HashSet<uint>(); public override SupportedInputInfo GetSupportedInputInfo(uint sourceId) { SupportedInputInfo retVal = SupportedInputInfo.None; SourceData sourceData; if (sourceIdToData.TryGetValue(sourceId, out sourceData)) { if (sourceData.HasPosition) { retVal |= SupportedInputInfo.Position; } } return retVal; } public override bool TryGetMenu(uint sourceId, out bool isPressed) { throw new NotImplementedException(); } public override bool TryGetPointerPosition(uint sourceId, out Vector3 position) { SourceData sourceData; if (sourceIdToData.TryGetValue(sourceId, out sourceData)) { if (sourceData.HasPosition) { position = sourceData.SourcePosition; return true; } } // Else, the source doesn't have positional information position = Vector3.zero; return false; } public override bool TryGetPointerRotation(uint sourceId, out Quaternion orientation) { // Orientation is not supported by any Windows interaction sources orientation = Quaternion.identity; return false; } public override bool TryGetSourceKind(uint sourceId, out InteractionSourceInfo sourceKind) { throw new NotImplementedException(); } public override bool TryGetGripPosition(uint sourceId, out Vector3 position) { throw new NotImplementedException(); } public override bool TryGetGripRotation(uint sourceId, out Quaternion rotation) { throw new NotImplementedException(); } public override bool TryGetThumbstick(uint sourceId, out bool isPressed, out Vector2 position) { throw new NotImplementedException(); } public override bool TryGetTouchpad(uint sourceId, out bool isPressed, out bool isTouched, out Vector2 position) { throw new NotImplementedException(); } public override bool TryGetSelect(uint sourceId, out bool isPressed, out double pressedValue) { throw new NotImplementedException(); } public override bool TryGetGrasp(uint sourceId, out bool isPressed) { throw new NotImplementedException(); } public override bool TryGetPointingRay(uint sourceId, out Ray pointingRay) { throw new NotImplementedException(); } private void Update() { newSources.Clear(); currentSources.Clear(); UpdateSourceData(); SendSourceVisibilityEvents(); } /// <summary> /// Update the source data for the currently detected sources. /// </summary> private void UpdateSourceData() { #if UNITY_WSA && !UNITY_2017_2_OR_NEWER // Poll for updated reading from hands InteractionSourceState[] sourceStates = InteractionManager.GetCurrentReading(); if (sourceStates != null) { for (var i = 0; i < sourceStates.Length; ++i) { InteractionSourceState handSource = sourceStates[i]; SourceData sourceData = GetOrAddSourceData(handSource.source); currentSources.Add(handSource.source.id); UpdateSourceState(handSource, sourceData); } } #endif } #if UNITY_WSA && !UNITY_2017_2_OR_NEWER /// <summary> /// Gets the source data for the specified interaction source if it already exists, otherwise creates it. /// </summary> /// <param name="interactionSource">Interaction source for which data should be retrieved.</param> /// <returns>The source data requested.</returns> private SourceData GetOrAddSourceData(InteractionSource interactionSource) { SourceData sourceData; if (!sourceIdToData.TryGetValue(interactionSource.id, out sourceData)) { sourceData = new SourceData(interactionSource.id); sourceIdToData.Add(sourceData.SourceId, sourceData); newSources.Add(sourceData.SourceId); } return sourceData; } /// <summary> /// Updates the source positional information. /// </summary> /// <param name="interactionSource">Interaction source to use to update the position.</param> /// <param name="sourceData">SourceData structure to update.</param> private void UpdateSourceState(InteractionSourceState interactionSource, SourceData sourceData) { // Update source position Vector3 sourcePosition; if (interactionSource.properties.location.TryGetPosition(out sourcePosition)) { sourceData.HasPosition = true; sourceData.SourcePosition = sourcePosition; } // Check for source presses if (interactionSource.pressed != sourceData.IsSourceDownPending) { sourceData.IsSourceDownPending = interactionSource.pressed; sourceData.SourceStateUpdateTimer = SourcePressDelay; } // Source presses are delayed to mitigate issue with hand position shifting during air tap sourceData.SourceStateChanged = false; if (sourceData.SourceStateUpdateTimer >= 0) { float deltaTime = UseUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; sourceData.SourceStateUpdateTimer -= deltaTime; if (sourceData.SourceStateUpdateTimer < 0) { sourceData.IsSourceDown = sourceData.IsSourceDownPending; sourceData.SourceStateChanged = true; } } SendSourceStateEvents(sourceData); } #endif /// <summary> /// Sends the events for source state changes. /// </summary> /// <param name="sourceData">Source data for which events should be sent.</param> private void SendSourceStateEvents(SourceData sourceData) { // Source pressed/released events if (sourceData.SourceStateChanged) { if (sourceData.IsSourceDown) { InputManager.Instance.RaiseSourceDown(this, sourceData.SourceId, InteractionSourcePressInfo.Select); } else { InputManager.Instance.RaiseSourceUp(this, sourceData.SourceId, InteractionSourcePressInfo.Select); } } } /// <summary> /// Sends the events for source visibility changes. /// </summary> private void SendSourceVisibilityEvents() { // Send event for new sources that were added foreach (uint newSource in newSources) { InputManager.Instance.RaiseSourceDetected(this, newSource); } // Send event for sources that are no longer visible and remove them from our dictionary foreach (uint existingSource in sourceIdToData.Keys) { if (!currentSources.Contains(existingSource)) { pendingSourceIdDeletes.Add(existingSource); InputManager.Instance.RaiseSourceLost(this, existingSource); } } // Remove pending source IDs for (int i = 0; i < pendingSourceIdDeletes.Count; ++i) { sourceIdToData.Remove(pendingSourceIdDeletes[i]); } pendingSourceIdDeletes.Clear(); } } }