```
chore(bridge): 删除桥接包的提交记录和Git属性配置 删除了 .codely-bridge-commit 文件中存储的提交哈希值 移除了 .gitattributes 文件中的行结束符配置规则 ```
This commit is contained in:
1 parent
d26d323912
commit
13d0c80335
392 files changed
+1814
-1895
No files matched your search
-127
@@ -1,127 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
using Cn.Tuanjie.Codely.Editor;
|
||||
#endif
|
||||
|
||||
namespace UnityTcp.Editor.Notifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Pushes messages from the Editor/engine into the input field of the
|
||||
/// embedded AI agent (CodelyWindow) as an "input_from_editor" event:
|
||||
/// user-typed messages, engine errors, stack traces, or any other info the
|
||||
/// agent should react to. Distinguish the kind of message via the
|
||||
/// <c>source</c>/<c>type</c> arguments.
|
||||
///
|
||||
/// If no CodelyWindow is open, one is opened and the message is queued
|
||||
/// instead of broadcast; the frontend drains the queue via manage_editor
|
||||
/// action "drain_agent_input" once its page is fully loaded, so cold-start
|
||||
/// messages are not lost. With a window already open, the message is
|
||||
/// broadcast directly.
|
||||
///
|
||||
/// Payload shape: <c>{ text, source, type, autoSubmit, timestamp, attachments? }</c>.
|
||||
/// </summary>
|
||||
public static class AgentInputNotifier
|
||||
{
|
||||
private const string EventType = "input_from_editor";
|
||||
private const int MaxPendingMessages = 100;
|
||||
|
||||
private static readonly object _gate = new object();
|
||||
private static readonly List<JObject> _pending = new List<JObject>();
|
||||
|
||||
/// <summary>
|
||||
/// Sends <paramref name="text"/> to the embedded agent's input field.
|
||||
/// Must be called from the main thread (may open an EditorWindow).
|
||||
/// </summary>
|
||||
/// <param name="text">Message to place into the agent input field.</param>
|
||||
/// <param name="source">Origin of the message; defaults to "editor_window".</param>
|
||||
/// <param name="type">Kind of message; defaults to "user_message".</param>
|
||||
/// <param name="autoSubmit">True to have the frontend submit the message immediately instead of just appending it.</param>
|
||||
/// <param name="timestamp">Time the message was created; defaults to now (UTC).</param>
|
||||
/// <param name="attachments">Optional file paths the frontend attaches (@) to the message; may be null.</param>
|
||||
/// <returns>
|
||||
/// True if broadcast to at least one client; false if invalid or queued
|
||||
/// for the frontend to drain (no CodelyWindow was open yet).
|
||||
/// </returns>
|
||||
public static bool Send(
|
||||
string text,
|
||||
string source = "editor_window",
|
||||
string type = "user_message",
|
||||
bool autoSubmit = false,
|
||||
DateTime? timestamp = null,
|
||||
IReadOnlyList<string> attachments = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var payload = new JObject
|
||||
{
|
||||
["text"] = text,
|
||||
["source"] = source,
|
||||
["type"] = type,
|
||||
["autoSubmit"] = autoSubmit,
|
||||
["timestamp"] = (timestamp ?? DateTime.UtcNow).ToString("o"),
|
||||
};
|
||||
|
||||
var attachmentPaths = attachments
|
||||
?.Select(p => p?.Trim())
|
||||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||||
.ToList();
|
||||
if (attachmentPaths != null && attachmentPaths.Count > 0)
|
||||
payload["attachments"] = JArray.FromObject(attachmentPaths);
|
||||
|
||||
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
|
||||
// The CodelyWindow's presence is the one reliable signal here:
|
||||
// connection state can lie (e.g. a standalone app client makes
|
||||
// NotifyAll report delivered while the embedded page is still
|
||||
// loading). No window → open one, queue the message, and let
|
||||
// the frontend drain the queue once its page is fully loaded.
|
||||
if (!EditorWindow.HasOpenInstances<CodelyWindow>())
|
||||
{
|
||||
CodelyWindow.ShowWindow();
|
||||
QueuePending(payload);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return UnityTcpBridge.IsRunning
|
||||
&& UnityTcpBridge.NotifyAll(EventType, payload);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try { CodelyLogger.LogError($"[AgentInputNotifier] send failed: {e.Message}"); } catch { }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void QueuePending(JObject payload)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_pending.Count >= MaxPendingMessages)
|
||||
_pending.RemoveAt(0); // drop the oldest
|
||||
_pending.Add(payload);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all queued (undelivered) messages and clears the queue.
|
||||
/// Called by manage_editor's "drain_agent_input" action on behalf of
|
||||
/// the frontend once its page is fully loaded.
|
||||
/// </summary>
|
||||
public static List<JObject> DrainPending()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var items = new List<JObject>(_pending);
|
||||
_pending.Clear();
|
||||
return items;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 414cb485b5676eda8624d218acd342b6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Codely.Newtonsoft.Json;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Notifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Batches new Unity console activity and forwards it to notifier clients as a
|
||||
/// "console_log_changed" event (one flush per editor tick).
|
||||
///
|
||||
/// Source: <see cref="Application.logMessageReceivedThreaded"/>, which fires for
|
||||
/// every console entry regardless of the thread it was raised on — Debug.* calls,
|
||||
/// uncaught exceptions, AND C# compiler errors/warnings (Unity routes those through
|
||||
/// the log system too; their file/line/column are embedded in the message text,
|
||||
/// e.g. "Assets/Foo.cs(12,3): error CS1002: ; expected"). Subscribing to this one
|
||||
/// callback covers all of them without duplicates — also hooking
|
||||
/// CompilationPipeline.assemblyCompilationFinished would re-deliver every compiler
|
||||
/// message a second time.
|
||||
///
|
||||
/// The payload mirrors what <see cref="UnityTcp.Editor.Tools.ReadConsole"/>'s
|
||||
/// action="get" with types=["all"] returns, i.e.
|
||||
/// <see cref="Response.Success(string,object,bool,object)"/> wrapping the entry
|
||||
/// array: <c>{ success, message, rev, data: [ { type, message, file, line, stackTrace }, … ] }</c>.
|
||||
/// </summary>
|
||||
// [InitializeOnLoad] // disable this notifier
|
||||
public static class ConsoleLogChangedNotifier
|
||||
{
|
||||
private const string EventType = "console_log_changed";
|
||||
|
||||
// Safety valve for pathological single-tick bursts (e.g. Debug.Log in a tight
|
||||
// loop): never queue more than this many entries per flush.
|
||||
private const int MaxEntriesPerFlush = 5000;
|
||||
|
||||
private static readonly object _gate = new object();
|
||||
private static readonly List<object> _pending = new List<object>();
|
||||
|
||||
// Re-entrancy guard: a log emitted while we're flushing (e.g. CodelyLogger.LogError
|
||||
// on failure) must not be re-queued and re-flushed forever.
|
||||
private static volatile bool _flushing;
|
||||
|
||||
static ConsoleLogChangedNotifier()
|
||||
{
|
||||
Application.logMessageReceivedThreaded -= OnLogMessageThreaded;
|
||||
Application.logMessageReceivedThreaded += OnLogMessageThreaded;
|
||||
|
||||
// Drained on the main thread regardless of which thread enqueued.
|
||||
EditorApplication.update -= Tick;
|
||||
EditorApplication.update += Tick;
|
||||
}
|
||||
|
||||
// ---- Source ---------------------------------------------------------- //
|
||||
|
||||
private static void OnLogMessageThreaded(string message, string stackTrace, LogType type)
|
||||
{
|
||||
if (_flushing) return; // ignore logs caused by our own flush
|
||||
Enqueue(MakeEntry(
|
||||
type: type.ToString(),
|
||||
message: message,
|
||||
file: null,
|
||||
line: 0,
|
||||
stackTrace: string.IsNullOrEmpty(stackTrace) ? null : stackTrace));
|
||||
}
|
||||
|
||||
// Entry shape matches ReadConsole's "detailed" formatting exactly:
|
||||
// { type, message, file, line, stackTrace }
|
||||
private static Dictionary<string, object> MakeEntry(
|
||||
string type, string message, string file, int line, string stackTrace)
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "type", type },
|
||||
{ "message", message },
|
||||
{ "file", file },
|
||||
{ "line", line },
|
||||
{ "stackTrace", stackTrace },
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Queue + flush --------------------------------------------------- //
|
||||
|
||||
private static void Enqueue(Dictionary<string, object> entry)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_pending.Count >= MaxEntriesPerFlush) return;
|
||||
_pending.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Tick() => Flush();
|
||||
|
||||
private static void Flush()
|
||||
{
|
||||
object[] entries;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_pending.Count == 0) return;
|
||||
entries = _pending.ToArray();
|
||||
_pending.Clear();
|
||||
}
|
||||
|
||||
_flushing = true;
|
||||
try
|
||||
{
|
||||
if (!UnityTcpBridge.IsRunning) return; // can't deliver — drop the batch
|
||||
|
||||
// Same shape as ReadConsole.HandleCommand(action="get", types=["all"]):
|
||||
// Response.Success("Retrieved N log entries.", <entries array>)
|
||||
object response = Response.Success($"Retrieved {entries.Length} log entries.", entries);
|
||||
JObject payload = JObject.FromObject(response);
|
||||
UnityTcpBridge.NotifyAll(EventType, payload);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try { CodelyLogger.LogError($"[ConsoleLogChangedNotifier] flush failed: {e.Message}"); } catch { }
|
||||
}
|
||||
finally
|
||||
{
|
||||
_flushing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 556bc6845e3743828cc9ad3410ac6344
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-273
@@ -1,273 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Notifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Broadcasts the active scene's hierarchy to notifier clients whenever it
|
||||
/// changes. The emitted payload mirrors the structure returned by
|
||||
/// <see cref="ManageScene"/>'s get_hierarchy action (including the
|
||||
/// roots_only fallback for large scenes).
|
||||
/// </summary>
|
||||
// [InitializeOnLoad] // disable this notifier
|
||||
public static class SceneHierarchyNotifier
|
||||
{
|
||||
private const string EventType = "scene_hierarchy_changed";
|
||||
|
||||
// Matches ManageScene.GetSceneHierarchy cap.
|
||||
private const int LargeSceneThreshold = 500;
|
||||
|
||||
// Debounce window: every event extends the deadline; flush only after this much idle time.
|
||||
// Keeps flush rate sane during bursts of edits (drag, multi-select rename, scripted spawns).
|
||||
private const double DebounceSeconds = 0.25;
|
||||
|
||||
private static double _flushDeadline;
|
||||
private static bool _tickRegistered;
|
||||
|
||||
static SceneHierarchyNotifier()
|
||||
{
|
||||
EditorApplication.hierarchyChanged += OnHierarchyChanged;
|
||||
EditorSceneManager.sceneOpened += OnSceneOpened;
|
||||
EditorSceneManager.sceneClosed += OnSceneClosed;
|
||||
EditorSceneManager.sceneSaved += OnSceneSaved;
|
||||
EditorSceneManager.activeSceneChangedInEditMode += OnActiveSceneChanged;
|
||||
}
|
||||
|
||||
// ---- Event hooks (debounced) -------------------------------------- //
|
||||
|
||||
private static void OnHierarchyChanged() => ScheduleFlush();
|
||||
private static void OnSceneOpened(Scene s, OpenSceneMode mode) => ScheduleFlush();
|
||||
private static void OnSceneClosed(Scene s) => ScheduleFlush();
|
||||
private static void OnSceneSaved(Scene s) => ScheduleFlush();
|
||||
private static void OnActiveSceneChanged(Scene previous, Scene current) => ScheduleFlush();
|
||||
|
||||
private static void ScheduleFlush()
|
||||
{
|
||||
_flushDeadline = EditorApplication.timeSinceStartup + DebounceSeconds;
|
||||
if (_tickRegistered) return;
|
||||
_tickRegistered = true;
|
||||
EditorApplication.update += DebounceTick;
|
||||
}
|
||||
|
||||
private static void DebounceTick()
|
||||
{
|
||||
if (EditorApplication.timeSinceStartup < _flushDeadline) return;
|
||||
|
||||
EditorApplication.update -= DebounceTick;
|
||||
_tickRegistered = false;
|
||||
Flush();
|
||||
}
|
||||
|
||||
private static void Flush()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!UnityTcpBridge.IsRunning) return;
|
||||
|
||||
JObject payload = BuildHierarchyPayload();
|
||||
if (payload == null) return;
|
||||
|
||||
UnityTcpBridge.NotifyAll(EventType, payload);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try { CodelyLogger.LogError($"[SceneHierarchyNotifier] flush failed: {e.Message}"); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Payload builder (shape mirrors ManageScene.GetSceneHierarchy) -- //
|
||||
|
||||
private static JObject BuildHierarchyPayload()
|
||||
{
|
||||
Scene activeScene = EditorSceneManager.GetActiveScene();
|
||||
if (!activeScene.IsValid() || !activeScene.isLoaded)
|
||||
{
|
||||
return JObject.FromObject(new
|
||||
{
|
||||
sceneName = string.Empty,
|
||||
scenePath = string.Empty,
|
||||
partial = false,
|
||||
mode = "empty",
|
||||
totalObjectCount = 0,
|
||||
rootCount = 0,
|
||||
hierarchy = new List<object>(),
|
||||
});
|
||||
}
|
||||
|
||||
GameObject[] rootObjects = activeScene.GetRootGameObjects();
|
||||
|
||||
int totalObjectCount = 0;
|
||||
foreach (var rootObj in rootObjects)
|
||||
{
|
||||
totalObjectCount += CountGameObjectsRecursive(rootObj);
|
||||
}
|
||||
|
||||
// Large scene: mirror ManageScene's roots_only fallback.
|
||||
if (totalObjectCount > LargeSceneThreshold)
|
||||
{
|
||||
var roots = rootObjects
|
||||
.Where(go => go != null)
|
||||
.Select(go => new Dictionary<string, object>
|
||||
{
|
||||
{ "name", go.name },
|
||||
{ "instanceID", go.GetStableInstanceId() },
|
||||
{ "activeSelf", go.activeSelf },
|
||||
{ "activeInHierarchy", go.activeInHierarchy },
|
||||
{ "tag", go.tag },
|
||||
{ "layer", go.layer },
|
||||
{ "isStatic", go.isStatic },
|
||||
{ "childCount", go.transform != null ? go.transform.childCount : 0 },
|
||||
{ "children", new List<object>() },
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return JObject.FromObject(new
|
||||
{
|
||||
sceneName = activeScene.name,
|
||||
scenePath = activeScene.path,
|
||||
partial = true,
|
||||
mode = "roots_only",
|
||||
totalObjectCount = totalObjectCount,
|
||||
rootCount = roots.Count,
|
||||
roots = roots,
|
||||
hints = new[]
|
||||
{
|
||||
"Use unity_gameobject action='list_children' on a specific root GameObject to expand its subtree with a depth limit.",
|
||||
"Use unity_gameobject action='find' with searchMethod='by_path' to locate a specific object (e.g. 'Root/Child') before listing children.",
|
||||
"If you only need a specific area, pick a root from this list and then drill down incrementally (depth=1..N)."
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var hierarchy = rootObjects
|
||||
.Where(go => go != null)
|
||||
.Select(go => BuildGameObjectTree(go))
|
||||
.ToList();
|
||||
|
||||
return JObject.FromObject(new
|
||||
{
|
||||
sceneName = activeScene.name,
|
||||
scenePath = activeScene.path,
|
||||
partial = false,
|
||||
mode = "full",
|
||||
totalObjectCount = totalObjectCount,
|
||||
rootCount = hierarchy.Count,
|
||||
hierarchy = hierarchy,
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Helpers (kept in sync with ManageScene's private equivalents) -- //
|
||||
|
||||
private static int CountGameObjectsRecursive(GameObject go)
|
||||
{
|
||||
if (go == null) return 0;
|
||||
int count = 0;
|
||||
var stack = new Stack<Transform>();
|
||||
if (go.transform != null) stack.Push(go.transform);
|
||||
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var tr = stack.Pop();
|
||||
if (tr == null) continue;
|
||||
count++;
|
||||
int cc = tr.childCount;
|
||||
for (int i = 0; i < cc; i++)
|
||||
{
|
||||
var child = tr.GetChild(i);
|
||||
if (child != null) stack.Push(child);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Iterative DFS that builds the tree without recursion, so deeply nested hierarchies
|
||||
// (e.g. UI layouts hundreds of levels deep) don't blow the managed stack regardless
|
||||
// of total object count.
|
||||
private static object BuildGameObjectTree(GameObject root)
|
||||
{
|
||||
if (root == null) return null;
|
||||
var rootTransform = root.transform;
|
||||
if (rootTransform == null) return null;
|
||||
|
||||
var nodes = new Dictionary<Transform, Dictionary<string, object>>();
|
||||
var stack = new Stack<Transform>();
|
||||
stack.Push(rootTransform);
|
||||
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var tr = stack.Pop();
|
||||
if (tr == null || nodes.ContainsKey(tr)) continue;
|
||||
nodes[tr] = BuildGameObjectNode(tr.gameObject);
|
||||
int cc = tr.childCount;
|
||||
for (int i = 0; i < cc; i++)
|
||||
{
|
||||
var child = tr.GetChild(i);
|
||||
if (child != null) stack.Push(child);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: link children in original sibling order.
|
||||
foreach (var kv in nodes)
|
||||
{
|
||||
var tr = kv.Key;
|
||||
var children = (List<object>)kv.Value["children"];
|
||||
int cc = tr.childCount;
|
||||
for (int i = 0; i < cc; i++)
|
||||
{
|
||||
var child = tr.GetChild(i);
|
||||
if (child != null && nodes.TryGetValue(child, out var childNode))
|
||||
children.Add(childNode);
|
||||
}
|
||||
}
|
||||
|
||||
return nodes[rootTransform];
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> BuildGameObjectNode(GameObject go)
|
||||
{
|
||||
var tr = go.transform;
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "name", go.name },
|
||||
{ "activeSelf", go.activeSelf },
|
||||
{ "activeInHierarchy", go.activeInHierarchy },
|
||||
{ "tag", go.tag },
|
||||
{ "layer", go.layer },
|
||||
{ "isStatic", go.isStatic },
|
||||
{ "instanceID", go.GetStableInstanceId() },
|
||||
{
|
||||
"transform",
|
||||
new
|
||||
{
|
||||
position = new
|
||||
{
|
||||
x = tr.localPosition.x,
|
||||
y = tr.localPosition.y,
|
||||
z = tr.localPosition.z,
|
||||
},
|
||||
rotation = new
|
||||
{
|
||||
x = tr.localRotation.eulerAngles.x,
|
||||
y = tr.localRotation.eulerAngles.y,
|
||||
z = tr.localRotation.eulerAngles.z,
|
||||
},
|
||||
scale = new
|
||||
{
|
||||
x = tr.localScale.x,
|
||||
y = tr.localScale.y,
|
||||
z = tr.localScale.z,
|
||||
},
|
||||
}
|
||||
},
|
||||
{ "children", new List<object>() },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc36c8035d2dd474196149ff776a0472
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Notifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Broadcasts the current Editor selection to notifier clients whenever it
|
||||
/// changes. The emitted payload mirrors the structure returned by
|
||||
/// <see cref="UnityTcp.Editor.Tools.ManageEditor"/>'s get_selection action.
|
||||
/// </summary>
|
||||
[InitializeOnLoad]
|
||||
public static class SelectionChangedNotifier
|
||||
{
|
||||
private const string EventType = "selection_changed";
|
||||
|
||||
private static bool _flushScheduled;
|
||||
|
||||
static SelectionChangedNotifier()
|
||||
{
|
||||
Selection.selectionChanged += OnSelectionChanged;
|
||||
}
|
||||
|
||||
private static void OnSelectionChanged() => ScheduleFlush();
|
||||
|
||||
private static void ScheduleFlush()
|
||||
{
|
||||
if (_flushScheduled) return;
|
||||
_flushScheduled = true;
|
||||
EditorApplication.delayCall += Flush;
|
||||
}
|
||||
|
||||
private static void Flush()
|
||||
{
|
||||
_flushScheduled = false;
|
||||
try
|
||||
{
|
||||
if (!UnityTcpBridge.IsRunning) return;
|
||||
|
||||
JObject payload = BuildSelectionPayload();
|
||||
if (payload == null) return;
|
||||
|
||||
UnityTcpBridge.NotifyAll(EventType, payload);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try { CodelyLogger.LogError($"[SelectionChangedNotifier] flush failed: {e.Message}"); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static JObject BuildSelectionPayload()
|
||||
{
|
||||
var assetGUIDs = Selection.assetGUIDs;
|
||||
var assetPaths = assetGUIDs?.Select(AssetDatabase.GUIDToAssetPath).ToArray();
|
||||
|
||||
string activeAssetPath = Selection.activeObject != null
|
||||
? AssetDatabase.GetAssetPath(Selection.activeObject)
|
||||
: null;
|
||||
string activeAsset = string.IsNullOrEmpty(activeAssetPath)
|
||||
? null
|
||||
: Path.GetFullPath(activeAssetPath).Replace('\\', '/');
|
||||
|
||||
var payload = new
|
||||
{
|
||||
activeObject = Selection.activeObject?.name,
|
||||
activeAsset = activeAsset,
|
||||
activeGameObject = Selection.activeGameObject?.name,
|
||||
activeTransform = Selection.activeTransform?.name,
|
||||
activeInstanceID = InstanceIdExtensions.ActiveSelectionInstanceId(),
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
count = Selection.count,
|
||||
#else
|
||||
count = Selection.objects?.Length ?? 0,
|
||||
#endif
|
||||
objects = Selection.objects
|
||||
.Where(obj => obj != null)
|
||||
.Select(obj => new
|
||||
{
|
||||
name = obj.name,
|
||||
type = obj.GetType().FullName,
|
||||
instanceID = obj.GetStableInstanceId(),
|
||||
})
|
||||
.ToList(),
|
||||
gameObjects = Selection.gameObjects
|
||||
.Where(go => go != null)
|
||||
.Select(go => new
|
||||
{
|
||||
name = go.name,
|
||||
path = GetGameObjectHierarchyPath(go),
|
||||
instanceID = go.GetStableInstanceId(),
|
||||
})
|
||||
.ToList(),
|
||||
assetGUIDs = assetGUIDs,
|
||||
assetPaths = assetPaths,
|
||||
};
|
||||
|
||||
return JObject.FromObject(payload);
|
||||
}
|
||||
|
||||
private static string GetGameObjectHierarchyPath(GameObject obj)
|
||||
{
|
||||
if (obj == null) return string.Empty;
|
||||
|
||||
var path = obj.name;
|
||||
var parent = obj.transform != null ? obj.transform.parent : null;
|
||||
while (parent != null)
|
||||
{
|
||||
path = $"{parent.name}/{path}";
|
||||
parent = parent.parent;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c8e5f3a2d1b4e5a9b6c8d7e1f2a3b4c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in new issue
Block a user