// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using System; using System.Collections.Generic; using System.Reflection; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace HoloToolkit.Unity { #if UNITY_EDITOR /// <summary> /// To use this class in a MonoBehavior or ScriptableObject, add this line at the bottom of your class: /// /// public class ClassName { /// ... /// #if UNITY_EDITOR /// [UnityEditor.CustomEditor(typeof(ClassName))] /// public class CustomEditor : MRTKEditor { } /// #endif /// } /// /// </summary> public class MRTKEditor : Editor { #region static vars const string mrtkShowEditorKey = "_Show_MRTK_Editors"; // Toggles custom editors on / off public static bool ShowCustomEditors { get { return GetEditorPref(mrtkShowEditorKey, true); } private set { SetEditorPref(mrtkShowEditorKey, value); } } public static bool CustomEditorActive { get; private set; } public static GameObject lastTarget; // Styles private static GUIStyle toggleButtonOffStyle = null; private static GUIStyle toggleButtonOnStyle = null; private static GUIStyle sectionStyle = null; private static GUIStyle toolTipStyle = null; private static GUIStyle inProgressStyle = null; // Colors protected readonly static Color defaultColor = new Color(1f, 1f, 1f); protected readonly static Color disabledColor = new Color(0.6f, 0.6f, 0.6f); protected readonly static Color borderedColor = new Color(0.8f, 0.8f, 0.8f); protected readonly static Color warningColor = new Color(1f, 0.85f, 0.6f); protected readonly static Color errorColor = new Color(1f, 0.55f, 0.5f); protected readonly static Color successColor = new Color(0.8f, 1f, 0.75f); protected readonly static Color objectColor = new Color(0.85f, 0.9f, 1f); protected readonly static Color helpBoxColor = new Color(0.70f, 0.75f, 0.80f, 0.5f); protected readonly static Color sectionColor = new Color(0.85f, 0.9f, 1f); protected readonly static Color darkColor = new Color(0.1f, 0.1f, 0.1f); protected readonly static Color objectColorEmpty = new Color(0.75f, 0.8f, 0.9f); protected readonly static Color profileColor = new Color(0.88f, 0.7f, .97f); protected readonly static Color handleColorSquare = new Color(0.0f, 0.9f, 1f); protected readonly static Color handleColorCircle = new Color(1f, 0.5f, 1f); protected readonly static Color handleColorSphere = new Color(1f, 0.5f, 1f); protected readonly static Color handleColorAxis = new Color(0.0f, 1f, 0.2f); protected readonly static Color handleColorRotation = new Color(0.0f, 1f, 0.2f); protected readonly static Color handleColorTangent = new Color(0.1f, 0.8f, 0.5f, 0.7f); public const float DottedLineScreenSpace = 4.65f; // Toggles visible tooltips private static bool showHelp = false; // Stores the show / hide values of displayed sections by target name + section name private static Dictionary<string, bool> displayedSections = new Dictionary<string, bool>(); private static int indentOnSectionStart = 0; private static BindingFlags defaultBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private bool recordingUndo = false; #endregion public override void OnInspectorGUI() { // Set this to true so we can track which kind of headers to draw CustomEditorActive = true; try { CreateStyles(); DrawInspectorHeader(); Undo.RecordObject(target, target.name); if (ShowCustomEditors) { BeginInspectorStyle(); DrawCustomEditor(); DrawCustomFooter(); EndInspectorStyle(); } else { base.DrawDefaultInspector(); } SaveChanges(); } catch (Exception e) { DrawError(e.ToString()); } CustomEditorActive = false; } public void OnSceneGUI() { recordingUndo = false; DrawCustomSceneGUI(); } protected virtual void BeginInspectorStyle() { // Empty by default } protected virtual void EndInspectorStyle() { // Empty by default } protected virtual bool DisplayHeader { get { return true; } } /// <summary> /// Draws buttons for turning custom editors on/off, as well as DocType, Tutorial and UseWith attributes /// </summary> private void DrawInspectorHeader() { if (!DisplayHeader) { return; } EditorGUILayout.Space(); GUILayout.BeginHorizontal(); if (GUILayout.Button(ShowCustomEditors ? "Toggle Custom Editors (ON)" : "Toggle Custom Editors (OFF)", ShowCustomEditors ? toggleButtonOnStyle : toggleButtonOffStyle)) { ShowCustomEditors = !ShowCustomEditors; } if (ShowCustomEditors) { if (GUILayout.Button(showHelp ? "Toggle Help (ON)" : "Toggle Help (OFF)", showHelp ? toggleButtonOnStyle : toggleButtonOffStyle)) { showHelp = !showHelp; } if (GUILayout.Button("Expand Sections", toggleButtonOffStyle)) { Type targetType = target.GetType(); foreach (MemberInfo member in targetType.GetMembers(defaultBindingFlags)) { if (member.IsDefined(typeof(HeaderAttribute), true)) { HeaderAttribute h = member.GetCustomAttributes(typeof(HeaderAttribute), true)[0] as HeaderAttribute; string lookupName = targetType.Name + h.header; if (!displayedSections.ContainsKey(lookupName)) { displayedSections.Add(lookupName, true); } else { displayedSections[lookupName] = true; } } } } if (GUILayout.Button("Collapse Sections", toggleButtonOffStyle)) { Type targetType = target.GetType(); foreach (MemberInfo member in targetType.GetMembers(defaultBindingFlags)) { if (member.IsDefined(typeof(HeaderAttribute), true)) { HeaderAttribute h = member.GetCustomAttributes(typeof(HeaderAttribute), true)[0] as HeaderAttribute; string lookupName = targetType.Name + h.header; if (!displayedSections.ContainsKey(lookupName)) { displayedSections.Add(lookupName, false); } else { displayedSections[lookupName] = false; } } } } } GUILayout.EndHorizontal(); if (ShowCustomEditors) { GUI.color = defaultColor; GUILayout.BeginVertical(); GUILayout.BeginHorizontal(); Type targetType = target.GetType(); foreach (DocLinkAttribute attribute in targetType.GetCustomAttributes(typeof(DocLinkAttribute), true)) { string description = attribute.Description; if (string.IsNullOrEmpty(description)) { description = "Click for documentation about " + targetType.Name; } if (GUILayout.Button(description, EditorStyles.toolbarButton)) { Application.OpenURL(attribute.DocURL); } } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); foreach (TutorialAttribute attribute in targetType.GetCustomAttributes(typeof(TutorialAttribute), true)) { string description = attribute.Description; if (string.IsNullOrEmpty(description)) { description = "Click for a tutorial on " + targetType.Name; } if (GUILayout.Button(description, EditorStyles.toolbarButton)) { Application.OpenURL(attribute.TutorialURL); } } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); List<Type> missingTypes = new List<Type>(); foreach (UseWithAttribute attribute in targetType.GetCustomAttributes(typeof(UseWithAttribute), true)) { Component targetGo = (Component)target; if (targetGo == null) { break; } foreach (Type type in attribute.UseWithTypes) { Component c = targetGo.GetComponent(type); if (c == null) { missingTypes.Add(type); } } } if (missingTypes.Count > 0) { string warningMessage = "This class is designed to be accompanied by scripts of (or inheriting from) types: \n"; for (int i = 0; i < missingTypes.Count; i++) { warningMessage += " - " + missingTypes[i].FullName; if (i < missingTypes.Count - 1) warningMessage += "\n"; } warningMessage += "\nIt may not function correctly without them."; DrawWarning(warningMessage); } GUILayout.EndHorizontal(); GUILayout.EndVertical(); } EditorGUILayout.Space(); } /// <summary> /// Draws main editor /// </summary> protected void DrawCustomEditor() { EditorGUILayout.BeginVertical(); Type targetType = target.GetType(); // Get all the members of this type, public and private List<MemberInfo> members = new List<MemberInfo>(targetType.GetMembers(defaultBindingFlags)); // Start drawing the editor int currentIndentLevel = 0; bool insideSectionBlock = false; bool drawCurrentSection = true; foreach (MemberInfo member in members) { try { switch (member.MemberType) { default: // We only want to draw fields, properties and arrays continue; case MemberTypes.Field: case MemberTypes.Property: break; } // First get header and indent settings if (member.IsDefined(typeof(HeaderAttribute), true)) { HeaderAttribute header = member.GetCustomAttributes(typeof(HeaderAttribute), true)[0] as HeaderAttribute; if (insideSectionBlock) { DrawSectionEnd(); } insideSectionBlock = true; drawCurrentSection = DrawSectionStart(target.GetType().Name, header.header); } // Then do basic show / hide based on ShowIfAttribute if ((insideSectionBlock && !drawCurrentSection) || !ShouldDrawMember(member, targetType, target)) { continue; } // Handle drawing stuff (indent, help) if (showHelp) { DrawToolTip(member); } if (member.IsDefined(typeof(SetIndentAttribute), true)) { SetIndentAttribute indent = member.GetCustomAttributes(typeof(SetIndentAttribute), true)[0] as SetIndentAttribute; currentIndentLevel += indent.Indent; EditorGUI.indentLevel = currentIndentLevel; } if (member.IsDefined(typeof(FeatureInProgressAttribute), true)) { GUI.color = helpBoxColor; EditorGUILayout.LabelField("(This field or property is marked as 'In Progress' and may have no effect.)", inProgressStyle); GUI.color = Color.Lerp(disabledColor, Color.clear, 0.5f); } else { GUI.color = defaultColor; } // Now get down to drawing the thing // Get an array ready for our override attributes object[] drawOverrideAttributes = null; if (member.MemberType == MemberTypes.Field) { FieldInfo field = targetType.GetField(member.Name, defaultBindingFlags); if (!field.IsPrivate || field.IsDefined(typeof(SerializeField), true)) { // If it's a profile field, take care of that first if (IsSubclassOf(field.FieldType, typeof(ProfileBase))) { UnityEngine.Object profile = (UnityEngine.Object)field.GetValue(target); profile = DrawProfileField(target, profile, field.FieldType); field.SetValue(target, profile); } else { drawOverrideAttributes = field.GetCustomAttributes(typeof(DrawOverrideAttribute), true); // If we fine overrides, draw using those if (drawOverrideAttributes.Length > 0) { if (drawOverrideAttributes.Length > 1) DrawWarning("You should only use one DrawOverride attribute per member. Drawing " + drawOverrideAttributes[0].GetType().Name + " only."); (drawOverrideAttributes[0] as DrawOverrideAttribute).DrawEditor(target, field, serializedObject.FindProperty(field.Name)); } else { // Otherwise just draw the default editor DrawSerializedField(serializedObject, field.Name); } } } } else // Property { // We have to draw properties manually PropertyInfo prop = targetType.GetProperty(member.Name, defaultBindingFlags); drawOverrideAttributes = prop.GetCustomAttributes(typeof(DrawOverrideAttribute), true); // If it's a profile field, take care of that first if (IsSubclassOf(prop.PropertyType, typeof(ProfileBase))) { UnityEngine.Object profile = (UnityEngine.Object)prop.GetValue(target, null); profile = DrawProfileField(target, profile, prop.PropertyType); prop.SetValue(target, profile, null); } // If we find overrides, draw using those else if (drawOverrideAttributes.Length > 0) { if (drawOverrideAttributes.Length > 1) { DrawWarning("You should only use one DrawOverride attribute per member. Drawing " + drawOverrideAttributes[0].GetType().Name + " only."); } (drawOverrideAttributes[0] as DrawOverrideAttribute).DrawEditor(target, prop); } } } catch (Exception e) { DrawWarning("There was a problem drawing the member " + member.Name + ":"); DrawError(System.Environment.NewLine + e.ToString()); } } if (insideSectionBlock) { DrawSectionEnd(); } EditorGUILayout.EndVertical(); } /// <summary> /// Override this to draw a scene editor /// </summary> protected virtual void DrawCustomSceneGUI() { } /// <summary> /// override this if you want to draw a footer at the bottom of your editor /// Typically used for validation and error / warning messages that are too complex for Validate attributes /// </summary> protected virtual void DrawCustomFooter() { //... } /// <summary> /// Ensures changes are saved once editor is finished /// </summary> protected void SaveChanges() { serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(target); } #region drawing /// <summary> /// Determines whether this member should be shown in the editor /// </summary> /// <param name="member"></param> /// <param name="targetType"></param> /// <param name="target"></param> /// <returns></returns> private bool ShouldDrawMember(MemberInfo member, Type targetType, object target) { object[] hideAttributes = member.GetCustomAttributes(typeof(HideInInspector), true); if (hideAttributes != null && hideAttributes.Length > 0) return false; bool shouldBeVisible = true; switch (member.MemberType) { case MemberTypes.Field: // Fields are visible by default unless they're hidden by a ShowIfAttribute foreach (ShowIfAttribute attribute in member.GetCustomAttributes(typeof(ShowIfAttribute), true)) { if (!attribute.ShouldShow(target)) { shouldBeVisible = false; break; } } break; case MemberTypes.Property: // Property types require at least one Attribute to be visible if (member.GetCustomAttributes(typeof(Attribute), true).Length == 0) { shouldBeVisible = false; } else { // Even if they have an editor attribute, they can still be hidden by a ShowIfAttribute foreach (ShowIfAttribute attribute in member.GetCustomAttributes(typeof(ShowIfAttribute), true)) { if (!attribute.ShouldShow(target)) { shouldBeVisible = false; break; } } } break; default: break; } return shouldBeVisible; } /// <summary> /// Draws default unity serialized field /// </summary> /// <param name="serializedObject"></param> /// <param name="propertyPath"></param> protected void DrawSerializedField(SerializedObject serializedObject, string propertyPath) { SerializedProperty prop = serializedObject.FindProperty(propertyPath); if (prop != null) { EditorGUILayout.PropertyField(prop, true); } } /// <summary> /// Draws a section start (initiated by the Header attribute) /// </summary> /// <param name="targetName"></param> /// <param name="headerName"></param> /// <param name="toUpper"></param> /// <param name="drawInitially"></param> /// <returns></returns> public static bool DrawSectionStart(string targetName, string headerName, bool toUpper = true, bool drawInitially = true) { string lookupName = targetName + headerName; if (!displayedSections.ContainsKey(lookupName)) displayedSections.Add(lookupName, drawInitially); bool drawSection = displayedSections[lookupName]; EditorGUILayout.Space(); Color tColor = GUI.color; GUI.color = sectionColor; if (toUpper) { headerName = headerName.ToUpper(); } drawSection = EditorGUILayout.Foldout(drawSection, headerName, true, sectionStyle); displayedSections[lookupName] = drawSection; EditorGUILayout.BeginVertical(); GUI.color = tColor; indentOnSectionStart = EditorGUI.indentLevel; EditorGUI.indentLevel = 0;// indentOnSectionStart + 1; return drawSection; } /// <summary> /// Draws section end (initiated by next Header attribute) /// </summary> public static void DrawSectionEnd() { EditorGUILayout.EndVertical(); EditorGUI.indentLevel = indentOnSectionStart; } /// <summary> /// Draws a tooltip as text in the editor /// </summary> /// <param name="member"></param> public static void DrawToolTip(MemberInfo member) { if (member.IsDefined(typeof(TooltipAttribute), true)) { TooltipAttribute tooltip = member.GetCustomAttributes(typeof(TooltipAttribute), true)[0] as TooltipAttribute; Color prevColor = GUI.color; GUI.color = helpBoxColor; EditorGUI.indentLevel--; EditorGUILayout.LabelField(tooltip.tooltip, toolTipStyle); EditorGUI.indentLevel++; GUI.color = prevColor; } } public static void DrawWarning(string warning) { Color prevColor = GUI.color; GUI.color = warningColor; EditorGUILayout.BeginVertical(EditorStyles.textArea); EditorGUILayout.LabelField(warning, EditorStyles.wordWrappedMiniLabel); EditorGUILayout.EndVertical(); GUI.color = prevColor; } public static void DrawError(string error) { Color prevColor = GUI.color; GUI.color = errorColor; EditorGUILayout.BeginVertical(EditorStyles.textArea); EditorGUILayout.LabelField(error, EditorStyles.wordWrappedMiniLabel); EditorGUILayout.EndVertical(); GUI.color = prevColor; } public static void DrawDivider() { GUIStyle styleHR = new GUIStyle(GUI.skin.box); styleHR.stretchWidth = true; styleHR.fixedHeight = 2; GUILayout.Box("", styleHR); } private void CreateStyles() { if (toggleButtonOffStyle == null) { toggleButtonOffStyle = "ToolbarButton"; toggleButtonOffStyle.fontSize = 9; toggleButtonOnStyle = new GUIStyle(toggleButtonOffStyle); toggleButtonOnStyle.normal.background = toggleButtonOnStyle.active.background; sectionStyle = new GUIStyle(EditorStyles.foldout); sectionStyle.fontStyle = FontStyle.Bold; toolTipStyle = new GUIStyle(EditorStyles.wordWrappedMiniLabel); toolTipStyle.fontStyle = FontStyle.Normal; toolTipStyle.alignment = TextAnchor.LowerLeft; inProgressStyle = new GUIStyle(EditorStyles.wordWrappedMiniLabel); inProgressStyle.fontStyle = FontStyle.Italic; inProgressStyle.alignment = TextAnchor.LowerLeft; } } #endregion #region profiles /// <summary> /// Draws a field for scriptable object profiles /// Profiles are scriptable objects that contain shared information /// If base class is abstract, includes a button for creating a profile of each type that inherits from base class T /// Otherwise just includes button for creating a profile of type /// Also finds and draws the inspector for the profile /// </summary> /// <param name="target"></param> /// <param name="profile"></param> /// <param name="profileType"></param> /// <returns></returns> private static UnityEngine.Object DrawProfileField(UnityEngine.Object target, UnityEngine.Object profile, Type profileType) { Color prevColor = GUI.color; GUI.color = profileColor; EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUI.color = Color.Lerp(Color.white, Color.gray, 0.25f); EditorGUILayout.LabelField("Select a " + profileType.Name + " or create a new profile", EditorStyles.miniBoldLabel); UnityEngine.Object newProfile = profile; EditorGUILayout.BeginHorizontal(); newProfile = EditorGUILayout.ObjectField(profile, profileType, false); // is this an abstract class? if (profileType.IsAbstract) { EditorGUILayout.BeginVertical(); List<Type> types = GetDerivedTypes(profileType, Assembly.GetAssembly(profileType)); EditorGUILayout.BeginHorizontal(); foreach (Type derivedType in types) { if (GUILayout.Button("Create " + derivedType.Name)) { profile = CreateProfile(derivedType); } } if (GUILayout.Button("What's a profile?")) { LaunchProfileHelp(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } else { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Create Profile")) { profile = CreateProfile(profileType); } if (GUILayout.Button("What's a profile?")) { LaunchProfileHelp(); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndHorizontal(); if (profile == null) { DrawError("You must choose a profile."); } else { EditorGUI.indentLevel++; // Draw the editor for this profile // Set it to false initially if (DrawSectionStart(target.GetType().Name, profile.name + " (Click to edit)", false, false)) { // Draw the profile inspector Editor inspector = Editor.CreateEditor(profile); ProfileInspector profileInspector = (ProfileInspector)inspector; if (profileInspector != null) { profileInspector.targetComponent = target as Component; } inspector.OnInspectorGUI(); } DrawSectionEnd(); EditorGUI.indentLevel--; } EditorGUILayout.EndVertical(); GUI.color = prevColor; return newProfile; } /// <summary> /// Displays a help window explaining profile objects /// </summary> private static void LaunchProfileHelp() { EditorUtility.DisplayDialog( "Profiles Help", "Profiles are assets that contain a set of common settings like colors or sound files." + "\n\nThose settings can be shared and used by any objects that keep a reference to the profile." + "\n\nThey make changing the style of a set of objects quicker and easier, and they reduce memory usage." + "\n\nA purple icon indicates that you're looking at a profile asset." + "\n\nFor more information please see the documentation at:" + "\n https://github.com/Microsoft/MRDesignLabs_Unity/" , "OK"); } /// <summary> /// Creates a new instance of the profile object /// </summary> /// <param name="profileType"></param> /// <returns></returns> private static UnityEngine.Object CreateProfile(Type profileType) { UnityEngine.Object asset = ScriptableObject.CreateInstance(profileType); if (asset != null) { AssetDatabase.CreateAsset(asset, "Assets/New" + profileType.Name + ".asset"); AssetDatabase.SaveAssets(); } else { Debug.LogError("Couldn't create profile of type " + profileType.Name); } return asset; } private static List<Type> GetDerivedTypes(Type baseType, Assembly assembly) { Type[] types = assembly.GetTypes(); List<Type> derivedTypes = new List<Type>(); for (int i = 0, count = types.Length; i < count; i++) { Type type = types[i]; if (IsSubclassOf(type, baseType)) { derivedTypes.Add(type); } } return derivedTypes; } private static bool IsSubclassOf(Type type, Type baseType) { if (type == null || baseType == null || type == baseType) return false; if (baseType.IsGenericType == false) { if (type.IsGenericType == false) { return type.IsSubclassOf(baseType); } } else { baseType = baseType.GetGenericTypeDefinition(); } type = type.BaseType; Type objectType = typeof(object); while (type != objectType && type != null) { Type curentType = type.IsGenericType ? type.GetGenericTypeDefinition() : type; if (curentType == baseType) { return true; } type = type.BaseType; } return false; } #endregion #region handles // These are a set of handles for OnSceneGUI. They automatically register Undo for the target object and they have a consistent look. protected float AxisMoveHandle(Vector3 origin, Vector3 direction, float distance, float handleSize = 0.2f, bool autoSize = true) { Vector3 position = origin + (direction.normalized * distance); Handles.color = handleColorAxis; if (autoSize) { handleSize = Mathf.Lerp(handleSize, HandleUtility.GetHandleSize(position) * handleSize, 0.75f); } Handles.DrawDottedLine(origin, position, DottedLineScreenSpace); Handles.ArrowHandleCap(0, position, Quaternion.LookRotation(direction), handleSize * 2, EventType.Repaint); Vector3 newPosition = Handles.FreeMoveHandle(position, Quaternion.identity, handleSize, Vector3.zero, Handles.CircleHandleCap); if (recordingUndo) return distance; float newDistance = Vector3.Distance(origin, newPosition); if (distance != newDistance) { recordingUndo = true; Undo.RegisterCompleteObjectUndo(target, target.name); distance = newDistance; } return distance; } protected Vector3 CircleMoveHandle(Vector3 position, float handleSize = 0.2f, bool autoSize = true, float xScale = 1f, float yScale = 1f, float zScale = 1f) { Handles.color = handleColorCircle; if (autoSize) { handleSize = Mathf.Lerp(handleSize, HandleUtility.GetHandleSize(position) * handleSize, 0.75f); } Vector3 newPosition = Handles.FreeMoveHandle(position, Quaternion.identity, handleSize, Vector3.zero, Handles.CircleHandleCap); if (recordingUndo) return position; if (position != newPosition) { recordingUndo = true; Undo.RegisterCompleteObjectUndo(target, target.name); position.x = Mathf.Lerp(position.x, newPosition.x, Mathf.Clamp01(xScale)); position.y = Mathf.Lerp(position.z, newPosition.y, Mathf.Clamp01(yScale)); position.z = Mathf.Lerp(position.y, newPosition.z, Mathf.Clamp01(zScale)); } return position; } protected Vector3 SquareMoveHandle(Vector3 position, float handleSize = 0.2f, bool autoSize = true, float xScale = 1f, float yScale = 1f, float zScale = 1f) { Handles.color = handleColorSquare; if (autoSize) { handleSize = Mathf.Lerp(handleSize, HandleUtility.GetHandleSize(position) * handleSize, 0.75f); } // Multiply square handle to match other types Vector3 newPosition = Handles.FreeMoveHandle(position, Quaternion.identity, handleSize * 0.8f, Vector3.zero, Handles.RectangleHandleCap); if (recordingUndo) return position; if (position != newPosition) { recordingUndo = true; Undo.RegisterCompleteObjectUndo(target, target.name); position.x = Mathf.Lerp(position.x, newPosition.x, Mathf.Clamp01(xScale)); position.y = Mathf.Lerp(position.z, newPosition.y, Mathf.Clamp01(yScale)); position.z = Mathf.Lerp(position.y, newPosition.z, Mathf.Clamp01(zScale)); } return position; } protected Vector3 SphereMoveHandle(Vector3 position, float handleSize = 0.2f, bool autoSize = true, float xScale = 1f, float yScale = 1f, float zScale = 1f) { Handles.color = handleColorSphere; if (autoSize) { handleSize = Mathf.Lerp(handleSize, HandleUtility.GetHandleSize(position) * handleSize, 0.75f); } // Multiply sphere handle size to match other types Vector3 newPosition = Handles.FreeMoveHandle(position, Quaternion.identity, handleSize * 2, Vector3.zero, Handles.SphereHandleCap); if (recordingUndo) return position; if (position != newPosition) { recordingUndo = true; Undo.RegisterCompleteObjectUndo(target, target.name); position.x = Mathf.Lerp(position.x, newPosition.x, Mathf.Clamp01(xScale)); position.y = Mathf.Lerp(position.z, newPosition.y, Mathf.Clamp01(yScale)); position.z = Mathf.Lerp(position.y, newPosition.z, Mathf.Clamp01(zScale)); } return position; } protected Vector3 VectorHandle(Vector3 origin, Vector3 vector, bool normalize = true, float handleLength = 1f, bool clamp = true, float handleSize = 0.1f, bool autoSize = true) { Handles.color = handleColorTangent; if (autoSize) { handleSize = Mathf.Lerp(handleSize, HandleUtility.GetHandleSize(origin) * handleSize, 0.75f); } Vector3 handlePosition = origin + (vector * handleLength); float distanceToOrigin = Vector3.Distance(origin, handlePosition) / handleLength; if (normalize) { vector.Normalize(); } else { // If the handle isn't normalized, brighten based on distance to origin Handles.color = Color.Lerp(Color.gray, handleColorTangent, distanceToOrigin * 0.85f); if (clamp) { // To indicate that we're at the clamped limit, make the handle 'pop' slightly larger if (distanceToOrigin >= 0.98f) { Handles.color = Color.Lerp(handleColorTangent, Color.white, 0.5f); handleSize *= 1.5f; } } } // Draw a line from origin to origin + direction Handles.DrawLine(origin, handlePosition); Quaternion rotation = Quaternion.identity; if (vector != Vector3.zero) { rotation = Quaternion.LookRotation(vector); } Vector3 newPosition = Handles.FreeMoveHandle(handlePosition, rotation, handleSize, Vector3.zero, Handles.DotHandleCap); if (recordingUndo) { return vector; } if (handlePosition != newPosition) { recordingUndo = true; Undo.RegisterCompleteObjectUndo(target, target.name); vector = (newPosition - origin).normalized; // If we normalize, we're done // Otherwise, multiply the vector by the distance between origin and target if (!normalize) { distanceToOrigin = Vector3.Distance(origin, newPosition) / handleLength; if (clamp) { distanceToOrigin = Mathf.Clamp01(distanceToOrigin); } vector *= distanceToOrigin; } } return vector; } protected Quaternion RotationHandle(Vector3 position, Quaternion rotation, float handleSize = 0.2f, bool autoSize = true) { Handles.color = handleColorRotation; if (autoSize) { handleSize = Mathf.Lerp(handleSize, HandleUtility.GetHandleSize(position) * handleSize, 0.75f); } // Make rotation handles larger so they can overlay movement handles Quaternion newRotation = Handles.FreeRotateHandle(rotation, position, handleSize * 2); if (recordingUndo) { return newRotation; } Handles.color = Handles.zAxisColor; Handles.ArrowHandleCap(0, position, Quaternion.LookRotation(newRotation * Vector3.forward), handleSize * 2, EventType.Repaint); Handles.color = Handles.xAxisColor; Handles.ArrowHandleCap(0, position, Quaternion.LookRotation(newRotation * Vector3.right), handleSize * 2, EventType.Repaint); Handles.color = Handles.yAxisColor; Handles.ArrowHandleCap(0, position, Quaternion.LookRotation(newRotation * Vector3.up), handleSize * 2, EventType.Repaint); if (rotation != newRotation) { recordingUndo = true; Undo.RegisterCompleteObjectUndo(target, target.name); rotation = newRotation; } return rotation; } #endregion #region editor prefs private static void SetEditorPref(string key, bool value) { EditorPrefs.SetBool(Application.productName + key, value); } private static bool GetEditorPref(string key, bool defaultValue) { if (EditorPrefs.HasKey(Application.productName + key)) { return EditorPrefs.GetBool(Application.productName + key); } EditorPrefs.SetBool(Application.productName + key, defaultValue); return defaultValue; } #endregion } #endif }