VR4RoboticArm2/VR4RoboticArm/Library/PackageCache/com.meta.xr.sdk.interaction/Editor/PoseDetection/FeatureStateThresholdsEditor.cs
IonutMocanu d7aba243a2 Main
2025-09-08 11:04:02 +03:00

499 lines
20 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 System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.Assertions;
namespace Oculus.Interaction.PoseDetection.Editor
{
public abstract class FeatureStateThresholdsEditor<TFeature> : UnityEditor.Editor
where TFeature : unmanaged, Enum
{
#region static helpers
public static readonly TFeature[] FeatureEnumValues = (TFeature[])Enum.GetValues(typeof(TFeature));
public static TFeature IntToFeature(int value)
{
return FeatureEnumValues[value];
}
public static int FeatureToInt(TFeature feature)
{
for (int i = 0; i < FeatureEnumValues.Length; i++)
{
TFeature enumVal = FeatureEnumValues[i];
if (enumVal.Equals(feature))
{
return i;
}
}
throw new ArgumentOutOfRangeException();
}
#endregion
#region Model Classes
public class FeatureStateThresholdsModel
{
private readonly SerializedProperty _thresholdsProp;
private readonly SerializedProperty _featureProp;
public FeatureStateThresholdsModel(SerializedProperty self)
{
_featureProp = self.FindPropertyRelative("_feature");
_thresholdsProp = self.FindPropertyRelative("_thresholds");
Assert.IsNotNull(_featureProp);
Assert.IsNotNull(_thresholdsProp);
}
public TFeature Feature
{
get => IntToFeature(_featureProp.enumValueIndex);
set => _featureProp.enumValueIndex = FeatureToInt(value);
}
public SerializedProperty ThresholdsProp => _thresholdsProp;
}
public class FeatureStateThresholdModel
{
private readonly SerializedProperty _thresholdMidpointProp;
private readonly SerializedProperty _thresholdWidthProp;
private readonly SerializedProperty _firstStateProp;
private readonly SerializedProperty _secondStateProp;
public FeatureStateThresholdModel(SerializedProperty self)
{
_thresholdMidpointProp = self.FindPropertyRelative("_thresholdMidpoint");
_thresholdWidthProp = self.FindPropertyRelative("_thresholdWidth");
_firstStateProp = self.FindPropertyRelative("_firstState");
_secondStateProp = self.FindPropertyRelative("_secondState");
Assert.IsNotNull(_thresholdMidpointProp);
Assert.IsNotNull(_thresholdWidthProp);
Assert.IsNotNull(_firstStateProp);
Assert.IsNotNull(_secondStateProp);
}
public float ThresholdMidpoint
{
get => _thresholdMidpointProp.floatValue;
set { _thresholdMidpointProp.floatValue = value; }
}
public float ThresholdWidth
{
get => _thresholdWidthProp.floatValue;
set { _thresholdWidthProp.floatValue = value; }
}
public string FirstStateId
{
get => _firstStateProp.stringValue;
set => _firstStateProp.stringValue = value;
}
public string SecondStateId
{
get => _secondStateProp.stringValue;
set => _secondStateProp.stringValue = value;
}
public float ToFirstWhenBelow => ThresholdMidpoint - ThresholdWidth * 0.5f;
public float ToSecondWhenAbove => ThresholdMidpoint + ThresholdWidth * 0.5f;
}
#endregion
private SerializedProperty _rootProperty;
private SerializedProperty _minTimeInStateProp;
private readonly bool[] _featureVisible = new bool[FeatureEnumValues.Length];
private readonly Color _visStateColorPro = new Color32(194, 194, 194, 255);
private readonly Color _visStateColorLight = new Color32(56, 56, 56, 255);
private readonly Color _visTransitionColorPro = new Color32(80, 80, 80, 255);
private readonly Color _visTransitionColorLight = new Color32(160, 160, 160, 255);
private readonly Color _visDragColor = new Color32(0, 0, 128, 255);
private readonly Color _visBorderColor = new Color32(0, 0, 0, 255);
private const float _visHeight = 20.0f;
private const float _visMargin = 10.0f;
private const float _dragMargin = 10f;
private IReadOnlyDictionary<TFeature, FeatureDescription> _featureDescriptions;
protected abstract IReadOnlyDictionary<TFeature, FeatureDescription> CreateFeatureDescriptions();
protected abstract string FeatureMidpointTooltip { get; }
protected abstract string FeatureWidthTooltip { get; }
void OnEnable()
{
if (_featureDescriptions == null)
{
_featureDescriptions = CreateFeatureDescriptions();
}
if (_featureDescriptions.Count != FeatureEnumValues.Length)
{
throw new InvalidOperationException(
"CreateFeatureDescriptions() must return one key for each enum value.");
}
_rootProperty = serializedObject.FindProperty("_featureThresholds");
_minTimeInStateProp = serializedObject.FindProperty("_minTimeInState");
for (var index = 0; index < _featureVisible.Length; index++)
{
_featureVisible[index] = true;
}
}
public override void OnInspectorGUI()
{
if (_rootProperty == null || !_rootProperty.isArray || _minTimeInStateProp == null)
{
return;
}
EditorGUILayout.LabelField("All Features", EditorStyles.whiteLargeLabel);
EditorGUILayout.PropertyField(_minTimeInStateProp);
GUILayout.Space(10);
EditorGUILayout.LabelField("Per Feature", EditorStyles.whiteLargeLabel);
foreach (TFeature feature in FeatureEnumValues)
{
FeatureStateThresholdsModel foundFeatureProp = null;
for (int i = 0; i < _rootProperty.arraySize; ++i)
{
var featureThresholdsProp =
new FeatureStateThresholdsModel(
_rootProperty.GetArrayElementAtIndex(i));
if (featureThresholdsProp.Feature.Equals(feature))
{
foundFeatureProp = featureThresholdsProp;
break;
}
}
ref bool isVisible = ref _featureVisible[FeatureToInt(feature)];
isVisible = EditorGUILayout.BeginFoldoutHeaderGroup(isVisible, $"{feature} Thresholds");
if (!isVisible)
{
EditorGUILayout.EndFoldoutHeaderGroup();
continue;
}
if (!IsFeatureThresholdsValid(foundFeatureProp))
{
if (GUILayout.Button("Create Config"))
{
foundFeatureProp = CreateFeatureStateConfig(feature);
}
else
{
foundFeatureProp = null;
}
}
if (foundFeatureProp != null)
{
RenderFeatureStates(feature, foundFeatureProp);
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
serializedObject.ApplyModifiedProperties();
}
private FeatureStateThresholdsModel CreateFeatureStateConfig(
TFeature feature)
{
// Delete any old invalid configs for this feature.
for (int i = 0; i < _rootProperty.arraySize;)
{
var model =
new FeatureStateThresholdsModel(
_rootProperty.GetArrayElementAtIndex(i));
if (model.Feature.Equals(feature))
{
_rootProperty.DeleteArrayElementAtIndex(i);
}
else
{
i++;
}
}
// Create a new config
int insertIndex = _rootProperty.arraySize;
_rootProperty.InsertArrayElementAtIndex(insertIndex);
var featureStateThresholds = new FeatureStateThresholdsModel(
_rootProperty.GetArrayElementAtIndex(insertIndex))
{
Feature = feature
};
// Set initial state
ResetFeatureStates(featureStateThresholds);
return featureStateThresholds;
}
private void ResetFeatureStates(FeatureStateThresholdsModel foundFeatureProp)
{
var states = _featureDescriptions[foundFeatureProp.Feature].FeatureStates;
var thresholdsArrayProp = foundFeatureProp.ThresholdsProp;
foundFeatureProp.ThresholdsProp.arraySize = states.Length - 1;
var featureDescription = _featureDescriptions[foundFeatureProp.Feature];
float minExpectedValue = featureDescription.MinValueHint;
float maxExpectedValue = featureDescription.MaxValueHint;
float range = maxExpectedValue - minExpectedValue;
float initialWidth = range * 0.075f;
float numStatesMultiplier = range / (states.Length);
for (int stateIdx = 0; stateIdx < states.Length - 1; ++stateIdx)
{
var featureState = states[stateIdx];
FeatureStateThresholdModel model = new FeatureStateThresholdModel(
thresholdsArrayProp.GetArrayElementAtIndex(stateIdx));
model.ThresholdMidpoint = minExpectedValue + (stateIdx + 1) * numStatesMultiplier;
model.ThresholdWidth = initialWidth;
model.FirstStateId = featureState.Id;
model.SecondStateId = states[stateIdx + 1].Id;
}
}
private bool IsFeatureThresholdsValid(FeatureStateThresholdsModel foundFeatureModel)
{
if (foundFeatureModel == null)
{
return false;
}
var states = _featureDescriptions[foundFeatureModel.Feature].FeatureStates;
if (foundFeatureModel.ThresholdsProp.arraySize != states.Length - 1)
{
return false;
}
for (var firstStateIdx = 0; firstStateIdx < states.Length - 1; firstStateIdx++)
{
var model = new FeatureStateThresholdModel(
foundFeatureModel.ThresholdsProp.GetArrayElementAtIndex(firstStateIdx));
if (states[firstStateIdx].Id != model.FirstStateId ||
states[firstStateIdx + 1].Id != model.SecondStateId)
{
return false;
}
}
return true;
}
private void RenderFeatureStates(TFeature feature, FeatureStateThresholdsModel featureStateThresholdsModel)
{
FeatureDescription featureDescription = _featureDescriptions[feature];
// Indent block
using (new EditorGUI.IndentLevelScope())
{
RenderFeatureDescription(featureDescription);
var states = _featureDescriptions[feature].FeatureStates;
float minVal = float.MaxValue;
float maxVal = float.MinValue;
bool overlappingValues = false;
float thresholdMaxWidth =
featureDescription.MaxValueHint - featureDescription.MinValueHint;
for (var firstStateIdx = 0; firstStateIdx < states.Length - 1; firstStateIdx++)
{
var firstState = states[firstStateIdx];
var secondState = states[firstStateIdx + 1];
EditorGUILayout.LabelField($"{firstState.Name} ⟷ {secondState.Name}", EditorStyles.label);
// Indent block
using (new EditorGUI.IndentLevelScope())
{
var model = new FeatureStateThresholdModel(
featureStateThresholdsModel.ThresholdsProp.GetArrayElementAtIndex(firstStateIdx));
if (model.ToFirstWhenBelow <= maxVal || model.ToSecondWhenAbove <= maxVal)
{
overlappingValues = true;
}
float thresholdMidpoint = model.ThresholdMidpoint;
float thresholdWidth = model.ThresholdWidth;
float newMidpoint = EditorGUILayout.FloatField(new GUIContent("Midpoint", FeatureMidpointTooltip), thresholdMidpoint);
float newWidth = EditorGUILayout.Slider(new GUIContent("Width", FeatureWidthTooltip), thresholdWidth, 0.0f,
thresholdMaxWidth);
if (Math.Abs(newMidpoint - thresholdMidpoint) > float.Epsilon ||
Math.Abs(newWidth - thresholdWidth) > float.Epsilon)
{
model.ThresholdMidpoint = newMidpoint;
model.ThresholdWidth = newWidth;
}
minVal = Mathf.Min(minVal, model.ToFirstWhenBelow);
maxVal = Mathf.Max(maxVal, model.ToSecondWhenAbove);
}
}
float range = maxVal - minVal;
if (range <= 0.0f)
{
EditorGUILayout.HelpBox("Invalid threshold values", MessageType.Warning);
}
else
{
if (overlappingValues)
{
EditorGUILayout.HelpBox("Overlapping threshold values",
MessageType.Warning);
}
RenderFeatureStateGraphic(featureStateThresholdsModel,
states,
Mathf.Min(featureDescription.MinValueHint, minVal),
Mathf.Max(featureDescription.MaxValueHint, maxVal));
}
}
}
private void RenderFeatureDescription(FeatureDescription featureDescription)
{
if (!String.IsNullOrWhiteSpace(featureDescription.ShortDescription))
{
EditorGUILayout.HelpBox(featureDescription.ShortDescription, MessageType.Info);
}
EditorGUILayout.LabelField(
new GUIContent("Expected value range", featureDescription.Description),
new GUIContent($"[{featureDescription.MinValueHint}, {featureDescription.MaxValueHint}]"));
}
private void RenderFeatureStateGraphic(FeatureStateThresholdsModel prop,
FeatureStateDescription[] stateDescriptions,
float minVal, float maxVal)
{
Rect lastRect = GUILayoutUtility.GetLastRect();
float xOffset = lastRect.xMin + _visMargin;
float widgetWidth = lastRect.width - _visMargin;
GUILayout.Space(_visHeight + _visMargin * 2);
EditorGUI.DrawRect(
new Rect(xOffset - 1, lastRect.yMax + _visMargin - 1, widgetWidth + 2.0f,
_visHeight + 2.0f), _visBorderColor);
float range = maxVal - minVal;
Color stateColor = EditorGUIUtility.isProSkin
? _visStateColorPro
: _visStateColorLight;
Color transitionColor = EditorGUIUtility.isProSkin
? _visTransitionColorPro
: _visTransitionColorLight;
GUIStyle richTextStyle = new GUIStyle();
richTextStyle.alignment = TextAnchor.MiddleCenter;
richTextStyle.normal.textColor = transitionColor;
for (var firstStateIdx = 0;
firstStateIdx < prop.ThresholdsProp.arraySize;
firstStateIdx++)
{
var model = new FeatureStateThresholdModel(
prop.ThresholdsProp.GetArrayElementAtIndex(firstStateIdx));
float firstPc = ((model.ToFirstWhenBelow - minVal)) / range;
DrawStateRect(firstPc, firstStateIdx);
xOffset += widgetWidth * firstPc;
minVal = model.ToFirstWhenBelow;
float secondPc = ((model.ToSecondWhenAbove - minVal)) / range;
Rect rect = DrawTransitionRect(secondPc);
UpdateDrag(rect, model, range / widgetWidth);
xOffset += widgetWidth * secondPc;
minVal = model.ToSecondWhenAbove;
}
float lastPc = ((maxVal - minVal)) / range;
DrawStateRect(lastPc, prop.ThresholdsProp.arraySize);
Rect DrawStateRect(float pc, int stateIndex)
{
Rect rect = new Rect(xOffset, lastRect.yMax + _visMargin,
widgetWidth * pc, _visHeight);
EditorGUI.DrawRect(rect, stateColor);
EditorGUI.LabelField(rect,
$"{stateDescriptions[stateIndex].Name}", richTextStyle);
return rect;
}
Rect DrawTransitionRect(float pc)
{
Rect rect = new Rect(xOffset, lastRect.yMax + _visMargin,
widgetWidth * pc, _visHeight);
EditorGUI.DrawRect(rect, transitionColor);
Rect leftRect = new Rect(rect);
leftRect.width = _dragMargin;
Rect rightRect = new Rect(rect);
rightRect.width = _dragMargin;
rightRect.x = rect.x + rect.width - _dragMargin;
EditorGUI.DrawRect(leftRect, _visDragColor);
EditorGUI.DrawRect(rightRect, _visDragColor);
return rect;
}
}
private void UpdateDrag(Rect rect, FeatureStateThresholdModel model, float factor)
{
Event currentEvent = Event.current;
Vector2 prevPos = currentEvent.mousePosition - currentEvent.delta;
if (currentEvent.type != EventType.MouseDrag
|| !rect.Contains(prevPos))
{
return;
}
float delta = currentEvent.delta.x * factor;
if (prevPos.x < rect.x + _dragMargin)
{
model.ThresholdMidpoint += delta * 0.5f;
model.ThresholdWidth -= delta;
}
else if (prevPos.x > rect.x + rect.width - _dragMargin)
{
model.ThresholdMidpoint += delta * 0.5f;
model.ThresholdWidth += delta;
}
else
{
model.ThresholdMidpoint += delta;
}
}
}
}