From 654aec1aab8e30c71fe8d9f11de2c4965e8687bc Mon Sep 17 00:00:00 2001 From: bd_ Date: Thu, 14 Mar 2024 21:33:44 +0900 Subject: [PATCH] feat: add UI to show parameter usage (#773) --- .github/ProjectRoot/vpm-manifest-2019.json | 4 +- .github/ProjectRoot/vpm-manifest-2022.json | 4 +- Editor/HarmonyPatches/InjectParamsUsageUI.cs | 102 +++++++++ .../InjectParamsUsageUI.cs.meta | 3 + Editor/HarmonyPatches/PatchLoader.cs | 3 + ....dev.modular-avatar.harmony-patches.asmdef | 4 +- Editor/Inspector/PBBlockerEditor.cs | 18 +- Editor/Localization/UIElementLocalizer.cs | 7 +- Editor/Localization/en-US.json | 6 +- Editor/Localization/ja-JP.json | 6 +- Editor/ParamsUsage.meta | 3 + .../ParamsUsage/MAParametersIntrospection.cs | 100 +++++++++ .../MAParametersIntrospection.cs.meta | 3 + .../ParamsUsage/ModularAvatarInformation.cs | 23 ++ .../ModularAvatarInformation.cs.meta | 11 + Editor/ParamsUsage/ParamsUsage.uss | 75 +++++++ Editor/ParamsUsage/ParamsUsage.uss.meta | 3 + Editor/ParamsUsage/ParamsUsage.uxml | 35 +++ Editor/ParamsUsage/ParamsUsage.uxml.meta | 3 + Editor/ParamsUsage/ParamsUsageEditor.cs | 203 ++++++++++++++++++ Editor/ParamsUsage/ParamsUsageEditor.cs.meta | 13 ++ Editor/ParamsUsage/ParamsUsageUI.cs | 202 +++++++++++++++++ Editor/ParamsUsage/ParamsUsageUI.cs.meta | 3 + Editor/ParamsUsage/assembly-info.cs | 7 + Editor/ParamsUsage/assembly-info.cs.meta | 3 + ....modular-avatar.param-introspection.asmdef | 31 +++ ...lar-avatar.param-introspection.asmdef.meta | 7 + Editor/PluginDefinition/PluginDefinition.cs | 22 +- Editor/RenameParametersHook.cs | 33 ++- Editor/assembly-info.cs | 3 +- ...dena.dev.modular-avatar.core.editor.asmdef | 4 +- Runtime/assembly-info.cs | 3 +- package.json | 2 +- 33 files changed, 926 insertions(+), 23 deletions(-) create mode 100644 Editor/HarmonyPatches/InjectParamsUsageUI.cs create mode 100644 Editor/HarmonyPatches/InjectParamsUsageUI.cs.meta create mode 100644 Editor/ParamsUsage.meta create mode 100644 Editor/ParamsUsage/MAParametersIntrospection.cs create mode 100644 Editor/ParamsUsage/MAParametersIntrospection.cs.meta create mode 100644 Editor/ParamsUsage/ModularAvatarInformation.cs create mode 100644 Editor/ParamsUsage/ModularAvatarInformation.cs.meta create mode 100644 Editor/ParamsUsage/ParamsUsage.uss create mode 100644 Editor/ParamsUsage/ParamsUsage.uss.meta create mode 100644 Editor/ParamsUsage/ParamsUsage.uxml create mode 100644 Editor/ParamsUsage/ParamsUsage.uxml.meta create mode 100644 Editor/ParamsUsage/ParamsUsageEditor.cs create mode 100644 Editor/ParamsUsage/ParamsUsageEditor.cs.meta create mode 100644 Editor/ParamsUsage/ParamsUsageUI.cs create mode 100644 Editor/ParamsUsage/ParamsUsageUI.cs.meta create mode 100644 Editor/ParamsUsage/assembly-info.cs create mode 100644 Editor/ParamsUsage/assembly-info.cs.meta create mode 100644 Editor/ParamsUsage/nadena.dev.modular-avatar.param-introspection.asmdef create mode 100644 Editor/ParamsUsage/nadena.dev.modular-avatar.param-introspection.asmdef.meta diff --git a/.github/ProjectRoot/vpm-manifest-2019.json b/.github/ProjectRoot/vpm-manifest-2019.json index 40d5a88a..7f791070 100644 --- a/.github/ProjectRoot/vpm-manifest-2019.json +++ b/.github/ProjectRoot/vpm-manifest-2019.json @@ -4,7 +4,7 @@ "version": "3.4.2" }, "nadena.dev.ndmf": { - "version": "1.3.6" + "version": "1.4.0-rc.0" } }, "locked": { @@ -19,7 +19,7 @@ "dependencies": {} }, "nadena.dev.ndmf": { - "version": "1.3.6" + "version": "1.4.0-rc.0" } } } \ No newline at end of file diff --git a/.github/ProjectRoot/vpm-manifest-2022.json b/.github/ProjectRoot/vpm-manifest-2022.json index 6eb70c07..b80cf44e 100644 --- a/.github/ProjectRoot/vpm-manifest-2022.json +++ b/.github/ProjectRoot/vpm-manifest-2022.json @@ -4,7 +4,7 @@ "version": "3.5.0" }, "nadena.dev.ndmf": { - "version": "1.3.6" + "version": "1.4.0-rc.0" } }, "locked": { @@ -19,7 +19,7 @@ "dependencies": {} }, "nadena.dev.ndmf": { - "version": "1.3.6" + "version": "1.4.0-rc.0" } } } \ No newline at end of file diff --git a/Editor/HarmonyPatches/InjectParamsUsageUI.cs b/Editor/HarmonyPatches/InjectParamsUsageUI.cs new file mode 100644 index 00000000..e587fb8d --- /dev/null +++ b/Editor/HarmonyPatches/InjectParamsUsageUI.cs @@ -0,0 +1,102 @@ +#region + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; +using Object = UnityEngine.Object; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches +{ + internal static class InjectParamsUsageUI + { + private static readonly Type type = AccessTools.TypeByName("UnityEditor.PropertyEditor"); + private static readonly PropertyInfo _editorsElement = AccessTools.Property(type, "editorsElement"); + + private static readonly Type editorElem = AccessTools.TypeByName("UnityEditor.UIElements.EditorElement"); + private static readonly PropertyInfo editorElem_editor = AccessTools.Property(editorElem, "editor"); + + public static void Patch(Harmony h) + { + var type = AccessTools.TypeByName("UnityEditor.PropertyEditor"); + var drawEditors = AccessTools.Method(type, "DrawEditors"); + + h.Patch(drawEditors, transpiler: new HarmonyMethod(typeof(InjectParamsUsageUI), nameof(Transpile))); + + var objNames = AccessTools.TypeByName("UnityEditor.ObjectNames"); + var m_GetObjectTypeName = AccessTools.Method(objNames, "GetObjectTypeName"); + var postfix_GetObjectTypeName = + AccessTools.Method(typeof(InjectParamsUsageUI), nameof(Postfix_GetObjectTypeName)); + + h.Patch(m_GetObjectTypeName, postfix: new HarmonyMethod(postfix_GetObjectTypeName)); + } + + private static void Postfix_GetObjectTypeName(ref string __result, Object o) + { + if (o is ModularAvatarInformation) + { + __result = "Modular Avatar Information"; + } + } + + private static IEnumerable Transpile(IEnumerable ci) + { + var target = AccessTools.Method(typeof(VisualElement), "Add"); + + foreach (var i in ci) + { + if (i.opcode != OpCodes.Callvirt) + { + yield return i; + continue; + } + + if (i.opcode == OpCodes.Callvirt + && i.operand is MethodInfo method + && method == target + ) + { + yield return new CodeInstruction(OpCodes.Ldarg_0); + yield return new CodeInstruction(OpCodes.Call, + AccessTools.Method(typeof(InjectParamsUsageUI), nameof(EditorAdd))); + continue; + } + + yield return i; + } + } + + private static void EditorAdd(VisualElement container, VisualElement child, object caller) + { + container.Add(child); + + var editorsElement = _editorsElement.GetValue(caller) as VisualElement; + if (editorsElement != container) + { + return; + } + + if (!child.ClassListContains("game-object-inspector")) + { + return; + } + + var editor = editorElem_editor.GetValue(child) as Editor; + if (editor == null) return; + + if (editor.targets.Length != 1) return; + + if (editor.target is GameObject obj) + { + var elem = new ParamsUsageUI(); + container.Add(elem); + } + } + } +} \ No newline at end of file diff --git a/Editor/HarmonyPatches/InjectParamsUsageUI.cs.meta b/Editor/HarmonyPatches/InjectParamsUsageUI.cs.meta new file mode 100644 index 00000000..f5145038 --- /dev/null +++ b/Editor/HarmonyPatches/InjectParamsUsageUI.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5d62a8f41641443ea8bffdc0429e0ad1 +timeCreated: 1710223876 \ No newline at end of file diff --git a/Editor/HarmonyPatches/PatchLoader.cs b/Editor/HarmonyPatches/PatchLoader.cs index a2c1bf60..1de11221 100644 --- a/Editor/HarmonyPatches/PatchLoader.cs +++ b/Editor/HarmonyPatches/PatchLoader.cs @@ -17,6 +17,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches #if UNITY_2022_3_OR_NEWER HandleUtilityPatches.Patch_FilterInstanceIDs, PickingObjectPatch.Patch, + InjectParamsUsageUI.Patch, #endif }; @@ -36,6 +37,8 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches Debug.LogException(e); } } + + AssemblyReloadEvents.beforeAssemblyReload += () => { harmony.UnpatchAll(); }; } } } \ No newline at end of file diff --git a/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef b/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef index 97e44db7..215fb92a 100644 --- a/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef +++ b/Editor/HarmonyPatches/nadena.dev.modular-avatar.harmony-patches.asmdef @@ -1,9 +1,11 @@ { "name": "nadena.dev.modular-avatar.harmony-patches", + "rootNamespace": "", "references": [ "nadena.dev.modular-avatar.core", "nadena.dev.modular-avatar.core.editor", - "VRC.SDKBase.Editor" + "VRC.SDKBase.Editor", + "nadena.dev.modular-avatar.param-introspection" ], "includePlatforms": [ "Editor" diff --git a/Editor/Inspector/PBBlockerEditor.cs b/Editor/Inspector/PBBlockerEditor.cs index ca43a9b9..5b0719ff 100644 --- a/Editor/Inspector/PBBlockerEditor.cs +++ b/Editor/Inspector/PBBlockerEditor.cs @@ -1,18 +1,18 @@ /* * MIT License - * + * * Copyright (c) 2022 bd_ - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -22,7 +22,12 @@ * SOFTWARE. */ +#region + using UnityEditor; +using UnityEngine; + +#endregion namespace nadena.dev.modular_avatar.core.editor { @@ -30,6 +35,11 @@ namespace nadena.dev.modular_avatar.core.editor [CanEditMultipleObjects] internal class PBBlockerEditor : MAEditorBase { + public PBBlockerEditor() + { + Debug.Log("ctor"); + } + protected override void OnInnerInspectorGUI() { EditorGUILayout.HelpBox(Localization.S("pb_blocker.help"), MessageType.Info); diff --git a/Editor/Localization/UIElementLocalizer.cs b/Editor/Localization/UIElementLocalizer.cs index 04774fc4..6cff1e60 100644 --- a/Editor/Localization/UIElementLocalizer.cs +++ b/Editor/Localization/UIElementLocalizer.cs @@ -1,9 +1,13 @@ -using System; +#region + +using System; using System.Collections.Generic; using System.Reflection; using nadena.dev.ndmf.localization; using UnityEngine.UIElements; +#endregion + namespace nadena.dev.modular_avatar.core.editor { internal class UIElementLocalizer @@ -21,6 +25,7 @@ namespace nadena.dev.modular_avatar.core.editor internal void Localize(VisualElement elem) { WalkTree(elem); + LanguagePrefs.ApplyFontPreferences(elem); } private void WalkTree(VisualElement elem) diff --git a/Editor/Localization/en-US.json b/Editor/Localization/en-US.json index 8d108ab5..25727950 100644 --- a/Editor/Localization/en-US.json +++ b/Editor/Localization/en-US.json @@ -240,5 +240,9 @@ "scale_adjuster.scale": "Scale adjustment", "scale_adjuster.adjust_children": "Adjust position of child objects", "world_fixed_object.err.unsupported_platform": "World Fixed Object is not supported on this platform", - "world_fixed_object.err.unsupported_platform:description": "World Fixed Object is not supported on Android builds and will be ignored." + "world_fixed_object.err.unsupported_platform:description": "World Fixed Object is not supported on Android builds and will be ignored.", + "ma_info.param_usage_ui.header": "Expressions Parameter Usage", + "ma_info.param_usage_ui.other_objects": "Other objects on this avatar", + "ma_info.param_usage_ui.free_space": "Unused parameter space ({0} bits)", + "ma_info.param_usage_ui.bits_template": "{0} ({1} bits)" } \ No newline at end of file diff --git a/Editor/Localization/ja-JP.json b/Editor/Localization/ja-JP.json index 121d3ae8..6a8b355d 100644 --- a/Editor/Localization/ja-JP.json +++ b/Editor/Localization/ja-JP.json @@ -236,5 +236,9 @@ "scale_adjuster.scale": "Scale調整値", "scale_adjuster.adjust_children": "子オブジェクトの位置を調整", "world_fixed_object.err.unsupported_platform": "World Fixed Objectがこのプラットフォームに対応していません", - "world_fixed_object.err.unsupported_platform:description": "World Fixed ObjectはAndroid向けビルドには対応していないため、動作しません。" + "world_fixed_object.err.unsupported_platform:description": "World Fixed ObjectはAndroid向けビルドには対応していないため、動作しません。", + "ma_info.param_usage_ui.header": "Expressions Parameter 使用状況", + "ma_info.param_usage_ui.other_objects": "このアバター内の他のオブジェクト", + "ma_info.param_usage_ui.free_space": "未使用領域 ({0} 個のビット)", + "ma_info.param_usage_ui.bits_template": "{0} ({1} 個のビットを使用中)" } \ No newline at end of file diff --git a/Editor/ParamsUsage.meta b/Editor/ParamsUsage.meta new file mode 100644 index 00000000..bec26f52 --- /dev/null +++ b/Editor/ParamsUsage.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9914a6ac6399437dbcaa252282d02beb +timeCreated: 1710222101 \ No newline at end of file diff --git a/Editor/ParamsUsage/MAParametersIntrospection.cs b/Editor/ParamsUsage/MAParametersIntrospection.cs new file mode 100644 index 00000000..3eeee897 --- /dev/null +++ b/Editor/ParamsUsage/MAParametersIntrospection.cs @@ -0,0 +1,100 @@ +#region + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using nadena.dev.modular_avatar.core.editor.plugin; +using nadena.dev.ndmf; +using UnityEditor; +using UnityEngine; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor +{ + [ParameterProviderFor(typeof(ModularAvatarParameters))] + internal class MAParametersIntrospection : IParameterProvider + { + private readonly ModularAvatarParameters _component; + + public MAParametersIntrospection(ModularAvatarParameters parameters) + { + _component = parameters; + } + + public IEnumerable GetSuppliedParameters(ndmf.BuildContext context = null) + { + return _component.parameters.Select(p => + { + AnimatorControllerParameterType paramType; + bool animatorOnly = false; + + switch (p.syncType) + { + case ParameterSyncType.Bool: + paramType = AnimatorControllerParameterType.Bool; + break; + case ParameterSyncType.Float: + paramType = AnimatorControllerParameterType.Float; + break; + case ParameterSyncType.Int: + paramType = AnimatorControllerParameterType.Int; + break; + default: + paramType = AnimatorControllerParameterType.Float; + animatorOnly = true; + break; + } + + return new ProvidedParameter( + p.nameOrPrefix, + p.isPrefix ? ParameterNamespace.PhysBonesPrefix : ParameterNamespace.Animator, + _component, PluginDefinition.Instance, paramType) + { + IsAnimatorOnly = animatorOnly, + WantSynced = !p.localOnly, + IsHidden = p.internalParameter, + }; + }); + } + + public void RemapParameters(ref ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> nameMap, + ndmf.BuildContext context = null) + { + var remappings = context != null ? ParameterRenameMappings.Get(context) : null; + + // TODO - internal parameter handling + foreach (var p in _component.parameters) + { + ParameterNamespace ns = p.isPrefix ? ParameterNamespace.PhysBonesPrefix : ParameterNamespace.Animator; + string remapTo = null; + if (p.internalParameter) + { + if (remappings != null) + { + remapTo = remappings.Remap(_component, ns, p.nameOrPrefix); + } + else + { + remapTo = p.nameOrPrefix + "$" + GUID.Generate(); + } + } + else if (string.IsNullOrEmpty(p.remapTo)) + { + continue; + } + else + { + remapTo = p.remapTo; + } + + if (nameMap.TryGetKey((ns, remapTo), out var existingMapping)) + { + remapTo = existingMapping.Item2; + } + + nameMap = nameMap.SetItem((ns, p.nameOrPrefix), new ParameterMapping(remapTo, p.internalParameter)); + } + } + } +} \ No newline at end of file diff --git a/Editor/ParamsUsage/MAParametersIntrospection.cs.meta b/Editor/ParamsUsage/MAParametersIntrospection.cs.meta new file mode 100644 index 00000000..c9ff431c --- /dev/null +++ b/Editor/ParamsUsage/MAParametersIntrospection.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: eadfd1e62f714d06a8b9f693dec21940 +timeCreated: 1710229132 \ No newline at end of file diff --git a/Editor/ParamsUsage/ModularAvatarInformation.cs b/Editor/ParamsUsage/ModularAvatarInformation.cs new file mode 100644 index 00000000..f5c1bf89 --- /dev/null +++ b/Editor/ParamsUsage/ModularAvatarInformation.cs @@ -0,0 +1,23 @@ +#region + +using UnityEngine; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor +{ + [HelpURL("https://m-a.nadena.dev/docs/intro?lang=auto")] + internal class ModularAvatarInformation : ScriptableObject + { + internal static ModularAvatarInformation _instance; + + internal static ModularAvatarInformation instance + { + get + { + if (_instance == null) _instance = CreateInstance(); + return _instance; + } + } + } +} \ No newline at end of file diff --git a/Editor/ParamsUsage/ModularAvatarInformation.cs.meta b/Editor/ParamsUsage/ModularAvatarInformation.cs.meta new file mode 100644 index 00000000..b966108d --- /dev/null +++ b/Editor/ParamsUsage/ModularAvatarInformation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f902feee12ad4fcbb8a975bbea565ab1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: a8edd5bd1a0a64a40aa99cc09fb5f198, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ParamsUsage/ParamsUsage.uss b/Editor/ParamsUsage/ParamsUsage.uss new file mode 100644 index 00000000..6dbc4c52 --- /dev/null +++ b/Editor/ParamsUsage/ParamsUsage.uss @@ -0,0 +1,75 @@ +Label.header { + -unity-font-style: bold; + margin-top: 10px; +} + +#Outerbox { + margin-top: 4px; + border-top-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + border-right-width: 1px; + border-color: black; + + padding: 4px; +} + +#root-box { + margin-bottom: 12px; +} + +#UsageBox { + height: 16px; + flex-direction: row; + margin-bottom: 8px; +} + +#UsageBox VisualElement { + flex-grow: 0; +} + +#UsageBox VisualElement.Hovering { + border-top-width: 4px; + border-bottom-width: 4px; + /*border-left-width: 4px; + border-right-width: 4px; + margin: -4px; + */ + margin-top: -4px; + margin-bottom: -4px; + + border-color: black; +} + +.Entry { + flex-direction: row; +} + +.IconOuter { + border-top-width: 3px; + border-bottom-width: 3px; + border-left-width: 3px; + border-right-width: 3px; + border-color: grey; + + padding: 1px; + margin-right: 4px; + + align-items: center; + justify-content: center; + + height: 16px; + width: 16px; +} + +.IconInner { + height: 100%; + width: 100%; +} + +.Entry.Hovering { + margin-left: -4px; + margin-right: -4px; + border-left-width: 4px; + border-right-width: 4px; +} \ No newline at end of file diff --git a/Editor/ParamsUsage/ParamsUsage.uss.meta b/Editor/ParamsUsage/ParamsUsage.uss.meta new file mode 100644 index 00000000..da0fbe2d --- /dev/null +++ b/Editor/ParamsUsage/ParamsUsage.uss.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e6813d571517475dbf36efb2d266003a +timeCreated: 1710399136 \ No newline at end of file diff --git a/Editor/ParamsUsage/ParamsUsage.uxml b/Editor/ParamsUsage/ParamsUsage.uxml new file mode 100644 index 00000000..351a77a7 --- /dev/null +++ b/Editor/ParamsUsage/ParamsUsage.uxml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Editor/ParamsUsage/ParamsUsage.uxml.meta b/Editor/ParamsUsage/ParamsUsage.uxml.meta new file mode 100644 index 00000000..fac2157c --- /dev/null +++ b/Editor/ParamsUsage/ParamsUsage.uxml.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0d51876a8e634298aa6d0271bb820189 +timeCreated: 1710399118 \ No newline at end of file diff --git a/Editor/ParamsUsage/ParamsUsageEditor.cs b/Editor/ParamsUsage/ParamsUsageEditor.cs new file mode 100644 index 00000000..cbb261e3 --- /dev/null +++ b/Editor/ParamsUsage/ParamsUsageEditor.cs @@ -0,0 +1,203 @@ +#region + +using System.Collections.Generic; +using System.Linq; +using nadena.dev.ndmf; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; +using VRC.SDK3.Avatars.ScriptableObjects; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor +{ + internal class ParamsUsageEditor : MAEditorBase + { + [SerializeField] private StyleSheet uss; + [SerializeField] private VisualTreeAsset uxml; + + private VisualElement _root; + private VisualElement _entryTemplate; + private VisualElement _usageBoxContainer; + private VisualElement _legendContainer; + + private bool _visible = false; + + public bool Visible + { + get => _visible; + set + { + if (_visible == value) return; + _visible = value; + + if (_visible) Recalculate(); + } + } + + private void OnEnable() + { + #if UNITY_2022_1_OR_NEWER + ObjectChangeEvents.changesPublished += OnChangesPublished; + #endif + Recalculate(); + } + +#if UNITY_2022_1_OR_NEWER + private void OnChangesPublished(ref ObjectChangeEventStream stream) + { + Recalculate(); + } + + private void OnDisable() + { + ObjectChangeEvents.changesPublished -= OnChangesPublished; + } +#endif + + protected override VisualElement CreateInnerInspectorGUI() + { + _root = uxml.CloneTree(); + _root.styleSheets.Add(uss); + Localization.L.LocalizeUIElements(_root); + + _legendContainer = _root.Q("Legend"); + _usageBoxContainer = _root.Q("UsageBox"); + + Recalculate(); + + return _root; + } + + protected override void OnInnerInspectorGUI() + { + // no-op + } + + private static IEnumerable Colors() + { + // Spiral inwards on an HSV scale + float h_step = 0.33f; + float h_step_mult = 0.8f; + float h_step_min = 0.05f; + + float v_mult = 0.98f; + + float h = 0; + float s = 1; + float v = 0.9f; + + while (true) + { + yield return Color.HSVToRGB(h, s, v); + + h = (h + h_step) % 1; + h_step = h_step_min + ((h_step - h_step_min) * h_step_mult); + v *= v_mult; + } + } + + private void Recalculate() + { + if (_root == null || !_visible) return; + + var ctx = serializedObject.context as GameObject; + + var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(ctx.transform)?.gameObject; + if (ctx == null || avatarRoot == null) return; + + var orderedPlugins = ParameterInfo.ForUI.GetParametersForObject(ctx) + .GroupBy(p => p.Plugin) + .Select(group => (group.Key, group.Sum(p => p.BitUsage))) + .OrderBy(group => group.Key.DisplayName) + .ToList(); + + var byPlugin = orderedPlugins + .Zip(Colors(), (kv, color) => (kv.Key.DisplayName, kv.Item2, kv.Key.ThemeColor ?? color)) + .ToList(); + + int totalUsage = byPlugin.Sum(kv => kv.Item2); + + int avatarTotalUsage = + ParameterInfo.ForUI.GetParametersForObject(avatarRoot).Sum(p => p.BitUsage); + + int freeSpace = VRCExpressionParameters.MAX_PARAMETER_COST - avatarTotalUsage; + + float avatarTotalPerc = avatarTotalUsage / (float)VRCExpressionParameters.MAX_PARAMETER_COST; + float freeSpacePerc = freeSpace / (float)VRCExpressionParameters.MAX_PARAMETER_COST; + + if (avatarTotalUsage > totalUsage) + { + byPlugin.Add((Localization.S("ma_info.param_usage_ui.other_objects"), avatarTotalUsage - totalUsage, + Color.gray)); + } + + var bits_template = Localization.S("ma_info.param_usage_ui.bits_template"); + byPlugin = byPlugin.Select((tuple, _) => + (string.Format(bits_template, tuple.Item1, tuple.Item2), tuple.Item2, tuple.Item3)).ToList(); + + if (freeSpace > 0) + { + var free_space_label = Localization.S("ma_info.param_usage_ui.free_space"); + byPlugin.Add((string.Format(free_space_label, freeSpace), freeSpace, Color.white)); + } + + foreach (var child in _legendContainer.Children().ToList()) + { + child.RemoveFromHierarchy(); + } + + foreach (var child in _usageBoxContainer.Children().ToList()) + { + child.RemoveFromHierarchy(); + } + + foreach (var (label, usage, color) in byPlugin) + { + var colorBar = new VisualElement(); + colorBar.style.backgroundColor = color; + colorBar.style.width = + new StyleLength(new Length(100.0f * usage / (float)VRCExpressionParameters.MAX_PARAMETER_COST, + LengthUnit.Percent)); + _usageBoxContainer.Add(colorBar); + + var entry = new VisualElement(); + _legendContainer.Add(entry); + entry.AddToClassList("Entry"); + + var icon_outer = new VisualElement(); + icon_outer.AddToClassList("IconOuter"); + entry.Add(icon_outer); + + var icon_inner = new VisualElement(); + icon_inner.AddToClassList("IconInner"); + icon_outer.Add(icon_inner); + icon_inner.style.backgroundColor = color; + + var pluginLabel = new Label(label); + entry.Add(pluginLabel); + + entry.style.borderBottomColor = color; + entry.style.borderTopColor = color; + entry.style.borderLeftColor = color; + entry.style.borderRightColor = color; + + colorBar.style.borderBottomColor = color; + colorBar.style.borderTopColor = color; + colorBar.style.borderLeftColor = color; + colorBar.style.borderRightColor = color; + + SetMouseHover(entry, colorBar); + SetMouseHover(colorBar, entry); + } + } + + private void SetMouseHover(VisualElement src, VisualElement other) + { + src.RegisterCallback(ev => { other.AddToClassList("Hovering"); }); + + src.RegisterCallback(ev => { other.RemoveFromClassList("Hovering"); }); + } + } +} \ No newline at end of file diff --git a/Editor/ParamsUsage/ParamsUsageEditor.cs.meta b/Editor/ParamsUsage/ParamsUsageEditor.cs.meta new file mode 100644 index 00000000..e9b3ca8e --- /dev/null +++ b/Editor/ParamsUsage/ParamsUsageEditor.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: aedf0b915d844b2992b447f61bd56f54 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - uss: {fileID: 7433441132597879392, guid: e6813d571517475dbf36efb2d266003a, type: 3} + - uxml: {fileID: 9197481963319205126, guid: 0d51876a8e634298aa6d0271bb820189, type: 3} + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ParamsUsage/ParamsUsageUI.cs b/Editor/ParamsUsage/ParamsUsageUI.cs new file mode 100644 index 00000000..f4275800 --- /dev/null +++ b/Editor/ParamsUsage/ParamsUsageUI.cs @@ -0,0 +1,202 @@ +#region + +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using HarmonyLib; +using nadena.dev.ndmf.localization; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; +using Object = UnityEngine.Object; + +#endregion + +namespace nadena.dev.modular_avatar.core.editor +{ + internal class ParamsUsageUI : VisualElement + { + private static readonly Type editorElem = AccessTools.TypeByName("UnityEditor.UIElements.EditorElement"); + private static readonly PropertyInfo editorElem_editor = AccessTools.Property(editorElem, "editor"); + + private class FoldoutState + { + public bool Visible; + } + + private static ConditionalWeakTable FoldoutStateHolder = + new ConditionalWeakTable(); + + private VisualElement _gameObjectEditorElement; + private Editor _parentEditor; + private Object _rawTarget; + private GameObject _target; + private ParamsUsageEditor _editor; + private FoldoutState _foldoutState; + + private bool _recursing = false; + + public ParamsUsageUI() + { + RegisterCallback(OnAttach); + RegisterCallback(OnDetach); + + LanguagePrefs.RegisterLanguageChangeCallback(this, + (self) => self.OnLanguageChangedCallback()); + } + + private void OnLanguageChangedCallback() + { + if (_editor != null) + { + BuildContent(); + } + } + + private void OnDetach(DetachFromPanelEvent evt) + { + if (_recursing) return; + + Clear(); + + if (_editor != null) + { + Object.DestroyImmediate(_editor); + _editor = null; + } + } + + private void OnAttach(AttachToPanelEvent evt) + { + if (_recursing) return; + + Rebuild(); + } + + private void Rebuild() + { + if (parent == null) return; + + SetRedrawSensor(); + + if (_gameObjectEditorElement?.parent != parent) + { + _gameObjectEditorElement = null; + + var kv = FindEditorElement(); + if (kv != null) + { + var elem = kv.Value.Item1; + var index = kv.Value.Item2; + + if (index != parent.Children().ToList().IndexOf(this)) + { + _recursing = true; + var p = parent; + RemoveFromHierarchy(); + p.Insert(index + 1, this); + _recursing = false; + } + + _gameObjectEditorElement = elem; + } + } + + if (_gameObjectEditorElement == null) return; + + _parentEditor = editorElem_editor.GetValue(_gameObjectEditorElement) as Editor; + if (_parentEditor == null) return; + + _rawTarget = _parentEditor.target; + _target = _rawTarget as GameObject; + + if (_target == null) return; + + Clear(); + _redrawSensorActive = false; + BuildContent(); + } + + private (VisualElement, int)? FindEditorElement() + { + foreach (var (elem, index) in parent.Children().Select((e, i) => (e, i))) + { + if (elem.ClassListContains("game-object-inspector")) + { + return (elem, index); + } + } + + return null; + } + + private bool _redrawSensorActive = false; + + private void SetRedrawSensor() + { + if (_redrawSensorActive) return; + + Clear(); + _redrawSensorActive = true; + Add(new IMGUIContainer(() => EditorApplication.delayCall += Rebuild)); + } + + private void BuildContent() + { + Clear(); + + if (!FoldoutStateHolder.TryGetValue(parent, out _foldoutState)) + { + _foldoutState = new FoldoutState(); + FoldoutStateHolder.Add(parent, _foldoutState); + } + + if (RuntimeUtil.FindAvatarTransformInParents(_target.transform) == null) + { + return; + } + + _editor = Editor.CreateEditorWithContext(new Object[] { ModularAvatarInformation.instance }, _target, + typeof(ParamsUsageEditor)) + as ParamsUsageEditor; + + if (_editor == null) return; + + var inspectorElement = new InspectorElement(_editor); + + Add(new IMGUIContainer(() => + { + if (_gameObjectEditorElement?.parent != parent || _parentEditor == null || + _parentEditor.target != _rawTarget) + { + EditorApplication.delayCall += Rebuild; + return; + } + + switch (Event.current.rawType) + { + case EventType.Repaint: + case EventType.MouseMove: + case EventType.Layout: + break; + case EventType.MouseDrag: + case EventType.DragUpdated: + case EventType.DragPerform: + case EventType.DragExited: + return; + + default: + break; + } + + _foldoutState.Visible = EditorGUILayout.InspectorTitlebar(_foldoutState.Visible, _editor); + inspectorElement.style.display = _foldoutState.Visible ? DisplayStyle.Flex : DisplayStyle.None; + _editor.Visible = _foldoutState.Visible; + })); + _editor.Visible = _foldoutState.Visible; + Add(inspectorElement); + } + } +} \ No newline at end of file diff --git a/Editor/ParamsUsage/ParamsUsageUI.cs.meta b/Editor/ParamsUsage/ParamsUsageUI.cs.meta new file mode 100644 index 00000000..4269d505 --- /dev/null +++ b/Editor/ParamsUsage/ParamsUsageUI.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 18364f2754ed43c3baba0f1e18ac03cd +timeCreated: 1710226452 \ No newline at end of file diff --git a/Editor/ParamsUsage/assembly-info.cs b/Editor/ParamsUsage/assembly-info.cs new file mode 100644 index 00000000..98860d7d --- /dev/null +++ b/Editor/ParamsUsage/assembly-info.cs @@ -0,0 +1,7 @@ +#region + +using System.Runtime.CompilerServices; + +#endregion + +[assembly: InternalsVisibleTo("nadena.dev.modular-avatar.harmony-patches")] \ No newline at end of file diff --git a/Editor/ParamsUsage/assembly-info.cs.meta b/Editor/ParamsUsage/assembly-info.cs.meta new file mode 100644 index 00000000..708a08a0 --- /dev/null +++ b/Editor/ParamsUsage/assembly-info.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6899d9fe550c492d887ce9e02d2a758b +timeCreated: 1710404112 \ No newline at end of file diff --git a/Editor/ParamsUsage/nadena.dev.modular-avatar.param-introspection.asmdef b/Editor/ParamsUsage/nadena.dev.modular-avatar.param-introspection.asmdef new file mode 100644 index 00000000..082c54e2 --- /dev/null +++ b/Editor/ParamsUsage/nadena.dev.modular-avatar.param-introspection.asmdef @@ -0,0 +1,31 @@ +{ + "name": "nadena.dev.modular-avatar.param-introspection", + "rootNamespace": "", + "references": [ + "GUID:fc900867c0f47cd49b6e2ae4ef907300", + "GUID:5ce33783346c3124990afbe7b0390a06", + "GUID:62ced99b048af7f4d8dfe4bed8373d76", + "GUID:5718fb738711cd34ea54e9553040911d", + "GUID:b906909fcc54f634db50f2cad0f988d9", + "GUID:901e56b065a857d4483a77f8cae73588" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [ + "MODULAR_AVATAR_VRCSDK_AVATAR" + ], + "versionDefines": [ + { + "name": "com.vrchat.avatars", + "expression": "(0,999)", + "define": "MODULAR_AVATAR_VRCSDK_AVATAR" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Editor/ParamsUsage/nadena.dev.modular-avatar.param-introspection.asmdef.meta b/Editor/ParamsUsage/nadena.dev.modular-avatar.param-introspection.asmdef.meta new file mode 100644 index 00000000..f9561e17 --- /dev/null +++ b/Editor/ParamsUsage/nadena.dev.modular-avatar.param-introspection.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f906ad1132cf10c48a65d14ae0809457 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/PluginDefinition/PluginDefinition.cs b/Editor/PluginDefinition/PluginDefinition.cs index bb5307fd..ee7f63d1 100644 --- a/Editor/PluginDefinition/PluginDefinition.cs +++ b/Editor/PluginDefinition/PluginDefinition.cs @@ -1,12 +1,19 @@ -using System; +#region + +using System; using nadena.dev.modular_avatar.animation; +using nadena.dev.modular_avatar.core.ArmatureAwase; +using nadena.dev.modular_avatar.core.editor.plugin; using nadena.dev.modular_avatar.editor.ErrorReporting; using nadena.dev.ndmf; using nadena.dev.ndmf.fluent; using UnityEngine; +using Object = UnityEngine.Object; + +#endregion [assembly: ExportsPlugin( - typeof(nadena.dev.modular_avatar.core.editor.plugin.PluginDefinition) + typeof(PluginDefinition) )] namespace nadena.dev.modular_avatar.core.editor.plugin @@ -17,6 +24,9 @@ namespace nadena.dev.modular_avatar.core.editor.plugin public override string DisplayName => "Modular Avatar"; public override Texture2D LogoTexture => LogoDisplay.LOGO_ASSET; + // 00a0e9 + public override Color? ThemeColor => new Color(0x00 / 255f, 0xa0 / 255f, 0xe9 / 255f, 1); + protected override void OnUnhandledException(Exception e) { BuildReport.LogException(e); @@ -82,11 +92,11 @@ namespace nadena.dev.modular_avatar.core.editor.plugin { foreach (var component in ctx.AvatarRootTransform.GetComponentsInChildren(true)) { - UnityEngine.Object.DestroyImmediate(component); + Object.DestroyImmediate(component); } - foreach (var component in ctx.AvatarRootTransform.GetComponentsInChildren(true)) + foreach (var component in ctx.AvatarRootTransform.GetComponentsInChildren(true)) { - UnityEngine.Object.DestroyImmediate(component); + Object.DestroyImmediate(component); } }); #if MA_VRCSDK3_AVATARS @@ -140,7 +150,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin { foreach (var component in obj.GetComponentsInChildren(true)) { - UnityEngine.Object.DestroyImmediate(component); + Object.DestroyImmediate(component); } } else diff --git a/Editor/RenameParametersHook.cs b/Editor/RenameParametersHook.cs index b1a57e5d..4011a7af 100644 --- a/Editor/RenameParametersHook.cs +++ b/Editor/RenameParametersHook.cs @@ -1,5 +1,7 @@ #if MA_VRCSDK3_AVATARS +#region + using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -18,8 +20,33 @@ using Object = UnityEngine.Object; using UnityObject = UnityEngine.Object; +#endregion + namespace nadena.dev.modular_avatar.core.editor { + internal class ParameterRenameMappings + { + public static ParameterRenameMappings Get(ndmf.BuildContext ctx) + { + return ctx.GetState(); + } + + public Dictionary<(ModularAvatarParameters, ParameterNamespace, string), string> Remappings = + new Dictionary<(ModularAvatarParameters, ParameterNamespace, string), string>(); + + private int internalParamIndex; + + public string Remap(ModularAvatarParameters p, ParameterNamespace ns, string s) + { + var tuple = (p, ns, s); + + if (Remappings.TryGetValue(tuple, out var mapping)) return mapping; + + return s + "$$Internal_" + internalParamIndex++; + } + } + + internal class DefaultValues { public ImmutableDictionary InitialValueOverrides; @@ -609,6 +636,8 @@ namespace nadena.dev.modular_avatar.core.editor ref ImmutableDictionary prefixRemaps ) { + var remapper = ParameterRenameMappings.Get(_context.PluginBuildContext); + ImmutableDictionary parameterInfos = ImmutableDictionary.Empty; foreach (var param in p.parameters) @@ -618,7 +647,9 @@ namespace nadena.dev.modular_avatar.core.editor var remapTo = param.remapTo; if (param.internalParameter) { - remapTo = param.nameOrPrefix + "$$Internal_" + internalParamIndex++; + remapTo = remapper.Remap(p, + param.isPrefix ? ParameterNamespace.PhysBonesPrefix : ParameterNamespace.Animator, + param.nameOrPrefix); } else if (string.IsNullOrWhiteSpace(remapTo)) { diff --git a/Editor/assembly-info.cs b/Editor/assembly-info.cs index 467a811f..294e09a8 100644 --- a/Editor/assembly-info.cs +++ b/Editor/assembly-info.cs @@ -6,4 +6,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("net.fushizen.xdress")] [assembly: InternalsVisibleTo("net.fushizen.xdress.editor")] -[assembly: InternalsVisibleTo("nadena.dev.modular-avatar.harmony-patches")] \ No newline at end of file +[assembly: InternalsVisibleTo("nadena.dev.modular-avatar.harmony-patches")] +[assembly: InternalsVisibleTo("nadena.dev.modular-avatar.param-introspection")] \ No newline at end of file diff --git a/Editor/nadena.dev.modular-avatar.core.editor.asmdef b/Editor/nadena.dev.modular-avatar.core.editor.asmdef index a484a73b..70e69df1 100644 --- a/Editor/nadena.dev.modular-avatar.core.editor.asmdef +++ b/Editor/nadena.dev.modular-avatar.core.editor.asmdef @@ -1,10 +1,12 @@ { "name": "nadena.dev.modular-avatar.core.editor", + "rootNamespace": "", "references": [ "nadena.dev.modular-avatar.core", "VRC.SDK3A", "VRC.SDKBase", - "nadena.dev.ndmf" + "nadena.dev.ndmf", + "nadena.dev.ndmf.vrchat" ], "includePlatforms": [ "Editor" diff --git a/Runtime/assembly-info.cs b/Runtime/assembly-info.cs index 0386f743..21dba320 100644 --- a/Runtime/assembly-info.cs +++ b/Runtime/assembly-info.cs @@ -8,4 +8,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("net.fushizen.xdress")] [assembly: InternalsVisibleTo("net.fushizen.xdress.editor")] [assembly: InternalsVisibleTo("Tests")] -[assembly: InternalsVisibleTo("nadena.dev.modular-avatar.harmony-patches")] \ No newline at end of file +[assembly: InternalsVisibleTo("nadena.dev.modular-avatar.harmony-patches")] +[assembly: InternalsVisibleTo("nadena.dev.modular-avatar.param-introspection")] \ No newline at end of file diff --git a/package.json b/package.json index 2a40b776..ed5605f0 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,6 @@ }, "vpmDependencies": { "com.vrchat.avatars": ">=3.4.0", - "nadena.dev.ndmf": ">=1.3.6 <2.0.0-a" + "nadena.dev.ndmf": ">=1.4.0-rc.0 <2.0.0-a" } }