/************************************************************************************ Copyright : Copyright (c) Facebook Technologies, LLC and its affiliates. All rights reserved. Your use of this SDK or tool is subject to the Oculus SDK License Agreement, available at https://developer.oculus.com/licenses/oculussdk/ Unless required by applicable law or agreed to in writing, the Utilities SDK 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. ************************************************************************************/ using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Assertions; namespace Oculus.Interaction.PoseDetection { public enum FeatureStateActiveMode { Is, IsNot, } [Serializable] public abstract class FeatureConfigBase { [SerializeField] private FeatureStateActiveMode _mode; [SerializeField] private TFeature _feature; [SerializeField] private string _state; public FeatureStateActiveMode Mode { get => _mode; set { _mode = value; } } public TFeature Feature { get => _feature; set { _feature = value; } } public string State { get => _state; set { _state = value; } } } /// /// A helper class that keeps track of the current state of features, quantized into /// corresponding FeatureStates. /// /// /// An enum containing all features that can be tracked. /// /// /// An enum of all the possible states of each member of the param. /// The name of each member of this enum must be prefixed with one of the values of TFeature. /// public class FeatureStateProvider where TFeature : unmanaged, Enum where TFeatureState : IEquatable { /// /// This should be updated with current value of the input data frameId. It is used to /// determine if values need to be recalculated. /// public int LastUpdatedFrameId { get; set; } private struct FeatureStateSnapshot { public bool HasCurrentState; public TFeatureState State; public TFeatureState DesiredState; public int LastUpdatedFrameId; public double DesiredStateEntryTime; } // A map of Map of (int)Feature => current state private FeatureStateSnapshot[] _featureToCurrentState; // A map of Map of (int)Feature => threshold configuration private IFeatureStateThresholds[] _featureToThresholds; private readonly Func _valueReader; private readonly Func _featureToInt; private readonly Func _timeProvider; #region Lookup Helpers private int EnumToInt(TFeature value) => _featureToInt(value); private static readonly TFeature[] FeatureEnumValues = (TFeature[])Enum.GetValues(typeof(TFeature)); private IFeatureThresholds _featureThresholds; #endregion public FeatureStateProvider(Func valueReader, Func featureToInt, Func timeProvider) { _valueReader = valueReader; _featureToInt = featureToInt; _timeProvider = timeProvider; } public void InitializeThresholds(IFeatureThresholds featureThresholds) { _featureThresholds = featureThresholds; _featureToThresholds = ValidateFeatureThresholds(featureThresholds.FeatureStateThresholds); InitializeStates(); } public IFeatureStateThresholds[] ValidateFeatureThresholds( IReadOnlyList> featureStateThresholdsList) { var featureToFeatureStateThresholds = new IFeatureStateThresholds[Enum.GetNames(typeof(TFeature)).Length]; foreach (var featureStateThresholds in featureStateThresholdsList) { var featureIdx = EnumToInt(featureStateThresholds.Feature); featureToFeatureStateThresholds[featureIdx] = featureStateThresholds; // Just check that the thresholds are set correctly. for (var index = 0; index < featureStateThresholds.Thresholds.Count; index++) { var featureStateThreshold = featureStateThresholds.Thresholds[index]; if (featureStateThreshold.ToFirstWhenBelow > featureStateThreshold.ToSecondWhenAbove) { Assert.IsTrue(false, $"Feature {featureStateThresholds.Feature} threshold at index {index}: ToFirstWhenBelow should be less than ToSecondWhenAbove."); } } } for (int i = 0; i < featureToFeatureStateThresholds.Length; i++) { if (featureToFeatureStateThresholds[i] == null) { Assert.IsNotNull(featureToFeatureStateThresholds[i], $"StateThresholds does not contain an entry for feature with value {i}"); } } return featureToFeatureStateThresholds; } private void InitializeStates() { // Set up current state _featureToCurrentState = new FeatureStateSnapshot[FeatureEnumValues.Length]; foreach (TFeature feature in FeatureEnumValues) { int featureIdx = EnumToInt(feature); // Set default state. ref var currentState = ref _featureToCurrentState[featureIdx]; currentState.State = default; currentState.DesiredState = default; currentState.DesiredStateEntryTime = 0; } } private ref IFeatureStateThresholds GetFeatureThresholds(TFeature feature) { Assert.IsNotNull(_featureToThresholds, "Must call InitializeThresholds() before querying state"); return ref _featureToThresholds[EnumToInt(feature)]; } public TFeatureState GetCurrentFeatureState(TFeature feature) { Assert.IsNotNull(_featureToThresholds, "Must call InitializeThresholds() before querying state"); ref var currentState = ref _featureToCurrentState[EnumToInt(feature)]; if (currentState.LastUpdatedFrameId == LastUpdatedFrameId) { return currentState.State; } // Reads the raw value float? value = _valueReader(feature); if (!value.HasValue) { return currentState.State; } // Hand data changed since this was last queried. currentState.LastUpdatedFrameId = LastUpdatedFrameId; // Determine which state we should transition to based on the thresholds, and previous state. var featureStateThresholds = GetFeatureThresholds(feature).Thresholds; TFeatureState desiredState; if (!currentState.HasCurrentState) { desiredState = ReadDesiredState(value.Value, featureStateThresholds); } else { desiredState = ReadDesiredState(value.Value, featureStateThresholds, currentState.State); } // If this is the same as the current state, do nothing. if (desiredState.Equals(currentState.State)) { return currentState.State; } // If the desired state is different from the previous frame, reset the timer var currentTime = _timeProvider(); if (!desiredState.Equals(currentState.DesiredState)) { currentState.DesiredStateEntryTime = currentTime; currentState.DesiredState = desiredState; } // If the time in the desired state has exceeded the threshold, update the actual // state. if (currentState.DesiredStateEntryTime + _featureThresholds.MinTimeInState <= currentTime) { currentState.HasCurrentState = true; currentState.State = desiredState; } return currentState.State; } private TFeatureState ReadDesiredState(float value, IReadOnlyList> featureStateThresholds, TFeatureState previousState) { // Run it through the threshold calculation. var currentFeatureState = previousState; for (int i = 0; i < featureStateThresholds.Count; ++i) { var featureStateThreshold = featureStateThresholds[i]; if (currentFeatureState.Equals(featureStateThreshold.FirstState) && value > featureStateThreshold.ToSecondWhenAbove) { // In the first state and exceeded the threshold to enter the second state. return featureStateThreshold.SecondState; } if (currentFeatureState.Equals(featureStateThreshold.SecondState) && value < featureStateThreshold.ToFirstWhenBelow) { // In the second state and exceeded the threshold to enter the first state. return featureStateThreshold.FirstState; } } return previousState; } private TFeatureState ReadDesiredState(float value, IReadOnlyList> featureStateThresholds) { // Run it through the threshold calculation. TFeatureState currentFeatureState = default; for (int i = 0; i < featureStateThresholds.Count; ++i) { var featureStateThreshold = featureStateThresholds[i]; if (value <= featureStateThreshold.ToSecondWhenAbove) { currentFeatureState = featureStateThreshold.FirstState; break; } currentFeatureState = featureStateThreshold.SecondState; } return currentFeatureState; } public void ReadTouchedFeatureStates() { Assert.IsNotNull(_featureToThresholds, "Must call InitializeThresholds() before querying state"); for (var featureIdx = 0; featureIdx < _featureToCurrentState.Length; featureIdx++) { ref FeatureStateSnapshot stateSnapshot = ref _featureToCurrentState[featureIdx]; if (stateSnapshot.LastUpdatedFrameId == 0) { // This state has never been queried via IsStateActive, so don't // bother updating it. continue; } // Force evaluation with this new frame Id. GetCurrentFeatureState(FeatureEnumValues[featureIdx]); } } } }