diff --git a/Assets/Scripts/Docs/Graphics/Graphics_Blit.cs b/Assets/Scripts/Docs/Graphics/Graphics_Blit.cs index 6760898..805f4ba 100644 --- a/Assets/Scripts/Docs/Graphics/Graphics_Blit.cs +++ b/Assets/Scripts/Docs/Graphics/Graphics_Blit.cs @@ -26,7 +26,7 @@ void OnPostRender() { // Copies source texture into destination render texture with a shader // Destination RenderTexture is null to blit directly to screen - Graphics.Blit(displayTexture, null, mat); + Graphics.Blit(displayTexture, null as RenderTexture, mat); } } } \ No newline at end of file diff --git a/Assets/Scripts/Editor/ContextMenu/BoxColliderFitChildren.cs b/Assets/Scripts/Editor/ContextMenu/BoxColliderFitChildren.cs index 58f7b0d..ce3f298 100644 --- a/Assets/Scripts/Editor/ContextMenu/BoxColliderFitChildren.cs +++ b/Assets/Scripts/Editor/ContextMenu/BoxColliderFitChildren.cs @@ -1,7 +1,3 @@ -// Adjust Box Collider to fit child meshes inside -// Usage: You have empty parent transform, with child meshes inside, add box collider to parent then use this -// NOTE: Doesnt work if root transform is rotated - using UnityEngine; using UnityEditor; @@ -10,37 +6,66 @@ namespace UnityLibrary public class BoxColliderFitChildren : MonoBehaviour { [MenuItem("CONTEXT/BoxCollider/Fit to Children")] - static void FixSize(MenuCommand command) + static void FitColliderToChildren(MenuCommand command) { BoxCollider col = (BoxCollider)command.context; - // record undo + // Record undo Undo.RecordObject(col.transform, "Fit Box Collider To Children"); - // get child mesh bounds - var b = GetRecursiveMeshBounds(col.gameObject); + // Get world-space bounds of all child meshes + var worldBounds = GetRecursiveMeshBounds(col.gameObject); + + if (worldBounds.size == Vector3.zero) + { + Debug.LogWarning("No valid meshes found to fit the BoxCollider."); + return; + } + + // Convert world-space center to local space + Vector3 localCenter = col.transform.InverseTransformPoint(worldBounds.center); + + // Convert world-space size to local space + Vector3 localSize = col.transform.InverseTransformVector(worldBounds.size); + + // Ensure size is positive + localSize = new Vector3(Mathf.Abs(localSize.x), Mathf.Abs(localSize.y), Mathf.Abs(localSize.z)); + + // Fix potential center flipping + if (Vector3.Dot(col.transform.right, Vector3.right) < 0) + { + localCenter.x = -localCenter.x; + } + if (Vector3.Dot(col.transform.up, Vector3.up) < 0) + { + localCenter.y = -localCenter.y; + } + if (Vector3.Dot(col.transform.forward, Vector3.forward) < 0) + { + localCenter.z = -localCenter.z; + } - // set collider local center and size - col.center = col.transform.root.InverseTransformVector(b.center) - col.transform.position; - col.size = b.size; + // Apply to collider + col.center = localCenter; + col.size = localSize; } public static Bounds GetRecursiveMeshBounds(GameObject go) { - var r = go.GetComponentsInChildren(); - if (r.Length > 0) - { - var b = r[0].bounds; - for (int i = 1; i < r.Length; i++) - { - b.Encapsulate(r[i].bounds); - } - return b; - } - else // TODO no renderers + Renderer[] renderers = go.GetComponentsInChildren(); + + if (renderers.Length == 0) + return new Bounds(); + + // Start with the first renderer’s bounds in world space + Bounds worldBounds = renderers[0].bounds; + + for (int i = 1; i < renderers.Length; i++) { - return new Bounds(Vector3.one, Vector3.one); + worldBounds.Encapsulate(renderers[i].bounds); } + + return worldBounds; } } } diff --git a/Assets/Scripts/Editor/ContextMenu/LineRendererToLocalSpace.cs b/Assets/Scripts/Editor/ContextMenu/LineRendererToLocalSpace.cs new file mode 100644 index 0000000..6cc6cc7 --- /dev/null +++ b/Assets/Scripts/Editor/ContextMenu/LineRendererToLocalSpace.cs @@ -0,0 +1,60 @@ +// converts line renderer points from world space to local space + +using UnityEngine; +using UnityEditor; +using UnityEditor.SceneManagement; + +namespace UnityLibrary.ContextMenu +{ + public static class LineRendererToLocalSpace + { + private const string MenuPath = "CONTEXT/LineRenderer/Convert Points To Local Space"; + + [MenuItem(MenuPath, true)] + private static bool Validate(MenuCommand command) + { + return command != null && command.context is LineRenderer; + } + + [MenuItem(MenuPath)] + private static void Convert(MenuCommand command) + { + var lr = (LineRenderer)command.context; + if (lr == null) return; + + int count = lr.positionCount; + if (count == 0) return; + + Transform t = lr.transform; + + Undo.RecordObject(lr, "Convert LineRenderer To Local Space"); + + // Get current positions in world space no matter what mode it's in. + Vector3[] world = new Vector3[count]; + if (lr.useWorldSpace) + { + lr.GetPositions(world); + } + else + { + Vector3[] local = new Vector3[count]; + lr.GetPositions(local); + for (int i = 0; i < count; i++) + world[i] = t.TransformPoint(local[i]); + } + + // Convert world -> local, switch mode, write back. + Vector3[] newLocal = new Vector3[count]; + for (int i = 0; i < count; i++) + newLocal[i] = t.InverseTransformPoint(world[i]); + + lr.useWorldSpace = false; + lr.SetPositions(newLocal); + + EditorUtility.SetDirty(lr); + + if (!Application.isPlaying) + EditorSceneManager.MarkSceneDirty(lr.gameObject.scene); + } + } +} diff --git a/Assets/Scripts/Editor/ContextMenu/LineRendererToWorldSpace.cs b/Assets/Scripts/Editor/ContextMenu/LineRendererToWorldSpace.cs new file mode 100644 index 0000000..90cc2d2 --- /dev/null +++ b/Assets/Scripts/Editor/ContextMenu/LineRendererToWorldSpace.cs @@ -0,0 +1,54 @@ +// converts LineRenderer points from local space to world space via context menu in Unity Editor + +using UnityEngine; +using UnityEditor; +using UnityEditor.SceneManagement; + +namespace UnityLibrary.ContextMenu +{ + public static class LineRendererToWorldSpace + { + private const string MenuPath = "CONTEXT/LineRenderer/Convert Points To World Space"; + + [MenuItem(MenuPath, true)] + private static bool Validate(MenuCommand command) + { + return command != null && command.context is LineRenderer; + } + + [MenuItem(MenuPath)] + private static void Convert(MenuCommand command) + { + var lr = (LineRenderer)command.context; + if (lr == null) return; + + if (lr.useWorldSpace) + { + Debug.Log("LineRenderer is already using World Space."); + return; + } + + int count = lr.positionCount; + if (count == 0) return; + + Transform t = lr.transform; + + Undo.RecordObject(lr, "Convert LineRenderer To World Space"); + + Vector3[] local = new Vector3[count]; + lr.GetPositions(local); + + Vector3[] world = new Vector3[count]; + for (int i = 0; i < count; i++) + world[i] = t.TransformPoint(local[i]); + + lr.useWorldSpace = true; + lr.SetPositions(world); + + EditorUtility.SetDirty(lr); + + if (!Application.isPlaying) + EditorSceneManager.MarkSceneDirty(lr.gameObject.scene); + } + } +} diff --git a/Assets/Scripts/Editor/CustomInspector/CustomRectTransformCopyInspector.cs b/Assets/Scripts/Editor/CustomInspector/CustomRectTransformCopyInspector.cs new file mode 100644 index 0000000..0a5de2b --- /dev/null +++ b/Assets/Scripts/Editor/CustomInspector/CustomRectTransformCopyInspector.cs @@ -0,0 +1,114 @@ +#if UNITY_EDITOR +using System; +using System.Reflection; +using UnityEngine; + +namespace UnityEditor +{ + [CustomEditor(typeof(RectTransform), true)] + [CanEditMultipleObjects] + public class CustomRectTransformCopyInspector : Editor + { + // Unity's built-in editor + Editor defaultEditor = null; + RectTransform rectTransform; + + private static RectTransformData copiedData; + + void OnEnable() + { + // Use reflection to get the default Unity RectTransform editor + defaultEditor = Editor.CreateEditor(targets, Type.GetType("UnityEditor.RectTransformEditor, UnityEditor")); + rectTransform = target as RectTransform; + } + + void OnDisable() + { + // Destroy the default editor to avoid memory leaks + if (defaultEditor != null) + { + MethodInfo disableMethod = defaultEditor.GetType().GetMethod("OnDisable", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + if (disableMethod != null) + disableMethod.Invoke(defaultEditor, null); + + DestroyImmediate(defaultEditor); + } + } + + public override void OnInspectorGUI() + { + // Draw Unity's default RectTransform Inspector + defaultEditor.OnInspectorGUI(); + + // Add Copy and Paste buttons + EditorGUILayout.Space(); + GUILayout.BeginHorizontal(); + + if (GUILayout.Button("C", GUILayout.Width(30))) // Copy + { + CopyRectTransform(rectTransform); + } + + if (GUILayout.Button("P", GUILayout.Width(30))) // Paste + { + PasteRectTransform(rectTransform); + } + + GUILayout.EndHorizontal(); + } + + private void CopyRectTransform(RectTransform rectTransform) + { + copiedData = new RectTransformData(rectTransform); + Debug.Log("RectTransform copied!"); + } + + private void PasteRectTransform(RectTransform rectTransform) + { + if (copiedData == null) + { + Debug.LogWarning("No RectTransform data to paste!"); + return; + } + + Undo.RecordObject(rectTransform, "Paste RectTransform"); + + copiedData.ApplyTo(rectTransform); + Debug.Log("RectTransform pasted!"); + + EditorUtility.SetDirty(rectTransform); + } + + private class RectTransformData + { + public Vector2 anchorMin; + public Vector2 anchorMax; + public Vector2 anchoredPosition; + public Vector2 sizeDelta; + public Vector2 pivot; + public Quaternion rotation; + + public RectTransformData(RectTransform rectTransform) + { + anchorMin = rectTransform.anchorMin; + anchorMax = rectTransform.anchorMax; + anchoredPosition = rectTransform.anchoredPosition; + sizeDelta = rectTransform.sizeDelta; + pivot = rectTransform.pivot; + rotation = rectTransform.rotation; + } + + public void ApplyTo(RectTransform rectTransform) + { + rectTransform.anchorMin = anchorMin; + rectTransform.anchorMax = anchorMax; + rectTransform.anchoredPosition = anchoredPosition; + rectTransform.sizeDelta = sizeDelta; + rectTransform.pivot = pivot; + rectTransform.rotation = rotation; + } + } + } +} +#endif diff --git a/Assets/Scripts/Editor/CustomInspector/CustomRectTransformInspector.cs b/Assets/Scripts/Editor/CustomInspector/CustomRectTransformInspector.cs new file mode 100644 index 0000000..02c51af --- /dev/null +++ b/Assets/Scripts/Editor/CustomInspector/CustomRectTransformInspector.cs @@ -0,0 +1,294 @@ +// source https://gist.github.com/GieziJo/f80bcb24c4caa68ebfb204148ccd4b18 +// =============================== +// AUTHOR : J. Giezendanner +// CREATE DATE : 12.03.2020 +// MODIFIED DATE : +// PURPOSE : Adds helper functions to the RectTransform to align the rect to the anchors and vise-versa +// SPECIAL NOTES : Sources for certain informations: +// Display anchors gizmos: +// https://forum.unity.com/threads/recttransform-custom-editor-ontop-of-unity-recttransform-custom-editor.455925/ +// Draw default inspector: +// https://forum.unity.com/threads/extending-instead-of-replacing-built-in-inspectors.407612/ +// =============================== +// Change History: +//================================== + +#if UNITY_EDITOR +using System; +using System.Reflection; +using UnityEditor.SceneManagement; +using UnityEngine; + + +namespace UnityEditor +{ + [CustomEditor(typeof(RectTransform), true)] + [CanEditMultipleObjects] + public class CustomRectTransformInspector : Editor + { + //Unity's built-in editor + Editor defaultEditor = null; + RectTransform rectTransform; + + bool rect2Anchors_foldout = false; + bool anchors2Rect_foldout = false; + bool rect2Anchors__previousState = false; + bool anchors2Rect_previousState = false; + + private bool playerPrefsChecked = false; + + void OnEnable() + { + //When this inspector is created, also create the built-in inspector + defaultEditor = Editor.CreateEditor(targets, Type.GetType("UnityEditor.RectTransformEditor, UnityEditor")); + rectTransform = target as RectTransform; + } + + void OnDisable() + { + //When OnDisable is called, the default editor we created should be destroyed to avoid memory leakage. + //Also, make sure to call any required methods like OnDisable + + if (defaultEditor != null) + { + MethodInfo disableMethod = defaultEditor.GetType().GetMethod("OnDisable", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + if (disableMethod != null) + disableMethod.Invoke(defaultEditor, null); + DestroyImmediate(defaultEditor); + } + } + + void checkPlayerPrefs() + { + rect2Anchors_foldout = PlayerPrefs.GetInt("giezi_tools_rect2Anchors_foldout_bool", 0) != 0; + anchors2Rect_foldout = PlayerPrefs.GetInt("giezi_tools_anchors2Rect_foldout_bool", 0) != 0; + + rect2Anchors__previousState = rect2Anchors_foldout; + anchors2Rect_previousState = anchors2Rect_foldout; + } + + + public override void OnInspectorGUI() + { + if (!playerPrefsChecked) + { + checkPlayerPrefs(); + playerPrefsChecked = true; + } + + defaultEditor.OnInspectorGUI(); + + + if (rectTransform.parent != null) + { + var centerButtonStyle = new GUIStyle(GUI.skin.button); + centerButtonStyle.fontStyle = FontStyle.Bold; + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Helper Functions", EditorStyles.boldLabel); + + rect2Anchors_foldout = EditorGUILayout.Foldout(rect2Anchors_foldout, "Set Rect to Anchors"); + + if (rect2Anchors_foldout) + { + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(); + if (GUILayout.Button("Top Left")) + setRectValue("topLeft"); + if (GUILayout.Button("Left")) + setRectValue("left"); + if (GUILayout.Button("Bottom Left")) + setRectValue("bottomLeft"); + GUILayout.EndVertical(); + GUILayout.BeginVertical(); + if (GUILayout.Button("Top")) + setRectValue("top"); + if (GUILayout.Button("All", centerButtonStyle)) + setRectValue("all"); + if (GUILayout.Button("Bottom")) + setRectValue("bottom"); + GUILayout.EndVertical(); + GUILayout.BeginVertical(); + if (GUILayout.Button("Top Right")) + setRectValue("topRight"); + if (GUILayout.Button("Right")) + setRectValue("right"); + if (GUILayout.Button("Bottom Right")) + setRectValue("bottomRight"); + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + + anchors2Rect_foldout = EditorGUILayout.Foldout(anchors2Rect_foldout, "Set Anchors to Rect"); + + if (anchors2Rect_foldout) + { + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(); + if (GUILayout.Button("Top Left")) + setAnchorsToRect("topLeft"); + if (GUILayout.Button("Left")) + setAnchorsToRect("left"); + if (GUILayout.Button("Bottom Left")) + setAnchorsToRect("bottomLeft"); + GUILayout.EndVertical(); + GUILayout.BeginVertical(); + if (GUILayout.Button("Top")) + setAnchorsToRect("top"); + if (GUILayout.Button("All", centerButtonStyle)) + setAnchorsToRect("all"); + if (GUILayout.Button("Bottom")) + setAnchorsToRect("bottom"); + GUILayout.EndVertical(); + GUILayout.BeginVertical(); + if (GUILayout.Button("Top Right")) + setAnchorsToRect("topRight"); + if (GUILayout.Button("Right")) + setAnchorsToRect("right"); + if (GUILayout.Button("Bottom Right")) + setAnchorsToRect("bottomRight"); + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + + + if (rect2Anchors_foldout != rect2Anchors__previousState) + { + rect2Anchors__previousState = rect2Anchors_foldout; + PlayerPrefs.SetInt("giezi_tools_rect2Anchors_foldout_bool", rect2Anchors_foldout ? 1 : 0); + } + + if (anchors2Rect_foldout != anchors2Rect_previousState) + { + anchors2Rect_previousState = anchors2Rect_foldout; + PlayerPrefs.SetInt("giezi_tools_anchors2Rect_foldout_bool", anchors2Rect_foldout ? 1 : 0); + } + } + } + + + private void OnSceneGUI() + { + MethodInfo onSceneGUI_Method = defaultEditor.GetType() + .GetMethod("OnSceneGUI", BindingFlags.NonPublic | BindingFlags.Instance); + onSceneGUI_Method.Invoke(defaultEditor, null); + } + + + private void setAnchorsToRect(string field) + { + Vector2 anchorMax = new Vector2(); + Vector2 anchorMin = new Vector2(); + var parent = rectTransform.parent; + anchorMin.x = rectTransform.offsetMin.x / parent.GetComponent().rect.size.x; + anchorMin.y = rectTransform.offsetMin.y / parent.GetComponent().rect.size.y; + anchorMax.x = rectTransform.offsetMax.x / parent.GetComponent().rect.size.x; + anchorMax.y = rectTransform.offsetMax.y / parent.GetComponent().rect.size.y; + + + switch (field) + { + case "topLeft": + anchorMax.x = 0; + rectTransform.anchorMax += anchorMax; + rectTransform.offsetMax = new Vector2(rectTransform.offsetMax.x, 0); + + anchorMin.y = 0; + rectTransform.anchorMin += anchorMin; + rectTransform.offsetMin = new Vector2(0, rectTransform.offsetMin.y); + break; + case "top": + anchorMax.x = 0; + rectTransform.anchorMax += anchorMax; + rectTransform.offsetMax = new Vector2(rectTransform.offsetMax.x, 0); + break; + case "topRight": + rectTransform.anchorMax += anchorMax; + rectTransform.offsetMax = Vector2.zero; + break; + case "bottomLeft": + rectTransform.anchorMin += anchorMin; + rectTransform.offsetMin = Vector2.zero; + break; + case "bottom": + anchorMin.x = 0; + rectTransform.anchorMin += anchorMin; + rectTransform.offsetMin = new Vector2(rectTransform.offsetMin.x, 0); + break; + case "bottomRight": + anchorMin.x = 0; + rectTransform.anchorMin += anchorMin; + rectTransform.offsetMin = new Vector2(rectTransform.offsetMin.x, 0); + anchorMax.y = 0; + rectTransform.anchorMax += anchorMax; + rectTransform.offsetMax = new Vector2(0, rectTransform.offsetMax.y); + break; + case "left": + anchorMin.y = 0; + rectTransform.anchorMin += anchorMin; + rectTransform.offsetMin = new Vector2(0, rectTransform.offsetMin.y); + break; + case "right": + anchorMax.y = 0; + rectTransform.anchorMax += anchorMax; + rectTransform.offsetMax = new Vector2(0, rectTransform.offsetMax.y); + break; + case "all": + rectTransform.anchorMax += anchorMax; + rectTransform.anchorMin += anchorMin; + rectTransform.offsetMin = Vector2.zero; + rectTransform.offsetMax = Vector2.zero; + break; + } + + handleChange(); + } + + + private void setRectValue(string field) + { + switch (field) + { + case "topLeft": + rectTransform.offsetMax = new Vector2(rectTransform.offsetMax.x, 0); + rectTransform.offsetMin = new Vector2(0, rectTransform.offsetMin.y); + break; + case "top": + rectTransform.offsetMax = new Vector2(rectTransform.offsetMax.x, 0); + break; + case "topRight": + rectTransform.offsetMax = Vector2.zero; + break; + case "bottomLeft": + rectTransform.offsetMin = Vector2.zero; + break; + case "bottom": + rectTransform.offsetMin = new Vector2(rectTransform.offsetMin.x, 0); + break; + case "bottomRight": + rectTransform.offsetMin = new Vector2(rectTransform.offsetMin.x, 0); + rectTransform.offsetMax = new Vector2(0, rectTransform.offsetMax.y); + break; + case "left": + rectTransform.offsetMin = new Vector2(0, rectTransform.offsetMin.y); + break; + case "right": + rectTransform.offsetMax = new Vector2(0, rectTransform.offsetMax.y); + break; + case "all": + rectTransform.offsetMin = new Vector2(0, 0); + rectTransform.offsetMax = new Vector2(0, 0); + break; + } + + handleChange(); + } + + private void handleChange() + { + EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); + } + } +} +#endif diff --git a/Assets/Scripts/Editor/CustomInspector/TransformEditor.cs b/Assets/Scripts/Editor/CustomInspector/TransformEditor.cs new file mode 100644 index 0000000..152989e --- /dev/null +++ b/Assets/Scripts/Editor/CustomInspector/TransformEditor.cs @@ -0,0 +1,436 @@ +// source https://gist.github.com/unitycoder/e5e6384f087639c0d9edc93aa3820468 + +using UnityEngine; +using UnityEditor; + +namespace OddTales.Framework.Core.EditorExtension +{ + /// + /// Custom inspector for Transform component. Using only DrawDefaultInspector would give different display. + /// Script based on Unity wiki implementation : https://wiki.unity3d.com/index.php/TransformInspector + /// Buttons to reset, copy, paste Transform values. + /// Context menu to round/truncate values, hide/show tools. + /// + [CanEditMultipleObjects, CustomEditor(typeof(Transform))] + public class TransformEditor : Editor + { + private const float FIELD_WIDTH = 212.0f; + private const bool WIDE_MODE = true; + + private const float POSITION_MAX = 100000.0f; + + private static GUIContent positionGUIContent = new GUIContent(LocalString("Position")); + private static GUIContent rotationGUIContent = new GUIContent(LocalString("Rotation")); + private static GUIContent scaleGUIContent = new GUIContent(LocalString("Scale")); + + private static string positionWarningText = LocalString("Due to floating-point precision limitations, it is recommended to bring the world coordinates of the GameObject within a smaller range."); + + private SerializedProperty positionProperty, rotationProperty, scaleProperty; + + private static Vector3? positionClipboard = null; + private static Quaternion? rotationClipboard = null; + private static Vector3? scaleClipboard = null; + + private const string SHOW_TOOLS_KEY = "TransformEditor_ShowTools"; + private const string SHOW_RESET_TOOLS_KEY = "TransformEditor_ShowResetTools"; + private const string SHOW_PASTE_TOOLS_KEY = "TransformEditor_ShowPasteTools"; + private const string SHOW_ADVANCED_PASTE_TOOLS_KEY = "TransformEditor_ShowAdvancedPasteTools"; + private const string SHOW_CLIPBOARD_INFORMATIONS_KEY = "TransformEditor_ShowClipboardInformations"; + private const string SHOW_SHORTCUTS_KEY = "TransformEditor_ShowHelpbox"; + + +#if UNITY_2017_3_OR_NEWER + private static System.Reflection.MethodInfo getLocalizedStringMethod; +#endif + + + /// Get translated Transform label + private static string LocalString(string text) + { +#if UNITY_2017_3_OR_NEWER + // Since Unity 2017.3, static class LocalizationDatabase is no longer public. Need to use reflection to access it. + if (getLocalizedStringMethod == null) + { + System.Reflection.Assembly assembly = typeof(UnityEditor.EditorWindow).Assembly; + System.Type localizationDatabaseType = assembly.GetType("UnityEditor.LocalizationDatabase"); + + getLocalizedStringMethod = localizationDatabaseType.GetMethod("GetLocalizedString"); + } + + return (string)getLocalizedStringMethod.Invoke(null, new object[] { text }); +#else + return LocalizationDatabase.GetLocalizedString(text); +#endif + } + + public void OnEnable() + { + positionProperty = serializedObject.FindProperty("m_LocalPosition"); + rotationProperty = serializedObject.FindProperty("m_LocalRotation"); + scaleProperty = serializedObject.FindProperty("m_LocalScale"); + + // Init options + if (!EditorPrefs.HasKey(SHOW_TOOLS_KEY)) EditorPrefs.SetBool(SHOW_TOOLS_KEY, true); + if (!EditorPrefs.HasKey(SHOW_RESET_TOOLS_KEY)) EditorPrefs.SetBool(SHOW_RESET_TOOLS_KEY, true); + if (!EditorPrefs.HasKey(SHOW_PASTE_TOOLS_KEY)) EditorPrefs.SetBool(SHOW_PASTE_TOOLS_KEY, true); + if (!EditorPrefs.HasKey(SHOW_ADVANCED_PASTE_TOOLS_KEY)) EditorPrefs.SetBool(SHOW_ADVANCED_PASTE_TOOLS_KEY, true); + if (!EditorPrefs.HasKey(SHOW_CLIPBOARD_INFORMATIONS_KEY)) EditorPrefs.SetBool(SHOW_CLIPBOARD_INFORMATIONS_KEY, true); + if (!EditorPrefs.HasKey(SHOW_SHORTCUTS_KEY)) EditorPrefs.SetBool(SHOW_SHORTCUTS_KEY, true); + } + + + public override void OnInspectorGUI() + { + Rect beginRect = GUILayoutUtility.GetRect(0, 0); + + EditorGUIUtility.wideMode = TransformEditor.WIDE_MODE; + EditorGUIUtility.labelWidth = EditorGUIUtility.currentViewWidth - TransformEditor.FIELD_WIDTH; // align field to right of inspector + + serializedObject.Update(); + + EditorGUIUtility.labelWidth = 60; // To allow float fields to expand when inspector width is increased + + // Position GUI + EditorGUILayout.BeginHorizontal(); + PositionPropertyField(positionProperty, positionGUIContent); // Note : Can't add generic menu if we use EditorGUILayout.PropertyField instead + if (EditorPrefs.GetBool(SHOW_TOOLS_KEY) && EditorPrefs.GetBool(SHOW_RESET_TOOLS_KEY)) + { + if (GUILayout.Button("Reset", GUILayout.Width(50))) + { + Undo.RecordObjects(targets, "Reset Positions"); + for (int i = 0; i < targets.Length; i++) + { + ((Transform)targets[i]).localPosition = Vector3.zero; + } + GUI.FocusControl(null); + } + } + EditorGUILayout.EndHorizontal(); + + // Rotation GUI + EditorGUILayout.BeginHorizontal(); + RotationPropertyField(rotationProperty, rotationGUIContent); // Note : Can't add generic menu if we use EditorGUILayout.PropertyField instead + if (EditorPrefs.GetBool(SHOW_TOOLS_KEY) && EditorPrefs.GetBool(SHOW_RESET_TOOLS_KEY)) + { + if (GUILayout.Button("Reset", GUILayout.Width(50))) + { + Undo.RecordObjects(targets, "Reset Rotations"); + for (int i = 0; i < targets.Length; i++) + { + TransformUtils.SetInspectorRotation(((Transform)targets[i]), Vector3.zero); + } + GUI.FocusControl(null); + } + + } + EditorGUILayout.EndHorizontal(); + + // Scale GUI + EditorGUILayout.BeginHorizontal(); + ScalePropertyField(scaleProperty, scaleGUIContent); // Note : Can't add generic menu if we use EditorGUILayout.PropertyField instead + if (EditorPrefs.GetBool(SHOW_TOOLS_KEY) && EditorPrefs.GetBool(SHOW_RESET_TOOLS_KEY)) + { + if (GUILayout.Button("Reset", GUILayout.Width(50))) + { + Undo.RecordObjects(targets, "Reset Scales"); + for (int i = 0; i < targets.Length; i++) + { + ((Transform)targets[i]).localScale = Vector3.one; + } + GUI.FocusControl(null); + } + } + EditorGUILayout.EndHorizontal(); + + + if (!ValidatePosition(((Transform)target).position)) EditorGUILayout.HelpBox(positionWarningText, MessageType.Warning); // Display floating-point warning message if values are too high + + if (EditorPrefs.GetBool(SHOW_TOOLS_KEY)) + { + // Paste Tools GUI + if (EditorPrefs.GetBool(SHOW_PASTE_TOOLS_KEY)) + { + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Copy")) + { + positionClipboard = ((Transform)target).localPosition; + rotationClipboard = ((Transform)target).localRotation; + scaleClipboard = ((Transform)target).localScale; + } + + if (!positionClipboard.HasValue) EditorGUI.BeginDisabledGroup(true); + if (GUILayout.Button("Paste")) + { + Undo.RecordObjects(targets, "Paste Clipboard Values"); + for (int i = 0; i < targets.Length; i++) + { + ((Transform)targets[i]).localPosition = positionClipboard.Value; + ((Transform)targets[i]).localRotation = rotationClipboard.Value; + ((Transform)targets[i]).localScale = scaleClipboard.Value; + } + GUI.FocusControl(null); + } + if (!positionClipboard.HasValue) EditorGUI.EndDisabledGroup(); + GUILayout.EndHorizontal(); + } + + // Advanced Paste Tools GUI + if (EditorPrefs.GetBool(SHOW_ADVANCED_PASTE_TOOLS_KEY)) + { + GUILayout.BeginHorizontal(); + + if (!positionClipboard.HasValue) EditorGUI.BeginDisabledGroup(true); + if (GUILayout.Button("Paste position")) + { + Undo.RecordObjects(targets, "Paste Position Clipboard Value"); + for (int i = 0; i < targets.Length; i++) + { + ((Transform)targets[i]).localPosition = positionClipboard.Value; + } + GUI.FocusControl(null); + } + + if (GUILayout.Button("Paste rotation")) + { + Undo.RecordObjects(targets, "Paste Rotation Clipboard Value"); + for (int i = 0; i < targets.Length; i++) + { + ((Transform)targets[i]).rotation = rotationClipboard.Value; + } + GUI.FocusControl(null); + } + + if (GUILayout.Button("Paste scale")) + { + Undo.RecordObjects(targets, "Paste Scale Clipboard Value"); + for (int i = 0; i < targets.Length; i++) + { + ((Transform)targets[i]).localScale = scaleClipboard.Value; + } + GUI.FocusControl(null); + } + if (!positionClipboard.HasValue) EditorGUI.EndDisabledGroup(); + + GUILayout.EndHorizontal(); + } + + // Clipboard GUI + if (EditorPrefs.GetBool(SHOW_CLIPBOARD_INFORMATIONS_KEY)) + { + if (positionClipboard.HasValue && rotationClipboard.HasValue && scaleClipboard.HasValue) + { + + GUIStyle helpboxStyle = new GUIStyle(EditorStyles.helpBox); + helpboxStyle.richText = true; + + EditorGUILayout.TextArea("Clipboard values :\n" + + "Position : " + positionClipboard.Value.ToString("f2") + "\n" + + "Rotation : " + rotationClipboard.Value.ToString("f2") + "\n" + + "Scale : " + scaleClipboard.Value.ToString("f2"), helpboxStyle); + } + } + + + // Shortcuts GUI - Related to InspectorShortcuts.cs https://github.com/VoxelBoy/Useful-Unity-Scripts/blob/master/InspectorShortcuts.cs + if (EditorPrefs.GetBool(SHOW_SHORTCUTS_KEY)) + { + EditorGUILayout.HelpBox("Inspector shortcuts :\n" + + "Toggle inspector lock : Ctrl + Shift + L\n" + + "Toggle inspector mode : Ctrl + Shift + D", MessageType.None); + } + } + Rect endRect = GUILayoutUtility.GetLastRect(); + endRect.y += endRect.height; + + + #region Context Menu + Rect componentRect = new Rect(beginRect.x, beginRect.y, beginRect.width, endRect.y - beginRect.y); + //EditorGUI.DrawRect(componentRect, Color.green); // Debug : display GenericMenu zone + + Event currentEvent = Event.current; + + if (currentEvent.type == EventType.ContextClick) + { + if (componentRect.Contains(currentEvent.mousePosition)) + { + GUI.FocusControl(null); + + GenericMenu menu = new GenericMenu(); + + menu.AddItem(new GUIContent("Display/Tools"), EditorPrefs.GetBool(SHOW_TOOLS_KEY), ToggleOption, SHOW_TOOLS_KEY); + menu.AddSeparator("Display/"); + menu.AddItem(new GUIContent("Display/Reset Tools"), EditorPrefs.GetBool(SHOW_RESET_TOOLS_KEY), ToggleOption, SHOW_RESET_TOOLS_KEY); + menu.AddItem(new GUIContent("Display/Paste Tools"), EditorPrefs.GetBool(SHOW_PASTE_TOOLS_KEY), ToggleOption, SHOW_PASTE_TOOLS_KEY); + menu.AddItem(new GUIContent("Display/Advanced Paste Tools"), EditorPrefs.GetBool(SHOW_ADVANCED_PASTE_TOOLS_KEY), ToggleOption, SHOW_ADVANCED_PASTE_TOOLS_KEY); + menu.AddItem(new GUIContent("Display/Clipboard informations"), EditorPrefs.GetBool(SHOW_CLIPBOARD_INFORMATIONS_KEY), ToggleOption, SHOW_CLIPBOARD_INFORMATIONS_KEY); + menu.AddItem(new GUIContent("Display/Shortcuts informations"), EditorPrefs.GetBool(SHOW_SHORTCUTS_KEY), ToggleOption, SHOW_SHORTCUTS_KEY); + + // Round menu + menu.AddItem(new GUIContent("Round/Three Decimals"), false, Round, 3); + menu.AddItem(new GUIContent("Round/Two Decimals"), false, Round, 2); + menu.AddItem(new GUIContent("Round/One Decimal"), false, Round, 1); + menu.AddItem(new GUIContent("Round/Integer"), false, Round, 0); + + // Truncate menu + menu.AddItem(new GUIContent("Truncate/Three Decimals"), false, Truncate, 3); + menu.AddItem(new GUIContent("Truncate/Two Decimals"), false, Truncate, 2); + menu.AddItem(new GUIContent("Truncate/One Decimal"), false, Truncate, 1); + menu.AddItem(new GUIContent("Truncate/Integer"), false, Truncate, 0); + + menu.ShowAsContext(); + currentEvent.Use(); + } + } + #endregion + + serializedObject.ApplyModifiedProperties(); + } + + + private bool ValidatePosition(Vector3 position) + { + if (Mathf.Abs(position.x) > POSITION_MAX) return false; + if (Mathf.Abs(position.y) > POSITION_MAX) return false; + if (Mathf.Abs(position.z) > POSITION_MAX) return false; + return true; + } + + private void PositionPropertyField(SerializedProperty positionProperty, GUIContent content) + { + Transform transform = (Transform)targets[0]; + Vector3 localPosition = transform.localPosition; + for (int i = 0; i < targets.Length; i++) + { + if (!localPosition.Equals(((Transform)targets[i]).localPosition)) + { + EditorGUI.showMixedValue = true; + break; + } + } + + EditorGUI.BeginChangeCheck(); + Vector3 newLocalPosition = EditorGUILayout.Vector3Field(content, localPosition); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObjects(targets, "Position Changed"); + for (int i = 0; i < targets.Length; i++) + { + ((Transform)targets[i]).localPosition = newLocalPosition; + } + positionProperty.serializedObject.SetIsDifferentCacheDirty(); + } + EditorGUI.showMixedValue = false; + } + + private void RotationPropertyField(SerializedProperty rotationProperty, GUIContent content) + { + Transform transform = (Transform)targets[0]; + Vector3 localRotation = TransformUtils.GetInspectorRotation(transform); + + + for (int i = 0; i < targets.Length; i++) + { + if (!localRotation.Equals(TransformUtils.GetInspectorRotation((Transform)targets[i]))) + { + EditorGUI.showMixedValue = true; + break; + } + } + + EditorGUI.BeginChangeCheck(); + Vector3 eulerAngles = EditorGUILayout.Vector3Field(content, localRotation); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObjects(targets, "Rotation Changed"); + for (int i = 0; i < targets.Length; i++) + { + //((Transform)targets[i]).localEulerAngles = eulerAngles; + TransformUtils.SetInspectorRotation(((Transform)targets[i]), eulerAngles); + } + rotationProperty.serializedObject.SetIsDifferentCacheDirty(); + } + EditorGUI.showMixedValue = false; + } + + private void ScalePropertyField(SerializedProperty scaleProperty, GUIContent content) + { + Transform transform = (Transform)targets[0]; + Vector3 localScale = transform.localScale; + for (int i = 0; i < targets.Length; i++) + { + if (!localScale.Equals(((Transform)targets[i]).localScale)) + { + EditorGUI.showMixedValue = true; + break; + } + } + + EditorGUI.BeginChangeCheck(); + Vector3 newLocalScale = EditorGUILayout.Vector3Field(content, localScale); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObjects(targets, "Scale Changed"); + for (int i = 0; i < targets.Length; i++) + { + ((Transform)targets[i]).localScale = newLocalScale; + } + scaleProperty.serializedObject.SetIsDifferentCacheDirty(); + } + EditorGUI.showMixedValue = false; + } + + + #region Generic Menu Callbacks + private void ToggleOption(object obj) + { + EditorPrefs.SetBool(obj.ToString(), !EditorPrefs.GetBool(obj.ToString())); + } + + /// Round all values of the Transform to a given number of decimals + private void Round(object objNumberOfDecimals) + { + int numberOfDecimals = (int)objNumberOfDecimals; + + Undo.RecordObjects(targets, "Round to " + numberOfDecimals + " decimals"); + for (int i = 0; i < targets.Length; i++) + { + ((Transform)targets[i]).localPosition = RoundVector(((Transform)targets[i]).localPosition, numberOfDecimals); + ((Transform)targets[i]).localEulerAngles = RoundVector(((Transform)targets[i]).localEulerAngles, numberOfDecimals); + ((Transform)targets[i]).localScale = RoundVector(((Transform)targets[i]).localScale, numberOfDecimals); + } + } + + /// Round all components of a Vector3 + private Vector3 RoundVector(Vector3 vector, int numberOfDecimals) + { + vector.x = Mathf.Round(vector.x * Mathf.Pow(10.0f, (float)numberOfDecimals)) / Mathf.Pow(10.0f, (float)numberOfDecimals); + vector.y = Mathf.Round(vector.y * Mathf.Pow(10.0f, (float)numberOfDecimals)) / Mathf.Pow(10.0f, (float)numberOfDecimals); + vector.z = Mathf.Round(vector.z * Mathf.Pow(10.0f, (float)numberOfDecimals)) / Mathf.Pow(10.0f, (float)numberOfDecimals); + return vector; + } + + /// Truncate all values of the Transform to a given number of decimals + private void Truncate(object objNumberOfDecimals) + { + int numberOfDecimals = (int)objNumberOfDecimals; + + Undo.RecordObjects(targets, "Truncate to " + numberOfDecimals + " decimals"); + for (int i = 0; i < targets.Length; i++) + { + ((Transform)targets[i]).localPosition = TruncateVector(((Transform)targets[i]).localPosition, numberOfDecimals); + ((Transform)targets[i]).localEulerAngles = TruncateVector(((Transform)targets[i]).localEulerAngles, numberOfDecimals); + ((Transform)targets[i]).localScale = TruncateVector(((Transform)targets[i]).localScale, numberOfDecimals); + } + } + + /// Truncate all components of a Vector3 + private Vector3 TruncateVector(Vector3 vector, int numberOfDecimals) + { + vector.x = Mathf.Floor(vector.x * Mathf.Pow(10.0f, (float)numberOfDecimals)) / Mathf.Pow(10.0f, (float)numberOfDecimals); + vector.y = Mathf.Floor(vector.y * Mathf.Pow(10.0f, (float)numberOfDecimals)) / Mathf.Pow(10.0f, (float)numberOfDecimals); + vector.z = Mathf.Floor(vector.z * Mathf.Pow(10.0f, (float)numberOfDecimals)) / Mathf.Pow(10.0f, (float)numberOfDecimals); + return vector; + } + #endregion + } +} diff --git a/Assets/Scripts/Editor/Importers/AnimationClipListImporter.cs b/Assets/Scripts/Editor/Importers/AnimationClipListImporter.cs new file mode 100644 index 0000000..733ac03 --- /dev/null +++ b/Assets/Scripts/Editor/Importers/AnimationClipListImporter.cs @@ -0,0 +1,65 @@ +// Checks for a .txt file with the same name as an imported .fbx file (in Assets/Models/ folder), containing a list of animation clips to add to the ModelImporter. +// .txt file should be tab-delimited with the following columns: "title", "start frame", "end frame" (and optional description, not used). +// example: +// Take0 10 40 asdf +// Take1 50 80 wasdf.. + +using System; +using System.IO; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; + +namespace UnityLibrary.Importers +{ + public class AnimationClipListImporter : AssetPostprocessor + { + void OnPreprocessModel() + { + ModelImporter modelImporter = assetImporter as ModelImporter; + if (modelImporter == null) return; + + string assetPath = assetImporter.assetPath; + if (!assetPath.StartsWith("Assets/Models") || !assetPath.EndsWith(".fbx", StringComparison.OrdinalIgnoreCase)) return; + + string txtPath = Path.ChangeExtension(assetPath, ".txt"); + if (!File.Exists(txtPath)) return; + + try + { + List clips = new List(); + string[] lines = File.ReadAllLines(txtPath); + + foreach (string line in lines) + { + string[] parts = line.Split('\t'); + if (parts.Length < 3) continue; // Ensure we have at least "title, start, end" + + string title = parts[0].Trim(); + if (!int.TryParse(parts[1], out int startFrame) || !int.TryParse(parts[2], out int endFrame)) + continue; + + ModelImporterClipAnimation clip = new ModelImporterClipAnimation + { + name = title, + firstFrame = startFrame, + lastFrame = endFrame, + loopTime = false + }; + + clips.Add(clip); + } + + if (clips.Count > 0) + { + modelImporter.clipAnimations = clips.ToArray(); + Debug.Log($"Added {clips.Count} animation clips to {assetPath}"); + } + } + catch (Exception ex) + { + Debug.LogError($"Failed to process animation data for {assetPath}: {ex.Message}"); + } + } + } +} diff --git a/Assets/Scripts/Editor/PackageManager/PackageManagerFavorites.cs b/Assets/Scripts/Editor/PackageManager/PackageManagerFavorites.cs new file mode 100644 index 0000000..1ccc0f1 --- /dev/null +++ b/Assets/Scripts/Editor/PackageManager/PackageManagerFavorites.cs @@ -0,0 +1,326 @@ +// adds custom buttons into PackageManager left panel (tested in 6.5) +// edit your own favorites in the code + +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEditor.Compilation; +using UnityEditor.PackageManager; +using UnityEditor.PackageManager.Requests; +using UnityEngine; +using UnityEngine.UIElements; + +namespace UnityLibrary.Editor.Tools +{ + + [InitializeOnLoad] + public static class PackageManagerFavorites + { + private const string InjectedElementName = "kelobyte-package-manager-dummy-panel"; + private static double nextScanTime; + private static AddRequest addRequest; + private static string pendingPackageName; + + static PackageManagerFavorites() + { + EditorApplication.update += OnEditorUpdate; + } + + [MenuItem("Window/Package Management/Open Package Manager With Dummy Button")] + private static void OpenPackageManagerAndInject() + { + Type windowType = GetPackageManagerWindowType(); + + if (windowType == null) + { + Debug.LogWarning("Package Manager window type was not found."); + return; + } + + EditorWindow window = EditorWindow.GetWindow(windowType); + window.Show(); + + EditorApplication.delayCall += TryInject; + } + + private static void OnEditorUpdate() + { + if (EditorApplication.timeSinceStartup < nextScanTime) + return; + + nextScanTime = EditorApplication.timeSinceStartup + 0.5; + TryInject(); + } + + private static void TryInject() + { + Type windowType = GetPackageManagerWindowType(); + + if (windowType == null) + return; + + UnityEngine.Object[] windows = Resources.FindObjectsOfTypeAll(windowType); + + foreach (UnityEngine.Object obj in windows) + { + EditorWindow window = obj as EditorWindow; + + if (window == null) + continue; + + InjectIntoWindow(window); + } + } + + private static Type GetPackageManagerWindowType() + { + Type type = Type.GetType("UnityEditor.PackageManager.UI.PackageManagerWindow, UnityEditor.PackageManagerUIModule"); + + if (type != null) + return type; + + foreach (System.Reflection.Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + type = assembly.GetType("UnityEditor.PackageManager.UI.PackageManagerWindow"); + + if (type != null) + return type; + } + + return null; + } + + private static void InjectIntoWindow(EditorWindow window) + { + VisualElement root = window.rootVisualElement; + + if (root == null) + return; + + if (root.Q(InjectedElementName) != null) + return; + + VisualElement insertParent = FindInsertParent(root); + int insertIndex = FindInsertIndex(insertParent); + + if (insertParent == null) + return; + + VisualElement panel = CreateDummyPanel(); + + if (insertIndex >= 0 && insertIndex <= insertParent.childCount) + insertParent.Insert(insertIndex, panel); + else + insertParent.Add(panel); + } + + private static VisualElement CreateDummyPanel() + { + VisualElement wrapper = new VisualElement(); + wrapper.name = InjectedElementName; + + wrapper.style.marginTop = 8; + wrapper.style.marginBottom = 8; + wrapper.style.paddingLeft = 6; + wrapper.style.paddingRight = 6; + + Foldout foldout = new Foldout(); + foldout.text = "Favorites"; + foldout.value = true; + + Button newtonsoftButton = CreatePackageButton( + "Newtonsoft Json", + "com.unity.nuget.newtonsoft-json" + ); + + Button gltfastButton = CreatePackageButton( + "Unity glTFast", + "com.unity.cloud.gltfast" + ); + + Button unityGltfButton = CreatePackageButton( + "Khronos UnityGLTF", + "https://github.com/KhronosGroup/UnityGLTF.git" + ); + + Button myEssentials = CreatePackageButton( + "Essentials", + " https://github.com/unitycoder/UnityEditorEssentials.git" + ); + + foldout.Add(newtonsoftButton); + foldout.Add(gltfastButton); + foldout.Add(unityGltfButton); + foldout.Add(myEssentials); + + wrapper.Add(foldout); + + return wrapper; + } + + private static Button CreatePackageButton(string label, string packageNameOrUrl) + { + Button button = new Button(() => + { + AddPackage(packageNameOrUrl); + }); + + button.text = label; + button.style.marginTop = 4; + button.style.height = 24; + + return button; + } + + private static void AddPackage(string packageNameOrUrl) + { + string packageToAdd = packageNameOrUrl.Trim(); + + if (addRequest != null && !addRequest.IsCompleted) + { + Debug.LogWarning("A package add request is already in progress."); + return; + } + + Debug.Log($"Adding package: {packageToAdd}"); + + pendingPackageName = packageToAdd; + addRequest = Client.Add(packageToAdd); + + EditorApplication.update -= MonitorAddPackageRequest; + EditorApplication.update += MonitorAddPackageRequest; + } + + private static void MonitorAddPackageRequest() + { + if (addRequest == null || !addRequest.IsCompleted) + return; + + EditorApplication.update -= MonitorAddPackageRequest; + + if (addRequest.Status == StatusCode.Success) + { + Debug.Log($"Package added: {pendingPackageName}"); + Client.Resolve(); + AssetDatabase.Refresh(); + CompilationPipeline.RequestScriptCompilation(); + } + else + { + string errorMessage = addRequest.Error != null ? addRequest.Error.message : "Unknown error."; + Debug.LogError($"Failed to add package '{pendingPackageName}': {errorMessage}"); + } + + addRequest = null; + pendingPackageName = null; + } + + private static VisualElement FindInsertParent(VisualElement root) + { + Label servicesLabel = FindLabel(root, "Services"); + + if (servicesLabel != null && servicesLabel.parent != null && servicesLabel.parent.parent != null) + return servicesLabel.parent.parent; + + Label cloudLabel = FindLabel(root, "Cloud"); + + if (cloudLabel != null && cloudLabel.parent != null && cloudLabel.parent.parent != null) + return cloudLabel.parent.parent; + + Label sourcesLabel = FindLabel(root, "Sources"); + + if (sourcesLabel != null && sourcesLabel.parent != null && sourcesLabel.parent.parent != null) + return sourcesLabel.parent.parent; + + return FindLikelyLeftPanel(root); + } + + private static int FindInsertIndex(VisualElement parent) + { + if (parent == null) + return -1; + + for (int i = 0; i < parent.childCount; i++) + { + VisualElement child = parent[i]; + + if (ContainsLabel(child, "Services")) + return i + 1; + } + + for (int i = 0; i < parent.childCount; i++) + { + VisualElement child = parent[i]; + + if (ContainsLabel(child, "Cloud")) + return i + 1; + } + + return -1; + } + + private static VisualElement FindLikelyLeftPanel(VisualElement root) + { + List all = new List(); + Collect(root, all); + + foreach (VisualElement element in all) + { + Rect rect = element.worldBound; + + if (rect.width > 120 && rect.width < 260 && rect.height > 250 && rect.x < 250) + return element; + } + + return null; + } + + private static Label FindLabel(VisualElement root, string text) + { + List all = new List(); + Collect(root, all); + + foreach (VisualElement element in all) + { + Label label = element as Label; + + if (label != null && label.text == text) + return label; + } + + return null; + } + + private static bool ContainsLabel(VisualElement root, string text) + { + if (root == null) + return false; + + Label label = root as Label; + + if (label != null && label.text == text) + return true; + + for (int i = 0; i < root.childCount; i++) + { + if (ContainsLabel(root[i], text)) + return true; + } + + return false; + } + + private static void Collect(VisualElement root, List elements) + { + if (root == null) + return; + + elements.Add(root); + + for (int i = 0; i < root.childCount; i++) + Collect(root[i], elements); + } + } +} +#endif diff --git a/Assets/Scripts/Editor/Tools/AndroidStoreCaptureTool.cs b/Assets/Scripts/Editor/Tools/AndroidStoreCaptureTool.cs new file mode 100644 index 0000000..8a73f3a --- /dev/null +++ b/Assets/Scripts/Editor/Tools/AndroidStoreCaptureTool.cs @@ -0,0 +1,513 @@ +// AndroidStoreCaptureTool.cs +// Put this file anywhere under an "Editor" folder. +// Usage: +// 1) Enter Play Mode. +// 2) Open: Tools/Android Store Capture +// 3) Pick output folder and click "Capture All Presets" + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +namespace UnityLibrary.Tools +{ + public class AndroidStoreCaptureTool : EditorWindow + { + [Serializable] + private class Preset + { + public string name; // base file name (without _WxH) + public int width; + public int height; + public CropMode cropMode; + + public Preset(string name, int w, int h, CropMode cropMode) + { + this.name = name; + width = w; + height = h; + this.cropMode = cropMode; + } + } + + private enum CropMode + { + Stretch, // no crop, just scale to target (may distort) + CropToFit // center-crop to target aspect, then scale (no distortion) + } + + private string _outputFolder = "StoreCaptures"; + private int _phoneCount = 2; // Play Console: 2-8 phone screenshots + + // Jobs + private class CaptureJob + { + public Preset preset; + public string filename; + } + + private readonly Queue _queue = new Queue(); + private bool _isRunning; + + // Hidden helper MonoBehaviour that runs coroutines in Play Mode + private CaptureHelper _helper; + + // Presets based on Play Console rules in your message. + // Phone/tablet sizes are common choices within allowed ranges. + private List BuildPresets() + { + var list = new List(); + + // App icon and feature graphic + list.Add(new Preset("appicon", 512, 512, CropMode.CropToFit)); + list.Add(new Preset("featuregraphic", 1024, 500, CropMode.CropToFit)); + + // Phone screenshots (2-8). 9:16 or 16:9. Each side 320..3840. + // We capture portrait by default; toggle to landscape if you want. + for (int i = 1; i <= Mathf.Clamp(_phoneCount, 2, 8); i++) + list.Add(new Preset("phone_" + i.ToString("00"), 1080, 1920, CropMode.CropToFit)); + + // 7-inch tablet screenshots (allowed: 320..3840 each side) + list.Add(new Preset("tablet7_01", 1920, 1200, CropMode.CropToFit)); // landscape 16:10 + list.Add(new Preset("tablet7_02", 1200, 1920, CropMode.CropToFit)); // portrait 10:16 + + // 10-inch tablet screenshots (each side 1080..7680) + list.Add(new Preset("tablet10_01", 2560, 1600, CropMode.CropToFit)); // landscape 16:10 + list.Add(new Preset("tablet10_02", 1600, 2560, CropMode.CropToFit)); // portrait 10:16 + + return list; + } + + [MenuItem("Tools/Android Store Capture")] + public static void Open() + { + var w = GetWindow("Android Store Capture"); + w.minSize = new Vector2(420, 340); + w.Show(); + } + + private void OnDisable() + { + StopRunner(); + } + + private void OnGUI() + { + EditorGUILayout.LabelField("Capture from Game View (Play Mode)", EditorStyles.boldLabel); + + using (new EditorGUILayout.VerticalScope("box")) + { + EditorGUILayout.LabelField("Output", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + _outputFolder = EditorGUILayout.TextField("Folder", _outputFolder); + if (GUILayout.Button("Browse", GUILayout.Width(80))) + { + string picked = EditorUtility.OpenFolderPanel("Pick output folder", Application.dataPath, ""); + if (!string.IsNullOrEmpty(picked)) + { + // Make it project-relative when possible + string proj = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + string full = Path.GetFullPath(picked); + if (full.StartsWith(proj, StringComparison.OrdinalIgnoreCase)) + { + _outputFolder = full.Substring(proj.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + else + { + _outputFolder = full; + } + } + } + EditorGUILayout.EndHorizontal(); + + _phoneCount = EditorGUILayout.IntSlider("Phone screenshots (2-8)", _phoneCount, 2, 8); + } + + using (new EditorGUILayout.VerticalScope("box")) + { + EditorGUILayout.LabelField("Actions", EditorStyles.boldLabel); + + if (!EditorApplication.isPlaying) + { + EditorGUILayout.HelpBox("Enter Play Mode first. This tool captures the rendered Game View.", MessageType.Warning); + } + + GUI.enabled = EditorApplication.isPlaying && !_isRunning; + if (GUILayout.Button("Capture All Presets")) + { + EnqueueAll(); + StartRunner(); + } + + if (GUILayout.Button("Capture Only Icon + Feature Graphic")) + { + EnqueueIconAndFeatureOnly(); + StartRunner(); + } + GUI.enabled = true; + + GUI.enabled = _isRunning; + if (GUILayout.Button("Stop")) + { + StopRunner(); + } + GUI.enabled = true; + + if (_isRunning) + { + EditorGUILayout.Space(6); + EditorGUILayout.LabelField("Running...", EditorStyles.boldLabel); + EditorGUILayout.LabelField("Remaining", _queue.Count.ToString()); + } + } + + using (new EditorGUILayout.VerticalScope("box")) + { + EditorGUILayout.LabelField("Notes", EditorStyles.boldLabel); + EditorGUILayout.LabelField("- Files are named like: appicon_512x512.png, featuregraphic_1024x500.png"); + EditorGUILayout.LabelField("- Phone screenshots are named like: phone_01_1080x1920.png"); + EditorGUILayout.LabelField("- Captures center-crop to match target aspect (no stretching)."); + } + } + + private void EnqueueAll() + { + _queue.Clear(); + + string folder = ResolveOutputFolder(); + Directory.CreateDirectory(folder); + + foreach (var p in BuildPresets()) + { + string fn = $"{p.name}_{p.width}x{p.height}.png"; + _queue.Enqueue(new CaptureJob { preset = p, filename = Path.Combine(folder, fn) }); + } + } + + private void EnqueueIconAndFeatureOnly() + { + _queue.Clear(); + + string folder = ResolveOutputFolder(); + Directory.CreateDirectory(folder); + + var icon = new Preset("appicon", 512, 512, CropMode.CropToFit); + var feature = new Preset("featuregraphic", 1024, 500, CropMode.CropToFit); + + _queue.Enqueue(new CaptureJob { preset = icon, filename = Path.Combine(folder, $"appicon_512x512.png") }); + _queue.Enqueue(new CaptureJob { preset = feature, filename = Path.Combine(folder, $"featuregraphic_1024x500.png") }); + } + + private string ResolveOutputFolder() + { + // If user gave absolute path, use it. Otherwise, place under project root. + if (Path.IsPathRooted(_outputFolder)) + return _outputFolder; + + string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + return Path.Combine(projectRoot, _outputFolder); + } + + private CaptureHelper EnsureHelper() + { + if (_helper != null) return _helper; + + var go = new GameObject("[AndroidStoreCaptureHelper]") + { + hideFlags = HideFlags.HideAndDontSave + }; + _helper = go.AddComponent(); + return _helper; + } + + private void StartRunner() + { + if (_isRunning) return; + if (!EditorApplication.isPlaying) return; + + _isRunning = true; + + GetMainGameView(); + + var helper = EnsureHelper(); + helper.StartCoroutine(RunCaptures()); + } + + private void StopRunner() + { + _isRunning = false; + _queue.Clear(); + + if (_helper != null) + { + _helper.StopAllCoroutines(); + DestroyImmediate(_helper.gameObject); + _helper = null; + } + } + + private IEnumerator RunCaptures() + { + while (_queue.Count > 0) + { + if (!EditorApplication.isPlaying) + { + StopRunner(); + yield break; + } + + var job = _queue.Dequeue(); + + SetGameViewSize(job.preset.width, job.preset.height); + + // Wait for the GameView to resize and re-render + for (int i = 0; i < 6; i++) + yield return null; + + // Wait for end of frame — this is required for ScreenCapture to work + yield return new WaitForEndOfFrame(); + + try + { + ProcessCaptureJob(job); + } + catch (Exception ex) + { + Debug.LogError("Capture failed: " + ex); + } + + Repaint(); + } + + _isRunning = false; + AssetDatabase.Refresh(); + Debug.Log("Android Store Capture: All captures finished."); + Repaint(); + + if (_helper != null) + { + DestroyImmediate(_helper.gameObject); + _helper = null; + } + } + + private void ProcessCaptureJob(CaptureJob job) + { + int targetW = job.preset.width; + int targetH = job.preset.height; + + Texture2D src = ScreenCapture.CaptureScreenshotAsTexture(); + if (src == null) + { + Debug.LogError($"CaptureScreenshotAsTexture returned null for {job.filename}. Skipping."); + return; + } + + Texture2D processed; + if (job.preset.cropMode == CropMode.CropToFit) + processed = CropToAspectThenScale(src, targetW, targetH); + else + processed = ScaleTexture(src, targetW, targetH); + + byte[] png = processed.EncodeToPNG(); + File.WriteAllBytes(job.filename, png); + + DestroyImmediate(src); + if (processed != src) + DestroyImmediate(processed); + + Debug.Log("Saved: " + job.filename); + } + + private static Texture2D CropToAspectThenScale(Texture2D src, int targetW, int targetH) + { + float srcAspect = (float)src.width / src.height; + float dstAspect = (float)targetW / targetH; + + int cropW = src.width; + int cropH = src.height; + + if (srcAspect > dstAspect) + { + // too wide -> crop width + cropW = Mathf.RoundToInt(src.height * dstAspect); + cropH = src.height; + } + else + { + // too tall -> crop height + cropW = src.width; + cropH = Mathf.RoundToInt(src.width / dstAspect); + } + + // Crop from top-left: x starts at 0, y starts from top + int x0 = 0; + int y0 = src.height - cropH; + + Color[] pixels = src.GetPixels(x0, y0, cropW, cropH); + Texture2D cropped = new Texture2D(cropW, cropH, TextureFormat.RGBA32, false); + cropped.SetPixels(pixels); + cropped.Apply(false, false); + + Texture2D scaled = ScaleTexture(cropped, targetW, targetH); + DestroyImmediate(cropped); + return scaled; + } + + private static Texture2D ScaleTexture(Texture2D src, int targetW, int targetH) + { + Texture2D dst = new Texture2D(targetW, targetH, TextureFormat.RGBA32, false); + + for (int y = 0; y < targetH; y++) + { + float v = (targetH == 1) ? 0f : (float)y / (targetH - 1); + for (int x = 0; x < targetW; x++) + { + float u = (targetW == 1) ? 0f : (float)x / (targetW - 1); + Color c = SampleBilinear(src, u, v); + dst.SetPixel(x, y, c); + } + } + + dst.Apply(false, false); + return dst; + } + + private static Color SampleBilinear(Texture2D tex, float u, float v) + { + float x = u * (tex.width - 1); + float y = v * (tex.height - 1); + + int x0 = Mathf.Clamp((int)Mathf.Floor(x), 0, tex.width - 1); + int y0 = Mathf.Clamp((int)Mathf.Floor(y), 0, tex.height - 1); + int x1 = Mathf.Clamp(x0 + 1, 0, tex.width - 1); + int y1 = Mathf.Clamp(y0 + 1, 0, tex.height - 1); + + float tx = x - x0; + float ty = y - y0; + + Color c00 = tex.GetPixel(x0, y0); + Color c10 = tex.GetPixel(x1, y0); + Color c01 = tex.GetPixel(x0, y1); + Color c11 = tex.GetPixel(x1, y1); + + Color a = Color.Lerp(c00, c10, tx); + Color b = Color.Lerp(c01, c11, tx); + return Color.Lerp(a, b, ty); + } + + // --------------------------- + // GameView sizing (internal) + // --------------------------- + + private static EditorWindow GetMainGameView() + { + Type t = Type.GetType("UnityEditor.GameView,UnityEditor"); + if (t == null) return null; + + // Try "GetMainGameView" first (older Unity versions) + MethodInfo getMain = t.GetMethod("GetMainGameView", BindingFlags.NonPublic | BindingFlags.Static); + if (getMain != null) + { + var result = getMain.Invoke(null, null) as EditorWindow; + if (result != null) return result; + } + + // Fallback: try "GetMainGameViewRenderRect" or just find an open GameView window + var gameView = GetWindow(t, false, null, false); + return gameView; + } + + private static void SetGameViewSize(int width, int height) + { + // Creates/uses a fixed resolution entry in the current platform group, then selects it. + // Unity does not expose this publicly; reflection is used. + + Type sizesType = Type.GetType("UnityEditor.GameViewSizes,UnityEditor"); + Type sizeType = Type.GetType("UnityEditor.GameViewSize,UnityEditor"); + Type groupType = Type.GetType("UnityEditor.GameViewSizeGroupType,UnityEditor"); + + if (sizesType == null || sizeType == null || groupType == null) + return; + + var instanceProp = sizesType.GetProperty("instance", BindingFlags.Public | BindingFlags.Static); + if (instanceProp == null) return; + object sizesInstance = instanceProp.GetValue(null, null); + if (sizesInstance == null) return; + + MethodInfo getGroup = sizesType.GetMethod("GetGroup"); + if (getGroup == null) return; + object group = getGroup.Invoke(sizesInstance, new object[] { (int)Enum.Parse(groupType, "Standalone") }); + if (group == null) return; + + // Find existing + MethodInfo getBuiltinCount = group.GetType().GetMethod("GetBuiltinCount"); + MethodInfo getCustomCount = group.GetType().GetMethod("GetCustomCount"); + MethodInfo getGameViewSize = group.GetType().GetMethod("GetGameViewSize"); + + if (getBuiltinCount == null || getCustomCount == null || getGameViewSize == null) return; + + int builtin = (int)getBuiltinCount.Invoke(group, null); + int custom = (int)getCustomCount.Invoke(group, null); + + int total = builtin + custom; + int foundIndex = -1; + + for (int i = 0; i < total; i++) + { + object gvSize = getGameViewSize.Invoke(group, new object[] { i }); + if (gvSize == null) continue; + var widthProp = gvSize.GetType().GetProperty("width"); + var heightProp = gvSize.GetType().GetProperty("height"); + if (widthProp == null || heightProp == null) continue; + + int w = (int)widthProp.GetValue(gvSize, null); + int h = (int)heightProp.GetValue(gvSize, null); + + if (w == width && h == height) + { + foundIndex = i; + break; + } + } + + if (foundIndex < 0) + { + // Add custom size + Type gvSizeType = Type.GetType("UnityEditor.GameViewSizeType,UnityEditor"); + if (gvSizeType == null) return; + object fixedRes = Enum.Parse(gvSizeType, "FixedResolution"); + + ConstructorInfo ctor = sizeType.GetConstructor(new[] { gvSizeType, typeof(int), typeof(int), typeof(string) }); + if (ctor == null) return; + object newSize = ctor.Invoke(new object[] { fixedRes, width, height, width + "x" + height }); + + MethodInfo addCustom = group.GetType().GetMethod("AddCustomSize"); + if (addCustom == null) return; + addCustom.Invoke(group, new object[] { newSize }); + + custom = (int)getCustomCount.Invoke(group, null); + foundIndex = builtin + (custom - 1); + } + + // Select size in GameView + EditorWindow gv = GetMainGameView(); + if (gv == null) return; + + Type gvType = gv.GetType(); + PropertyInfo selectedSizeIndex = gvType.GetProperty("selectedSizeIndex", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (selectedSizeIndex != null) + selectedSizeIndex.SetValue(gv, foundIndex, null); + + gv.Repaint(); + } + + // Hidden MonoBehaviour to run coroutines from the editor tool + private class CaptureHelper : MonoBehaviour { } + } +} diff --git a/Assets/Scripts/Editor/Tools/FindWhoReferencesThisGameObject.cs b/Assets/Scripts/Editor/Tools/FindWhoReferencesThisGameObject.cs new file mode 100644 index 0000000..b3f4b15 --- /dev/null +++ b/Assets/Scripts/Editor/Tools/FindWhoReferencesThisGameObject.cs @@ -0,0 +1,167 @@ +// find what scripts reference selected GameObject in the scene (in events, public fields..) + +using System.Collections.Generic; +using System.Reflection; +using UnityEditor; +using UnityEngine; +using UnityEngine.Events; + +namespace UnityLibrary.Editor +{ + public class FindWhoReferencesThisGameObject : EditorWindow + { + private GameObject target; + private Vector2 scroll; + + private class ReferenceResult + { + public string message; + public GameObject owner; + } + + private List results = new List(); + + [MenuItem("Tools/UnityLibrary/Find References To GameObject")] + public static void ShowWindow() + { + var win = GetWindow("Find References"); + win.minSize = new Vector2(500, 300); + } + + private void OnGUI() + { + GUILayout.Label("Find scripts that reference this GameObject", EditorStyles.boldLabel); + target = EditorGUILayout.ObjectField("Target GameObject", target, typeof(GameObject), true) as GameObject; + + if (GUILayout.Button("Find References")) + { + results.Clear(); + if (target != null) + { + FindReferences(target); + } + else + { + Debug.LogWarning("Please assign a GameObject."); + } + } + + if (results.Count > 0) + { + GUILayout.Label("Results:", EditorStyles.boldLabel); + scroll = GUILayout.BeginScrollView(scroll, GUILayout.Height(400)); + foreach (var res in results) + { + if (GUILayout.Button(res.message, GUILayout.ExpandWidth(true))) + { + EditorGUIUtility.PingObject(res.owner); + Selection.activeGameObject = res.owner; + } + } + GUILayout.EndScrollView(); + } + } + + private void FindReferences(GameObject target) + { + var allObjects = Object.FindObjectsByType(findObjectsInactive: FindObjectsInactive.Include, sortMode: FindObjectsSortMode.None); + + foreach (var mono in allObjects) + { + if (mono == null || mono.gameObject == target) continue; + + var type = mono.GetType(); + var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + foreach (var field in fields) + { + if (typeof(UnityEventBase).IsAssignableFrom(field.FieldType)) + { + var unityEvent = field.GetValue(mono) as UnityEventBase; + if (unityEvent != null) + { + int count = unityEvent.GetPersistentEventCount(); + for (int i = 0; i < count; i++) + { + var listener = unityEvent.GetPersistentTarget(i); + if (listener == target) + { + results.Add(new ReferenceResult + { + message = $"{mono.name} ({type.Name}) -> UnityEvent '{field.Name}'", + owner = mono.gameObject + }); + } + } + } + + continue; + } + + if (!typeof(UnityEngine.Object).IsAssignableFrom(field.FieldType)) + continue; + + var value = field.GetValue(mono) as UnityEngine.Object; + if (ReferencesTarget(value, target)) + { + results.Add(new ReferenceResult + { + message = $"{mono.name} ({type.Name}) -> Field '{field.Name}'", + owner = mono.gameObject + }); + } + } + + // Also scan serialized properties (handles public fields, [SerializeField] private, arrays/lists, etc.) + FindSerializedReferences(mono, target); + } + + if (results.Count == 0) + { + results.Add(new ReferenceResult + { + message = "No references found.", + owner = null + }); + } + } + + private void FindSerializedReferences(MonoBehaviour mono, GameObject target) + { + var so = new SerializedObject(mono); + var it = so.GetIterator(); + + // enterChildren=true on first call to include all fields + bool enterChildren = true; + while (it.NextVisible(enterChildren)) + { + enterChildren = false; + + if (it.propertyType != SerializedPropertyType.ObjectReference) + continue; + + var obj = it.objectReferenceValue; + if (!ReferencesTarget(obj, target)) + continue; + + results.Add(new ReferenceResult + { + message = $"{mono.name} ({mono.GetType().Name}) -> Serialized '{it.propertyPath}'", + owner = mono.gameObject + }); + } + } + + private static bool ReferencesTarget(UnityEngine.Object value, GameObject target) + { + if (value == null || target == null) return false; + + if (value == target) return true; + + // most common case: field is Transform/Component referencing the target GO + if (value is Component c && c.gameObject == target) return true; + + return false; + } + } +} diff --git a/Assets/Scripts/Editor/Tools/GameViewGridOverlay.cs b/Assets/Scripts/Editor/Tools/GameViewGridOverlay.cs new file mode 100644 index 0000000..06acb2e --- /dev/null +++ b/Assets/Scripts/Editor/Tools/GameViewGridOverlay.cs @@ -0,0 +1,57 @@ +using UnityEngine; + +namespace UnityLibrary.EditorTools +{ + [ExecuteAlways] + public class GameViewGridOverlay : MonoBehaviour + { +#if UNITY_EDITOR + public bool drawGrid = true; + + [Header("Grid Cell Size (visible area)")] + public int gridSizeX = 64; + public int gridSizeY = 64; + + [Header("Spacing Between Cells (invisible gap)")] + public int spacingX = 16; + public int spacingY = 16; + + [Header("Start Offsets")] + public int startOffsetX = 0; + public int startOffsetY = 0; + + public Color gridColor = new Color(1f, 1f, 1f, 0.5f); + + private void OnGUI() + { + if (!drawGrid || Application.isPlaying) return; + + Color oldColor = GUI.color; + GUI.color = gridColor; + + int cellStrideX = gridSizeX + spacingX; + int cellStrideY = gridSizeY + spacingY; + + // Loop until start of the cell is beyond screen, not end of cell + for (int y = startOffsetY; y < Screen.height; y += cellStrideY) + { + for (int x = startOffsetX; x < Screen.width; x += cellStrideX) + { + // Draw full box even if it goes beyond screen edges + + // Left + GUI.DrawTexture(new Rect(x, y, 1, gridSizeY), Texture2D.whiteTexture); + // Right + GUI.DrawTexture(new Rect(x + gridSizeX - 1, y, 1, gridSizeY), Texture2D.whiteTexture); + // Top + GUI.DrawTexture(new Rect(x, y, gridSizeX, 1), Texture2D.whiteTexture); + // Bottom + GUI.DrawTexture(new Rect(x, y + gridSizeY - 1, gridSizeX, 1), Texture2D.whiteTexture); + } + } + + GUI.color = oldColor; + } +#endif + } +} diff --git a/Assets/Scripts/Editor/Tools/InspectorFilter.cs b/Assets/Scripts/Editor/Tools/InspectorFilter.cs new file mode 100644 index 0000000..ab37407 --- /dev/null +++ b/Assets/Scripts/Editor/Tools/InspectorFilter.cs @@ -0,0 +1,289 @@ +// filters the fields of all components of a GameObject based on a user-provided string +// matching against both field names and types, with a UI to input the filter and visual highlights for matched fields. + +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace UnityLibrary.EditorTools +{ + [InitializeOnLoad] + public static class InspectorFilter + { + private static readonly Dictionary FiltersByGameObjectId = new Dictionary(); + private static readonly Dictionary> MatchedFieldsByComponentId = new Dictionary>(); + + static InspectorFilter() + { + Editor.finishedDefaultHeaderGUI += OnFinishedDefaultHeaderGUI; + Selection.selectionChanged += ApplyFilterForCurrentSelection; + Undo.undoRedoPerformed += ApplyFilterForCurrentSelection; + EditorApplication.delayCall += ApplyFilterForCurrentSelection; + } + + internal static bool TryGetFilterForGameObject(GameObject go, out string filter) + { + filter = string.Empty; + if (go == null) + { + return false; + } + + if (!FiltersByGameObjectId.TryGetValue(go.GetInstanceID(), out string value) || string.IsNullOrWhiteSpace(value)) + { + return false; + } + + filter = value.Trim(); + return filter.Length > 0; + } + + internal static bool IsPropertyMatch(SerializedProperty property, string filter) + { + if (property == null || string.IsNullOrWhiteSpace(filter)) + { + return false; + } + + if (property.propertyPath == "m_Script") + { + return false; + } + + return property.name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0 + || property.displayName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static void OnFinishedDefaultHeaderGUI(Editor editor) + { + if (editor.target is GameObject go) + { + DrawGameObjectFilterUI(go); + return; + } + + if (!(editor.target is Component component)) + { + return; + } + + int gameObjectId = component.gameObject.GetInstanceID(); + if (!FiltersByGameObjectId.TryGetValue(gameObjectId, out string filter) || string.IsNullOrWhiteSpace(filter)) + { + return; + } + + if (!MatchedFieldsByComponentId.TryGetValue(component.GetInstanceID(), out List matchedFields) || matchedFields.Count == 0) + { + return; + } + + Color old = GUI.color; + GUI.color = new Color(1f, 0.95f, 0.55f, 1f); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + GUI.color = old; + EditorGUILayout.LabelField("Matched fields: " + string.Join(", ", matchedFields), EditorStyles.miniLabel); + EditorGUILayout.EndVertical(); + } + + private static void DrawGameObjectFilterUI(GameObject go) + { + int id = go.GetInstanceID(); + FiltersByGameObjectId.TryGetValue(id, out string currentFilter); + currentFilter ??= string.Empty; + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + const string filterControlName = "InspectorFilter_FilterField"; + EditorGUILayout.BeginHorizontal(); + EditorGUI.BeginChangeCheck(); + GUI.SetNextControlName(filterControlName); + string newFilter = EditorGUILayout.TextField("Filter", currentFilter); + bool clearClicked = GUILayout.Button("x", EditorStyles.miniButton, GUILayout.Width(20f)); + bool changed = EditorGUI.EndChangeCheck(); + EditorGUILayout.EndHorizontal(); + + Event e = Event.current; + bool escapePressed = e.type == EventType.KeyDown + && e.keyCode == KeyCode.Escape + && GUI.GetNameOfFocusedControl() == filterControlName; + + if (clearClicked || escapePressed) + { + newFilter = string.Empty; + changed = true; + GUI.FocusControl(null); + if (escapePressed) + { + e.Use(); + } + } + + if (changed) + { + if (string.IsNullOrWhiteSpace(newFilter)) + { + FiltersByGameObjectId.Remove(id); + newFilter = string.Empty; + } + else + { + FiltersByGameObjectId[id] = newFilter; + } + + ApplyFilter(go, newFilter); + ActiveEditorTracker.sharedTracker.ForceRebuild(); + } + else + { + ApplyFilter(go, currentFilter); + } + + EditorGUILayout.EndVertical(); + } + + private static void ApplyFilterForCurrentSelection() + { + if (!(Selection.activeGameObject is GameObject go)) + { + return; + } + + int id = go.GetInstanceID(); + FiltersByGameObjectId.TryGetValue(id, out string filter); + ApplyFilter(go, filter); + ActiveEditorTracker.sharedTracker.ForceRebuild(); + } + + private static void ApplyFilter(GameObject go, string filter) + { + ActiveEditorTracker tracker = ActiveEditorTracker.sharedTracker; + Editor[] editors = tracker.activeEditors; + if (editors == null || editors.Length == 0) + { + return; + } + + bool hasFilter = !string.IsNullOrWhiteSpace(filter); + string normalizedFilter = hasFilter ? filter.Trim() : string.Empty; + + for (int i = 0; i < editors.Length; i++) + { + UnityEngine.Object target = editors[i].target; + if (!(target is Component component) || component.gameObject != go) + { + tracker.SetVisible(i, 1); + continue; + } + + int componentId = component.GetInstanceID(); + + if (!hasFilter) + { + MatchedFieldsByComponentId.Remove(componentId); + tracker.SetVisible(i, 1); + continue; + } + + bool typeMatch = component.GetType().Name.IndexOf(normalizedFilter, StringComparison.OrdinalIgnoreCase) >= 0; + List fieldMatches = GetMatchingSerializedFields(editors[i], normalizedFilter); + bool fieldMatch = fieldMatches.Count > 0; + + if (fieldMatch) + { + MatchedFieldsByComponentId[componentId] = fieldMatches; + } + else + { + MatchedFieldsByComponentId.Remove(componentId); + } + + tracker.SetVisible(i, (typeMatch || fieldMatch) ? 1 : 0); + } + } + + private static List GetMatchingSerializedFields(Editor editor, string filter) + { + List matches = new List(); + + SerializedObject serializedObject = editor.serializedObject; + if (serializedObject == null) + { + return matches; + } + + SerializedProperty iterator = serializedObject.GetIterator(); + bool enterChildren = true; + + while (iterator.NextVisible(enterChildren)) + { + enterChildren = false; + + if (!IsPropertyMatch(iterator, filter)) + { + continue; + } + + string label = iterator.displayName; + if (!matches.Contains(label)) + { + matches.Add(label); + } + } + + return matches; + } + } + + [CustomEditor(typeof(Component), true, isFallback = true)] + [CanEditMultipleObjects] + public class InspectorFilterComponentEditor : Editor + { + private static readonly Color HighlightColor = new Color(0.5058824f, 0.7058824f, 1f, 1f); + + public override void OnInspectorGUI() + { + Component component = target as Component; + if (component == null || !InspectorFilter.TryGetFilterForGameObject(component.gameObject, out string filter)) + { + DrawDefaultInspector(); + return; + } + + serializedObject.Update(); + + SerializedProperty property = serializedObject.GetIterator(); + bool enterChildren = true; + + while (property.NextVisible(enterChildren)) + { + enterChildren = false; + + using (new EditorGUI.DisabledScope(property.propertyPath == "m_Script")) + { + float height = EditorGUI.GetPropertyHeight(property, true); + Rect rect = EditorGUILayout.GetControlRect(true, height); + + if (InspectorFilter.IsPropertyMatch(property, filter)) + { + DrawBorder(rect, HighlightColor, 1f); + } + + EditorGUI.PropertyField(rect, property, true); + } + } + + serializedObject.ApplyModifiedProperties(); + } + + private static void DrawBorder(Rect rect, Color color, float thickness) + { + EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMin, rect.width, thickness), color); + EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMax - thickness, rect.width, thickness), color); + EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMin, thickness, rect.height), color); + EditorGUI.DrawRect(new Rect(rect.xMax - thickness, rect.yMin, thickness, rect.height), color); + } + } + +} diff --git a/Assets/Scripts/Editor/Tools/MeshThumbnailGrabberWindow.cs b/Assets/Scripts/Editor/Tools/MeshThumbnailGrabberWindow.cs new file mode 100644 index 0000000..ea28fba --- /dev/null +++ b/Assets/Scripts/Editor/Tools/MeshThumbnailGrabberWindow.cs @@ -0,0 +1,886 @@ +// editortool to capture thumbnails of meshes, prefabs, or gameobjects with custom settings and save as PNG + +using System.Collections.Generic; +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace UnityLibrary.Editor.Tools +{ + public class MeshThumbnailGrabberWindow : EditorWindow + { + private Object sourceObject; + + private PreviewRenderUtility previewUtility; + + private GameObject previewRoot; + private GameObject previewInstance; + + private Texture2D exportPreviewTexture; + + private readonly List drawables = new List(); + + private Vector2 orbit = new Vector2(135f, -20f); + + private float zoom = 1f; + private float fitDistance = 3f; + private float cameraDistanceMultiplier = 1f; + private float lightIntensity = 1.25f; + + private int outputWidth = 512; + private int outputHeight = 512; + + private Color backgroundColor = new Color(0f, 0f, 0f, 0f); + private Color ambientColor = new Color(0.35f, 0.35f, 0.35f, 1f); + private Color lightColor = Color.white; + + private bool drawGround = false; + + private struct PreviewDrawable + { + public Mesh Mesh; + public Matrix4x4 Matrix; + public Material[] Materials; + } + + [MenuItem("Tools/UnityLibrary/Mesh Thumbnail Grabber")] + public static void Open() + { + var win = GetWindow("Mesh Thumbnail Grabber"); + win.minSize = new Vector2(500, 680); + } + + private const string PrefOrbitX = "MeshThumbGrabber_OrbitX"; + private const string PrefOrbitY = "MeshThumbGrabber_OrbitY"; + private const string PrefZoom = "MeshThumbGrabber_Zoom"; + private const string PrefCamDistMult = "MeshThumbGrabber_CamDistMult"; + private const string PrefLightIntensity = "MeshThumbGrabber_LightIntensity"; + private const string PrefOutputWidth = "MeshThumbGrabber_OutputWidth"; + private const string PrefOutputHeight = "MeshThumbGrabber_OutputHeight"; + private const string PrefBgColorR = "MeshThumbGrabber_BgR"; + private const string PrefBgColorG = "MeshThumbGrabber_BgG"; + private const string PrefBgColorB = "MeshThumbGrabber_BgB"; + private const string PrefBgColorA = "MeshThumbGrabber_BgA"; + private const string PrefAmbientColorR = "MeshThumbGrabber_AmbR"; + private const string PrefAmbientColorG = "MeshThumbGrabber_AmbG"; + private const string PrefAmbientColorB = "MeshThumbGrabber_AmbB"; + private const string PrefAmbientColorA = "MeshThumbGrabber_AmbA"; + private const string PrefLightColorR = "MeshThumbGrabber_LightR"; + private const string PrefLightColorG = "MeshThumbGrabber_LightG"; + private const string PrefLightColorB = "MeshThumbGrabber_LightB"; + private const string PrefLightColorA = "MeshThumbGrabber_LightA"; + private const string PrefDrawGround = "MeshThumbGrabber_DrawGround"; + + private void LoadPrefs() + { + orbit.x = EditorPrefs.GetFloat(PrefOrbitX, orbit.x); + orbit.y = EditorPrefs.GetFloat(PrefOrbitY, orbit.y); + zoom = EditorPrefs.GetFloat(PrefZoom, zoom); + cameraDistanceMultiplier = EditorPrefs.GetFloat(PrefCamDistMult, cameraDistanceMultiplier); + lightIntensity = EditorPrefs.GetFloat(PrefLightIntensity, lightIntensity); + outputWidth = EditorPrefs.GetInt(PrefOutputWidth, outputWidth); + outputHeight = EditorPrefs.GetInt(PrefOutputHeight, outputHeight); + backgroundColor = new Color( + EditorPrefs.GetFloat(PrefBgColorR, backgroundColor.r), + EditorPrefs.GetFloat(PrefBgColorG, backgroundColor.g), + EditorPrefs.GetFloat(PrefBgColorB, backgroundColor.b), + EditorPrefs.GetFloat(PrefBgColorA, backgroundColor.a)); + ambientColor = new Color( + EditorPrefs.GetFloat(PrefAmbientColorR, ambientColor.r), + EditorPrefs.GetFloat(PrefAmbientColorG, ambientColor.g), + EditorPrefs.GetFloat(PrefAmbientColorB, ambientColor.b), + EditorPrefs.GetFloat(PrefAmbientColorA, ambientColor.a)); + lightColor = new Color( + EditorPrefs.GetFloat(PrefLightColorR, lightColor.r), + EditorPrefs.GetFloat(PrefLightColorG, lightColor.g), + EditorPrefs.GetFloat(PrefLightColorB, lightColor.b), + EditorPrefs.GetFloat(PrefLightColorA, lightColor.a)); + drawGround = EditorPrefs.GetBool(PrefDrawGround, drawGround); + } + + private void SavePrefs() + { + EditorPrefs.SetFloat(PrefOrbitX, orbit.x); + EditorPrefs.SetFloat(PrefOrbitY, orbit.y); + EditorPrefs.SetFloat(PrefZoom, zoom); + EditorPrefs.SetFloat(PrefCamDistMult, cameraDistanceMultiplier); + EditorPrefs.SetFloat(PrefLightIntensity, lightIntensity); + EditorPrefs.SetInt(PrefOutputWidth, outputWidth); + EditorPrefs.SetInt(PrefOutputHeight, outputHeight); + EditorPrefs.SetFloat(PrefBgColorR, backgroundColor.r); + EditorPrefs.SetFloat(PrefBgColorG, backgroundColor.g); + EditorPrefs.SetFloat(PrefBgColorB, backgroundColor.b); + EditorPrefs.SetFloat(PrefBgColorA, backgroundColor.a); + EditorPrefs.SetFloat(PrefAmbientColorR, ambientColor.r); + EditorPrefs.SetFloat(PrefAmbientColorG, ambientColor.g); + EditorPrefs.SetFloat(PrefAmbientColorB, ambientColor.b); + EditorPrefs.SetFloat(PrefAmbientColorA, ambientColor.a); + EditorPrefs.SetFloat(PrefLightColorR, lightColor.r); + EditorPrefs.SetFloat(PrefLightColorG, lightColor.g); + EditorPrefs.SetFloat(PrefLightColorB, lightColor.b); + EditorPrefs.SetFloat(PrefLightColorA, lightColor.a); + EditorPrefs.SetBool(PrefDrawGround, drawGround); + } + + private void OnEnable() + { + LoadPrefs(); + CreatePreviewUtility(); + } + + private void OnDisable() + { + SavePrefs(); + Cleanup(); + } + + private void CreatePreviewUtility() + { + if (previewUtility != null) + { + previewUtility.Cleanup(); + } + + previewUtility = new PreviewRenderUtility(true); + previewUtility.cameraFieldOfView = 30f; + + ApplyLightSettings(); + } + + private void Cleanup() + { + DestroyPreviewObjects(); + + if (previewUtility != null) + { + previewUtility.Cleanup(); + previewUtility = null; + } + + ClearExportPreview(); + } + + private void DestroyPreviewObjects() + { + drawables.Clear(); + + if (previewRoot != null) + { + DestroyImmediate(previewRoot); + previewRoot = null; + previewInstance = null; + } + } + + private void ClearExportPreview() + { + if (exportPreviewTexture != null) + { + DestroyImmediate(exportPreviewTexture); + exportPreviewTexture = null; + } + } + + private void OnGUI() + { + EditorGUILayout.Space(); + + using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) + { + EditorGUI.BeginChangeCheck(); + + sourceObject = EditorGUILayout.ObjectField( + "Source", + sourceObject, + typeof(Object), + false + ); + + if (EditorGUI.EndChangeCheck()) + { + ClearExportPreview(); + CreatePreviewInstance(); + FitToView(); + Repaint(); + } + } + + using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) + { + EditorGUI.BeginChangeCheck(); + + outputWidth = Mathf.Clamp(EditorGUILayout.IntField("Output Width", outputWidth), 16, 8192); + outputHeight = Mathf.Clamp(EditorGUILayout.IntField("Output Height", outputHeight), 16, 8192); + + if (EditorGUI.EndChangeCheck()) + { + ClearExportPreview(); + SavePrefs(); + } + + zoom = EditorGUILayout.Slider("Zoom", zoom, 0.05f, 100f); + cameraDistanceMultiplier = EditorGUILayout.Slider("Distance Multiplier", cameraDistanceMultiplier, 0.1f, 5f); + + EditorGUI.BeginChangeCheck(); + + lightIntensity = EditorGUILayout.Slider("Light Intensity", lightIntensity, 0f, 5f); + ambientColor = EditorGUILayout.ColorField("Ambient Color", ambientColor); + lightColor = EditorGUILayout.ColorField("Light Color", lightColor); + drawGround = EditorGUILayout.Toggle("Draw Ground", drawGround); + + if (EditorGUI.EndChangeCheck()) + { + ApplyLightSettings(); + ClearExportPreview(); + + CenterObjectToOrigin(); + CacheDrawables(); + SavePrefs(); + Repaint(); + } + } + + Rect previewRect = GUILayoutUtility.GetRect( + 10, + 10000, + 10, + 10000, + GUILayout.ExpandWidth(true), + GUILayout.ExpandHeight(true) + ); + + DrawInteractivePreview(previewRect); + + using (new EditorGUILayout.HorizontalScope()) + { + GUI.enabled = sourceObject != null; + + if (GUILayout.Button("Fit To View", GUILayout.Height(28))) + { + FitToView(); + ClearExportPreview(); + SavePrefs(); + Repaint(); + } + + if (GUILayout.Button("Reset Rotation", GUILayout.Height(28))) + { + orbit = new Vector2(135f, -20f); + ClearExportPreview(); + SavePrefs(); + Repaint(); + } + + if (GUILayout.Button("Preview Export Size", GUILayout.Height(28))) + { + UpdateExportPreview(); + } + + if (GUILayout.Button("Save PNG", GUILayout.Height(28))) + { + SavePng(); + } + + GUI.enabled = true; + } + + DrawExportPreviewPanel(); + } + + private void ApplyLightSettings() + { + if (previewUtility == null) + { + return; + } + + previewUtility.lights[0].intensity = lightIntensity; + previewUtility.lights[0].color = lightColor; + previewUtility.lights[0].transform.rotation = Quaternion.Euler(40f, 40f, 0f); + + previewUtility.lights[1].intensity = lightIntensity * 0.35f; + previewUtility.lights[1].color = lightColor; + previewUtility.lights[1].transform.rotation = Quaternion.Euler(340f, 218f, 177f); + } + + private void DrawInteractivePreview(Rect rect) + { + if (previewUtility == null) + { + CreatePreviewUtility(); + } + + if (sourceObject == null) + { + EditorGUI.DrawRect(rect, new Color(0.15f, 0.15f, 0.15f, 1f)); + GUI.Label(rect, "Assign a prefab, model, GameObject, or Mesh", EditorStyles.centeredGreyMiniLabel); + return; + } + + if (previewInstance == null) + { + CreatePreviewInstance(); + FitToView(); + } + + HandlePreviewInput(rect); + + Texture texture = RenderPreview(rect.width, rect.height); + + if (texture != null) + { + GUI.DrawTexture(rect, texture, ScaleMode.StretchToFill, true); + } + } + + private void HandlePreviewInput(Rect rect) + { + Event e = Event.current; + + if (!rect.Contains(e.mousePosition)) + { + return; + } + + if (e.type == EventType.MouseDrag && e.button == 0) + { + orbit += new Vector2(e.delta.x, -e.delta.y); + orbit.y = Mathf.Clamp(orbit.y, -89f, 89f); + + ClearExportPreview(); + + e.Use(); + Repaint(); + } + + if (e.type == EventType.MouseUp && e.button == 0) + { + SavePrefs(); + } + + if (e.type == EventType.ScrollWheel) + { + zoom *= 1f - e.delta.y * 0.08f; + zoom = Mathf.Clamp(zoom, 0.05f, 100f); + + ClearExportPreview(); + SavePrefs(); + + e.Use(); + Repaint(); + } + } + + private void CreatePreviewInstance() + { + DestroyPreviewObjects(); + + if (sourceObject == null) + { + return; + } + + previewRoot = new GameObject("Thumbnail Preview Root"); + previewRoot.hideFlags = HideFlags.HideAndDontSave; + previewRoot.transform.position = Vector3.zero; + previewRoot.transform.rotation = Quaternion.identity; + previewRoot.transform.localScale = Vector3.one; + + GameObject sourceGameObject = sourceObject as GameObject; + Mesh sourceMesh = sourceObject as Mesh; + + if (sourceGameObject != null) + { + previewInstance = Instantiate(sourceGameObject, previewRoot.transform); + previewInstance.hideFlags = HideFlags.HideAndDontSave; + } + else if (sourceMesh != null) + { + previewInstance = new GameObject("Mesh Preview Instance"); + previewInstance.hideFlags = HideFlags.HideAndDontSave; + previewInstance.transform.SetParent(previewRoot.transform, false); + + MeshFilter meshFilter = previewInstance.AddComponent(); + meshFilter.sharedMesh = sourceMesh; + + MeshRenderer meshRenderer = previewInstance.AddComponent(); + meshRenderer.sharedMaterial = GetDefaultMaterial(); + } + else + { + string path = AssetDatabase.GetAssetPath(sourceObject); + GameObject loaded = AssetDatabase.LoadAssetAtPath(path); + + if (loaded != null) + { + previewInstance = Instantiate(loaded, previewRoot.transform); + previewInstance.hideFlags = HideFlags.HideAndDontSave; + } + } + + if (previewInstance == null) + { + DestroyPreviewObjects(); + return; + } + + DisableSceneOnlyComponents(previewInstance); + + previewInstance.transform.localPosition = Vector3.zero; + previewInstance.transform.localRotation = Quaternion.identity; + previewInstance.transform.localScale = Vector3.one; + + CenterObjectToOrigin(); + CacheDrawables(); + } + + private void CenterObjectToOrigin() + { + if (previewRoot == null || previewInstance == null) + { + return; + } + + previewRoot.transform.position = Vector3.zero; + previewRoot.transform.rotation = Quaternion.identity; + previewRoot.transform.localScale = Vector3.one; + + previewInstance.transform.localPosition = Vector3.zero; + + Bounds bounds = GetWorldBounds(previewRoot); + Vector3 centerOffset = bounds.center; + + previewInstance.transform.position -= centerOffset; + + if (drawGround) + { + Bounds centeredBounds = GetWorldBounds(previewRoot); + previewInstance.transform.position -= new Vector3(0f, centeredBounds.min.y, 0f); + } + } + + private void FitToView() + { + if (previewInstance == null || previewUtility == null) + { + return; + } + + CenterObjectToOrigin(); + CacheDrawables(); + + Bounds bounds = GetWorldBounds(previewRoot); + + float radius = Mathf.Max(bounds.extents.magnitude, 0.001f); + float fov = previewUtility.cameraFieldOfView; + float fovRad = fov * Mathf.Deg2Rad; + + fitDistance = radius / Mathf.Sin(fovRad * 0.5f); + fitDistance *= 1.15f; + + zoom = 1f; + } + + private void CacheDrawables() + { + drawables.Clear(); + + if (previewRoot == null) + { + return; + } + + Renderer[] renderers = previewRoot.GetComponentsInChildren(true); + + foreach (Renderer renderer in renderers) + { + if (renderer == null || !renderer.enabled) + { + continue; + } + + MeshFilter meshFilter = renderer.GetComponent(); + + if (meshFilter != null && meshFilter.sharedMesh != null) + { + drawables.Add(new PreviewDrawable + { + Mesh = meshFilter.sharedMesh, + Matrix = renderer.localToWorldMatrix, + Materials = renderer.sharedMaterials + }); + + continue; + } + + SkinnedMeshRenderer skinned = renderer as SkinnedMeshRenderer; + + if (skinned != null && skinned.sharedMesh != null) + { + drawables.Add(new PreviewDrawable + { + Mesh = skinned.sharedMesh, + Matrix = skinned.localToWorldMatrix, + Materials = skinned.sharedMaterials + }); + } + } + } + + private Texture RenderPreview(float width, float height) + { + if (previewUtility == null || previewInstance == null) + { + return null; + } + + Rect rect = new Rect(0f, 0f, Mathf.Max(1f, width), Mathf.Max(1f, height)); + + previewUtility.BeginPreview(rect, GUIStyle.none); + RenderCurrentView(); + return previewUtility.EndPreview(); + } + + private void RenderCurrentView() + { + Camera camera = previewUtility.camera; + + camera.clearFlags = CameraClearFlags.Color; + camera.backgroundColor = backgroundColor; + camera.nearClipPlane = 0.01f; + camera.farClipPlane = 10000f; + camera.fieldOfView = previewUtility.cameraFieldOfView; + camera.allowHDR = false; + camera.allowMSAA = true; + + RenderSettings.ambientLight = ambientColor; + + Quaternion rotation = Quaternion.Euler(-orbit.y, orbit.x, 0f); + + float distance = fitDistance * cameraDistanceMultiplier / Mathf.Max(zoom, 0.001f); + distance = Mathf.Max(distance, 0.01f); + + Vector3 cameraDirection = rotation * Vector3.forward; + + camera.transform.position = -cameraDirection * distance; + camera.transform.rotation = Quaternion.LookRotation(cameraDirection, Vector3.up); + + ApplyLightSettings(); + + DrawCachedRenderers(); + + if (drawGround) + { + DrawGround(); + } + + camera.Render(); + } + + private void DrawCachedRenderers() + { + for (int i = 0; i < drawables.Count; i++) + { + PreviewDrawable drawable = drawables[i]; + + if (drawable.Mesh == null) + { + continue; + } + + DrawMeshWithMaterials(drawable.Mesh, drawable.Matrix, drawable.Materials); + } + } + + private void DrawMeshWithMaterials(Mesh mesh, Matrix4x4 matrix, Material[] materials) + { + if (mesh == null) + { + return; + } + + int subMeshCount = Mathf.Max(1, mesh.subMeshCount); + + for (int i = 0; i < subMeshCount; i++) + { + Material material = GetDefaultMaterial(); + + if (materials != null && i < materials.Length && materials[i] != null) + { + material = materials[i]; + } + + if (material == null) + { + continue; + } + + previewUtility.DrawMesh(mesh, matrix, material, i); + } + } + + private void DrawGround() + { + Mesh planeMesh = Resources.GetBuiltinResource("Plane.fbx"); + + if (planeMesh == null) + { + return; + } + + Bounds bounds = GetWorldBounds(previewRoot); + + float size = Mathf.Max(bounds.size.x, bounds.size.z, 1f) * 1.5f; + + Matrix4x4 matrix = Matrix4x4.TRS( + new Vector3(0f, bounds.min.y, 0f), + Quaternion.identity, + new Vector3(size * 0.1f, 1f, size * 0.1f) + ); + + previewUtility.DrawMesh(planeMesh, matrix, GetDefaultMaterial(), 0); + } + + private Bounds GetWorldBounds(GameObject root) + { + if (root == null) + { + return new Bounds(Vector3.zero, Vector3.one); + } + + Renderer[] renderers = root.GetComponentsInChildren(true); + + bool hasBounds = false; + Bounds bounds = new Bounds(Vector3.zero, Vector3.one); + + foreach (Renderer renderer in renderers) + { + if (renderer == null) + { + continue; + } + + if (!hasBounds) + { + bounds = renderer.bounds; + hasBounds = true; + } + else + { + bounds.Encapsulate(renderer.bounds); + } + } + + if (!hasBounds) + { + return new Bounds(Vector3.zero, Vector3.one); + } + + return bounds; + } + + private static Material GetDefaultMaterial() + { + Material material = AssetDatabase.GetBuiltinExtraResource("Default-Material.mat"); + + if (material != null) + { + return material; + } + + Shader shader = Shader.Find("Standard"); + + if (shader != null) + { + return new Material(shader); + } + + return null; + } + + private static void DisableSceneOnlyComponents(GameObject root) + { + MonoBehaviour[] behaviours = root.GetComponentsInChildren(true); + + foreach (MonoBehaviour behaviour in behaviours) + { + if (behaviour != null) + { + behaviour.enabled = false; + } + } + + Camera[] cameras = root.GetComponentsInChildren(true); + + foreach (Camera camera in cameras) + { + camera.enabled = false; + } + + Light[] lights = root.GetComponentsInChildren(true); + + foreach (Light light in lights) + { + light.enabled = false; + } + } + + private void SavePng() + { + if (sourceObject == null || previewInstance == null) + { + return; + } + + string defaultName = sourceObject.name + "_thumbnail_" + outputWidth + "x" + outputHeight + ".png"; + + string path = EditorUtility.SaveFilePanel( + "Save Thumbnail PNG", + Application.dataPath, + defaultName, + "png" + ); + + if (string.IsNullOrEmpty(path)) + { + return; + } + + Texture2D texture = RenderToTexture2D(outputWidth, outputHeight); + + if (texture == null) + { + Debug.LogError("Failed to render thumbnail."); + return; + } + + byte[] png = texture.EncodeToPNG(); + File.WriteAllBytes(path, png); + + DestroyImmediate(texture); + + AssetDatabase.Refresh(); + + PingSavedAsset(path); + UpdateExportPreview(); + + Debug.Log("Saved thumbnail: " + path); + } + + private Texture2D RenderToTexture2D(int width, int height) + { + if (previewUtility == null || previewInstance == null) + { + return null; + } + + RenderTexture rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); + RenderTexture prevActive = RenderTexture.active; + + try + { + // BeginPreview activates the preview scene lights; we then swap its + // internal RGB target for our ARGB32 RT before camera.Render() fires. + Rect previewRect = new Rect(0f, 0f, width, height); + previewUtility.BeginPreview(previewRect, GUIStyle.none); + + previewUtility.camera.targetTexture = rt; + + RenderCurrentView(); + + // EndPreview would blit its internal RT to screen – skip that and + // read directly from our ARGB32 RT instead. + previewUtility.EndPreview(); + + RenderTexture.active = rt; + + Texture2D copy = new Texture2D(width, height, TextureFormat.RGBA32, false); + copy.ReadPixels(new Rect(0, 0, width, height), 0, 0); + copy.Apply(); + + return copy; + } + finally + { + RenderTexture.active = prevActive; + RenderTexture.ReleaseTemporary(rt); + } + } + + private static void PingSavedAsset(string absolutePath) + { + string projectPath = Path.GetFullPath(Application.dataPath + "/..").Replace("\\", "/"); + string fixedPath = Path.GetFullPath(absolutePath).Replace("\\", "/"); + + if (!fixedPath.StartsWith(projectPath)) + { + EditorUtility.RevealInFinder(absolutePath); + return; + } + + string assetPath = fixedPath.Substring(projectPath.Length + 1); + + Object asset = AssetDatabase.LoadAssetAtPath(assetPath); + + if (asset != null) + { + Selection.activeObject = asset; + EditorGUIUtility.PingObject(asset); + } + else + { + EditorUtility.RevealInFinder(absolutePath); + } + } + + private void UpdateExportPreview() + { + if (sourceObject == null || previewInstance == null) + { + return; + } + + ClearExportPreview(); + + exportPreviewTexture = RenderToTexture2D(outputWidth, outputHeight); + Repaint(); + } + + private void DrawExportPreviewPanel() + { + if (exportPreviewTexture == null) + { + return; + } + + EditorGUILayout.Space(); + + using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) + { + EditorGUILayout.LabelField( + "Export Preview: " + exportPreviewTexture.width + "x" + exportPreviewTexture.height + " px", + EditorStyles.boldLabel + ); + + Rect rect = GUILayoutUtility.GetRect( + exportPreviewTexture.width, + exportPreviewTexture.height, + GUILayout.Width(exportPreviewTexture.width), + GUILayout.Height(exportPreviewTexture.height) + ); + + EditorGUI.DrawTextureTransparent( + rect, + exportPreviewTexture, + ScaleMode.StretchToFill + ); + + if (GUILayout.Button("Clear Preview")) + { + ClearExportPreview(); + } + } + } + + } // class +} // namespace diff --git a/Assets/Scripts/Editor/Tools/PasteScript.cs b/Assets/Scripts/Editor/Tools/PasteScript.cs new file mode 100644 index 0000000..84a1561 --- /dev/null +++ b/Assets/Scripts/Editor/Tools/PasteScript.cs @@ -0,0 +1,85 @@ +// new version of the https://github.com/UnityCommunity/UnityLibrary/blob/master/Assets/Scripts/Editor/Tools/CopyPasteHelper.cs +// creates c# script from clipboard when click button + +using UnityEngine; +using UnityEditor; +using System.IO; +using System.Text.RegularExpressions; + +namespace UnityLibrary.Tools +{ + public class PasteScript : EditorWindow + { + private string statusMessage = ""; + private string lastCreatedScriptPath = ""; + + [MenuItem("Tools/UnityLibrary/Clipboard To C# Script")] + public static void ShowWindow() + { + GetWindow("Clipboard to Script"); + } + + void OnGUI() + { + if (GUILayout.Button("Create Script from Clipboard")) + { + TryCreateScriptFromClipboard(); + } + + GUILayout.Space(10); + + // Draw clickable HelpBox + EditorGUILayout.HelpBox(statusMessage, MessageType.Info); + + Rect helpBoxRect = GUILayoutUtility.GetLastRect(); + if (!string.IsNullOrEmpty(lastCreatedScriptPath) && Event.current.type == EventType.MouseDown && helpBoxRect.Contains(Event.current.mousePosition)) + { + var asset = AssetDatabase.LoadAssetAtPath(lastCreatedScriptPath); + if (asset != null) + { + EditorGUIUtility.PingObject(asset); + } + Event.current.Use(); // Consume the click + } + } + + void TryCreateScriptFromClipboard() + { + string clipboard = EditorGUIUtility.systemCopyBuffer; + + if (IsProbablyCSharp(clipboard)) + { + string folderPath = "Assets/Scripts/Generated"; + Directory.CreateDirectory(folderPath); + + string className = GetClassName(clipboard) ?? "GeneratedScript"; + string path = AssetDatabase.GenerateUniqueAssetPath($"{folderPath}/{className}.cs"); + + File.WriteAllText(path, clipboard); + AssetDatabase.Refresh(); + + statusMessage = $"Script created: {path}"; + lastCreatedScriptPath = path; + } + else + { + statusMessage = "Clipboard does not contain valid C# code."; + lastCreatedScriptPath = ""; + } + } + + bool IsProbablyCSharp(string text) + { + if (string.IsNullOrWhiteSpace(text)) return false; + + // Basic heuristic checks + return Regex.IsMatch(text, @"\b(class|struct|interface|using|namespace)\b"); + } + + string GetClassName(string text) + { + Match match = Regex.Match(text, @"\bclass\s+(\w+)"); + return match.Success ? match.Groups[1].Value : null; + } + } +} diff --git a/Assets/Scripts/Editor/Tools/PivotAligner.cs b/Assets/Scripts/Editor/Tools/PivotAligner.cs new file mode 100644 index 0000000..e24d762 --- /dev/null +++ b/Assets/Scripts/Editor/Tools/PivotAligner.cs @@ -0,0 +1,879 @@ +/// +/// PivotAligner — Unity Editor Window +/// Align two 3D models by picking a custom pivot point (vertex or face center) +/// on the source model, then rotating/translating it around that point. +/// +/// Place this file inside any Editor/ folder in your project. +/// Open via: Tools/UnityLibrary/Pivot Aligner +/// +using UnityEngine; +using UnityEditor; + +namespace UnityLibrary.Tools +{ + public class PivotAligner : EditorWindow + { + enum PickMode { Vertex, Face } + enum ToolState { Idle, Picking, Transforming } + + const string MENU_PATH = "Tools/Pivot Aligner"; + const float GIZMO_RADIUS = 0.06f; + const float GIZMO_CROSS = 0.25f; + + [SerializeField] private GameObject sourceObject; + [SerializeField] private PickMode pickMode = PickMode.Vertex; + + // Rotation + private float rotX; + private float rotY; + private float rotZ; + + private bool showFinetune = false; + private float fineTuneRange = 5f; + private float fineX; + private float fineY; + private float fineZ; + + // Scale + private bool showScale = true; + private bool uniformScale = true; + private float uniformScaleValue = 1f; + private float scaleX = 1f; + private float scaleY = 1f; + private float scaleZ = 1f; + + // Position offset + private bool showPosOffset = false; + private float posOffsetX; + private float posOffsetY; + private float posOffsetZ; + private float finePosRange = 0.1f; + + // Runtime state + private ToolState state = ToolState.Idle; + private Vector3 pivotWorld = Vector3.zero; + private bool hasPivot = false; + + private Vector3 basePosition; + private Quaternion baseRotation; + private Vector3 baseScale; + + private Vector3 highlightPoint = Vector3.zero; + private Vector3 highlightNormal = Vector3.up; + private bool hasHighlight = false; + + private Vector2 scroll; + + private GUIStyle headerStyle; + private GUIStyle sectionStyle; + private GUIStyle stateStyle; + private GUIStyle subLabelStyle; + private bool stylesInit; + + private int undoGroupIndex = 0; + private int lastHotControl = 0; + private string lastUndoLabel = ""; + + [MenuItem(MENU_PATH)] + public static void ShowWindow() + { + PivotAligner win = GetWindow("Pivot Aligner"); + win.minSize = new Vector2(440, 560); + } + + private void OnEnable() + { + SceneView.duringSceneGui += OnSceneGUI; + titleContent = new GUIContent("Pivot Aligner"); + } + + private void OnDisable() + { + SceneView.duringSceneGui -= OnSceneGUI; + CancelPicking(); + } + + private void OnGUI() + { + InitStyles(); + + scroll = EditorGUILayout.BeginScrollView(scroll); + + EditorGUILayout.Space(6); + EditorGUILayout.LabelField("PIVOT ALIGNER", headerStyle); + EditorGUILayout.Space(2); + DrawHR(); + + EditorGUILayout.Space(6); + EditorGUILayout.LabelField("1. Source Model", sectionStyle); + + EditorGUI.BeginChangeCheck(); + sourceObject = (GameObject)EditorGUILayout.ObjectField( + "Game Object", + sourceObject, + typeof(GameObject), + true); + + if (EditorGUI.EndChangeCheck()) + ResetTool(); + + if (sourceObject == null) + { + EditorGUILayout.HelpBox("Assign a GameObject to begin.", MessageType.Info); + EditorGUILayout.EndScrollView(); + return; + } + + EditorGUILayout.Space(8); + EditorGUILayout.LabelField("2. Pick Mode", sectionStyle); + + EditorGUILayout.BeginHorizontal(); + + if (DrawModeButton("Vertex", pickMode == PickMode.Vertex)) + { + pickMode = PickMode.Vertex; + hasHighlight = false; + } + + if (DrawModeButton("Face", pickMode == PickMode.Face)) + { + pickMode = PickMode.Face; + hasHighlight = false; + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(8); + EditorGUILayout.LabelField("3. Select Pivot Point", sectionStyle); + + using (new EditorGUI.DisabledScope(state == ToolState.Transforming)) + { + if (state != ToolState.Picking) + { + if (GUILayout.Button("Select Target Point in Scene", GUILayout.Height(30))) + BeginPicking(); + } + else + { + Color prev = GUI.backgroundColor; + GUI.backgroundColor = new Color(1f, 0.55f, 0.15f); + + if (GUILayout.Button("Cancel Picking", GUILayout.Height(30))) + CancelPicking(); + + GUI.backgroundColor = prev; + + string modeText = pickMode == PickMode.Vertex ? "vertex" : "face center"; + EditorGUILayout.HelpBox( + "Hover over the model. The nearest " + modeText + " will highlight. Click to confirm pivot.", + MessageType.None); + } + } + + if (hasPivot) + { + EditorGUILayout.Space(2); + + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField("Pivot " + pivotWorld.ToString("F4"), subLabelStyle); + + if (GUILayout.Button("Re-pick", GUILayout.Width(56), GUILayout.Height(18))) + BeginPicking(); + } + } + + EditorGUILayout.Space(8); + DrawHR(); + EditorGUILayout.Space(4); + EditorGUILayout.LabelField("4. Rotation Around Pivot", sectionStyle); + + using (new EditorGUI.DisabledScope(!hasPivot)) + { + bool dirty = false; + + EditorGUI.BeginChangeCheck(); + + DrawRotRow("Pitch X", ref rotX); + DrawRotRow("Yaw Y", ref rotY); + DrawRotRow("Roll Z", ref rotZ); + + if (EditorGUI.EndChangeCheck()) + dirty = true; + + EditorGUILayout.Space(4); + + using (new EditorGUILayout.HorizontalScope()) + { + showFinetune = EditorGUILayout.ToggleLeft("Fine-tune +/-", showFinetune, GUILayout.Width(102)); + + using (new EditorGUI.DisabledScope(!showFinetune)) + fineTuneRange = Mathf.Max(0.001f, EditorGUILayout.FloatField(fineTuneRange, GUILayout.Width(52))); + + EditorGUILayout.LabelField("deg", subLabelStyle, GUILayout.Width(28)); + } + + if (showFinetune) + { + EditorGUI.BeginChangeCheck(); + + fineX = DrawFineRotSlider("Delta X", fineX, fineTuneRange); + fineY = DrawFineRotSlider("Delta Y", fineY, fineTuneRange); + fineZ = DrawFineRotSlider("Delta Z", fineZ, fineTuneRange); + + if (EditorGUI.EndChangeCheck()) + dirty = true; + } + + if (dirty && hasPivot) + ApplyAll("Pivot Aligner - Rotate"); + + EditorGUILayout.Space(4); + + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Button("Reset All Rotation", GUILayout.Height(24))) + ResetRotation(); + + if (showFinetune && GUILayout.Button("Reset Fine", GUILayout.Width(80), GUILayout.Height(24))) + { + fineX = 0f; + fineY = 0f; + fineZ = 0f; + + if (hasPivot) + ApplyAll("Pivot Aligner - Rotate"); + } + } + } + + EditorGUILayout.Space(8); + DrawHR(); + EditorGUILayout.Space(4); + + using (new EditorGUILayout.HorizontalScope()) + { + showScale = EditorGUILayout.Foldout(showScale, "5. Scale Around Pivot", true, sectionStyle); + EditorGUILayout.LabelField("(uses selected point)", subLabelStyle); + } + + if (showScale) + { + using (new EditorGUI.DisabledScope(!hasPivot)) + { + EditorGUI.BeginChangeCheck(); + + uniformScale = EditorGUILayout.ToggleLeft("Uniform Scale", uniformScale); + + if (uniformScale) + { + uniformScaleValue = EditorGUILayout.FloatField("Scale", uniformScaleValue); + uniformScaleValue = Mathf.Max(0.0001f, uniformScaleValue); + + scaleX = uniformScaleValue; + scaleY = uniformScaleValue; + scaleZ = uniformScaleValue; + } + else + { + scaleX = Mathf.Max(0.0001f, EditorGUILayout.FloatField("Scale X", scaleX)); + scaleY = Mathf.Max(0.0001f, EditorGUILayout.FloatField("Scale Y", scaleY)); + scaleZ = Mathf.Max(0.0001f, EditorGUILayout.FloatField("Scale Z", scaleZ)); + + uniformScaleValue = scaleX; + } + + if (EditorGUI.EndChangeCheck() && hasPivot) + ApplyAll("Pivot Aligner - Scale"); + + EditorGUILayout.Space(3); + + if (GUILayout.Button("Reset Scale", GUILayout.Height(24))) + ResetScale(); + } + } + + EditorGUILayout.Space(8); + DrawHR(); + EditorGUILayout.Space(4); + + using (new EditorGUILayout.HorizontalScope()) + { + showPosOffset = EditorGUILayout.Foldout(showPosOffset, "6. Position Offset", true, sectionStyle); + EditorGUILayout.LabelField("(moves pivot too)", subLabelStyle); + } + + if (showPosOffset) + { + using (new EditorGUI.DisabledScope(!hasPivot)) + { + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField("Slider range +/-", subLabelStyle, GUILayout.Width(102)); + finePosRange = Mathf.Max(0.0001f, EditorGUILayout.FloatField(finePosRange, GUILayout.Width(52))); + EditorGUILayout.LabelField("m", subLabelStyle, GUILayout.Width(14)); + } + + EditorGUILayout.Space(2); + + EditorGUI.BeginChangeCheck(); + + posOffsetX = DrawOffsetSliderRow("Offset X", posOffsetX, finePosRange); + posOffsetY = DrawOffsetSliderRow("Offset Y", posOffsetY, finePosRange); + posOffsetZ = DrawOffsetSliderRow("Offset Z", posOffsetZ, finePosRange); + + if (EditorGUI.EndChangeCheck() && hasPivot) + ApplyAll("Pivot Aligner - Move"); + + EditorGUILayout.Space(3); + + if (GUILayout.Button("Reset Position Offset", GUILayout.Height(24))) + ResetPositionOffset(); + } + } + + EditorGUILayout.Space(8); + DrawHR(); + EditorGUILayout.Space(4); + + using (new EditorGUI.DisabledScope(!hasPivot)) + { + Color prev = GUI.backgroundColor; + GUI.backgroundColor = new Color(0.22f, 0.80f, 0.40f); + + if (GUILayout.Button("Apply and Clear Pivot", GUILayout.Height(36))) + Apply(); + + GUI.backgroundColor = prev; + } + + if (hasPivot) + { + if (GUILayout.Button("Cancel and Revert to Original", GUILayout.Height(26))) + RevertAndReset(); + } + + EditorGUILayout.Space(4); + + string totalInfo = ""; + + if (hasPivot) + { + totalInfo = + "rot(" + + (rotX + fineX).ToString("F3") + ", " + + (rotY + fineY).ToString("F3") + ", " + + (rotZ + fineZ).ToString("F3") + ") deg scale(" + + scaleX.ToString("F3") + ", " + + scaleY.ToString("F3") + ", " + + scaleZ.ToString("F3") + ") pos offset(" + + posOffsetX.ToString("F4") + ", " + + posOffsetY.ToString("F4") + ", " + + posOffsetZ.ToString("F4") + ") m"; + } + + string stateLabel; + + switch (state) + { + case ToolState.Picking: + stateLabel = "PICKING"; + break; + + case ToolState.Transforming: + stateLabel = "TRANSFORMING " + totalInfo; + break; + + default: + stateLabel = "idle"; + break; + } + + EditorGUILayout.LabelField(stateLabel, stateStyle); + EditorGUILayout.Space(4); + + EditorGUILayout.EndScrollView(); + + SceneView.RepaintAll(); + } + + private bool DrawModeButton(string label, bool active) + { + Color prev = GUI.backgroundColor; + + if (active) + GUI.backgroundColor = new Color(0.3f, 0.65f, 1f); + + bool clicked = GUILayout.Button(label, GUILayout.Height(26)); + + GUI.backgroundColor = prev; + + return clicked; + } + + private void DrawRotRow(string label, ref float value) + { + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(label, GUILayout.Width(72)); + + value = EditorGUILayout.FloatField(value, GUILayout.Width(74)); + + if (GUILayout.Button("-90", GUILayout.Width(36), GUILayout.Height(18))) + value -= 90f; + + if (GUILayout.Button("-45", GUILayout.Width(36), GUILayout.Height(18))) + value -= 45f; + + if (GUILayout.Button("0", GUILayout.Width(28), GUILayout.Height(18))) + value = 0f; + + if (GUILayout.Button("+45", GUILayout.Width(36), GUILayout.Height(18))) + value += 45f; + + if (GUILayout.Button("+90", GUILayout.Width(36), GUILayout.Height(18))) + value += 90f; + } + } + + private float DrawFineRotSlider(string label, float value, float range) + { + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(label, subLabelStyle, GUILayout.Width(76)); + + value = GUILayout.HorizontalSlider(value, -range, range); + value = EditorGUILayout.FloatField(value, GUILayout.Width(64)); + + EditorGUILayout.LabelField("deg", subLabelStyle, GUILayout.Width(28)); + } + + return value; + } + + private float DrawOffsetSliderRow(string label, float value, float range) + { + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(label, GUILayout.Width(72)); + + value = GUILayout.HorizontalSlider(value, -range, range); + value = EditorGUILayout.FloatField(value, GUILayout.Width(74)); + + EditorGUILayout.LabelField("m", subLabelStyle, GUILayout.Width(14)); + + if (GUILayout.Button("0", GUILayout.Width(24), GUILayout.Height(18))) + value = 0f; + } + + return value; + } + + private void OnSceneGUI(SceneView sv) + { + if (sourceObject == null) + return; + + if (hasPivot) + DrawPivotGizmo(pivotWorld, Color.cyan); + + if (state == ToolState.Picking) + HandlePicking(sv); + } + + private void HandlePicking(SceneView sv) + { + Event e = Event.current; + + if (e.type == EventType.Layout) + HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive)); + + if (e.type == EventType.MouseMove || e.type == EventType.MouseDown) + { + Ray ray = HandleUtility.GUIPointToWorldRay(e.mousePosition); + + TryRaycast(ray, out hasHighlight, out highlightPoint, out highlightNormal); + + if (hasHighlight) + DrawPivotGizmo(highlightPoint, new Color(1f, 0.8f, 0.1f, 0.9f)); + + if (e.type == EventType.MouseDown && e.button == 0 && hasHighlight) + { + ConfirmPivot(highlightPoint); + e.Use(); + } + + sv.Repaint(); + } + + if (hasHighlight) + DrawPivotGizmo(highlightPoint, new Color(1f, 0.8f, 0.1f, 0.9f)); + } + + private void TryRaycast(Ray ray, out bool hit, out Vector3 point, out Vector3 normal) + { + hit = false; + point = Vector3.zero; + normal = Vector3.up; + + MeshFilter[] filters = sourceObject.GetComponentsInChildren(); + float bestDist = float.MaxValue; + + foreach (MeshFilter mf in filters) + { + if (mf.sharedMesh == null) + continue; + + Mesh mesh = mf.sharedMesh; + Transform t = mf.transform; + + Ray localRay = new Ray( + t.InverseTransformPoint(ray.origin), + t.InverseTransformDirection(ray.direction).normalized); + + Vector3[] verts = mesh.vertices; + int[] tris = mesh.triangles; + Vector3[] normals = mesh.normals; + + for (int i = 0; i < tris.Length; i += 3) + { + Vector3 v0 = verts[tris[i]]; + Vector3 v1 = verts[tris[i + 1]]; + Vector3 v2 = verts[tris[i + 2]]; + + float dist; + float u; + float v; + + if (!RayTriangle(localRay, v0, v1, v2, out dist, out u, out v)) + continue; + + if (dist < 0f || dist >= bestDist) + continue; + + bestDist = dist; + hit = true; + + if (pickMode == PickMode.Vertex) + { + float w = 1f - u - v; + int vi = FindNearestVertex(u, v, w); + + if (vi == 0) + point = t.TransformPoint(v0); + else if (vi == 1) + point = t.TransformPoint(v1); + else + point = t.TransformPoint(v2); + } + else + { + point = t.TransformPoint((v0 + v1 + v2) / 3f); + } + + Vector3 n0 = normals.Length > tris[i] ? normals[tris[i]] : Vector3.up; + Vector3 n1 = normals.Length > tris[i + 1] ? normals[tris[i + 1]] : Vector3.up; + Vector3 n2 = normals.Length > tris[i + 2] ? normals[tris[i + 2]] : Vector3.up; + + normal = t.TransformDirection((n0 * (1f - u - v) + n1 * u + n2 * v).normalized); + } + } + } + + private static bool RayTriangle( + Ray ray, + Vector3 v0, + Vector3 v1, + Vector3 v2, + out float dist, + out float u, + out float v) + { + dist = 0f; + u = 0f; + v = 0f; + + Vector3 e1 = v1 - v0; + Vector3 e2 = v2 - v0; + Vector3 h = Vector3.Cross(ray.direction, e2); + + float det = Vector3.Dot(e1, h); + + if (Mathf.Abs(det) < 1e-6f) + return false; + + float f = 1f / det; + Vector3 s = ray.origin - v0; + + u = f * Vector3.Dot(s, h); + + if (u < 0f || u > 1f) + return false; + + Vector3 q = Vector3.Cross(s, e1); + + v = f * Vector3.Dot(ray.direction, q); + + if (v < 0f || u + v > 1f) + return false; + + dist = f * Vector3.Dot(e2, q); + + return dist > 1e-5f; + } + + private static int FindNearestVertex(float u, float v, float w) + { + if (w >= u && w >= v) + return 0; + + if (u >= w && u >= v) + return 1; + + return 2; + } + + private void DrawPivotGizmo(Vector3 pos, Color color) + { + Handles.color = color; + + float handleSize = HandleUtility.GetHandleSize(pos); + + Handles.SphereHandleCap( + 0, + pos, + Quaternion.identity, + GIZMO_RADIUS * handleSize, + EventType.Repaint); + + float sz = GIZMO_CROSS * handleSize; + + Handles.DrawLine(pos - Vector3.right * sz, pos + Vector3.right * sz); + Handles.DrawLine(pos - Vector3.up * sz, pos + Vector3.up * sz); + Handles.DrawLine(pos - Vector3.forward * sz, pos + Vector3.forward * sz); + + GUIStyle s = new GUIStyle(EditorStyles.miniLabel); + s.normal.textColor = color; + + string label = hasPivot && Vector3.Distance(pos, pivotWorld) < 0.0001f ? "PIVOT" : "point"; + + Handles.Label(pos + Vector3.up * sz * 1.5f, label, s); + } + + private void BeginPicking() + { + if (sourceObject == null) + return; + + state = ToolState.Picking; + hasHighlight = false; + + SceneView.RepaintAll(); + } + + private void CancelPicking() + { + if (state == ToolState.Picking) + state = hasPivot ? ToolState.Transforming : ToolState.Idle; + + hasHighlight = false; + + SceneView.RepaintAll(); + } + + private void ConfirmPivot(Vector3 worldPoint) + { + if (hasPivot && state == ToolState.Transforming) + RevertTransform(); + + pivotWorld = worldPoint; + hasPivot = true; + state = ToolState.Transforming; + hasHighlight = false; + + basePosition = sourceObject.transform.position; + baseRotation = sourceObject.transform.rotation; + baseScale = sourceObject.transform.localScale; + + rotX = 0f; + rotY = 0f; + rotZ = 0f; + + fineX = 0f; + fineY = 0f; + fineZ = 0f; + + scaleX = 1f; + scaleY = 1f; + scaleZ = 1f; + uniformScaleValue = 1f; + + posOffsetX = 0f; + posOffsetY = 0f; + posOffsetZ = 0f; + + Repaint(); + SceneView.RepaintAll(); + } + + private void ApplyAll(string undoLabel = "Pivot Aligner") + { + if (sourceObject == null || !hasPivot) + return; + + int hot = GUIUtility.hotControl; + + if (hot != lastHotControl || undoLabel != lastUndoLabel) + { + undoGroupIndex++; + lastHotControl = hot; + lastUndoLabel = undoLabel; + } + + Undo.RecordObject(sourceObject.transform, undoLabel); + + Quaternion deltaRotation = Quaternion.Euler( + rotX + fineX, + rotY + fineY, + rotZ + fineZ); + + Vector3 scaleMultiplier = new Vector3(scaleX, scaleY, scaleZ); + Vector3 positionOffset = new Vector3(posOffsetX, posOffsetY, posOffsetZ); + + Vector3 baseOffset = basePosition - pivotWorld; + + Vector3 scaledOffset = new Vector3( + baseOffset.x * scaleMultiplier.x, + baseOffset.y * scaleMultiplier.y, + baseOffset.z * scaleMultiplier.z); + + sourceObject.transform.position = + pivotWorld + + deltaRotation * scaledOffset + + positionOffset; + + sourceObject.transform.rotation = deltaRotation * baseRotation; + sourceObject.transform.localScale = Vector3.Scale(baseScale, scaleMultiplier); + + Undo.CollapseUndoOperations(Undo.GetCurrentGroup() - undoGroupIndex + 1); + } + + private void ResetRotation() + { + rotX = 0f; + rotY = 0f; + rotZ = 0f; + + fineX = 0f; + fineY = 0f; + fineZ = 0f; + + ApplyAll("Pivot Aligner - Reset Rotation"); + } + + private void ResetScale() + { + scaleX = 1f; + scaleY = 1f; + scaleZ = 1f; + uniformScaleValue = 1f; + + ApplyAll("Pivot Aligner - Reset Scale"); + } + + private void ResetPositionOffset() + { + posOffsetX = 0f; + posOffsetY = 0f; + posOffsetZ = 0f; + + ApplyAll("Pivot Aligner - Reset Offset"); + } + + private void Apply() + { + if (sourceObject == null) + return; + + Undo.SetCurrentGroupName("Pivot Aligner Apply"); + Undo.CollapseUndoOperations(Undo.GetCurrentGroup()); + + ResetTool(); + } + + private void RevertAndReset() + { + RevertTransform(); + ResetTool(); + } + + private void RevertTransform() + { + if (sourceObject == null || !hasPivot) + return; + + Undo.RecordObject(sourceObject.transform, "Pivot Aligner Revert"); + + sourceObject.transform.position = basePosition; + sourceObject.transform.rotation = baseRotation; + sourceObject.transform.localScale = baseScale; + } + + private void ResetTool() + { + state = ToolState.Idle; + + hasPivot = false; + hasHighlight = false; + + rotX = 0f; + rotY = 0f; + rotZ = 0f; + + fineX = 0f; + fineY = 0f; + fineZ = 0f; + + scaleX = 1f; + scaleY = 1f; + scaleZ = 1f; + uniformScaleValue = 1f; + + posOffsetX = 0f; + posOffsetY = 0f; + posOffsetZ = 0f; + + SceneView.RepaintAll(); + Repaint(); + } + + private void InitStyles() + { + if (stylesInit) + return; + + stylesInit = true; + + headerStyle = new GUIStyle(EditorStyles.boldLabel); + headerStyle.fontSize = 13; + headerStyle.alignment = TextAnchor.MiddleCenter; + headerStyle.normal.textColor = new Color(0.65f, 0.88f, 1f); + + sectionStyle = new GUIStyle(EditorStyles.boldLabel); + sectionStyle.normal.textColor = new Color(0.85f, 0.85f, 0.85f); + + stateStyle = new GUIStyle(EditorStyles.miniLabel); + stateStyle.alignment = TextAnchor.MiddleRight; + stateStyle.normal.textColor = new Color(0.40f, 0.75f, 0.50f); + + subLabelStyle = new GUIStyle(EditorStyles.miniLabel); + subLabelStyle.normal.textColor = new Color(0.55f, 0.55f, 0.55f); + } + + private void DrawHR() + { + Rect r = EditorGUILayout.GetControlRect(false, 1); + EditorGUI.DrawRect(r, new Color(0.35f, 0.35f, 0.35f, 0.6f)); + } + } +} diff --git a/Assets/Scripts/Editor/Tools/PreciseOffsetEditor.cs b/Assets/Scripts/Editor/Tools/PreciseOffsetEditor.cs new file mode 100644 index 0000000..f3c5a44 --- /dev/null +++ b/Assets/Scripts/Editor/Tools/PreciseOffsetEditor.cs @@ -0,0 +1,118 @@ +// gameobject position fiddling tool (when dragging x,y,z axist in transform panel is too fast) + +using UnityEngine; +using UnityEditor; + +namespace UnityLibrary.SceneTools +{ + public class PreciseOffsetEditor : EditorWindow + { + private const string SpeedMultiplierKey = "UnityLibrary_PreciseOffset_SpeedMultiplier"; + + private GameObject selectedObject; + private Vector3 offsetSliderValues = Vector3.zero; + private float speedMultiplier = 0.01f; + + private Vector3 originalPosition; + private GameObject lastSelectedObject; + + [MenuItem("Tools/UnityLibrary/Precise Model Offset")] + public static void ShowWindow() + { + var win = GetWindow("Precise Model Offset"); + win.minSize = new Vector2(300, 220); + win.maxSize = new Vector2(300, 220); + } + + private void OnEnable() + { + speedMultiplier = EditorPrefs.GetFloat(SpeedMultiplierKey, 0.01f); + } + + private void OnDisable() + { + EditorPrefs.SetFloat(SpeedMultiplierKey, speedMultiplier); + } + + private void OnGUI() + { + EditorGUILayout.LabelField("Precise Model Offset", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + int selectedCount = Selection.gameObjects.Length; + selectedObject = Selection.activeGameObject; + + if (selectedCount == 0) + { + EditorGUILayout.HelpBox("No GameObject selected.", MessageType.Warning); + return; + } + + if (selectedCount == 1) + { + EditorGUILayout.LabelField("Selected: " + selectedObject.name); + } + else + { + EditorGUILayout.LabelField("Selected: (multiple)"); + } + + if (selectedObject != lastSelectedObject) + { + originalPosition = selectedObject.transform.position; + offsetSliderValues = Vector3.zero; + lastSelectedObject = selectedObject; + } + + EditorGUILayout.Space(); + EditorGUI.BeginChangeCheck(); + speedMultiplier = EditorGUILayout.Slider("Speed Multiplier", speedMultiplier, 0.001f, 1f); + if (EditorGUI.EndChangeCheck()) + { + EditorPrefs.SetFloat(SpeedMultiplierKey, speedMultiplier); + } + + EditorGUILayout.Space(); + EditorGUI.BeginChangeCheck(); + + // Store old values to detect changes + Vector3 oldOffset = offsetSliderValues; + + offsetSliderValues.x = EditorGUILayout.Slider("X Offset", offsetSliderValues.x, -100f, 100f); + offsetSliderValues.y = EditorGUILayout.Slider("Y Offset", offsetSliderValues.y, -100f, 100f); + offsetSliderValues.z = EditorGUILayout.Slider("Z Offset", offsetSliderValues.z, -100f, 100f); + + Vector3 delta = offsetSliderValues - oldOffset; + + if (delta != Vector3.zero) + { + Undo.RecordObject(selectedObject.transform, "Precise Offset"); + + Vector3 newPosition = selectedObject.transform.position; + + if (!Mathf.Approximately(delta.x, 0f)) + newPosition.x = originalPosition.x + offsetSliderValues.x * speedMultiplier; + + if (!Mathf.Approximately(delta.y, 0f)) + newPosition.y = originalPosition.y + offsetSliderValues.y * speedMultiplier; + + if (!Mathf.Approximately(delta.z, 0f)) + newPosition.z = originalPosition.z + offsetSliderValues.z * speedMultiplier; + + selectedObject.transform.position = newPosition; + + EditorUtility.SetDirty(selectedObject); + } + + EditorGUILayout.Space(); + if (GUILayout.Button("Reset Offset")) + { + offsetSliderValues = Vector3.zero; + if (selectedObject != null) + { + selectedObject.transform.position = originalPosition; + } + } + } + } +} diff --git a/Assets/Scripts/Editor/Tools/README.md b/Assets/Scripts/Editor/Tools/README.md new file mode 100644 index 0000000..fcdb9c8 --- /dev/null +++ b/Assets/Scripts/Editor/Tools/README.md @@ -0,0 +1,5 @@ +### GameViewGridOverlay.cs +![Image](https://github.com/user-attachments/assets/48fbced4-48e0-49fe-9acc-666f5449a958) + +### SceneTextSearchWindow.cs +image diff --git a/Assets/Scripts/Editor/Tools/RectTransformCloner.cs b/Assets/Scripts/Editor/Tools/RectTransformCloner.cs new file mode 100644 index 0000000..fd55097 --- /dev/null +++ b/Assets/Scripts/Editor/Tools/RectTransformCloner.cs @@ -0,0 +1,151 @@ +// clones UI RectTransform values from one GameObject to another in Unity Editor (only for identical hierarchy) + +using UnityEngine; +using UnityEditor; +using TMPro; +using UnityEngine.UI; + +namespace UnityLibrary.Tools +{ + public class RectTransformCloner : EditorWindow + { + private GameObject source; + private GameObject target; + private bool requireIdenticalNames = true; + private bool cloneTMPAlignment = false; + + [MenuItem("Tools/RectTransform Cloner")] + private static void ShowWindow() + { + var window = GetWindow(); + window.titleContent = new GUIContent("RectTransform Cloner"); + window.Show(); + } + + private void OnGUI() + { + GUILayout.Label("Clone RectTransform", EditorStyles.boldLabel); + + source = (GameObject)EditorGUILayout.ObjectField("Source", source, typeof(GameObject), true); + target = (GameObject)EditorGUILayout.ObjectField("Target", target, typeof(GameObject), true); + requireIdenticalNames = EditorGUILayout.Toggle("Require Identical Names", requireIdenticalNames); + cloneTMPAlignment = EditorGUILayout.Toggle("Clone TMP Alignment", cloneTMPAlignment); + + if (GUILayout.Button("Clone RectTransforms")) + { + if (source == null || target == null) + { + Debug.LogError("Source and Target must be assigned."); + return; + } + + string errorMessage; + if (!CompareHierarchies(source.transform, target.transform, out errorMessage)) + { + Debug.LogError("Source and Target hierarchies do not match!\n" + errorMessage, target); + return; + } + + Undo.RegisterFullObjectHierarchyUndo(target, "Clone RectTransform Values"); + CopyRectTransforms(source.transform, target.transform); + + Debug.Log("RectTransform values cloned successfully.", target); + + if (target.transform.parent != null) + { + RectTransform parentRect = target.transform.parent as RectTransform; + if (parentRect != null) + { + LayoutRebuilder.ForceRebuildLayoutImmediate(parentRect); + } + else + { + Debug.LogWarning("Target's parent is not a RectTransform, cannot force layout rebuild.", target); + } + } + else + { + Debug.LogWarning("Target has no parent, cannot force layout rebuild.", target); + } + EditorUtility.SetDirty(target); + SceneView.RepaintAll(); + } + } + + private bool CompareHierarchies(Transform source, Transform target, out string errorMessage) + { + errorMessage = ""; + + if (source.childCount != target.childCount) + { + errorMessage = $"Child count mismatch at {GetTransformPath(source)}: Source has {source.childCount}, Target has {target.childCount}"; + return false; + } + + for (int i = 0; i < source.childCount; i++) + { + var sourceChild = source.GetChild(i); + var targetChild = target.GetChild(i); + + if (requireIdenticalNames && sourceChild.name != targetChild.name) + { + errorMessage = $"Child name mismatch at {GetTransformPath(sourceChild)}: Source has '{sourceChild.name}', Target has '{targetChild.name}'"; + return false; + } + + if (!CompareHierarchies(sourceChild, targetChild, out errorMessage)) + { + return false; + } + } + + return true; + } + + private void CopyRectTransforms(Transform source, Transform target) + { + var sourceRect = source as RectTransform; + var targetRect = target as RectTransform; + + if (sourceRect != null && targetRect != null) + { + CopyRectTransformValues(sourceRect, targetRect); + + if (cloneTMPAlignment) + { + var sourceTMP = source.GetComponent(); + var targetTMP = target.GetComponent(); + if (sourceTMP != null && targetTMP != null) + { + Undo.RecordObject(targetTMP, "Clone TMP Alignment"); + targetTMP.alignment = sourceTMP.alignment; + } + } + } + + for (int i = 0; i < source.childCount; i++) + { + CopyRectTransforms(source.GetChild(i), target.GetChild(i)); + } + } + + private void CopyRectTransformValues(RectTransform source, RectTransform target) + { + target.anchoredPosition = source.anchoredPosition; + target.sizeDelta = source.sizeDelta; + target.anchorMin = source.anchorMin; + target.anchorMax = source.anchorMax; + target.pivot = source.pivot; + target.localRotation = source.localRotation; + target.localScale = source.localScale; + target.localPosition = source.localPosition; + } + + private string GetTransformPath(Transform t) + { + if (t.parent == null) + return t.name; + return GetTransformPath(t.parent) + "/" + t.name; + } + } +} diff --git a/Assets/Scripts/Editor/Tools/ReferenceImageViewer2.cs b/Assets/Scripts/Editor/Tools/ReferenceImageViewer2.cs new file mode 100644 index 0000000..e8371c5 --- /dev/null +++ b/Assets/Scripts/Editor/Tools/ReferenceImageViewer2.cs @@ -0,0 +1,110 @@ +// reference image viewer from external folder + +using UnityEngine; +using UnityEditor; +using System.IO; + +namespace UnityLibrary.Tools +{ + public class ReferenceImageViewer2 : EditorWindow + { + private const string EditorPrefsKey = "ReferenceImageViewer_ImagePath"; + + [SerializeField] + private string imagePath = ""; + [SerializeField] + private Texture2D loadedImage; + + [MenuItem("Window/Reference Image Viewer")] + public static void ShowWindow() + { + GetWindow("Reference Image Viewer"); + } + + private void OnEnable() + { + // Delay the reload to ensure Unity's layout system is ready + EditorApplication.delayCall += TryLoadImage; + } + + private void OnDisable() + { + if (!string.IsNullOrEmpty(imagePath)) + EditorPrefs.SetString(EditorPrefsKey, imagePath); + else + EditorPrefs.DeleteKey(EditorPrefsKey); + } + + private void TryLoadImage() + { + imagePath = EditorPrefs.GetString(EditorPrefsKey, ""); + if (!string.IsNullOrEmpty(imagePath) && File.Exists(imagePath)) + { + LoadImageFromPath(imagePath); + Repaint(); // Ensure it's drawn + } + } + + void OnGUI() + { + GUILayout.Label("Reference Image Viewer", EditorStyles.boldLabel); + + if (GUILayout.Button("Browse for Image")) + { + string path = EditorUtility.OpenFilePanel("Select Image", "", "png,jpg,jpeg"); + if (!string.IsNullOrEmpty(path)) + { + imagePath = path; + EditorPrefs.SetString(EditorPrefsKey, imagePath); + LoadImageFromPath(imagePath); + } + } + + // Fallback in case delayCall missed + if (loadedImage == null && !string.IsNullOrEmpty(imagePath) && File.Exists(imagePath)) + { + LoadImageFromPath(imagePath); + Repaint(); + } + + if (loadedImage != null) + { + GUILayout.Space(10); + + float windowWidth = position.width - 20; + float imageAspect = (float)loadedImage.width / loadedImage.height; + + float displayWidth = windowWidth; + float displayHeight = windowWidth / imageAspect; + + if (displayHeight > position.height - 100) + { + displayHeight = position.height - 100; + displayWidth = displayHeight * imageAspect; + } + + Rect imageRect = GUILayoutUtility.GetRect(displayWidth, displayHeight, GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(false)); + EditorGUI.DrawPreviewTexture(imageRect, loadedImage, null, ScaleMode.ScaleToFit); + } + } + + private void LoadImageFromPath(string path) + { + try + { + byte[] fileData = File.ReadAllBytes(path); + loadedImage = new Texture2D(2, 2, TextureFormat.RGBA32, false); + if (!loadedImage.LoadImage(fileData)) + { + Debug.LogError("Failed to load image."); + loadedImage = null; + } + } + catch (System.Exception e) + { + Debug.LogError($"Could not load image from path: {path}\n{e.Message}"); + loadedImage = null; + } + } + } +} diff --git a/Assets/Scripts/Editor/Tools/SceneTextSearchWindow.cs b/Assets/Scripts/Editor/Tools/SceneTextSearchWindow.cs new file mode 100644 index 0000000..8467f3e --- /dev/null +++ b/Assets/Scripts/Editor/Tools/SceneTextSearchWindow.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEditor; +using UnityEngine; +using UnityEngine.UI; +using TMPro; + +namespace UnityLibrary.EditorTools +{ + public class SceneTextSearchWindow : EditorWindow + { + [Serializable] + private class SearchResult + { + public Component component; + public string text; + } + + private string searchTerm = string.Empty; + private string previousSearchTerm = string.Empty; + private bool caseSensitive = false; + private bool automaticSearch = true; + + private readonly List results = new List(); + private readonly HashSet seenComponents = new HashSet(); + private Vector2 scrollPos; + + [MenuItem("Tools/UnityLibrary/Scene Text Search")] + public static void Open() + { + var window = GetWindow("Scene Text Search"); + window.minSize = new Vector2(600, 300); + } + + private void OnGUI() + { + EditorGUILayout.LabelField("Search text in loaded scenes", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + EditorGUILayout.BeginHorizontal(); + + EditorGUILayout.LabelField("Search term", GUILayout.Width(80)); + + string newSearchTerm = EditorGUILayout.TextField(searchTerm); + + if (GUILayout.Button("Search", GUILayout.Width(80))) + { + DoSearch(); + } + + EditorGUILayout.EndHorizontal(); + + if (newSearchTerm != searchTerm) + { + searchTerm = newSearchTerm; + + if (automaticSearch) + { + if (!string.IsNullOrEmpty(searchTerm) && searchTerm.Length > 1) + { + DoSearch(); + } + else + { + results.Clear(); + seenComponents.Clear(); + } + } + + previousSearchTerm = searchTerm; + } + + EditorGUILayout.BeginHorizontal(); + caseSensitive = EditorGUILayout.Toggle("Case sensitive", caseSensitive); + automaticSearch = EditorGUILayout.Toggle("Automatic search", automaticSearch); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField($"Results: {results.Count}", EditorStyles.boldLabel); + + scrollPos = EditorGUILayout.BeginScrollView(scrollPos); + foreach (var r in results) + { + if (r.component == null) + continue; + + EditorGUILayout.BeginHorizontal(); + + EditorGUILayout.ObjectField(r.component, typeof(Component), true, GUILayout.Width(220)); + + using (new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.TextField(Truncate(r.text, 200)); + } + + EditorGUILayout.EndHorizontal(); + } + EditorGUILayout.EndScrollView(); + } + + private void DoSearch() + { + results.Clear(); + seenComponents.Clear(); + + if (string.IsNullOrEmpty(searchTerm)) + return; + + string term = caseSensitive ? searchTerm : searchTerm.ToLowerInvariant(); + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + // UnityEngine.UI.Text + SearchComponents(t => t.text, term); + + // TMP UGUI + SearchComponents(t => t.text, term); + + // TMP 3D + SearchComponents(t => t.text, term); + + // Legacy TextMesh + SearchComponents(t => t.text, term); + + // Generic "other text components" with a string 'text' property/field + SearchGenericTextComponents(term); + + stopwatch.Stop(); + Debug.Log($"SceneTextSearchWindow: Found {results.Count} results in {stopwatch.ElapsedMilliseconds} ms"); + } + + private void SearchComponents(Func getText, string term) where T : Component + { + var objects = Resources.FindObjectsOfTypeAll(); + foreach (var comp in objects) + { + if (!IsSceneObject(comp)) + continue; + + string value = getText(comp); + if (StringMatches(value, term)) + { + AddResult(comp, value); + } + } + } + + private void SearchGenericTextComponents(string term) + { + var monos = Resources.FindObjectsOfTypeAll(); + foreach (var mb in monos) + { + if (!IsSceneObject(mb)) + continue; + + if (seenComponents.Contains(mb)) + continue; + + Type type = mb.GetType(); + + try + { + var prop = type.GetProperty("text", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (prop != null && prop.PropertyType == typeof(string) && prop.CanRead) + { + string value = prop.GetValue(mb, null) as string; + if (StringMatches(value, term)) + { + AddResult(mb, value); + continue; + } + } + } + catch + { + } + + try + { + var field = type.GetField("text", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (field != null && field.FieldType == typeof(string)) + { + string value = field.GetValue(mb) as string; + if (StringMatches(value, term)) + { + AddResult(mb, value); + } + } + } + catch + { + } + } + } + + private bool StringMatches(string value, string term) + { + if (string.IsNullOrEmpty(value)) + return false; + + if (!caseSensitive) + value = value.ToLowerInvariant(); + + return value.Contains(term); + } + + private void AddResult(Component component, string text) + { + if (component == null) + return; + + if (seenComponents.Add(component)) + { + results.Add(new SearchResult + { + component = component, + text = text + }); + } + } + + private static bool IsSceneObject(Component comp) + { + if (comp == null) + return false; + + var go = comp.gameObject; + if (go == null) + return false; + + if (EditorUtility.IsPersistent(go)) + return false; + + if (!go.scene.IsValid() || !go.scene.isLoaded) + return false; + + return true; + } + + private static string Truncate(string input, int maxLength) + { + if (string.IsNullOrEmpty(input)) + return input; + if (input.Length <= maxLength) + return input; + return input.Substring(0, maxLength) + "..."; + } + } +} diff --git a/Assets/Scripts/UI/ToggleEvents.cs b/Assets/Scripts/UI/ToggleEvents.cs new file mode 100644 index 0000000..b4f0952 --- /dev/null +++ b/Assets/Scripts/UI/ToggleEvents.cs @@ -0,0 +1,44 @@ +// easy way to get Toggle state as events (compared to the default OnChanged, which triggers on both..) + +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; + +namespace UnityLibrary.UI +{ + [RequireComponent(typeof(Toggle))] + public class ToggleEvents : MonoBehaviour + { + [Header("Events")] + public UnityEvent OnChecked; + public UnityEvent OnUnchecked; + + private Toggle toggle; + + private void Awake() + { + toggle = GetComponent(); + toggle.onValueChanged.AddListener(HandleToggleChanged); + } + + private void OnDestroy() + { + if (toggle != null) + { + toggle.onValueChanged.RemoveListener(HandleToggleChanged); + } + } + + private void HandleToggleChanged(bool isOn) + { + if (isOn) + { + OnChecked?.Invoke(); + } + else + { + OnUnchecked?.Invoke(); + } + } + } +} diff --git a/Assets/Shaders/3D/Debug/UVChannelDebug.shader b/Assets/Shaders/3D/Debug/UVChannelDebug.shader new file mode 100644 index 0000000..b0b2c86 --- /dev/null +++ b/Assets/Shaders/3D/Debug/UVChannelDebug.shader @@ -0,0 +1,150 @@ +// debug view what channels contain UV data + +Shader "UnityLibrary/Debug/UVChannelDebug" +{ + Properties + { + _MainTex ("Texture", 2D) = "white" {} + _ColorUV0 ("UV0 Color", Color) = (1,0,0,1) + _ColorUV1 ("UV1 Color", Color) = (0,1,0,1) + _ColorUV2 ("UV2 Color", Color) = (0,0,1,1) + _ColorUV3 ("UV3 Color", Color) = (1,1,0,1) + _ColorUV4 ("UV4 Color", Color) = (1,0,1,1) + _ColorUV5 ("UV5 Color", Color) = (0,1,1,1) + _ColorUV6 ("UV6 Color", Color) = (1,1,1,1) + _ColorUV7 ("UV7 Color", Color) = (0,0,0,1) + + [Toggle] _EnableUV0 ("Enable UV0", Float) = 1 + [Toggle] _EnableUV1 ("Enable UV1", Float) = 1 + [Toggle] _EnableUV2 ("Enable UV2", Float) = 1 + [Toggle] _EnableUV3 ("Enable UV3", Float) = 1 + [Toggle] _EnableUV4 ("Enable UV4", Float) = 1 + [Toggle] _EnableUV5 ("Enable UV5", Float) = 1 + [Toggle] _EnableUV6 ("Enable UV6", Float) = 1 + [Toggle] _EnableUV7 ("Enable UV7", Float) = 1 + } + SubShader + { + Tags { "RenderType"="Opaque" } + LOD 100 + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + + #include "UnityCG.cginc" + + struct appdata + { + float4 vertex : POSITION; + float2 uv0 : TEXCOORD0; + float2 uv1 : TEXCOORD1; + float2 uv2 : TEXCOORD2; + float2 uv3 : TEXCOORD3; + float2 uv4 : TEXCOORD4; + float2 uv5 : TEXCOORD5; + float2 uv6 : TEXCOORD6; + float2 uv7 : TEXCOORD7; + }; + + struct v2f + { + float2 uv0 : TEXCOORD0; + float2 uv1 : TEXCOORD1; + float2 uv2 : TEXCOORD2; + float2 uv3 : TEXCOORD3; + float2 uv4 : TEXCOORD4; + float2 uv5 : TEXCOORD5; + float2 uv6 : TEXCOORD6; + float2 uv7 : TEXCOORD7; + float4 vertex : SV_POSITION; + }; + + sampler2D _MainTex; + float4 _MainTex_ST; + float4 _ColorUV0; + float4 _ColorUV1; + float4 _ColorUV2; + float4 _ColorUV3; + float4 _ColorUV4; + float4 _ColorUV5; + float4 _ColorUV6; + float4 _ColorUV7; + + float _EnableUV0; + float _EnableUV1; + float _EnableUV2; + float _EnableUV3; + float _EnableUV4; + float _EnableUV5; + float _EnableUV6; + float _EnableUV7; + + fixed UVChecker(float2 uv) + { + float2 cell = floor(uv * 8.0); + return fmod(cell.x + cell.y, 2.0); + } + + fixed HasUVData(float2 uv) + { + float variation = fwidth(uv.x) + fwidth(uv.y); + float magnitude = abs(uv.x) + abs(uv.y); + return step(1e-5, max(variation, magnitude)); + } + + v2f vert (appdata v) + { + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + + o.uv0 = TRANSFORM_TEX(v.uv0, _MainTex); + o.uv1 = TRANSFORM_TEX(v.uv1, _MainTex); + o.uv2 = TRANSFORM_TEX(v.uv2, _MainTex); + o.uv3 = TRANSFORM_TEX(v.uv3, _MainTex); + o.uv4 = TRANSFORM_TEX(v.uv4, _MainTex); + o.uv5 = TRANSFORM_TEX(v.uv5, _MainTex); + o.uv6 = TRANSFORM_TEX(v.uv6, _MainTex); + o.uv7 = TRANSFORM_TEX(v.uv7, _MainTex); + + return o; + } + + fixed4 frag (v2f i) : SV_Target + { + fixed4 col0 = tex2D(_MainTex, i.uv0) * _ColorUV0; + fixed4 col1 = tex2D(_MainTex, i.uv1) * _ColorUV1; + fixed4 col2 = tex2D(_MainTex, i.uv2) * _ColorUV2; + fixed4 col3 = tex2D(_MainTex, i.uv3) * _ColorUV3; + fixed4 col4 = tex2D(_MainTex, i.uv4) * _ColorUV4; + fixed4 col5 = tex2D(_MainTex, i.uv5) * _ColorUV5; + fixed4 col6 = tex2D(_MainTex, i.uv6) * _ColorUV6; + fixed4 col7 = tex2D(_MainTex, i.uv7) * _ColorUV7; + + float e0 = _EnableUV0 * HasUVData(i.uv0); + float e1 = _EnableUV1 * HasUVData(i.uv1); + float e2 = _EnableUV2 * HasUVData(i.uv2); + float e3 = _EnableUV3 * HasUVData(i.uv3); + float e4 = _EnableUV4 * HasUVData(i.uv4); + float e5 = _EnableUV5 * HasUVData(i.uv5); + float e6 = _EnableUV6 * HasUVData(i.uv6); + float e7 = _EnableUV7 * HasUVData(i.uv7); + + fixed4 col = + col0 * e0 + + col1 * e1 + + col2 * e2 + + col3 * e3 + + col4 * e4 + + col5 * e5 + + col6 * e6 + + col7 * e7; + + return saturate(col); + } + ENDCG + } + } +} diff --git a/Assets/Shaders/3D/Effects/Diffuse Stipple Transparency.shader b/Assets/Shaders/3D/Effects/Diffuse Stipple Transparency.shader new file mode 100644 index 0000000..853c4cf --- /dev/null +++ b/Assets/Shaders/3D/Effects/Diffuse Stipple Transparency.shader @@ -0,0 +1,52 @@ +// Diffuse shader with stipple transparency +// by Alex Ocias - https://ocias.com +// source: https://ocias.com/blog/unity-stipple-transparency-shader/ +// based on an article by Digital Rune: https://www.digitalrune.com/Blog/Post/1743/Screen-Door-Transparency + +// Simplified Diffuse shader. Differences from regular Diffuse one: +// - no Main Color +// - fully supports only 1 directional light. Other lights can affect it, but it will be per-vertex/SH. + +Shader "Ocias/Diffuse (Stipple Transparency)" { +Properties { + _MainTex ("Base (RGB)", 2D) = "white" {} + _Transparency ("Transparency", Range(0,1)) = 1.0 +} +SubShader { + Tags { "RenderType"="Opaque" } + LOD 150 + +CGPROGRAM +#pragma surface surf Lambert noforwardadd + +sampler2D _MainTex; + +struct Input { + float2 uv_MainTex; + float4 screenPos; +}; + +half _Transparency; + +void surf (Input IN, inout SurfaceOutput o) { + fixed4 c = tex2D(_MainTex, IN.uv_MainTex); + o.Albedo = c.rgb; + o.Alpha = c.a; + + // Screen-door transparency: Discard pixel if below threshold. + float4x4 thresholdMatrix = + { 1.0 / 17.0, 9.0 / 17.0, 3.0 / 17.0, 11.0 / 17.0, + 13.0 / 17.0, 5.0 / 17.0, 15.0 / 17.0, 7.0 / 17.0, + 4.0 / 17.0, 12.0 / 17.0, 2.0 / 17.0, 10.0 / 17.0, + 16.0 / 17.0, 8.0 / 17.0, 14.0 / 17.0, 6.0 / 17.0 + }; + float4x4 _RowAccess = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 }; + float2 pos = IN.screenPos.xy / IN.screenPos.w; + pos *= _ScreenParams.xy; // pixel position + clip(_Transparency - thresholdMatrix[fmod(pos.x, 4)] * _RowAccess[fmod(pos.y, 4)]); +} +ENDCG +} + +Fallback "Mobile/VertexLit" +} diff --git a/Assets/Shaders/3D/Effects/Standard Stipple Transparency.shader b/Assets/Shaders/3D/Effects/Standard Stipple Transparency.shader new file mode 100644 index 0000000..2e312da --- /dev/null +++ b/Assets/Shaders/3D/Effects/Standard Stipple Transparency.shader @@ -0,0 +1,58 @@ +// Standard shader with stipple transparency +// by Alex Ocias - https://ocias.com +// source: https://ocias.com/blog/unity-stipple-transparency-shader/ +// based on an article by Digital Rune: https://www.digitalrune.com/Blog/Post/1743/Screen-Door-Transparency + +Shader "Ocias/Standard (Stipple Transparency)" { + Properties { + _Color ("Color", Color) = (1,1,1,1) + _MainTex ("Albedo (RGB)", 2D) = "white" {} + _Glossiness ("Smoothness", Range(0,1)) = 0.5 + _Metallic ("Metallic", Range(0,1)) = 0.0 + } + SubShader { + Tags { "RenderType"="Opaque" } + LOD 100 + + CGPROGRAM + // Physically based Standard lighting model, and enable shadows on all light types + #pragma surface surf Standard fullforwardshadows + + // Use shader model 3.0 target, to get nicer looking lighting + #pragma target 3.0 + + sampler2D _MainTex; + + struct Input { + float2 uv_MainTex; + float4 screenPos; + }; + + half _Glossiness; + half _Metallic; + fixed4 _Color; + + void surf (Input IN, inout SurfaceOutputStandard o) { + // Albedo comes from a texture tinted by color + fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; + o.Albedo = c.rgb; + // Metallic and smoothness come from slider variables + o.Metallic = _Metallic; + o.Smoothness = _Glossiness; + + // Screen-door transparency: Discard pixel if below threshold. + float4x4 thresholdMatrix = + { 1.0 / 17.0, 9.0 / 17.0, 3.0 / 17.0, 11.0 / 17.0, + 13.0 / 17.0, 5.0 / 17.0, 15.0 / 17.0, 7.0 / 17.0, + 4.0 / 17.0, 12.0 / 17.0, 2.0 / 17.0, 10.0 / 17.0, + 16.0 / 17.0, 8.0 / 17.0, 14.0 / 17.0, 6.0 / 17.0 + }; + float4x4 _RowAccess = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 }; + float2 pos = IN.screenPos.xy / IN.screenPos.w; + pos *= _ScreenParams.xy; // pixel position + clip(c.a - thresholdMatrix[fmod(pos.x, 4)] * _RowAccess[fmod(pos.y, 4)]); + } + ENDCG + } + FallBack "Diffuse" +}