summaryrefslogtreecommitdiff
path: root/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor
diff options
context:
space:
mode:
Diffstat (limited to 'VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor')
-rw-r--r--VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3EditorSupport.cs268
-rw-r--r--VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3EditorSupport.cs.meta11
-rw-r--r--VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3MenuEditor.cs343
-rw-r--r--VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3MenuEditor.cs.meta3
4 files changed, 625 insertions, 0 deletions
diff --git a/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3EditorSupport.cs b/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3EditorSupport.cs
new file mode 100644
index 00000000..66c73166
--- /dev/null
+++ b/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3EditorSupport.cs
@@ -0,0 +1,268 @@
+/* Copyright (c) 2020-2022 Lyuma <xn.lyuma@gmail.com>
+
+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
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. */
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+using UnityEngine.Animations;
+using UnityEditor.Animations;
+using UnityEditor.Compilation;
+using UnityEditor.Playables;
+using UnityEngine.Playables;
+using VRC.SDK3.Avatars.Components;
+
+[InitializeOnLoadAttribute]
+public static class LyumaAv3EditorSupport
+{
+ static Dictionary<VRCAvatarDescriptor.AnimLayerType, string> animLayerToDefaultFile = new Dictionary<VRCAvatarDescriptor.AnimLayerType, string> {
+ {VRCAvatarDescriptor.AnimLayerType.TPose, "vrc_AvatarV3UtilityTPose"},
+ {VRCAvatarDescriptor.AnimLayerType.IKPose, "vrc_AvatarV3UtilityIKPose"},
+ {VRCAvatarDescriptor.AnimLayerType.Base, "vrc_AvatarV3LocomotionLayer"},
+ {VRCAvatarDescriptor.AnimLayerType.Sitting, "vrc_AvatarV3SittingLayer"},
+ {VRCAvatarDescriptor.AnimLayerType.Additive, "vrc_AvatarV3IdleLayer"},
+ {VRCAvatarDescriptor.AnimLayerType.FX, "vrc_AvatarV3FaceLayer"},
+ {VRCAvatarDescriptor.AnimLayerType.Action, "vrc_AvatarV3ActionLayer"},
+ {VRCAvatarDescriptor.AnimLayerType.Gesture, "vrc_AvatarV3HandsLayer"},
+ };
+ static Dictionary<VRCAvatarDescriptor.AnimLayerType, string> animLayerToDefaultAvaMaskFile = new Dictionary<VRCAvatarDescriptor.AnimLayerType, string>
+ {
+ {VRCAvatarDescriptor.AnimLayerType.TPose, "vrc_MusclesOnly"},
+ {VRCAvatarDescriptor.AnimLayerType.IKPose, "vrc_MusclesOnly"},
+ {VRCAvatarDescriptor.AnimLayerType.Base, null},//"LyumaFullMask"},
+ {VRCAvatarDescriptor.AnimLayerType.Sitting, null},//"LyumaFullMask"},
+ {VRCAvatarDescriptor.AnimLayerType.Additive, null},//"LyumaFullMask"},
+ {VRCAvatarDescriptor.AnimLayerType.FX, "LyumaEmptyMask"}, // TODO
+ {VRCAvatarDescriptor.AnimLayerType.Action, null},//"vrc_MusclesOnly"},
+ {VRCAvatarDescriptor.AnimLayerType.Gesture, "vrc_HandsOnly"},
+ };
+
+ static void InitDefaults() {
+ foreach (var kv in animLayerToDefaultFile) {
+ if (kv.Value == null) {
+ LyumaAv3Runtime.animLayerToDefaultController[kv.Key] = null;
+ } else
+ {
+ AnimatorController ac = AssetDatabase.LoadAssetAtPath<AnimatorController>("Assets/VRCSDK/Examples3/Animation/Controllers/" + kv.Value + ".controller");
+ if (ac == null)
+ {
+ Debug.LogWarning("Failed to resolve animator controller " + kv.Value + " for " + kv.Key);
+ foreach (var guid in AssetDatabase.FindAssets(kv.Value))
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ if (path.EndsWith("/" + kv.Value + ".controller")) {
+ ac = AssetDatabase.LoadAssetAtPath<AnimatorController>(path);
+ break;
+ }
+ }
+ }
+ LyumaAv3Runtime.animLayerToDefaultController[kv.Key] = ac;
+ }
+ }
+ foreach (var kv in animLayerToDefaultAvaMaskFile) {
+ if (kv.Value == null) {
+ LyumaAv3Runtime.animLayerToDefaultAvaMask[kv.Key] = null;
+ } else
+ {
+ AvatarMask mask = null;
+ foreach (var guid in AssetDatabase.FindAssets(kv.Value))
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ mask = AssetDatabase.LoadAssetAtPath<AvatarMask>(path);
+ }
+ if (mask == null)
+ {
+ Debug.LogWarning("Failed to resolve avatar mask " + kv.Value + " for " + kv.Key);
+ mask = new AvatarMask();
+ }
+ LyumaAv3Runtime.animLayerToDefaultAvaMask[kv.Key] = mask;
+ }
+ }
+ foreach (string guid in AssetDatabase.FindAssets("EmptyController")) {
+ LyumaAv3Emulator.EmptyController = AssetDatabase.LoadAssetAtPath<RuntimeAnimatorController>(AssetDatabase.GUIDToAssetPath(guid));
+ }
+
+ LyumaAv3Runtime.updateSelectionDelegate = (obj) => {
+ if (obj == null && LyumaAv3Emulator.emulatorInstance != null) {
+ // Debug.Log("Resetting selected object: " + LyumaAv3Emulator.emulatorInstance);
+ obj = LyumaAv3Emulator.emulatorInstance.gameObject;
+ }
+ // Debug.Log("Setting selected object: " + go);
+ Selection.SetActiveObjectWithContext(obj, obj);
+ // Highlighter.Highlight("Inspector", "Animator To Debug");
+ };
+
+ LyumaAv3Runtime.updateSceneLayersDelegate = (layers) => {
+ if (Tools.visibleLayers == layers) {
+ return;
+ }
+ // Debug.Log("Setting selected layers: " + layers);
+ Tools.visibleLayers = layers;
+ Camera[] cameras = new Camera[255];
+ Camera.GetAllCameras(cameras);
+ foreach (Camera c in cameras) {
+ if (c != null && c.targetTexture == null && c.GetComponentInParent<LyumaAv3Runtime>() == null && c.gameObject.activeInHierarchy && c.isActiveAndEnabled) {
+ c.cullingMask = layers;
+ }
+ }
+ // Highlighter.Highlight("Inspector", "Animator To Debug");
+ };
+
+ LyumaAv3Runtime.addRuntimeDelegate = (runtime) => {
+ MoveComponentToTop(runtime);
+ };
+
+ // Currently PhysBone and ContactManager cause exceptions if scripts reload during Play mode.
+ // This applies a workaround: disable the objects before compile; call RuntimeInit to recreate them after.
+ LyumaAv3Runtime.ApplyOnEnableWorkaroundDelegate = () => {
+ CompilationPipeline.assemblyCompilationStarted -= WorkaroundDestroyManagersBeforeCompile;
+ CompilationPipeline.assemblyCompilationStarted += WorkaroundDestroyManagersBeforeCompile;
+ GameObject gotmp = GameObject.Find("/TempReloadDontDestroy");
+ if (gotmp != null) {
+ GameObject.DestroyImmediate(gotmp);
+ var avatarDynamicsSetup = typeof(VRCExpressionsMenuEditor).Assembly.GetType("VRC.SDK3.Avatars.AvatarDynamicsSetup");
+ if (avatarDynamicsSetup != null) {
+ var RuntimeInit = avatarDynamicsSetup.GetMethod("RuntimeInit", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
+ if (RuntimeInit != null) {
+ Debug.Log("Caling avatarDynamicsSetup.RuntimeInit(): " + RuntimeInit);
+ RuntimeInit.Invoke(null, new object[0]);
+ }
+ }
+ Debug.Log("DONE workaround");
+ }
+ };
+
+ LyumaAv3Osc.GetEditorViewportDelegate = () => {
+ try {
+ Rect ret = UnityEditor.SceneView.currentDrawingSceneView.position;
+ // Gizmos are relative to the active window in terms of x and y.
+ ret.x = 1.0f;
+ ret.y = 1.0f;
+ ret.height -= 7.0f;
+ return ret;
+ } catch {
+ Vector2 gvsize = Handles.GetMainGameViewSize();
+ return new Rect(0, -18, gvsize.x, gvsize.y);
+ }
+ };
+ LyumaAv3Osc.DrawDebugRectDelegate = (Rect pos, Color col, Color outlineCol) => {
+ // Debug.Log("Debug raw rect " + pos);
+ Color origColor = GUI.color;
+ GUI.color = col;
+ UnityEditor.Handles.BeginGUI();
+ UnityEditor.Handles.DrawSolidRectangleWithOutline(pos, col, outlineCol);
+ UnityEditor.Handles.EndGUI();
+ GUI.color = origColor;
+ };
+ LyumaAv3Osc.DrawDebugTextDelegate = (Rect pos, Color backgroundCol, Color outlineCol, Color textCol, string str, TextAnchor alignment) => {
+ // Debug.Log("Debug raw text " + str + " at " + pos);
+ Color origColor = GUI.color;
+ GUI.color = backgroundCol;
+ var view = UnityEditor.SceneView.currentDrawingSceneView;
+ // Vector2 size = GUI.skin.label.CalcSize(new GUIContent(str));
+ // Rect pos = new Rect(location.x, location.y, size.x, size.y);
+ UnityEditor.Handles.BeginGUI();
+ UnityEditor.Handles.DrawSolidRectangleWithOutline(pos, backgroundCol, outlineCol);
+ GUI.color = textCol.r + textCol.b + textCol.g > 0.5f ? new Color(0,0,0,textCol.a * 0.5f) : new Color(1,1,1,textCol.a * 0.5f);//new Color(1.0f, 1.0f, 1.0f, textCol.a * 0.25f);
+ var style = new GUIStyle();
+ style.fontStyle = FontStyle.Bold;
+ style.alignment = alignment;
+ style.normal.textColor = GUI.color;
+ pos.y += 1;
+ GUI.Label(pos, str, style);
+ pos.x += 1;
+ GUI.Label(pos, str, style);
+ pos.y -= 1;
+ GUI.Label(pos, str, style);
+ pos.x -= 1;
+ GUI.Label(pos, str, style);
+ pos.x += 0.5f;
+ pos.y += 0.5f;
+ GUI.color = textCol;
+ style.normal.textColor = GUI.color;
+ GUI.Label(pos, str, style);
+ UnityEditor.Handles.EndGUI();
+ GUI.color = origColor;
+ };
+ }
+
+ public static void OnPlayModeStateChange(UnityEditor.PlayModeStateChange pmsc) {
+ // We don't want any of our callbacks causing trouble outside of play mode.
+ if (pmsc != UnityEditor.PlayModeStateChange.EnteredPlayMode) {
+ CompilationPipeline.assemblyCompilationStarted -= WorkaroundDestroyManagersBeforeCompile;
+ }
+ }
+
+ private static void WorkaroundDestroyManagersBeforeCompile(string obj) {
+ Debug.Log("Compile Started");
+ GameObject gotmp = new GameObject("TempReloadDontDestroy");
+ Object.DontDestroyOnLoad(gotmp);
+ GameObject go;
+ go = GameObject.Find("/TriggerManager");
+ if (go != null) {
+ Object.DestroyImmediate(go);
+ }
+ go = GameObject.Find("/PhysBoneManager");
+ if (go != null) {
+ Object.DestroyImmediate(go);
+ }
+ }
+
+ static void MoveComponentToTop(Component c) {
+ GameObject go = c.gameObject;
+ Component[] components = go.GetComponents<Component>();
+ for (int i = 0; i < components.Length; i++) {
+ if (components[i].GetType().Name.Contains("PipelineSaver")) {
+ return;
+ }
+ }
+ try {
+ if (PrefabUtility.IsPartOfAnyPrefab(go)) {
+ PrefabUtility.UnpackPrefabInstance(go, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
+ }
+ } catch (System.Exception) {}
+ int moveUpCalls = components.Length - 2;
+ if (!PrefabUtility.IsPartOfAnyPrefab(go.GetComponents<Component>()[1])) {
+ for (int i = 0; i < moveUpCalls; i++) {
+ UnityEditorInternal.ComponentUtility.MoveComponentUp(c);
+ }
+ }
+ }
+
+ // register an event handler when the class is initialized
+ static LyumaAv3EditorSupport()
+ {
+ InitDefaults();
+ EditorApplication.playModeStateChanged += OnPlayModeStateChange;
+ }
+
+ [MenuItem("Tools/Enable Avatars 3.0 Emulator")]
+ public static void EnableAv3Testing() {
+ GameObject go = GameObject.Find("/Avatars 3.0 Emulator Control");
+ if (go != null) {
+ go.SetActive(true);
+ } else {
+ go = new GameObject("Avatars 3.0 Emulator Control");
+ }
+ Selection.SetActiveObjectWithContext(go, go);
+ go.GetOrAddComponent<LyumaAv3Emulator>();
+ go.GetOrAddComponent<LyumaAv3Osc>();
+ EditorGUIUtility.PingObject(go);
+ }
+}
diff --git a/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3EditorSupport.cs.meta b/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3EditorSupport.cs.meta
new file mode 100644
index 00000000..79bb5d5f
--- /dev/null
+++ b/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3EditorSupport.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 77ddade7c6475a242a8e34b2b7554adc
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3MenuEditor.cs b/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3MenuEditor.cs
new file mode 100644
index 00000000..4da25e95
--- /dev/null
+++ b/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3MenuEditor.cs
@@ -0,0 +1,343 @@
+/* Copyright (c) 2020-2022 Lyuma <xn.lyuma@gmail.com>
+
+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
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. */
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using VRC.SDK3.Avatars.ScriptableObjects;
+
+[CustomEditor(typeof(LyumaAv3Menu), true)]
+public class LyumaAv3MenuEditor : Editor
+{
+ private VRCExpressionsMenu _currentMenu;
+
+ public override void OnInspectorGUI()
+ {
+ var menu = (LyumaAv3Menu)target;
+ GUILayout.BeginHorizontal();
+ if (GUILayout.Button(menu.IsMenuOpen ? "Close menu" : "Open menu"))
+ {
+ menu.ToggleMenu();
+ }
+
+ if (menu.gameObject.GetComponents<LyumaAv3Menu>().Length == 1)
+ {
+ if (GUILayout.Button("+", GUILayout.Width(20)))
+ {
+ OpenMenuForTwoHandedSupport(menu);
+ }
+ }
+
+ GUILayout.EndHorizontal();
+
+ RenderButtonMenu();
+ }
+
+ protected void RenderButtonMenu() {
+
+ var menu = (LyumaAv3Menu)target;
+ if (menu.Runtime == null) return;
+ if (menu.RootMenu == null)
+ {
+ menu.RootMenu = (VRCExpressionsMenu)EditorGUILayout.ObjectField(new GUIContent("Expressions Menu"), null, typeof(VRCExpressionsMenu), false);
+ return;
+ }
+
+ var isInRootMenu = menu.MenuStack.Count == 0;
+
+ GUILayout.Label(
+ (isInRootMenu ? "Expressions" : LabelizeMenu()) +
+ (menu.IsMenuOpen ? "" : " [Menu is closed]"),
+ EditorStyles.boldLabel);
+
+ if (!menu.IsMenuOpen) {
+ return;
+ }
+
+ _currentMenu = menu.MenuStack.Count == 0 ? menu.RootMenu : menu.MenuStack.Last().ExpressionsMenu;
+
+ EditorGUI.BeginDisabledGroup(true);
+ EditorGUILayout.ObjectField(_currentMenu, typeof(VRCExpressionsMenu), false);
+ EditorGUI.EndDisabledGroup();
+
+ EditorGUI.BeginDisabledGroup(isInRootMenu || menu.HasActiveControl());
+ if (GUILayout.Button("Back"))
+ {
+ menu.UserBack();
+ }
+ EditorGUI.EndDisabledGroup();
+ if (_currentMenu == null) {
+ EditorGUILayout.LabelField("(This submenu is null)");
+ return;
+ }
+ for (var controlIndex = 0; controlIndex < _currentMenu.controls.Count; controlIndex++)
+ {
+ var control = _currentMenu.controls[controlIndex];
+ switch (control.type)
+ {
+ case VRCExpressionsMenu.Control.ControlType.Button:
+ FromToggle(control, "Button");
+ break;
+ case VRCExpressionsMenu.Control.ControlType.Toggle:
+ FromToggle(control, "Toggle");
+ break;
+ case VRCExpressionsMenu.Control.ControlType.SubMenu:
+ FromSubMenu(control);
+ break;
+ case VRCExpressionsMenu.Control.ControlType.TwoAxisPuppet:
+ FromTwoAxis(control, controlIndex);
+ break;
+ case VRCExpressionsMenu.Control.ControlType.FourAxisPuppet:
+ FromFourAxis(control, controlIndex);
+ break;
+ case VRCExpressionsMenu.Control.ControlType.RadialPuppet:
+ FromRadial(control, controlIndex);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ if (_currentMenu.controls.Count == 0)
+ {
+ EditorGUILayout.LabelField("(This menu has no controls)");
+ }
+ }
+
+ private static void OpenMenuForTwoHandedSupport(LyumaAv3Menu menu)
+ {
+ var mainMenu = menu.Runtime.gameObject.AddComponent<LyumaAv3Menu>();
+ mainMenu.useLegacyMenu = menu.useLegacyMenu;
+ mainMenu.Runtime = menu.Runtime;
+ mainMenu.RootMenu = menu.RootMenu;
+ }
+
+ private string LabelizeMenu()
+ {
+ var menu = (LyumaAv3Menu)target;
+
+ var lastMenu = menu.MenuStack.Last();
+ if (lastMenu.MandatedParam == null)
+ {
+ if (lastMenu.ExpressionsMenu == null) {
+ return "SubMenu linked to null menu!";
+ }
+ return lastMenu.ExpressionsMenu.name;
+ }
+
+ return lastMenu.ExpressionsMenu.name + " (" + lastMenu.MandatedParam.name + " = " + lastMenu.MandatedParam.value + ")";
+ }
+
+ private void FromToggle(VRCExpressionsMenu.Control control, string labelType)
+ {
+ var menu = (LyumaAv3Menu)target;
+
+ var parameterName = control.parameter.name;
+ var controlValue = control.value;
+
+ var isActive = menu.IsVisualActive(parameterName, controlValue);
+
+ EditorGUILayout.BeginHorizontal();
+ EditorGUI.BeginDisabledGroup(menu.HasActiveControl());
+ if (GreenBackground(isActive, () => ParameterizedButton(control, parameterName, controlValue)))
+ {
+ menu.UserToggle(parameterName, controlValue);
+ }
+ EditorGUI.EndDisabledGroup();
+ LabelType(labelType);
+ EditorGUILayout.EndHorizontal();
+ }
+
+ private void FromSubMenu(VRCExpressionsMenu.Control control)
+ {
+ var menu = (LyumaAv3Menu)target;
+
+ var parameterName = control.parameter.name;
+ var wantedValue = control.value;
+
+ EditorGUILayout.BeginHorizontal();
+ EditorGUI.BeginDisabledGroup(menu.HasActiveControl());
+ if (ParameterizedButton(control, parameterName, wantedValue))
+ {
+ if (IsValidParameterName(parameterName))
+ {
+ menu.UserSubMenu(control.subMenu, parameterName, wantedValue);
+ }
+ else
+ {
+ menu.UserSubMenu(control.subMenu);
+ }
+ }
+ EditorGUI.EndDisabledGroup();
+ LabelType("SubMenu");
+ EditorGUILayout.EndHorizontal();
+ }
+
+ private void FromRadial(VRCExpressionsMenu.Control control, int controlIndex)
+ {
+ var menu = (LyumaAv3Menu)target;
+
+ SubControl(control, controlIndex, menu, "Radial");
+
+ if (menu.IsActiveControl(controlIndex))
+ {
+ if (control.subParameters.Length > 0)
+ {
+ SliderFloat(menu, control.subParameters[0], "Rotation", 0f, 1f);
+ }
+ }
+ }
+
+ private void FromTwoAxis(VRCExpressionsMenu.Control control, int controlIndex)
+ {
+ var menu = (LyumaAv3Menu)target;
+
+ SubControl(control, controlIndex, menu, "TwoAxis");
+
+ if (menu.IsActiveControl(controlIndex))
+ {
+ var sanitySubParamLength = control.subParameters.Length;
+ if (sanitySubParamLength > 0) SliderFloat(menu, control.subParameters[0], "Horizontal", -1f, 1f);
+ if (sanitySubParamLength > 1) SliderFloat(menu, control.subParameters[1], "Vertical", -1f, 1f);
+
+ var oldColor = Color.HSVToRGB(
+ 0,
+ sanitySubParamLength > 0 ? menu.FindFloat(control.subParameters[0].name) * 0.5f + 0.5f : 0,
+ sanitySubParamLength > 1 ? menu.FindFloat(control.subParameters[1].name) * 0.5f + 0.5f : 0);
+ var newColor = EditorGUILayout.ColorField(oldColor);
+ if (oldColor.r != newColor.r || oldColor.g != newColor.g || oldColor.b != newColor.b)
+ {
+ Color.RGBToHSV(newColor, out _, out var s, out var v);
+ if (sanitySubParamLength > 0) menu.UserFloat(control.subParameters[0].name, s * 2 - 1);
+ if (sanitySubParamLength > 1) menu.UserFloat(control.subParameters[1].name, v * 2 - 1);
+ }
+ }
+ }
+
+ private void FromFourAxis(VRCExpressionsMenu.Control control, int controlIndex)
+ {
+ var menu = (LyumaAv3Menu)target;
+
+ SubControl(control, controlIndex, menu, "FourAxis");
+
+ if (menu.IsActiveControl(controlIndex))
+ {
+ var sanitySubParamLength = control.subParameters.Length;
+ if (sanitySubParamLength > 0) SliderFloat(menu, control.subParameters[0], "Up", 0f, 1f);
+ if (sanitySubParamLength > 1) SliderFloat(menu, control.subParameters[1], "Right", 0f, 1f);
+ if (sanitySubParamLength > 2) SliderFloat(menu, control.subParameters[2], "Down", 0f, 1f);
+ if (sanitySubParamLength > 3) SliderFloat(menu, control.subParameters[3], "Left", 0f, 1f);
+
+ var oldColor = Color.HSVToRGB(
+ 0,
+ (sanitySubParamLength > 0 ? menu.FindFloat(control.subParameters[0].name) : 0) * 0.5f + 0.5f
+ -(sanitySubParamLength > 2 ? menu.FindFloat(control.subParameters[2].name) : 0) * 0.5f + 0.5f,
+ (sanitySubParamLength > 1 ? menu.FindFloat(control.subParameters[1].name) : 0) * 0.5f + 0.5f
+ -(sanitySubParamLength > 3 ? menu.FindFloat(control.subParameters[3].name) : 0) * 0.5f + 0.5f);
+ var newColor = EditorGUILayout.ColorField(oldColor);
+ if (oldColor.r != newColor.r || oldColor.g != newColor.g || oldColor.b != newColor.b)
+ {
+ Color.RGBToHSV(newColor, out _, out var s, out var v);
+ if (sanitySubParamLength > 0) menu.UserFloat(control.subParameters[0].name, Mathf.Clamp(v * 2 - 1, 0f, 1f));
+ if (sanitySubParamLength > 1) menu.UserFloat(control.subParameters[1].name, Mathf.Clamp(s * 2 - 1, 0f, 1f));
+ if (sanitySubParamLength > 2) menu.UserFloat(control.subParameters[2].name, -Mathf.Clamp(v * 2 - 1, -1f, 0f));
+ if (sanitySubParamLength > 3) menu.UserFloat(control.subParameters[3].name, -Mathf.Clamp(s * 2 - 1, -1f, 0f));
+ }
+ }
+ }
+
+ private void SubControl(VRCExpressionsMenu.Control control, int controlIndex, LyumaAv3Menu menu, string labelType)
+ {
+ var parameterName = control.parameter.name;
+ var intValue = (int) control.value;
+
+ var isActive = menu.IsVisualActive(parameterName, intValue);
+
+ EditorGUILayout.BeginHorizontal();
+ EditorGUI.BeginDisabledGroup(menu.HasActiveControl() && !menu.IsActiveControl(controlIndex));
+ if (GreenBackground(isActive || menu.IsActiveControl(controlIndex), () => ParameterizedButton(control, parameterName, intValue)))
+ {
+ if (!menu.IsActiveControl(controlIndex))
+ {
+ if (IsValidParameterName(parameterName))
+ {
+ menu.UserControlEnter(controlIndex, parameterName, intValue);
+ }
+ else
+ {
+ menu.UserControlEnter(controlIndex);
+ }
+ }
+ else
+ {
+ menu.UserControlExit();
+ }
+ }
+
+ EditorGUI.EndDisabledGroup();
+ LabelType(labelType);
+ EditorGUILayout.EndHorizontal();
+ }
+
+ private static void SliderFloat(LyumaAv3Menu menu, VRCExpressionsMenu.Control.Parameter subParam, string intent, float left, float right)
+ {
+ if (subParam == null || subParam.name == "")
+ {
+ EditorGUI.BeginDisabledGroup(true);
+ EditorGUILayout.Slider(intent, 0, left, right);
+ EditorGUI.EndDisabledGroup();
+ return;
+ }
+
+ menu.UserFloat(subParam.name, EditorGUILayout.Slider(intent + " (" + subParam.name + ")", menu.FindFloat(subParam.name), left, right));
+ }
+
+ private bool ParameterizedButton(VRCExpressionsMenu.Control control, string parameterName, float wantedValue)
+ {
+ var hasParameter = IsValidParameterName(parameterName);
+ return GUILayout.Button(new GUIContent(control.name + (hasParameter ? " (" + parameterName + " = " + wantedValue + ")" : ""), control.icon), GUILayout.Height(36),GUILayout.MinWidth(40));
+ }
+
+ private static T GreenBackground<T>(bool isActive, Func<T> inside)
+ {
+ var col = GUI.color;
+ try
+ {
+ if (isActive) GUI.color = Color.green;
+ return inside();
+ }
+ finally
+ {
+ GUI.color = col;
+ }
+ }
+
+ private static void LabelType(string toggle)
+ {
+ EditorGUILayout.LabelField(toggle, GUILayout.Width(70), GUILayout.ExpandHeight(true));
+ }
+
+ private static bool IsValidParameterName(string parameterName)
+ {
+ return !string.IsNullOrEmpty(parameterName);
+ }
+}
diff --git a/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3MenuEditor.cs.meta b/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3MenuEditor.cs.meta
new file mode 100644
index 00000000..f613af7d
--- /dev/null
+++ b/VRCSDK3Avatars/Assets/Resources/Lyuma/Av3Emulator/Editor/LyumaAv3MenuEditor.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 187ea10b8ccd4441a6399698c23122e3
+timeCreated: 1604152513 \ No newline at end of file