329 lines
13 KiB
C#
329 lines
13 KiB
C#
/*
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
* All rights reserved.
|
|
*
|
|
* Licensed under the Oculus SDK License Agreement (the "License");
|
|
* you may not use the Oculus SDK except in compliance with the License,
|
|
* which is provided at the time of installation or download, or which
|
|
* otherwise accompanies this software in either electronic or hard copy form.
|
|
*
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* https://developer.oculus.com/licenses/oculussdk/
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, the Oculus 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<TFeature>
|
|
{
|
|
[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; }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A generic provider that manages feature states through state collections in <see cref="FingerFeatureStateDictionary"/> and <see cref="TransformFeatureStateCollection"/>,
|
|
/// supporting the high-level providers <see cref="FingerFeatureStateProvider"/> and <see cref="TransformFeatureStateProvider"/>.
|
|
/// This provider implements hysteresis-based state management with time-based validation to ensure
|
|
/// stable and reliable feature state detection.
|
|
/// </summary>
|
|
/// <typeparam name="TFeature">An enum containing all trackable features</typeparam>
|
|
/// <typeparam name="TFeatureState">An enum of all possible states for each feature</typeparam>
|
|
public class FeatureStateProvider<TFeature, TFeatureState>
|
|
where TFeature : unmanaged, Enum
|
|
where TFeatureState : IEquatable<TFeatureState>
|
|
{
|
|
/// <summary>
|
|
/// This should be updated with current value of the input data frameId. It is used to
|
|
/// determine if values need to be recalculated.
|
|
/// </summary>
|
|
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<TFeature, TFeatureState>[] _featureToThresholds;
|
|
|
|
private readonly Func<TFeature, float?> _valueReader;
|
|
private readonly Func<TFeature, int> _featureToInt;
|
|
private readonly Func<float> _timeProvider;
|
|
|
|
|
|
|
|
#region Lookup Helpers
|
|
private int EnumToInt(TFeature value) => _featureToInt(value);
|
|
|
|
private static readonly TFeature[] FeatureEnumValues = (TFeature[])Enum.GetValues(typeof(TFeature));
|
|
|
|
private IFeatureThresholds<TFeature, TFeatureState> _featureThresholds;
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Initializes the provider with value reading, feature mapping, and time tracking functions.
|
|
/// </summary>
|
|
/// <param name="valueReader">Function that reads the current value of a feature</param>
|
|
/// <param name="featureToInt">Function that maps feature enum values to integer indices</param>
|
|
/// <param name="timeProvider">Function that provides the current time value</param>
|
|
public FeatureStateProvider(Func<TFeature, float?> valueReader,
|
|
Func<TFeature, int> featureToInt,
|
|
Func<float> timeProvider)
|
|
{
|
|
_valueReader = valueReader;
|
|
_featureToInt = featureToInt;
|
|
_timeProvider = timeProvider;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the threshold configuration for all features.
|
|
/// </summary>
|
|
/// <param name="featureThresholds">Configuration defining state transition thresholds</param>
|
|
public void InitializeThresholds(IFeatureThresholds<TFeature, TFeatureState> featureThresholds)
|
|
{
|
|
_featureThresholds = featureThresholds;
|
|
_featureToThresholds = ValidateFeatureThresholds(featureThresholds.FeatureStateThresholds);
|
|
|
|
InitializeStates();
|
|
}
|
|
|
|
public IFeatureStateThresholds<TFeature, TFeatureState>[] ValidateFeatureThresholds(
|
|
IReadOnlyList<IFeatureStateThresholds<TFeature, TFeatureState>> featureStateThresholdsList)
|
|
{
|
|
var featureToFeatureStateThresholds =
|
|
new IFeatureStateThresholds<TFeature, TFeatureState>[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<TFeature, TFeatureState> GetFeatureThresholds(TFeature feature)
|
|
{
|
|
Assert.IsNotNull(_featureToThresholds, "Must call InitializeThresholds() before querying state");
|
|
return ref _featureToThresholds[EnumToInt(feature)];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the current state of a specified feature, calculating and potentially updating it based on the latest feature value.
|
|
/// </summary>
|
|
/// <param name="feature">The feature to query</param>
|
|
/// <returns>The current state of the specified feature, which may or may not have changed this frame.</returns>
|
|
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<IFeatureStateThreshold<TFeatureState>> 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<IFeatureStateThreshold<TFeatureState>> 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]);
|
|
}
|
|
}
|
|
}
|
|
}
|