Files
2025-07-19 01:03:02 +02:00

1113 lines
41 KiB
C#

using System;
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor.Animations;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
using Object = UnityEngine.Object;
namespace DreadScripts.QuickToggle
{
public class QuickToggle : EditorWindow
{
#region VRC
#if VRC_SDK_VRCSDK3
private static VRCAvatarDescriptor avatar;
private static VRCExpressionsMenu menu;
private static VRCToggleFlags vrcAddFlags = VRCToggleFlags.All;
private static string _parameter = "Toggle";
private static string parameter
{
get => _parameter;
set
{
if (_parameter == value) return;
_parameter = value;
RefreshUniqueParameter();
}
}
private static bool uniqueParameter = true;
private static bool useWriteDefaults = false;
private enum VRCToggleFlags
{
None = 0,
FX = 1 << 0,
Menu = 1 << 1,
Parameters = 1 << 2,
All = ~0
}
#endif
#endregion
#region Private Variables
private static bool init;
private static bool clipValid = false;
private static string folderPath;
private static UnityEditorInternal.ReorderableList targetList;
private static Vector2 scroll;
public static int toolbarIndex;
private static readonly string[] toolbarOptions = {"Toggle", "Blendshape", "Settings"};
private const string PREF_KEY = "QuickToggleDataKey";
private enum BlendClipMode
{
SingleClip,
ClipPerRenderer,
ClipPerBlendshape
}
#endregion
#region Input
public static GameObject _root;
public static GameObject root
{
get => _root;
set
{
if (_root == value) return;
_root = value;
OnRootChanged();
}
}
public static List<ToggleObject> targets = new List<ToggleObject>();
private static List<SkinnedShapeSlot> skinnedRenderers = new List<SkinnedShapeSlot>() {new SkinnedShapeSlot()};
public static string clipName;
#endregion
#region Settings
[SerializeField] private int frameSpan = 1;
[SerializeField] private bool loopTime;
[SerializeField] private bool pingFolder = true;
[SerializeField] private bool autoClose = true;
[SerializeField] private string savePath = "Assets/DreadScripts/Quick Actions/Quick Toggle/Generated Clips";
private static bool autoName = true;
private static bool individualToggle;
private static bool createOpposite = true;
private static bool useAllShapes;
private static BlendClipMode shapeClipMode = BlendClipMode.SingleClip;
#endregion
[MenuItem("DreadTools/Quick Toggle", false, 840)]
[MenuItem("GameObject/Quick Toggle", false, -10)]
public static void ShowWindow()
{
GetWindow();
GameObject[] targetObjs = Selection.GetFiltered<GameObject>(SelectionMode.Editable);
targets = targetObjs.Select(t => new ToggleObject(t)).ToList();
skinnedRenderers = targetObjs.Select(t => new SkinnedShapeSlot(t.GetComponent<SkinnedMeshRenderer>())).Where(s => s.renderer).ToList();
GameObject[] selectedObj = Selection.GetFiltered<GameObject>(SelectionMode.TopLevel);
if (!root && selectedObj.Length > 0)
root = selectedObj[0].transform.root.gameObject;
#if VRC_SDK_VRCSDK3
if (!root) root = FindObjectOfType<VRCAvatarDescriptor>()?.gameObject;
#endif
if (autoName)
{
clipName = string.Empty;
switch (targetObjs.Length)
{
case 0:
clipName = "Objects";
break;
case 1:
clipName = targetObjs[0].name;
#if VRC_SDK_VRCSDK3
parameter = clipName;
#endif
break;
default:
{
for (int i = 0; i < targetObjs.Length; i++)
{
int letterCount = Mathf.Clamp(7 - targetObjs.Length, 2, 5);
clipName += targetObjs[i].name.Substring(0, Mathf.Clamp(letterCount, 1, targetObjs[i].name.Length));
if (i != targetObjs.Length - 1)
clipName += "-";
}
#if VRC_SDK_VRCSDK3
parameter = clipName;
#endif
break;
}
}
clipName += " Enable";
}
CheckIfValid();
init = false;
}
private static QuickToggle GetWindow() => GetWindow<QuickToggle>(false, "Quick Toggle", true);
#region Main GUI Methods
private void OnGUI()
{
if (!init)
RefreshList();
toolbarIndex = GUILayout.Toolbar(toolbarIndex, toolbarOptions, EditorStyles.toolbarButton);
scroll = EditorGUILayout.BeginScrollView(scroll);
switch (toolbarIndex)
{
case 0:
case 1:
DrawCreatorGUI();
break;
case 2:
DrawSettingsGUI();
break;
}
using (new GUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
if (GUILayout.Button("Made by Dreadrith#3238", "boldlabel"))
Application.OpenURL("https://github.com/Dreadrith/DreadScripts");
}
EditorGUILayout.EndScrollView();
if (!init)
{
GUI.FocusControl("CreateClip");
init = true;
}
if (GUI.GetNameOfFocusedControl() == "CreateClip" && Event.current.type == EventType.KeyDown && (Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter))
{
StartCreateClip(false);
}
}
private void DrawCreatorGUI()
{
bool creatingToggle = toolbarIndex == 0;
using (new GUILayout.VerticalScope(EditorStyles.helpBox))
{
#if VRC_SDK_VRCSDK3
bool showAvatarField = avatar && creatingToggle;
EditorGUI.BeginChangeCheck();
Object tempObject = EditorGUILayout.ObjectField(showAvatarField ? "Avatar" : "Root", showAvatarField ? (Object) avatar : (Object) root, showAvatarField ? typeof(VRCAvatarDescriptor) : typeof(GameObject), true);
if (EditorGUI.EndChangeCheck())
{
avatar = tempObject as VRCAvatarDescriptor;
if (avatar) root = avatar.gameObject;
else root = (GameObject) tempObject;
}
#else
root = (GameObject)EditorGUILayout.ObjectField( "Root", root, typeof(GameObject), true);
#endif
}
using (new GUILayout.VerticalScope(EditorStyles.helpBox))
{
if (creatingToggle) targetList.DoLayoutList();
else
{
for (int i = 0; i < skinnedRenderers.Count; i++)
skinnedRenderers[i].Draw();
if (GUILayout.Button("Add Skinned Renderer")) skinnedRenderers.Add(new SkinnedShapeSlot());
}
}
EditorGUILayout.Space();
using (new GUILayout.VerticalScope(EditorStyles.helpBox))
{
using (new GUILayout.HorizontalScope())
{
using (new GUILayout.VerticalScope())
{
DoToggle(ref createOpposite, Styles.createOppositeContent);
using (new GUILayout.VerticalScope()) clipName = EditorGUILayout.TextField("Clip Name", clipName);
using (new EditorGUI.DisabledScope(creatingToggle))
shapeClipMode = (BlendClipMode) EditorGUILayout.EnumPopup("Clip Mode", shapeClipMode);
}
using (new GUILayout.VerticalScope())
{
using (new EditorGUI.DisabledScope(!creatingToggle))
{
DoToggle(ref individualToggle, Styles.individualToggleContent);
autoName = EditorGUILayout.Toggle(new GUIContent("AutoName", "Automatically generate a clip name when using context menu button"), autoName);
}
using (new EditorGUI.DisabledScope(creatingToggle || shapeClipMode == BlendClipMode.ClipPerBlendshape))
DoToggle(ref useAllShapes, Styles.useAllShapesContent);
}
}
}
EditorGUILayout.Space();
#if VRC_SDK_VRCSDK3
if (avatar && creatingToggle)
{
using (new GUILayout.HorizontalScope(EditorStyles.helpBox))
{
using (new GUILayout.VerticalScope())
{
using (new EditorGUI.DisabledScope(!creatingToggle || !avatar))
vrcAddFlags = (VRCToggleFlags) EditorGUILayout.EnumFlagsField("Add To", vrcAddFlags);
using (new EditorGUI.DisabledScope(!vrcAddFlags.HasFlag(VRCToggleFlags.Menu)))
menu = (VRCExpressionsMenu) EditorGUILayout.ObjectField("Target Menu", menu, typeof(VRCExpressionsMenu), false);
}
using (new GUILayout.VerticalScope())
{
using (new GUILayout.HorizontalScope())
{
using (new EditorGUI.DisabledScope(!creatingToggle || !avatar || vrcAddFlags == VRCToggleFlags.None))
parameter = EditorGUILayout.TextField("Parameter", parameter);
if (!avatar) GUILayout.Label(Styles.noteIcon, GUILayout.Width(18));
var og = GUI.backgroundColor;
GUI.backgroundColor = uniqueParameter ? Color.green : Color.grey;
EditorGUI.BeginChangeCheck();
using (new EditorGUI.DisabledScope(vrcAddFlags == VRCToggleFlags.None))
uniqueParameter = GUILayout.Toggle(uniqueParameter, "Unique", GUI.skin.button, GUILayout.ExpandWidth(false));
if (EditorGUI.EndChangeCheck()) RefreshUniqueParameter();
GUI.backgroundColor = og;
}
useWriteDefaults = EditorGUILayout.Toggle("Write Defaults", useWriteDefaults);
}
}
EditorGUILayout.Space();
}
#endif
GUI.SetNextControlName("CreateClip");
using (new EditorGUI.DisabledScope(string.IsNullOrWhiteSpace(clipName)))
{
if (creatingToggle)
{
using (new EditorGUI.DisabledScope(!clipValid))
if (GUILayout.Button("Create Toggle Clip", EditorStyles.toolbarButton))
StartCreateClip(false);
}
else
using (new EditorGUI.DisabledScope(!skinnedRenderers.Any(sr => sr.renderer)))
if (GUILayout.Button("Create Blendshape Clip", EditorStyles.toolbarButton))
StartCreateClip(true);
}
}
private void DrawSettingsGUI()
{
using (new GUILayout.HorizontalScope(EditorStyles.helpBox))
{
using (new GUILayout.VerticalScope())
{
frameSpan = Mathf.Max(1, EditorGUILayout.IntField(new GUIContent("Clip Length", "The length of the animation clip in frames (60 fps)"), frameSpan));
autoClose = EditorGUILayout.Toggle(new GUIContent("Close Window", "Close window upon clip creation."), autoClose);
}
using (new GUILayout.VerticalScope())
{
loopTime = EditorGUILayout.Toggle(new GUIContent("Loop Time", "Sets loop time to true on the created clips."), loopTime);
pingFolder = EditorGUILayout.Toggle(new GUIContent("Ping Folder", "Automatically highlights the folder where the clips were generated"), pingFolder);
}
}
EditorGUILayout.LabelField(string.Empty, GUI.skin.horizontalSlider);
savePath = DSHelper.AssetFolderPath(savePath, "Generated Assets Path");
}
#endregion
#region Main Methods
public void StartCreateClip(bool isBlendshapes)
{
#if VRC_SDK_VRCSDK3
if (!isBlendshapes)
{
RefreshUniqueParameter();
void VRCWarningCheck(string msg, bool condition)
{
if (!condition) return;
EditorUtility.DisplayDialog("Warning", msg, "Ok");
throw new Exception(msg);
}
VRCWarningCheck("FX Controller not set in Avatar Descriptor.", vrcAddFlags.HasFlag(VRCToggleFlags.FX) && avatar.GetPlayableLayer(VRCAvatarDescriptor.AnimLayerType.FX) == null);
int addCount = individualToggle ? targets.Count(o => o.valid) : 1;
if (vrcAddFlags.HasFlag(VRCToggleFlags.Parameters))
{
VRCWarningCheck("Expression Parameters not set in Avatar Descriptor", vrcAddFlags.HasFlag(VRCToggleFlags.Parameters) && avatar.expressionParameters == null);
if (avatar.expressionParameters.parameters == null)
{
avatar.expressionParameters.parameters = Array.Empty<VRCExpressionParameters.Parameter>();
EditorUtility.SetDirty(avatar.expressionParameters);
}
VRCWarningCheck($"Expression Parameters requires {addCount}/256 free memory.", avatar.expressionParameters.CalcTotalCost() + addCount > 256);
}
if (vrcAddFlags.HasFlag(VRCToggleFlags.Menu))
{
VRCWarningCheck("No Target Menu set.", vrcAddFlags.HasFlag(VRCToggleFlags.Menu) && menu == null);
VRCWarningCheck("Cannot add more than 8 toggles to an expression menu!", addCount > 8);
VRCWarningCheck($"Target Menu requires {addCount}/8 free control slots.", menu.controls.Count + addCount > 8);
}
}
#endif
DSHelper.ReadyPath(savePath);
folderPath = savePath + "/" + root.name;
DSHelper.ReadyPath(folderPath);
if (isBlendshapes)
CreateBlendshapeClips();
else CreateToggleClips();
if (autoClose) Close();
}
private void CreateToggleClips()
{
AnimationClip currentClip = new AnimationClip();
foreach (ToggleObject obj in targets.Where(o => o != null && o.valid))
{
string path = AnimationUtility.CalculateTransformPath(obj.gameObject.transform, root.transform);
System.Type myType = obj.GetActive().GetType();
currentClip.SetCurve(path, myType, myType == typeof(GameObject) ? "m_IsActive" : "m_Enabled", new AnimationCurve {keys = new Keyframe[] {new Keyframe {time = 0, value = obj.active ? 1 : 0}, new Keyframe {time = frameSpan / 60f, value = obj.active ? 1 : 0}}});
if (individualToggle)
{
SaveClip(currentClip, $" {obj.gameObject.name}", false);
RefreshUniqueParameter();
currentClip = new AnimationClip();
}
}
if (!individualToggle)
SaveClip(currentClip, string.Empty, false);
if (pingFolder)
EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath<Object>(folderPath));
}
private void CreateBlendshapeClips()
{
AnimationClip GetNewClip()
{
AnimationClip newClip = new AnimationClip();
return newClip;
}
AnimationClip currentClip = null;
for (int i = 0; i < skinnedRenderers.Count; i++)
{
if (!skinnedRenderers[i].renderer)
continue;
string renderPath = AnimationUtility.CalculateTransformPath(skinnedRenderers[i].renderer.transform, root.transform);
if ((shapeClipMode == BlendClipMode.ClipPerRenderer) || i == 0)
currentClip = GetNewClip();
bool anyShapeUsed = false;
for (int j = 0; j < skinnedRenderers[i].shapes.Count; j++)
{
SkinnedShape shape = skinnedRenderers[i].shapes[j];
if (shape.value > 0 || (useAllShapes && shapeClipMode != BlendClipMode.ClipPerBlendshape))
{
anyShapeUsed = true;
currentClip.SetCurve(renderPath, typeof(SkinnedMeshRenderer), "blendShape." + shape.name, new AnimationCurve() {keys = new Keyframe[] {new Keyframe(0, shape.value), new Keyframe(frameSpan / 60f, shape.value)}});
if (shapeClipMode == BlendClipMode.ClipPerBlendshape)
{
SaveClip(currentClip, " " + shape.name, true);
currentClip = GetNewClip();
}
}
}
if (shapeClipMode == BlendClipMode.ClipPerRenderer && anyShapeUsed)
{
SaveClip(currentClip, " " + skinnedRenderers[i].renderer.gameObject.name, true);
currentClip = GetNewClip();
}
}
if (shapeClipMode == BlendClipMode.SingleClip)
SaveClip(currentClip, string.Empty, true);
if (pingFolder)
EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath<Object>(folderPath));
}
private AnimationClip CreateOppositeClip(AnimationClip c, string clipPath, bool isBlendshapeClip)
{
AnimationClip newClip = new AnimationClip();
EditorUtility.CopySerialized(c, newClip);
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(newClip);
foreach (var b in bindings)
{
int myValue = isBlendshapeClip || AnimationUtility.GetEditorCurve(c, b)[0].value != 0 ? 0 : 1;
newClip.SetCurve(b.path, b.type, b.propertyName, new AnimationCurve() {keys = new Keyframe[] {new Keyframe(0, myValue), new Keyframe(frameSpan / 60f, myValue)}});
}
string oppPath = AssetDatabase.GenerateUniqueAssetPath(clipPath.Substring(0, clipPath.Length - 5) + " Opp.anim");
SaveClip(newClip, oppPath);
return newClip;
}
private void SaveClip(AnimationClip c, string suffix, bool isBlendShapeClip)
{
AnimationClip onClip = c, offClip = null;
string path = AssetDatabase.GenerateUniqueAssetPath($"{folderPath}/{clipName}{suffix}.anim");
SaveClip(c, path);
if (createOpposite) offClip = CreateOppositeClip(c, path, isBlendShapeClip);
#if VRC_SDK_VRCSDK3
DoVRCToggle(onClip, offClip);
#endif
}
private void SaveClip(AnimationClip c, string path)
{
AssetDatabase.CreateAsset(c, path);
Debug.Log("<color=green>[Quick Toggle]</color> " + System.IO.Path.GetFileNameWithoutExtension(path) + " Created.");
EnableLoopTime(c);
}
private void EnableLoopTime(AnimationClip c)
{
if (!loopTime) return;
AnimationClipSettings settings = AnimationUtility.GetAnimationClipSettings(c);
settings.loopTime = true;
AnimationUtility.SetAnimationClipSettings(c, settings);
}
#if VRC_SDK_VRCSDK3
private void DoVRCToggle(AnimationClip onClip, AnimationClip offClip)
{
if (vrcAddFlags == VRCToggleFlags.None) return;
if (vrcAddFlags.HasFlag(VRCToggleFlags.FX))
{
AnimatorController c = avatar.GetPlayableLayer(VRCAvatarDescriptor.AnimLayerType.FX);
if (!c) throw new NullReferenceException("Unexpected Error! FX Controller not found!");
else
{
c.AddParameter(parameter, AnimatorControllerParameterType.Bool);
AnimatorControllerLayer newLayer = c.AddLayer(parameter, 1);
AnimationClip buffer = ReadyBuffer();
AnimatorStateMachine m = newLayer.stateMachine;
m.exitPosition = Vector3.zero;
m.anyStatePosition = new Vector3(0, 40);
m.entryPosition = new Vector3(0, 80);
AnimatorState idleState = m.AddState("Idle", new Vector3(-20, 140));
AnimatorState onState = m.AddState($"{parameter} On", new Vector3(-140, 240));
AnimatorState offState = m.AddState($"{parameter} Off", new Vector3(100, 240));
idleState.writeDefaultValues = onState.writeDefaultValues = offState.writeDefaultValues = useWriteDefaults;
idleState.writeDefaultValues = onState.writeDefaultValues = offState.writeDefaultValues;
idleState.motion = buffer;
onState.motion = onClip;
offState.motion = offClip;
AnimatorStateTransition DoTransition(AnimatorState source, AnimatorState destination, bool state)
{
var t = source.AddTransition(destination);
t.hasExitTime = false;
t.duration = 0;
t.AddCondition(state ? AnimatorConditionMode.If : AnimatorConditionMode.IfNot, 0, parameter);
return t;
}
DoTransition(idleState, onState, true).offset = 1;
DoTransition(idleState, offState, false).offset = 1;
DoTransition(offState, onState, true);
DoTransition(onState, offState, false);
}
}
if (vrcAddFlags.HasFlag(VRCToggleFlags.Parameters))
{
var so = new SerializedObject(avatar.expressionParameters);
var prop = so.FindProperty("parameters");
prop.arraySize++;
var elem = prop.GetArrayElementAtIndex(prop.arraySize - 1);
elem.FindPropertyRelative("name").stringValue = parameter;
elem.FindPropertyRelative("valueType").enumValueIndex = 2;
elem.FindPropertyRelative("saved").boolValue = true;
elem.FindPropertyRelative("defaultValue").floatValue = 0;
so.ApplyModifiedPropertiesWithoutUndo();
}
if (vrcAddFlags.HasFlag(VRCToggleFlags.Menu))
{
if (menu.controls == null)
menu.controls = new List<VRCExpressionsMenu.Control>();
var newControl = new VRCExpressionsMenu.Control
{
name = parameter,
parameter = new VRCExpressionsMenu.Control.Parameter() { name = parameter },
type = VRCExpressionsMenu.Control.ControlType.Toggle,
value = 1
};
menu.controls.Add(newControl);
EditorUtility.SetDirty(menu);
}
}
private AnimationClip ReadyBuffer()
{
string bufferPath = $"{savePath}/1 Frame Buffer.anim";
AnimationClip buffer = AssetDatabase.LoadAssetAtPath<AnimationClip>(bufferPath);
if (buffer) return buffer;
buffer = new AnimationClip();
buffer.SetCurve("_Buffer", typeof(GameObject), "m_IsActive", new AnimationCurve(new Keyframe(0, 0), new Keyframe(1 / 60f, 0)));
AssetDatabase.CreateAsset(buffer, bufferPath);
return buffer;
}
#endif
private static void GreenLog(string msg, bool condition = true)
{
if (condition) Debug.Log($"<color=green>[QuickToggle]</color> {msg}");
}
private static void RedLog(string msg, bool condition = true)
{
if (condition) Debug.Log($"<color=red>[QuickToggle]</color> {msg}");
}
#endregion
#region Automated Methods
private static void AutoRename()
{
if (!autoName || targets.Count == 0)
return;
if (string.IsNullOrEmpty(clipName))
return;
string statusName = "";
bool enabled = false, disabled = false;
foreach (var t in targets)
{
if (t.gameObject)
if (t.active)
{
enabled = true;
statusName = " Enable";
}
else
{
disabled = true;
statusName = " Disable";
}
if (enabled && disabled)
{
statusName = " Toggle";
break;
}
}
if (clipName == (clipName = Regex.Replace(clipName, " enable", statusName, RegexOptions.IgnoreCase)))
{
if (clipName == (clipName = Regex.Replace(clipName, " disable", statusName, RegexOptions.IgnoreCase)))
{
clipName = Regex.Replace(clipName, " toggle", statusName, RegexOptions.IgnoreCase);
}
}
}
private static void OnRootChanged()
{
#if VRC_SDK_VRCSDK3
avatar = root?.GetComponent<VRCAvatarDescriptor>();
if (avatar != null) menu = avatar.expressionsMenu;
#endif
CheckIfValid();
}
private static void CheckIfValid()
{
clipValid = root;
if (!root) return;
foreach (ToggleObject obj in targets)
obj.valid = obj.gameObject && obj.gameObject.transform.IsChildOf(root.transform);
clipValid = targets.All(t => t.valid);
}
#if VRC_SDK_VRCSDK3
private static void RefreshUniqueParameter()
{
if (!uniqueParameter) return;
if (!avatar || string.IsNullOrEmpty(_parameter) ||
vrcAddFlags == VRCToggleFlags.None ||
vrcAddFlags == VRCToggleFlags.Menu) return;
if (avatar.expressionParameters && avatar.expressionParameters.parameters != null)
_parameter = DSHelper.GenerateUniqueString(_parameter, s => avatar.expressionParameters.parameters.All(p => p.name != s));
foreach (var c in avatar.baseAnimationLayers.Concat(avatar.specialAnimationLayers).Where(p => !p.isDefault).Select(p => p.animatorController as AnimatorController).Where(c => c))
_parameter = DSHelper.GenerateUniqueString(_parameter, s => c.parameters.All(p => p.name != s));
}
#endif
private void OnEnable()
{
this.Load(PREF_KEY);
RefreshList();
CheckIfValid();
init = false;
}
private void OnDisable() => this.Save(PREF_KEY);
#endregion
#region Extra GUI Methods
private static string CutString(string s, int maxLength)
=> s.Length <= maxLength ? s : s.Substring(0, maxLength - 3) + "...";
private static void DoToggle(ref bool b, GUIContent label) => b = EditorGUILayout.Toggle(label, b);
private void DrawHeader(Rect rect)
{
EditorGUI.LabelField(rect, "Targets");
if (GUI.Button(new Rect(rect.width - 10, rect.y + 2, 20, EditorGUIUtility.singleLineHeight), Styles.switchIcon, GUIStyle.none))
{
foreach (ToggleObject obj in targets)
obj.active = !obj.active;
AutoRename();
}
}
private void DrawElement(Rect rect, int index, bool isActive, bool isFocused)
{
if (!(index < targets.Count && index >= 0))
return;
if (GUI.Button(new Rect(rect.x, rect.y + 2, 20, EditorGUIUtility.singleLineHeight), "X"))
{
targets.RemoveAt(index);
CheckIfValid();
AutoRename();
return;
}
ToggleObject toggleObj = targets[index];
Rect myRect = new Rect(rect.x + 22, rect.y + 2, rect.width - 62, EditorGUIUtility.singleLineHeight);
EditorGUI.BeginChangeCheck();
Object dummy;
dummy = EditorGUI.ObjectField(myRect, toggleObj.GetActive(), typeof(GameObject), true);
if (EditorGUI.EndChangeCheck())
{
if (dummy == null)
targets[index] = new ToggleObject();
else
{
if (((GameObject) dummy).scene.IsValid())
{
targets[index] = new ToggleObject((GameObject) dummy);
}
else
Debug.LogWarning("[QuickToggle] GameObject must be a scene object!");
}
CheckIfValid();
}
float xCoord = rect.x + rect.width - 18;
if (!toggleObj.valid)
EditorGUI.LabelField(new Rect(xCoord - 60, rect.y + 2, 25, EditorGUIUtility.singleLineHeight), Styles.warnIcon);
EditorGUI.BeginDisabledGroup(!dummy || targets[index].allComps.Length < 2);
if (GUI.Button(new Rect(xCoord - 20, rect.y + 2, 20, 18), Styles.nextIcon, GUIStyle.none))
{
targets[index].next();
}
EditorGUI.EndDisabledGroup();
if (toggleObj.active)
if (GUI.Button(new Rect(xCoord, rect.y, 20, 18), Styles.greenLight, GUIStyle.none))
{
toggleObj.active = false;
AutoRename();
}
if (!toggleObj.active)
if (GUI.Button(new Rect(xCoord, rect.y, 20, 18), Styles.redLight, GUIStyle.none))
{
toggleObj.active = true;
AutoRename();
}
}
private void RefreshList()
{
targetList = new UnityEditorInternal.ReorderableList(targets, typeof(ToggleObject), false, true, true, false)
{
drawElementCallback = DrawElement,
drawHeaderCallback = DrawHeader
};
}
#endregion
#region Classes
public class ToggleObject
{
public GameObject gameObject = null;
public bool active = true;
public bool valid = true;
public Component[] allComps;
public int index;
public ToggleObject()
{
}
public ToggleObject(GameObject o)
{
this.gameObject = o;
allComps = gameObject.GetComponents<Component>();
}
public Object GetActive()
{
if (index == 0)
{
return gameObject;
}
else
{
return allComps[index];
}
}
public void next()
{
index++;
if (index >= allComps.Length)
index = 0;
}
}
private class SkinnedShapeSlot
{
private SkinnedMeshRenderer _renderer;
public SkinnedMeshRenderer renderer
{
get => _renderer;
private set
{
if (_renderer == value) return;
_renderer = value;
OnRendererChanged();
}
}
public readonly List<SkinnedShape> shapes = new List<SkinnedShape>();
private bool expanded;
private Vector2 scroll;
public SkinnedShapeSlot()
{
}
public SkinnedShapeSlot(SkinnedMeshRenderer renderer) => this.renderer = renderer;
void GetShapes()
{
shapes.Clear();
for (int i = 0; i < renderer.sharedMesh.blendShapeCount; i++)
{
shapes.Add(new SkinnedShape(renderer.sharedMesh, i));
}
SortShapes();
}
void SortShapes()
{
int CompareShape(SkinnedShape a, SkinnedShape b)
{
return a.name.CompareTo(b.name);
}
shapes.Sort(CompareShape);
}
public void Draw()
{
if (expanded && renderer != null)
EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.MaxHeight(300));
else
EditorGUILayout.BeginVertical(GUI.skin.box);
using (new GUILayout.HorizontalScope())
{
EditorGUIUtility.labelWidth = 10;
expanded = GUILayout.Toggle(expanded, string.Empty, GUI.skin.GetStyle("Foldout"), GUILayout.Width(15));
renderer = (SkinnedMeshRenderer) EditorGUILayout.ObjectField(string.Empty, renderer, typeof(SkinnedMeshRenderer), true);
EditorGUIUtility.labelWidth = 0;
if (GUILayout.Button("X", GUI.skin.label, GUILayout.Width(18)))
{
skinnedRenderers.Remove(this);
return;
}
}
if (expanded && shapes.Count > 0)
{
scroll = EditorGUILayout.BeginScrollView(scroll);
EditorGUIUtility.labelWidth = 100;
EditorGUI.indentLevel++;
foreach (var s in shapes) s.Draw();
EditorGUI.indentLevel--;
EditorGUILayout.EndScrollView();
EditorGUIUtility.labelWidth = 0;
}
EditorGUILayout.EndVertical();
}
private void OnRendererChanged()
{
if (renderer == null) shapes.Clear();
else if (root && !renderer.transform.IsChildOf(root.transform))
{
Debug.LogWarning("Renderer is not a child of root!");
renderer = null;
shapes.Clear();
}
else if (!renderer.sharedMesh)
{
Debug.LogWarning("Renderer is not using any mesh!");
renderer = null;
shapes.Clear();
}
else GetShapes();
}
}
private class SkinnedShape
{
public string name;
public int index;
public float value;
public SkinnedShape(Mesh m, int i)
{
index = i;
name = m.GetBlendShapeName(i);
value = 0;
}
public void Draw()
{
using (new GUILayout.HorizontalScope())
{
EditorGUILayout.LabelField(CutString(name, 28), GUILayout.Width(180));
value = EditorGUILayout.Slider(value, 0, 100);
}
}
}
private static class Styles
{
internal static readonly GUIContent
noteIcon = new GUIContent(EditorGUIUtility.IconContent("console.warnicon.inactive.sml")) {tooltip = "No Avatar Descriptor found on Root"},
warnIcon = new GUIContent(EditorGUIUtility.IconContent("d_console.warnicon.sml")) {tooltip = "Object is not a child of Root!"},
greenLight = new GUIContent(EditorGUIUtility.IconContent("d_greenLight")) {tooltip = "Enable"},
redLight = new GUIContent(EditorGUIUtility.IconContent("d_redLight")) {tooltip = "Disable"},
switchIcon = new GUIContent(EditorGUIUtility.IconContent("d_Animation.Record")) {tooltip = "Invert Toggles"},
nextIcon = new GUIContent(EditorGUIUtility.IconContent("Refresh")) {tooltip = "Cycle Components"},
createOppositeContent = new GUIContent("Create Opposite", "Make the opposite clip for created animation clips"),
individualToggleContent = new GUIContent("Individual Toggles", "Make an animation clip for each target separately"),
useAllShapesContent = new GUIContent("Use all Blendshapes", "When creating the clip, utilize all the blendshapes of the Renderers");
}
#endregion
}
internal static class DSHelper
{
internal static void ReadyPath(string folderPath)
{
if (Directory.Exists(folderPath)) return;
Directory.CreateDirectory(folderPath);
AssetDatabase.ImportAsset(folderPath);
}
internal static string AssetFolderPath(string variable, string title)
{
using (new GUILayout.HorizontalScope())
{
using (new EditorGUI.DisabledScope(true))
EditorGUILayout.TextField(title, variable);
if (!GUILayout.Button("...", GUILayout.Width(30))) return variable;
var dummyPath = EditorUtility.OpenFolderPanel(title, AssetDatabase.IsValidFolder(variable) ? variable : "Assets", string.Empty);
if (string.IsNullOrEmpty(dummyPath))
return variable;
string newPath = FileUtil.GetProjectRelativePath(dummyPath);
if (!newPath.StartsWith("Assets"))
{
Debug.LogWarning("New Path must be a folder within Assets!");
return variable;
}
variable = newPath;
}
return variable;
}
internal static void Save<T>(this T window, string prefs) where T : EditorWindow
{
string data = JsonUtility.ToJson(window, false);
PlayerPrefs.SetString(prefs, data);
}
internal static void Load<T>(this T window, string prefs) where T : EditorWindow
{
string defaultData = JsonUtility.ToJson(window, false);
string data = PlayerPrefs.GetString(prefs, defaultData);
JsonUtility.FromJsonOverwrite(data, window);
}
internal static string GenerateUniqueString(string s, System.Func<string, bool> check)
{
if (check(s))
return s;
int suffix = 0;
int.TryParse(s.Substring(s.Length - 2, 2), out int d);
if (d >= 0)
suffix = d;
if (suffix > 0) s = suffix > 9 ? s.Substring(0, s.Length - 2) : s.Substring(0, s.Length - 1);
s = s.Trim();
suffix++;
string newString = s + " " + suffix;
while (!check(newString))
{
suffix++;
newString = s + " " + suffix;
}
return newString;
}
internal static AnimatorControllerLayer AddLayer(this AnimatorController controller, string name, float defaultWeight)
{
var newLayer = new AnimatorControllerLayer
{
name = name,
defaultWeight = defaultWeight,
stateMachine = new AnimatorStateMachine
{
name = name,
hideFlags = HideFlags.HideInHierarchy
},
};
AssetDatabase.AddObjectToAsset(newLayer.stateMachine, controller);
controller.AddLayer(newLayer);
return newLayer;
}
#if VRC_SDK_VRCSDK3
internal static AnimatorController GetPlayableLayer(this VRCAvatarDescriptor avi, VRCAvatarDescriptor.AnimLayerType type)
=> avi.baseAnimationLayers.Concat(avi.specialAnimationLayers).FirstOrDefault(l => l.type == type).animatorController as AnimatorController;
#endif
}
}