超级NB Gemini大师手作快速选择器(shift+右键唤起)
Signed-off-by: TRAfoer <lhf190@outlook.com>
This commit is contained in:
231
Assets/Editor/QuickSelector.CS
Normal file
231
Assets/Editor/QuickSelector.CS
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
[InitializeOnLoad]
|
||||||
|
public class QuickSelectorHud
|
||||||
|
{
|
||||||
|
public static GameObject HoveredObject;
|
||||||
|
|
||||||
|
static QuickSelectorHud()
|
||||||
|
{
|
||||||
|
SceneView.duringSceneGui += OnSceneGUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnSceneGUI(SceneView sceneView)
|
||||||
|
{
|
||||||
|
if (HoveredObject != null)
|
||||||
|
{
|
||||||
|
DrawHighlight(HoveredObject);
|
||||||
|
sceneView.Repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
Event e = Event.current;
|
||||||
|
if (e.type == EventType.MouseDown && e.button == 1 && e.shift)
|
||||||
|
{
|
||||||
|
var entries = CollectData(sceneView, e.mousePosition);
|
||||||
|
QuickSelectorPopup popup = new QuickSelectorPopup(entries);
|
||||||
|
PopupWindow.Show(new Rect(e.mousePosition.x, e.mousePosition.y, 0, 0), popup);
|
||||||
|
e.Use();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawHighlight(GameObject go)
|
||||||
|
{
|
||||||
|
Handles.color = new Color(0f, 0.7f, 1f, 1f); // 亮蓝色
|
||||||
|
RectTransform rt = go.GetComponent<RectTransform>();
|
||||||
|
if (rt != null)
|
||||||
|
{
|
||||||
|
Vector3[] corners = new Vector3[4];
|
||||||
|
rt.GetWorldCorners(corners);
|
||||||
|
Handles.DrawPolyLine(corners[0], corners[1], corners[2], corners[3], corners[0]);
|
||||||
|
Handles.color = new Color(0f, 0.7f, 1f, 0.1f);
|
||||||
|
Handles.DrawAAConvexPolygon(corners);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Renderer r = go.GetComponent<Renderer>();
|
||||||
|
if (r != null) Handles.DrawWireCube(r.bounds.center, r.bounds.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<SelectionEntry> CollectData(SceneView sceneView, Vector2 mousePos)
|
||||||
|
{
|
||||||
|
bool enableUI = EditorPrefs.GetBool("QS_UI", true);
|
||||||
|
bool enable3D = EditorPrefs.GetBool("QS_3D", true);
|
||||||
|
Vector2 guiPos = mousePos;
|
||||||
|
guiPos.y = sceneView.camera.pixelHeight - guiPos.y;
|
||||||
|
Ray ray = HandleUtility.GUIPointToWorldRay(mousePos);
|
||||||
|
|
||||||
|
HashSet<GameObject> processed = new HashSet<GameObject>();
|
||||||
|
List<SelectionEntry> list = new List<SelectionEntry>();
|
||||||
|
|
||||||
|
GameObject smart = HandleUtility.PickGameObject(mousePos, false);
|
||||||
|
if (smart != null) AddEntry(list, processed, smart, "PICK");
|
||||||
|
|
||||||
|
if (enableUI)
|
||||||
|
{
|
||||||
|
var rects = GameObject.FindObjectsByType<RectTransform>(FindObjectsSortMode.None);
|
||||||
|
foreach (var rect in rects)
|
||||||
|
{
|
||||||
|
if (rect.gameObject.activeInHierarchy && RectTransformUtility.RectangleContainsScreenPoint(rect, guiPos, sceneView.camera))
|
||||||
|
AddEntry(list, processed, rect.gameObject, "UI");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enable3D)
|
||||||
|
{
|
||||||
|
RaycastHit[] hits = Physics.RaycastAll(ray, float.MaxValue);
|
||||||
|
foreach (var hit in hits) AddEntry(list, processed, hit.collider.gameObject, "3D");
|
||||||
|
foreach (var r in GameObject.FindObjectsByType<Renderer>(FindObjectsSortMode.None))
|
||||||
|
if (!processed.Contains(r.gameObject) && r.bounds.IntersectRay(ray, out _))
|
||||||
|
AddEntry(list, processed, r.gameObject, "Mesh");
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddEntry(List<SelectionEntry> list, HashSet<GameObject> set, GameObject go, string src)
|
||||||
|
{
|
||||||
|
if (go == null || !set.Add(go)) return;
|
||||||
|
string maj = (go.GetComponent<RectTransform>() != null) ? "UI" : (go.GetComponent<Collider>() != null ? "3D" : "Mesh");
|
||||||
|
string min = (maj == "UI") ? GetUIType(go) : (go.GetComponent<Renderer>()?.GetType().Name ?? "Object");
|
||||||
|
list.Add(new SelectionEntry { go = go, root = GetRoot(go.transform), depth = GetDepth(go.transform), sibling = go.transform.GetSiblingIndex(), major = maj, minor = min });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetUIType(GameObject go)
|
||||||
|
{
|
||||||
|
if (go.GetComponent("TextMeshProUGUI")) return "TMPro";
|
||||||
|
if (go.GetComponent<Text>()) return "Text";
|
||||||
|
if (go.GetComponent<Image>()) return "Image";
|
||||||
|
return "Rect";
|
||||||
|
}
|
||||||
|
private static GameObject GetRoot(Transform t) { while (t.parent != null) t = t.parent; return t.gameObject; }
|
||||||
|
private static int GetDepth(Transform t) { int d = 0; while (t.parent != null) { d++; t = t.parent; } return d; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SelectionEntry { public GameObject go, root; public int depth, sibling; public string major, minor; }
|
||||||
|
|
||||||
|
public class QuickSelectorPopup : PopupWindowContent
|
||||||
|
{
|
||||||
|
private List<SelectionEntry> _entries;
|
||||||
|
private List<DisplayItem> _displayItems;
|
||||||
|
private Vector2 _scroll;
|
||||||
|
private GUIStyle _hoverStyle;
|
||||||
|
private GUIStyle _richLabelStyle; // 修正点:手动创建支持富文本的 Style
|
||||||
|
|
||||||
|
public QuickSelectorPopup(List<SelectionEntry> entries)
|
||||||
|
{
|
||||||
|
_entries = entries;
|
||||||
|
RefreshList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Vector2 GetWindowSize() => new Vector2(280, Mathf.Min((_displayItems.Count * 22) + 30, 450));
|
||||||
|
|
||||||
|
public override void OnGUI(Rect rect)
|
||||||
|
{
|
||||||
|
// 样式初始化
|
||||||
|
if (_hoverStyle == null)
|
||||||
|
{
|
||||||
|
_hoverStyle = new GUIStyle(EditorStyles.label);
|
||||||
|
_hoverStyle.normal.background = MakeTex(2, 2, new Color(0.2f, 0.5f, 1f, 0.4f));
|
||||||
|
}
|
||||||
|
if (_richLabelStyle == null)
|
||||||
|
{
|
||||||
|
_richLabelStyle = new GUIStyle(EditorStyles.label);
|
||||||
|
_richLabelStyle.richText = true; // 关键修正:开启富文本支持
|
||||||
|
}
|
||||||
|
|
||||||
|
_scroll = EditorGUILayout.BeginScrollView(_scroll);
|
||||||
|
Event e = Event.current;
|
||||||
|
|
||||||
|
foreach (var item in _displayItems)
|
||||||
|
{
|
||||||
|
if (item.isSep) { EditorGUILayout.LabelField("", GUI.skin.horizontalSlider); continue; }
|
||||||
|
|
||||||
|
Rect r = EditorGUILayout.GetControlRect(false, 20);
|
||||||
|
bool isHover = r.Contains(e.mousePosition);
|
||||||
|
|
||||||
|
if (isHover)
|
||||||
|
{
|
||||||
|
GUI.Box(r, "", _hoverStyle);
|
||||||
|
if (QuickSelectorHud.HoveredObject != item.ent.go)
|
||||||
|
{
|
||||||
|
QuickSelectorHud.HoveredObject = item.ent.go;
|
||||||
|
EditorGUIUtility.PingObject(item.ent.go);
|
||||||
|
SceneView.RepaintAll();
|
||||||
|
}
|
||||||
|
if (e.type == EventType.MouseDown && e.button == 0)
|
||||||
|
{
|
||||||
|
Selection.activeGameObject = item.ent.go;
|
||||||
|
this.editorWindow.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用修正后的富文本样式绘制
|
||||||
|
GUI.Label(r, item.label, _richLabelStyle);
|
||||||
|
}
|
||||||
|
EditorGUILayout.EndScrollView();
|
||||||
|
|
||||||
|
DrawToolbar();
|
||||||
|
|
||||||
|
if (e.type == EventType.MouseMove && !rect.Contains(e.mousePosition))
|
||||||
|
{
|
||||||
|
QuickSelectorHud.HoveredObject = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawToolbar()
|
||||||
|
{
|
||||||
|
GUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||||
|
if (GUILayout.Toggle(EditorPrefs.GetBool("QS_UI", true), "UI", EditorStyles.toolbarButton) != EditorPrefs.GetBool("QS_UI", true))
|
||||||
|
{
|
||||||
|
EditorPrefs.SetBool("QS_UI", !EditorPrefs.GetBool("QS_UI")); RefreshList();
|
||||||
|
}
|
||||||
|
if (GUILayout.Toggle(EditorPrefs.GetBool("QS_3D", true), "3D", EditorStyles.toolbarButton) != EditorPrefs.GetBool("QS_3D", true))
|
||||||
|
{
|
||||||
|
EditorPrefs.SetBool("QS_3D", !EditorPrefs.GetBool("QS_3D")); RefreshList();
|
||||||
|
}
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
if (GUILayout.Toggle(EditorPrefs.GetBool("QS_REV", false), "Reverse", EditorStyles.toolbarButton) != EditorPrefs.GetBool("QS_REV", false))
|
||||||
|
{
|
||||||
|
EditorPrefs.SetBool("QS_REV", !EditorPrefs.GetBool("QS_REV")); RefreshList();
|
||||||
|
}
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnClose() { QuickSelectorHud.HoveredObject = null; SceneView.RepaintAll(); }
|
||||||
|
|
||||||
|
private void RefreshList()
|
||||||
|
{
|
||||||
|
_displayItems = new List<DisplayItem>();
|
||||||
|
bool rev = EditorPrefs.GetBool("QS_REV", false);
|
||||||
|
var groups = _entries.GroupBy(x => x.root).OrderByDescending(g => g.Any(e => e.major == "UI")).ThenBy(g => g.Key.name);
|
||||||
|
bool first = true;
|
||||||
|
foreach (var g in groups)
|
||||||
|
{
|
||||||
|
if (!first) _displayItems.Add(new DisplayItem { isSep = true });
|
||||||
|
first = false;
|
||||||
|
var sorted = rev ? g.OrderByDescending(x => x.depth).ThenByDescending(x => x.sibling) : g.OrderBy(x => x.depth).ThenBy(x => x.sibling);
|
||||||
|
int minD = g.Min(x => x.depth);
|
||||||
|
int maxD = g.Max(x => x.depth);
|
||||||
|
foreach (var ent in sorted)
|
||||||
|
{
|
||||||
|
int ind = rev ? (maxD - ent.depth) : (ent.depth - minD);
|
||||||
|
string color = (ent.major == "UI" ? "#00E6FF" : "#AAAAAA"); // 调整 UI 标签为更亮的青色
|
||||||
|
string label = $"{new string(' ', ind * 4)}<color={color}>[{ent.major}|{ent.minor}]</color> {ent.go.name}";
|
||||||
|
_displayItems.Add(new DisplayItem { ent = ent, label = label });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (editorWindow != null) editorWindow.Repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Texture2D MakeTex(int w, int h, Color col)
|
||||||
|
{
|
||||||
|
Color[] pix = new Color[w * h]; for (int i = 0; i < pix.Length; i++) pix[i] = col;
|
||||||
|
Texture2D t = new Texture2D(w, h); t.SetPixels(pix); t.Apply(); return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DisplayItem { public bool isSep; public SelectionEntry ent; public string label; }
|
||||||
|
}
|
||||||
11
Assets/Editor/QuickSelector.CS.meta
Normal file
11
Assets/Editor/QuickSelector.CS.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: cd4b3a300a1a28f448297100b29f7440
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
180
Assets/Editor/QuickSelector.cx
Normal file
180
Assets/Editor/QuickSelector.cx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
[InitializeOnLoad]
|
||||||
|
public class QuickSelector//原生菜单
|
||||||
|
{
|
||||||
|
// 配置键名
|
||||||
|
private const string PREF_UI = "QS_EnableUI";
|
||||||
|
private const string PREF_3D = "QS_Enable3D";
|
||||||
|
private const string PREF_REVERSE = "QS_ReverseOrder";
|
||||||
|
|
||||||
|
static QuickSelector()
|
||||||
|
{
|
||||||
|
SceneView.duringSceneGui += OnSceneGUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnSceneGUI(SceneView sceneView)
|
||||||
|
{
|
||||||
|
Event e = Event.current;
|
||||||
|
|
||||||
|
// Shift + 右键触发
|
||||||
|
if (e.type == EventType.MouseDown && e.button == 1 && e.shift)
|
||||||
|
{
|
||||||
|
// 加载配置
|
||||||
|
bool enableUI = EditorPrefs.GetBool(PREF_UI, true);
|
||||||
|
bool enable3D = EditorPrefs.GetBool(PREF_3D, true);
|
||||||
|
bool reverseOrder = EditorPrefs.GetBool(PREF_REVERSE, false);
|
||||||
|
|
||||||
|
Vector2 guiPos = e.mousePosition;
|
||||||
|
guiPos.y = sceneView.camera.pixelHeight - guiPos.y;
|
||||||
|
Ray ray = HandleUtility.GUIPointToWorldRay(e.mousePosition);
|
||||||
|
|
||||||
|
HashSet<GameObject> processedObjects = new HashSet<GameObject>();
|
||||||
|
List<SelectionEntry> entries = new List<SelectionEntry>();
|
||||||
|
|
||||||
|
// 1. 智能拾取 (PickGameObject)
|
||||||
|
GameObject smartPick = HandleUtility.PickGameObject(e.mousePosition, false);
|
||||||
|
if (smartPick != null) AddEntry(entries, processedObjects, smartPick, "PICK");
|
||||||
|
|
||||||
|
// 2. UI 扫描
|
||||||
|
if (enableUI)
|
||||||
|
{
|
||||||
|
var allRects = GameObject.FindObjectsByType<RectTransform>(FindObjectsSortMode.None);
|
||||||
|
foreach (var rect in allRects)
|
||||||
|
{
|
||||||
|
if (!rect.gameObject.activeInHierarchy) continue;
|
||||||
|
if (RectTransformUtility.RectangleContainsScreenPoint(rect, guiPos, sceneView.camera))
|
||||||
|
AddEntry(entries, processedObjects, rect.gameObject, "UI");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 3D/Mesh 扫描
|
||||||
|
if (enable3D)
|
||||||
|
{
|
||||||
|
RaycastHit[] hits = Physics.RaycastAll(ray, float.MaxValue);
|
||||||
|
foreach (var hit in hits) AddEntry(entries, processedObjects, hit.collider.gameObject, "3D");
|
||||||
|
|
||||||
|
Renderer[] allRenderers = GameObject.FindObjectsByType<Renderer>(FindObjectsSortMode.None);
|
||||||
|
foreach (var r in allRenderers)
|
||||||
|
{
|
||||||
|
if (processedObjects.Contains(r.gameObject)) continue;
|
||||||
|
if (r.bounds.IntersectRay(ray, out _)) AddEntry(entries, processedObjects, r.gameObject, "Mesh");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.Count > 0 || true)
|
||||||
|
{
|
||||||
|
GenericMenu menu = new GenericMenu();
|
||||||
|
|
||||||
|
// 根节点分组:UI组优先
|
||||||
|
var groups = entries.GroupBy(x => x.rootObject)
|
||||||
|
.OrderByDescending(g => g.Any(ent => ent.majorType == "UI"))
|
||||||
|
.ThenBy(g => g.Key.name);
|
||||||
|
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
if (menu.GetItemCount() > 0) menu.AddSeparator("");
|
||||||
|
|
||||||
|
// 组内排序
|
||||||
|
var sortedInGroup = reverseOrder
|
||||||
|
? group.OrderByDescending(x => x.depth).ThenByDescending(x => x.siblingIndex)
|
||||||
|
: group.OrderBy(x => x.depth).ThenBy(x => x.siblingIndex);
|
||||||
|
|
||||||
|
int minDepth = group.Min(x => x.depth);
|
||||||
|
int maxDepth = group.Max(x => x.depth);
|
||||||
|
|
||||||
|
foreach (var entry in sortedInGroup)
|
||||||
|
{
|
||||||
|
// --- 修复点:缩进计算 ---
|
||||||
|
// 顺序:深度越深缩进越多 (entry.depth - minDepth)
|
||||||
|
// 逆序:深度越浅缩进越多 (maxDepth - entry.depth)
|
||||||
|
int indentLevel = reverseOrder ? (maxDepth - entry.depth) : (entry.depth - minDepth);
|
||||||
|
|
||||||
|
string indent = new string(' ', indentLevel * 2);
|
||||||
|
string label = $"{indent}[ {entry.majorType} | {entry.minorType} ]: {entry.gameObject.name}";
|
||||||
|
|
||||||
|
menu.AddItem(new GUIContent(label), false, () =>
|
||||||
|
{
|
||||||
|
Selection.activeGameObject = entry.gameObject;
|
||||||
|
EditorGUIUtility.PingObject(entry.gameObject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置项
|
||||||
|
menu.AddSeparator("");
|
||||||
|
menu.AddItem(new GUIContent("Settings/Detect UI"), enableUI, () => EditorPrefs.SetBool(PREF_UI, !enableUI));
|
||||||
|
menu.AddItem(new GUIContent("Settings/Detect 3D"), enable3D, () => EditorPrefs.SetBool(PREF_3D, !enable3D));
|
||||||
|
menu.AddItem(new GUIContent("Settings/Reverse Order"), reverseOrder, () => EditorPrefs.SetBool(PREF_REVERSE, !reverseOrder));
|
||||||
|
|
||||||
|
menu.ShowAsContext();
|
||||||
|
e.Use();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddEntry(List<SelectionEntry> list, HashSet<GameObject> set, GameObject go, string source)
|
||||||
|
{
|
||||||
|
if (go == null || !set.Add(go)) return;
|
||||||
|
|
||||||
|
string major = "3D";
|
||||||
|
string minor = "Object";
|
||||||
|
|
||||||
|
if (source == "UI" || go.GetComponent<RectTransform>() != null)
|
||||||
|
{
|
||||||
|
major = "UI"; minor = GetUIComponentType(go);
|
||||||
|
}
|
||||||
|
else if (go.GetComponent<Collider>() != null)
|
||||||
|
{
|
||||||
|
major = "3D"; minor = "Collider";
|
||||||
|
}
|
||||||
|
else if (go.GetComponent<Renderer>() != null)
|
||||||
|
{
|
||||||
|
major = "Mesh"; minor = go.GetComponent<Renderer>().GetType().Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(new SelectionEntry
|
||||||
|
{
|
||||||
|
gameObject = go,
|
||||||
|
rootObject = GetRoot(go.transform),
|
||||||
|
depth = GetDepth(go.transform),
|
||||||
|
siblingIndex = go.transform.GetSiblingIndex(),
|
||||||
|
majorType = major,
|
||||||
|
minorType = minor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetUIComponentType(GameObject go)
|
||||||
|
{
|
||||||
|
if (go.GetComponent("TextMeshProUGUI")) return "TMPro";
|
||||||
|
if (go.GetComponent<Text>()) return "Text";
|
||||||
|
if (go.GetComponent<Button>()) return "Button";
|
||||||
|
if (go.GetComponent<Image>()) return "Image";
|
||||||
|
return "Rect";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameObject GetRoot(Transform t)
|
||||||
|
{
|
||||||
|
Transform curr = t; while (curr.parent != null) curr = curr.parent; return curr.gameObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetDepth(Transform t)
|
||||||
|
{
|
||||||
|
int d = 0; while (t.parent != null) { d++; t = t.parent; }
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SelectionEntry
|
||||||
|
{
|
||||||
|
public GameObject gameObject;
|
||||||
|
public GameObject rootObject;
|
||||||
|
public int depth;
|
||||||
|
public int siblingIndex;
|
||||||
|
public string majorType;
|
||||||
|
public string minorType;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Assets/Editor/QuickSelector.cx.meta
Normal file
7
Assets/Editor/QuickSelector.cx.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 095a8c36a79637a4a83ddb4ce8049d10
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Reference in New Issue
Block a user