Detect and display unconfigured properties

This commit is contained in:
bd_ 2022-10-15 15:57:17 -07:00 committed by bd_
parent 8ee6771229
commit b3bf7ea600
3 changed files with 445 additions and 17 deletions

View File

@ -1,7 +1,11 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Serialization;
namespace net.fushizen.modular_avatar.core.editor
{
@ -17,27 +21,81 @@ namespace net.fushizen.modular_avatar.core.editor
private ReorderableList _reorderableList;
private SerializedProperty _parameters;
private bool _needsRebuild = false;
private float elemHeight;
private readonly List<int> _selectedIndices = new List<int>();
private ImplicitParams _implicitParams = null;
private SerializedObject _implicitParamsObj;
private SerializedProperty _implicitParamsProp;
private class ImplicitParams : ScriptableObject
{
public List<ParameterConfig> ImplicitParameters = new List<ParameterConfig>();
}
private void OnEnable()
{
SetupList();
}
private SerializedProperty GetParamByIndex(int idx)
{
if (idx >= 0)
{
return _parameters.GetArrayElementAtIndex(idx);
}
else
{
idx = -(idx + 1);
return _implicitParamsProp.GetArrayElementAtIndex(idx);
}
}
private void DetectParameters()
{
if (_implicitParams == null || _implicitParamsProp == null)
{
_implicitParams = CreateInstance<ImplicitParams>();
_implicitParamsObj = new SerializedObject(_implicitParams);
_implicitParamsProp = _implicitParamsObj.FindProperty(nameof(ImplicitParams.ImplicitParameters));
}
var target = (ModularAvatarParameters) this.target;
var registered = target.parameters.Select(p => p.nameOrPrefix + (p.isPrefix ? "*" : ""))
.ToImmutableHashSet();
_implicitParams.ImplicitParameters.Clear();
var detected = ParameterPolicy.ProbeParameters(((ModularAvatarParameters) target).gameObject);
foreach (var param in detected.Values)
{
if (registered.Contains(param.MapKey)) continue;
var config = new ParameterConfig()
{
internalParameter = false,
isPrefix = param.IsPrefix,
nameOrPrefix = param.OriginalName,
remapTo = "",
};
_implicitParams.ImplicitParameters.Add(config);
_selectedIndices.Add(-_implicitParams.ImplicitParameters.Count);
}
_implicitParamsObj.Update();
}
private void SetupList()
{
_parameters = serializedObject.FindProperty(nameof(ModularAvatarParameters.parameters));
if (_devMode)
{
_reorderableList = new ReorderableList(
serializedObject,
_parameters,
true, true, true, true
);
_reorderableList.drawHeaderCallback = DrawHeader;
_reorderableList.drawElementCallback = DrawElement;
_reorderableList.onAddCallback = AddElement;
_reorderableList.onRemoveCallback = RemoveElement;
var target = (ModularAvatarParameters) this.target;
_selectedIndices.Clear();
_selectedIndices.AddRange(Enumerable.Range(0, target.parameters.Count));
}
else
{
@ -50,42 +108,120 @@ namespace net.fushizen.modular_avatar.core.editor
_selectedIndices.Add(i);
}
}
DetectParameters();
if (_reorderableList == null)
{
_reorderableList = new ReorderableList(
_selectedIndices,
typeof(int),
false, true, false, false
false, true, _devMode, _devMode
);
_reorderableList.drawHeaderCallback = DrawHeader;
_reorderableList.drawElementCallback = DrawElement;
_reorderableList.onAddCallback = AddElement;
_reorderableList.onRemoveCallback = RemoveElement;
_reorderableList.elementHeightCallback = ElementHeight;
_reorderableList.onReorderCallbackWithDetails = ReorderElements;
elemHeight = _reorderableList.elementHeight;
}
_reorderableList.displayAdd = _devMode;
_reorderableList.displayRemove = _devMode;
_reorderableList.draggable = _devMode;
_needsRebuild = false;
}
private void ReorderElements(ReorderableList list, int oldindex, int newindex)
{
if (_selectedIndices[oldindex] >= 0 && _selectedIndices[newindex] >= 0)
{
// We're in dev mode, so the "real" entries are in the same order in _selectedIndices
// as the underlying. So just reorder them in the underlying object.
serializedObject.ApplyModifiedProperties();
var target = (ModularAvatarParameters) this.target;
var tmp = target.parameters[oldindex];
target.parameters.RemoveAt(oldindex);
target.parameters.Insert(newindex, tmp);
serializedObject.Update();
}
// The reorderable list trashed our internal indices, so force a rebuild on next repaint.
_needsRebuild = true;
}
private void AddElement(ReorderableList list)
{
_parameters.arraySize += 1;
_selectedIndices.Insert(_parameters.arraySize - 1, _parameters.arraySize - 1);
list.index = _parameters.arraySize - 1;
}
private void RemoveElement(ReorderableList list)
{
if (list.index < 0) return;
if (list.index < 0 || _selectedIndices[list.index] < 0) return;
_parameters.DeleteArrayElementAtIndex(list.index);
_needsRebuild = true;
}
private float ElementHeight(int index)
{
if (_selectedIndices[index] == -1)
{
return elemHeight * 2;
}
else
{
return elemHeight;
}
}
private void DrawAutodetectHeader(ref Rect rect)
{
Rect top = rect;
top.height /= 2;
Rect bottom = rect;
bottom.y += top.height;
bottom.height -= top.height;
rect = bottom;
var style = new GUIStyle(EditorStyles.label);
style.fontStyle = FontStyle.Italic;
var content = new GUIContent(" Autodetected Parameters ");
var size = style.CalcSize(content);
var centeredRect = new Rect(
top.x + (top.width - size.x) / 2,
top.y + (top.height - size.y) / 2,
size.x,
size.y
);
EditorGUI.LabelField(centeredRect, content, style);
}
private void DrawElement(Rect rect, int index, bool isactive, bool isfocused)
{
var originalIndex = index;
index = _selectedIndices[index];
var elem = GetParamByIndex(index);
if (index == -1)
{
DrawAutodetectHeader(ref rect);
}
var margin = 20;
var halfMargin = margin / 2;
var leftHalf = new Rect(rect.x, rect.y, rect.width / 2 - halfMargin, rect.height);
var rightHalf = new Rect(rect.x + leftHalf.width + halfMargin, rect.y, leftHalf.width, rect.height);
if (!_devMode) index = _selectedIndices[index];
var elem = _parameters.GetArrayElementAtIndex(index);
var nameOrPrefix = elem.FindPropertyRelative(nameof(ParameterConfig.nameOrPrefix));
var remapTo = elem.FindPropertyRelative(nameof(ParameterConfig.remapTo));
var internalParameter = elem.FindPropertyRelative(nameof(ParameterConfig.internalParameter));
@ -94,6 +230,8 @@ namespace net.fushizen.modular_avatar.core.editor
var indentLevel = EditorGUI.indentLevel;
try
{
EditorGUI.BeginChangeCheck();
indentLevel = 0;
if (_devMode)
{
@ -114,6 +252,42 @@ namespace net.fushizen.modular_avatar.core.editor
EditorGUI.LabelField(leftHalf,
isPrefix.boolValue ? nameOrPrefix.stringValue + "*" : nameOrPrefix.stringValue);
EditorGUI.PropertyField(rightHalf, remapTo, GUIContent.none);
if (string.IsNullOrWhiteSpace(remapTo.stringValue))
{
var style = new GUIStyle(EditorStyles.label);
style.fontStyle = FontStyle.Italic;
var oldColor = GUI.color;
var color = GUI.color;
color.a = 0.5f;
GUI.color = color;
EditorGUI.LabelField(rightHalf, nameOrPrefix.stringValue, style);
GUI.color = oldColor;
}
}
if (EditorGUI.EndChangeCheck() && index < 0)
{
var target = (ModularAvatarParameters) this.target;
// Create this implicit parameter in the backing object
var implicitIndex = -(index + 1);
serializedObject.ApplyModifiedProperties();
Undo.RecordObject(target, "Updating parameters");
_implicitParamsObj.ApplyModifiedPropertiesWithoutUndo();
var config = _implicitParams.ImplicitParameters[implicitIndex];
target.parameters.Add(config);
_selectedIndices.RemoveAt(originalIndex);
_selectedIndices.Insert(target.parameters.Count - 1, target.parameters.Count - 1);
serializedObject.Update();
_reorderableList.index = target.parameters.Count - 1;
_needsRebuild = true;
}
}
finally
@ -135,7 +309,7 @@ namespace net.fushizen.modular_avatar.core.editor
{
EditorGUI.BeginChangeCheck();
_devMode = EditorGUILayout.Toggle("Developer mode", _devMode);
if (EditorGUI.EndChangeCheck()) SetupList();
if (EditorGUI.EndChangeCheck() || _reorderableList == null || _needsRebuild) SetupList();
_reorderableList.DoLayoutList();
serializedObject.ApplyModifiedProperties();
}

View File

@ -0,0 +1,251 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
using VRC.SDK3.Dynamics.Contact.Components;
using VRC.SDK3.Dynamics.PhysBone.Components;
namespace net.fushizen.modular_avatar.core.editor
{
public struct DetectedParameter
{
public string OriginalName;
public bool IsPrefix;
public string MapKey => IsPrefix ? OriginalName + "*" : OriginalName;
}
public static class ParameterPolicy
{
/// <summary>
/// Parameters predefined by the VRChat SDK which should not be offered as remappable.
/// </summary>
public static ImmutableHashSet<string> VRCSDKParameters = new string[]
{
"IsLocal",
"Viseme",
"Voice",
"GestureLeft",
"GestureRight",
"GestureLeftWeight",
"GestureRightWeight",
"AngularY",
"VelocityX",
"VelocityY",
"VelocityZ",
"Upright",
"Grounded",
"Seated",
"AFK",
"TrackingType",
"VRMode",
"MuteSelf",
"InStation",
"Earmuffs",
}.ToImmutableHashSet();
public static ImmutableList<string> PhysBoneSuffixes = new string[]
{
"_IsGrabbed",
"_Angle",
"_Stretch",
}.ToImmutableList();
public static ImmutableDictionary<string, DetectedParameter> ProbeParameters(GameObject root)
{
Dictionary<string, DetectedParameter> parameters = new Dictionary<string, DetectedParameter>();
WalkTree(ref parameters, root, false);
CleanPhysBoneParams(parameters);
return parameters.ToImmutableDictionary();
}
private static void WalkTree(ref Dictionary<string, DetectedParameter> parameters, GameObject root,
bool applyRemappings)
{
ModularAvatarParameters parametersComponent = null;
foreach (var component in root.GetComponents<Component>())
{
switch (component)
{
case ModularAvatarParameters p:
{
parametersComponent = p;
break;
}
case VRCPhysBone bone:
{
if (!string.IsNullOrWhiteSpace(bone.parameter))
{
var param = new DetectedParameter()
{
OriginalName = bone.parameter,
IsPrefix = true,
};
parameters[param.MapKey] = param;
}
break;
}
case VRCContactReceiver contact:
{
if (!string.IsNullOrWhiteSpace(contact.parameter))
{
var param = new DetectedParameter()
{
OriginalName = contact.parameter,
IsPrefix = false,
};
parameters[param.MapKey] = param;
}
break;
}
case Animator anim:
{
WalkAnimator(parameters, anim.runtimeAnimatorController as AnimatorController);
break;
}
case ModularAvatarMenuInstaller installer:
{
WalkMenu(parameters, installer.menuToAppend, new HashSet<VRCExpressionsMenu>());
break;
}
}
}
foreach (Transform child in root.transform)
{
WalkTree(ref parameters, child.gameObject, true);
}
if (parametersComponent != null && applyRemappings)
{
CleanPhysBoneParams(parameters);
ApplyRemappings(ref parameters, parametersComponent);
}
}
private static void WalkMenu(Dictionary<string, DetectedParameter> parameters, VRCExpressionsMenu menu,
HashSet<VRCExpressionsMenu> visited)
{
if (menu == null || visited.Contains(menu)) return;
visited.Add(menu);
void AddParam(string name)
{
var param = new DetectedParameter()
{
OriginalName = name,
IsPrefix = false
};
parameters[param.MapKey] = param;
}
foreach (var control in menu.controls)
{
if (!string.IsNullOrWhiteSpace(control.parameter.name))
{
AddParam(control.parameter.name);
}
foreach (var subParam in control.subParameters)
{
if (!string.IsNullOrWhiteSpace(subParam.name))
{
AddParam(subParam.name);
}
}
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
{
WalkMenu(parameters, control.subMenu, visited);
}
}
}
private static void CleanPhysBoneParams(Dictionary<string, DetectedParameter> parameters)
{
var physBonePrefixes = parameters.Values.Where(p => p.IsPrefix)
.Select(p => p.OriginalName);
foreach (var prefix in physBonePrefixes)
{
foreach (var suffix in PhysBoneSuffixes)
{
var key = prefix + suffix;
if (parameters.ContainsKey(key))
{
parameters.Remove(key);
}
}
}
}
private static void WalkAnimator(
Dictionary<string, DetectedParameter> parameters,
AnimatorController controller
)
{
if (controller == null) return;
foreach (var parameter in controller.parameters)
{
if (VRCSDKParameters.Contains(parameter.name)) continue;
var param = new DetectedParameter()
{
OriginalName = parameter.name,
IsPrefix = false
};
parameters[param.MapKey] = param;
}
}
private static void ApplyRemappings(ref Dictionary<string, DetectedParameter> parameters,
ModularAvatarParameters parametersComponent)
{
Dictionary<string, DetectedParameter> newParams = new Dictionary<string, DetectedParameter>();
foreach (var map in parametersComponent.parameters)
{
var dictKey = map.nameOrPrefix + (map.isPrefix ? "*" : "");
if (!parameters.ContainsKey(dictKey))
{
continue;
}
if (map.internalParameter)
{
parameters.Remove(dictKey);
}
else if (!string.IsNullOrWhiteSpace(map.remapTo))
{
var param = parameters[dictKey];
param.OriginalName = map.remapTo;
newParams[param.MapKey] = param;
parameters.Remove(dictKey);
}
}
// TODO - warn of overlap? could be intentional...
foreach (var param in parameters)
{
newParams[param.Key] = param.Value;
}
parameters = newParams;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fb45d824aec1444fb553cecca1e042e3
timeCreated: 1665870025