@mflrevan/ucp 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs +3 -0
- package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +45 -5
- package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +26 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +2 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +137 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs +60 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +56 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +207 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +1 -35
- package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +3 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/ProfilerController.cs +80 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/ReferenceController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +6 -6
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +26 -36
- package/bridge/com.ucp.bridge/Editor/Controllers/ScriptController.cs +82 -23
- package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +4 -4
- package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs +322 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +249 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +415 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +409 -63
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +131 -13
- package/bridge/com.ucp.bridge/Tests/Editor/MiniJsonSerializerTests.cs +221 -0
- package/bridge/com.ucp.bridge/Tests/Editor/MiniJsonSerializerTests.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +252 -0
- package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs.meta +2 -0
- package/bridge/com.ucp.bridge/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using UnityEngine;
|
|
4
|
+
using UnityEngine.SceneManagement;
|
|
5
|
+
|
|
6
|
+
namespace UCP.Bridge
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// Shared GameObject resolution for the spatial/visual controllers.
|
|
10
|
+
///
|
|
11
|
+
/// A target may be addressed three ways, tried in priority order:
|
|
12
|
+
/// 1. instanceId (int) — canonical, survives nothing but a domain reload; preferred.
|
|
13
|
+
/// 2. path (string) — hierarchy path "Root/Child/Leaf" (leading '/' optional),
|
|
14
|
+
/// resolved across all loaded scenes; survives reloads.
|
|
15
|
+
/// 3. name (string) — first GameObject whose name matches; ambiguous under
|
|
16
|
+
/// duplicates, so it is the last resort.
|
|
17
|
+
///
|
|
18
|
+
/// instanceId stays the deterministic handle. path/name are convenience fallbacks so an
|
|
19
|
+
/// agent does not have to re-snapshot after every reload just to re-acquire an id.
|
|
20
|
+
/// </summary>
|
|
21
|
+
internal static class ObjectLocator
|
|
22
|
+
{
|
|
23
|
+
/// <summary>
|
|
24
|
+
/// Resolve a GameObject from a params dictionary using whichever of
|
|
25
|
+
/// instanceId / id / path / name is present (in that priority order).
|
|
26
|
+
/// Throws ArgumentException if none are present or nothing resolves.
|
|
27
|
+
/// </summary>
|
|
28
|
+
internal static GameObject Resolve(Dictionary<string, object> p)
|
|
29
|
+
{
|
|
30
|
+
if (p == null)
|
|
31
|
+
throw new ArgumentException("Missing target: provide 'instanceId', 'path', or 'name'");
|
|
32
|
+
|
|
33
|
+
if ((p.TryGetValue("instanceId", out var idObj) || p.TryGetValue("id", out idObj)) && idObj != null)
|
|
34
|
+
{
|
|
35
|
+
var id = Convert.ToInt32(idObj);
|
|
36
|
+
var byId = FindByInstanceId(id);
|
|
37
|
+
if (byId != null)
|
|
38
|
+
return byId;
|
|
39
|
+
throw new ArgumentException($"GameObject not found for instanceId {id}");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (p.TryGetValue("path", out var pathObj) && pathObj != null)
|
|
43
|
+
{
|
|
44
|
+
var path = pathObj.ToString();
|
|
45
|
+
var byPath = FindByPath(path);
|
|
46
|
+
if (byPath != null)
|
|
47
|
+
return byPath;
|
|
48
|
+
throw new ArgumentException($"GameObject not found for path '{path}'");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (p.TryGetValue("name", out var nameObj) && nameObj != null)
|
|
52
|
+
{
|
|
53
|
+
var name = nameObj.ToString();
|
|
54
|
+
var byName = FindByName(name);
|
|
55
|
+
if (byName != null)
|
|
56
|
+
return byName;
|
|
57
|
+
throw new ArgumentException($"GameObject not found for name '{name}'");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new ArgumentException("Missing target: provide 'instanceId', 'path', or 'name'");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
internal static GameObject FindByInstanceId(int instanceId)
|
|
64
|
+
{
|
|
65
|
+
var direct = UnityObjectCompat.ResolveByInstanceId<GameObject>(instanceId);
|
|
66
|
+
if (direct != null)
|
|
67
|
+
return direct;
|
|
68
|
+
|
|
69
|
+
for (var i = 0; i < SceneManager.sceneCount; i++)
|
|
70
|
+
{
|
|
71
|
+
var scene = SceneManager.GetSceneAt(i);
|
|
72
|
+
if (!scene.isLoaded)
|
|
73
|
+
continue;
|
|
74
|
+
foreach (var root in scene.GetRootGameObjects())
|
|
75
|
+
{
|
|
76
|
+
var found = FindInHierarchyById(root, instanceId);
|
|
77
|
+
if (found != null)
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private static GameObject FindInHierarchyById(GameObject go, int instanceId)
|
|
86
|
+
{
|
|
87
|
+
if (go.GetId() == instanceId)
|
|
88
|
+
return go;
|
|
89
|
+
foreach (Transform child in go.transform)
|
|
90
|
+
{
|
|
91
|
+
var found = FindInHierarchyById(child.gameObject, instanceId);
|
|
92
|
+
if (found != null)
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
internal static GameObject FindByPath(string path)
|
|
99
|
+
{
|
|
100
|
+
if (string.IsNullOrEmpty(path))
|
|
101
|
+
return null;
|
|
102
|
+
|
|
103
|
+
var trimmed = path.Trim('/');
|
|
104
|
+
var segments = trimmed.Split('/');
|
|
105
|
+
if (segments.Length == 0)
|
|
106
|
+
return null;
|
|
107
|
+
|
|
108
|
+
for (var i = 0; i < SceneManager.sceneCount; i++)
|
|
109
|
+
{
|
|
110
|
+
var scene = SceneManager.GetSceneAt(i);
|
|
111
|
+
if (!scene.isLoaded)
|
|
112
|
+
continue;
|
|
113
|
+
foreach (var root in scene.GetRootGameObjects())
|
|
114
|
+
{
|
|
115
|
+
if (root.name != segments[0])
|
|
116
|
+
continue;
|
|
117
|
+
var resolved = WalkPath(root.transform, segments, 1);
|
|
118
|
+
if (resolved != null)
|
|
119
|
+
return resolved;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private static GameObject WalkPath(Transform current, string[] segments, int index)
|
|
127
|
+
{
|
|
128
|
+
if (index >= segments.Length)
|
|
129
|
+
return current.gameObject;
|
|
130
|
+
|
|
131
|
+
foreach (Transform child in current)
|
|
132
|
+
{
|
|
133
|
+
if (child.name == segments[index])
|
|
134
|
+
{
|
|
135
|
+
var resolved = WalkPath(child, segments, index + 1);
|
|
136
|
+
if (resolved != null)
|
|
137
|
+
return resolved;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
internal static GameObject FindByName(string name)
|
|
145
|
+
{
|
|
146
|
+
if (string.IsNullOrEmpty(name))
|
|
147
|
+
return null;
|
|
148
|
+
|
|
149
|
+
for (var i = 0; i < SceneManager.sceneCount; i++)
|
|
150
|
+
{
|
|
151
|
+
var scene = SceneManager.GetSceneAt(i);
|
|
152
|
+
if (!scene.isLoaded)
|
|
153
|
+
continue;
|
|
154
|
+
foreach (var root in scene.GetRootGameObjects())
|
|
155
|
+
{
|
|
156
|
+
var found = FindInHierarchyByName(root, name);
|
|
157
|
+
if (found != null)
|
|
158
|
+
return found;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private static GameObject FindInHierarchyByName(GameObject go, string name)
|
|
166
|
+
{
|
|
167
|
+
if (go.name == name)
|
|
168
|
+
return go;
|
|
169
|
+
foreach (Transform child in go.transform)
|
|
170
|
+
{
|
|
171
|
+
var found = FindInHierarchyByName(child.gameObject, name);
|
|
172
|
+
if (found != null)
|
|
173
|
+
return found;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// <summary>Compute a world-space AABB encapsulating the object's renderers and colliders.</summary>
|
|
179
|
+
internal static bool TryComputeWorldBounds(GameObject target, bool includeChildren, out Bounds bounds)
|
|
180
|
+
{
|
|
181
|
+
var hasBounds = false;
|
|
182
|
+
bounds = new Bounds(target.transform.position, Vector3.zero);
|
|
183
|
+
|
|
184
|
+
var renderers = includeChildren
|
|
185
|
+
? target.GetComponentsInChildren<Renderer>()
|
|
186
|
+
: target.GetComponents<Renderer>();
|
|
187
|
+
foreach (var renderer in renderers)
|
|
188
|
+
{
|
|
189
|
+
if (!hasBounds) { bounds = renderer.bounds; hasBounds = true; }
|
|
190
|
+
else bounds.Encapsulate(renderer.bounds);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
var colliders = includeChildren
|
|
194
|
+
? target.GetComponentsInChildren<Collider>()
|
|
195
|
+
: target.GetComponents<Collider>();
|
|
196
|
+
foreach (var collider in colliders)
|
|
197
|
+
{
|
|
198
|
+
if (!hasBounds) { bounds = collider.bounds; hasBounds = true; }
|
|
199
|
+
else bounds.Encapsulate(collider.bounds);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return hasBounds;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
internal static List<object> Vec3(Vector3 v) => new List<object> { v.x, v.y, v.z };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
using UnityEditor;
|
|
2
|
-
using UnityEditor.SceneManagement;
|
|
3
|
-
using UnityEngine.SceneManagement;
|
|
4
2
|
using System.Collections.Generic;
|
|
5
3
|
using System;
|
|
6
4
|
|
|
@@ -65,7 +63,7 @@ namespace UCP.Bridge
|
|
|
65
63
|
var saveDirtyScenes = GetBoolParam(paramsJson, "saveDirtyScenes", true);
|
|
66
64
|
var discardUntitled = GetBoolParam(paramsJson, "discardUntitled", true);
|
|
67
65
|
var logFile = GetStringParam(paramsJson, "logFile");
|
|
68
|
-
|
|
66
|
+
EditorModalGuard.SaveOpenDirtyScenes(saveDirtyScenes, discardUntitled);
|
|
69
67
|
if (!string.IsNullOrEmpty(logFile))
|
|
70
68
|
LogsController.StartFileCapture(logFile);
|
|
71
69
|
|
|
@@ -91,38 +89,6 @@ namespace UCP.Bridge
|
|
|
91
89
|
return defaultValue;
|
|
92
90
|
}
|
|
93
91
|
|
|
94
|
-
private static void SaveDirtyScenesIfRequested(bool saveDirtyScenes, bool discardUntitled)
|
|
95
|
-
{
|
|
96
|
-
if (!saveDirtyScenes)
|
|
97
|
-
return;
|
|
98
|
-
|
|
99
|
-
var requiresUntitledDiscard = false;
|
|
100
|
-
|
|
101
|
-
for (var index = 0; index < SceneManager.sceneCount; index++)
|
|
102
|
-
{
|
|
103
|
-
var scene = SceneManager.GetSceneAt(index);
|
|
104
|
-
if (!scene.isLoaded || !scene.isDirty)
|
|
105
|
-
continue;
|
|
106
|
-
|
|
107
|
-
if (string.IsNullOrEmpty(scene.path))
|
|
108
|
-
{
|
|
109
|
-
if (!discardUntitled)
|
|
110
|
-
throw new System.InvalidOperationException("Dirty untitled scene cannot be auto-saved. Retry with discardUntitled=true.");
|
|
111
|
-
|
|
112
|
-
requiresUntitledDiscard = true;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!EditorSceneManager.SaveScene(scene))
|
|
117
|
-
throw new System.InvalidOperationException($"Failed to auto-save dirty scene: {scene.path}");
|
|
118
|
-
|
|
119
|
-
SceneChangeTracker.ClearScene(scene);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (requiresUntitledDiscard)
|
|
123
|
-
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
92
|
private static object HandleStop(string paramsJson)
|
|
127
93
|
{
|
|
128
94
|
if (!EditorApplication.isPlaying)
|
|
@@ -179,8 +179,8 @@ namespace UCP.Bridge
|
|
|
179
179
|
["status"] = "ok",
|
|
180
180
|
["path"] = savePath,
|
|
181
181
|
["name"] = prefab.name,
|
|
182
|
-
["instanceId"] = prefab.
|
|
183
|
-
["sceneInstanceId"] = go.
|
|
182
|
+
["instanceId"] = prefab.GetId(),
|
|
183
|
+
["sceneInstanceId"] = go.GetId(),
|
|
184
184
|
["isPrefabInstance"] = PrefabUtility.IsPartOfPrefabInstance(go)
|
|
185
185
|
};
|
|
186
186
|
}
|
|
@@ -221,7 +221,7 @@ namespace UCP.Bridge
|
|
|
221
221
|
added.Add(new Dictionary<string, object>
|
|
222
222
|
{
|
|
223
223
|
["component"] = ac.instanceComponent.GetType().Name,
|
|
224
|
-
["instanceId"] = ac.instanceComponent.
|
|
224
|
+
["instanceId"] = ac.instanceComponent.GetId()
|
|
225
225
|
});
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -15,6 +15,11 @@ namespace UCP.Bridge
|
|
|
15
15
|
{
|
|
16
16
|
private const int DefaultFrameListLimit = 20;
|
|
17
17
|
private const int DefaultSummaryFrameWindow = 120;
|
|
18
|
+
|
|
19
|
+
// Summary aggregation walks every raw frame view on the main thread, so an unbounded
|
|
20
|
+
// range (e.g. --first-frame 0 on a long session) freezes the editor for seconds. Clamp it
|
|
21
|
+
// and say so in `warnings` rather than stalling.
|
|
22
|
+
private const int MaxSummaryFrameSpan = 600;
|
|
18
23
|
private const int DefaultJsonExportFrameWindow = 120;
|
|
19
24
|
private const int MaxThreadProbeCount = 128;
|
|
20
25
|
private const long MinimumProfilerMemoryBytes = 16L * 1024L * 1024L;
|
|
@@ -296,6 +301,7 @@ namespace UCP.Bridge
|
|
|
296
301
|
var limit = Math.Max(1, GetInt(parameters, "limit", 50));
|
|
297
302
|
var sort = GetString(parameters, "sort") ?? "total-time";
|
|
298
303
|
var maxDepth = GetNullableInt(parameters, "maxDepth");
|
|
304
|
+
var fields = GetFieldSet(parameters);
|
|
299
305
|
|
|
300
306
|
using (var view = GetHierarchyFrameDataView(frameIndex, threadIndex))
|
|
301
307
|
{
|
|
@@ -305,7 +311,8 @@ namespace UCP.Bridge
|
|
|
305
311
|
var items = CollectHierarchyItems(view, maxDepth);
|
|
306
312
|
items = SortHierarchyItems(items, sort);
|
|
307
313
|
|
|
308
|
-
var
|
|
314
|
+
var totalCount = items.Count;
|
|
315
|
+
var truncated = totalCount > limit;
|
|
309
316
|
if (truncated)
|
|
310
317
|
items = items.Take(limit).ToList();
|
|
311
318
|
|
|
@@ -315,8 +322,11 @@ namespace UCP.Bridge
|
|
|
315
322
|
["thread"] = threadIndex,
|
|
316
323
|
["sort"] = sort,
|
|
317
324
|
["count"] = items.Count,
|
|
325
|
+
// How many rows existed before truncation. Without it a caller cannot tell
|
|
326
|
+
// "50 of 52" from "50 of 50,000", so it cannot decide whether to look further.
|
|
327
|
+
["totalCount"] = totalCount,
|
|
318
328
|
["truncated"] = truncated,
|
|
319
|
-
["items"] = items.Select(item => (object)item.ToDictionary()).ToList(),
|
|
329
|
+
["items"] = items.Select(item => (object)ProjectFields(item.ToDictionary(), fields)).ToList(),
|
|
320
330
|
["warnings"] = new List<object>()
|
|
321
331
|
};
|
|
322
332
|
}
|
|
@@ -330,6 +340,7 @@ namespace UCP.Bridge
|
|
|
330
340
|
var limit = Math.Max(1, GetInt(parameters, "limit", 200));
|
|
331
341
|
var maxDepth = GetNullableInt(parameters, "maxDepth");
|
|
332
342
|
var includeMetadata = GetBool(parameters, "includeMetadata", false);
|
|
343
|
+
var fields = GetFieldSet(parameters);
|
|
333
344
|
|
|
334
345
|
using (var view = GetRawFrameDataView(frameIndex, threadIndex))
|
|
335
346
|
{
|
|
@@ -337,7 +348,8 @@ namespace UCP.Bridge
|
|
|
337
348
|
throw new ArgumentException($"Raw profiler data is unavailable for frame {frameIndex}, thread {threadIndex}");
|
|
338
349
|
|
|
339
350
|
var samples = CollectTimelineSamples(view, maxDepth, includeMetadata);
|
|
340
|
-
var
|
|
351
|
+
var totalCount = samples.Count;
|
|
352
|
+
var truncated = totalCount > limit;
|
|
341
353
|
if (truncated)
|
|
342
354
|
samples = samples.Take(limit).ToList();
|
|
343
355
|
|
|
@@ -346,8 +358,9 @@ namespace UCP.Bridge
|
|
|
346
358
|
["frame"] = frameIndex,
|
|
347
359
|
["thread"] = threadIndex,
|
|
348
360
|
["count"] = samples.Count,
|
|
361
|
+
["totalCount"] = totalCount,
|
|
349
362
|
["truncated"] = truncated,
|
|
350
|
-
["samples"] = samples.
|
|
363
|
+
["samples"] = samples.Select(sample => (object)ProjectFields(sample, fields)).ToList(),
|
|
351
364
|
["warnings"] = new List<object>()
|
|
352
365
|
};
|
|
353
366
|
}
|
|
@@ -427,10 +440,23 @@ namespace UCP.Bridge
|
|
|
427
440
|
if (firstFrame > lastFrame)
|
|
428
441
|
throw new ArgumentException("Requested frame range is empty");
|
|
429
442
|
|
|
443
|
+
var warnings = new List<object>();
|
|
444
|
+
var span = lastFrame - firstFrame + 1;
|
|
445
|
+
if (span > MaxSummaryFrameSpan)
|
|
446
|
+
{
|
|
447
|
+
// Aggregation runs on the editor's main thread; a huge span stalls the whole
|
|
448
|
+
// editor. Keep the most recent frames, which is what callers almost always want.
|
|
449
|
+
firstFrame = lastFrame - MaxSummaryFrameSpan + 1;
|
|
450
|
+
warnings.Add(
|
|
451
|
+
$"Requested {span} frames; aggregated the most recent {MaxSummaryFrameSpan} " +
|
|
452
|
+
$"(frames {firstFrame}-{lastFrame}) to avoid stalling the editor. " +
|
|
453
|
+
"Narrow the range with --first-frame/--last-frame for older windows.");
|
|
454
|
+
}
|
|
455
|
+
|
|
430
456
|
return new Dictionary<string, object>
|
|
431
457
|
{
|
|
432
458
|
["summary"] = BuildSummaryData(limit, threadIndex, firstFrame, lastFrame),
|
|
433
|
-
["warnings"] =
|
|
459
|
+
["warnings"] = warnings
|
|
434
460
|
};
|
|
435
461
|
}
|
|
436
462
|
|
|
@@ -1365,6 +1391,55 @@ namespace UCP.Bridge
|
|
|
1365
1391
|
StringComparison.OrdinalIgnoreCase);
|
|
1366
1392
|
}
|
|
1367
1393
|
|
|
1394
|
+
/// <summary>
|
|
1395
|
+
/// Optional caller-supplied field allow-list (`fields: ["name","selfMs"]`). Profiler rows
|
|
1396
|
+
/// are wide and agents pay per token for columns they never read, so let them ask for the
|
|
1397
|
+
/// two or three they actually want. Null means "every field".
|
|
1398
|
+
/// </summary>
|
|
1399
|
+
private static HashSet<string> GetFieldSet(Dictionary<string, object> parameters)
|
|
1400
|
+
{
|
|
1401
|
+
if (parameters == null || !parameters.TryGetValue("fields", out var raw) || raw == null)
|
|
1402
|
+
return null;
|
|
1403
|
+
|
|
1404
|
+
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
1405
|
+
if (raw is List<object> list)
|
|
1406
|
+
{
|
|
1407
|
+
foreach (var entry in list)
|
|
1408
|
+
{
|
|
1409
|
+
var name = entry?.ToString();
|
|
1410
|
+
if (!string.IsNullOrWhiteSpace(name)) set.Add(name.Trim());
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
else
|
|
1414
|
+
{
|
|
1415
|
+
foreach (var name in raw.ToString().Split(','))
|
|
1416
|
+
{
|
|
1417
|
+
if (!string.IsNullOrWhiteSpace(name)) set.Add(name.Trim());
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
return set.Count == 0 ? null : set;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/// Narrow one row to the requested fields. Unknown names are ignored rather than erroring,
|
|
1425
|
+
/// so a caller can ask for a superset across profiler surfaces without branching.
|
|
1426
|
+
private static Dictionary<string, object> ProjectFields(
|
|
1427
|
+
Dictionary<string, object> row,
|
|
1428
|
+
HashSet<string> fields)
|
|
1429
|
+
{
|
|
1430
|
+
if (fields == null || row == null) return row;
|
|
1431
|
+
|
|
1432
|
+
var projected = new Dictionary<string, object>();
|
|
1433
|
+
foreach (var pair in row)
|
|
1434
|
+
{
|
|
1435
|
+
if (fields.Contains(pair.Key)) projected[pair.Key] = pair.Value;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// Never hand back an empty row: a caller that misspelled every field would otherwise
|
|
1439
|
+
// get a silent wall of `{}` instead of a usable result.
|
|
1440
|
+
return projected.Count == 0 ? row : projected;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1368
1443
|
private static Dictionary<string, object> BuildSummaryData(
|
|
1369
1444
|
int limit,
|
|
1370
1445
|
int threadIndex,
|
|
@@ -538,7 +538,7 @@ namespace UCP.Bridge
|
|
|
538
538
|
|
|
539
539
|
private static GameObject FindInHierarchy(GameObject go, int instanceId)
|
|
540
540
|
{
|
|
541
|
-
if (go.
|
|
541
|
+
if (go.GetId() == instanceId) return go;
|
|
542
542
|
for (int i = 0; i < go.transform.childCount; i++)
|
|
543
543
|
{
|
|
544
544
|
var found = FindInHierarchy(go.transform.GetChild(i).gameObject, instanceId);
|
|
@@ -31,7 +31,7 @@ namespace UCP.Bridge
|
|
|
31
31
|
{
|
|
32
32
|
{ "serializationMode", mode },
|
|
33
33
|
{ "forceText", mode == 2 },
|
|
34
|
-
{ "visibleMetaFiles",
|
|
34
|
+
{ "visibleMetaFiles", VersionControlSettings.mode == "Visible Meta Files" }
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
finally
|
|
@@ -17,7 +17,7 @@ namespace UCP.Bridge
|
|
|
17
17
|
public HashSet<string> Components = new();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
private static readonly Dictionary<
|
|
20
|
+
private static readonly Dictionary<long, Dictionary<string, TrackedSceneChange>> s_changesByScene = new();
|
|
21
21
|
|
|
22
22
|
static SceneChangeTracker()
|
|
23
23
|
{
|
|
@@ -33,7 +33,7 @@ namespace UCP.Bridge
|
|
|
33
33
|
if (gameObject == null)
|
|
34
34
|
return;
|
|
35
35
|
|
|
36
|
-
RecordSceneChange(gameObject.scene, gameObject.
|
|
36
|
+
RecordSceneChange(gameObject.scene, gameObject.GetId(), gameObject.name, componentName);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
public static void RecordDeletedObject(Scene scene, int instanceId, string name, string componentName)
|
|
@@ -56,7 +56,7 @@ namespace UCP.Bridge
|
|
|
56
56
|
var modifications = new List<object>();
|
|
57
57
|
var omittedCount = 0;
|
|
58
58
|
|
|
59
|
-
if (scene.IsValid() && s_changesByScene.TryGetValue(scene
|
|
59
|
+
if (scene.IsValid() && s_changesByScene.TryGetValue(UnityObjectCompat.GetSceneHandle(scene), out var trackedChanges))
|
|
60
60
|
{
|
|
61
61
|
var ordered = trackedChanges.Values
|
|
62
62
|
.OrderBy(change => change.InstanceId.HasValue ? 0 : 1)
|
|
@@ -101,7 +101,7 @@ namespace UCP.Bridge
|
|
|
101
101
|
if (!scene.IsValid())
|
|
102
102
|
return;
|
|
103
103
|
|
|
104
|
-
s_changesByScene.Remove(scene
|
|
104
|
+
s_changesByScene.Remove(UnityObjectCompat.GetSceneHandle(scene));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
private static UndoPropertyModification[] OnPostprocessModifications(UndoPropertyModification[] modifications)
|
|
@@ -140,10 +140,10 @@ namespace UCP.Bridge
|
|
|
140
140
|
if (!scene.IsValid() || !scene.isLoaded)
|
|
141
141
|
return;
|
|
142
142
|
|
|
143
|
-
if (!s_changesByScene.TryGetValue(scene
|
|
143
|
+
if (!s_changesByScene.TryGetValue(UnityObjectCompat.GetSceneHandle(scene), out var sceneChanges))
|
|
144
144
|
{
|
|
145
145
|
sceneChanges = new Dictionary<string, TrackedSceneChange>();
|
|
146
|
-
s_changesByScene[scene
|
|
146
|
+
s_changesByScene[UnityObjectCompat.GetSceneHandle(scene)] = sceneChanges;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
var key = instanceId.HasValue ? instanceId.Value.ToString() : $"scene::{name}";
|
|
@@ -54,7 +54,7 @@ namespace UCP.Bridge
|
|
|
54
54
|
}
|
|
55
55
|
else
|
|
56
56
|
{
|
|
57
|
-
|
|
57
|
+
EditorModalGuard.SaveOpenDirtyScenes(saveDirtyScenes, discardUntitled);
|
|
58
58
|
EditorSceneManager.OpenScene(path, additive ? OpenSceneMode.Additive : OpenSceneMode.Single);
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -74,38 +74,6 @@ namespace UCP.Bridge
|
|
|
74
74
|
return defaultValue;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
private static void SaveDirtyScenesIfRequested(bool saveDirtyScenes, bool discardUntitled)
|
|
78
|
-
{
|
|
79
|
-
if (!saveDirtyScenes)
|
|
80
|
-
return;
|
|
81
|
-
|
|
82
|
-
var requiresUntitledDiscard = false;
|
|
83
|
-
|
|
84
|
-
for (var index = 0; index < SceneManager.sceneCount; index++)
|
|
85
|
-
{
|
|
86
|
-
var scene = SceneManager.GetSceneAt(index);
|
|
87
|
-
if (!scene.isLoaded || !scene.isDirty)
|
|
88
|
-
continue;
|
|
89
|
-
|
|
90
|
-
if (string.IsNullOrEmpty(scene.path))
|
|
91
|
-
{
|
|
92
|
-
if (!discardUntitled)
|
|
93
|
-
throw new System.InvalidOperationException("Dirty untitled scene cannot be auto-saved. Retry with discardUntitled=true.");
|
|
94
|
-
|
|
95
|
-
requiresUntitledDiscard = true;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!EditorSceneManager.SaveScene(scene))
|
|
100
|
-
throw new System.InvalidOperationException($"Failed to auto-save dirty scene: {scene.path}");
|
|
101
|
-
|
|
102
|
-
SceneChangeTracker.ClearScene(scene);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (requiresUntitledDiscard)
|
|
106
|
-
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
77
|
private static object HandleSaveActive(string paramsJson)
|
|
110
78
|
{
|
|
111
79
|
var scene = SceneManager.GetActiveScene();
|
|
@@ -200,19 +168,41 @@ namespace UCP.Bridge
|
|
|
200
168
|
sceneView.Repaint();
|
|
201
169
|
SceneView.RepaintAll();
|
|
202
170
|
|
|
171
|
+
ResolveSceneViewCamera(sceneView, out var cameraPosition, out var cameraRotation);
|
|
172
|
+
|
|
203
173
|
return new Dictionary<string, object>
|
|
204
174
|
{
|
|
205
175
|
["status"] = "ok",
|
|
206
176
|
["instanceId"] = instanceId,
|
|
207
177
|
["name"] = target.name,
|
|
208
178
|
["pivot"] = VectorToList(sceneView.pivot),
|
|
209
|
-
["cameraPosition"] = VectorToList(
|
|
210
|
-
["cameraRotationEuler"] = VectorToList(
|
|
179
|
+
["cameraPosition"] = VectorToList(cameraPosition),
|
|
180
|
+
["cameraRotationEuler"] = VectorToList(cameraRotation.eulerAngles),
|
|
211
181
|
["size"] = sceneView.size,
|
|
212
182
|
["axis"] = axis.HasValue ? VectorToList(axis.Value.normalized) : null
|
|
213
183
|
};
|
|
214
184
|
}
|
|
215
185
|
|
|
186
|
+
/// <summary>
|
|
187
|
+
/// Derive the Scene view camera pose from the authoritative view state
|
|
188
|
+
/// (<c>pivot</c>/<c>rotation</c>/<c>cameraDistance</c>) rather than reading
|
|
189
|
+
/// <c>sceneView.camera.transform</c>.
|
|
190
|
+
///
|
|
191
|
+
/// <c>LookAtDirect</c> updates the view state immediately, but the camera transform is only
|
|
192
|
+
/// synced when the Scene view actually repaints. <c>Repaint()</c> merely queues that, so in
|
|
193
|
+
/// batch mode - and for any caller reading the response in the same frame as the focus -
|
|
194
|
+
/// the camera transform still holds the *previous* pose and the reported values were simply
|
|
195
|
+
/// wrong.
|
|
196
|
+
/// </summary>
|
|
197
|
+
private static void ResolveSceneViewCamera(
|
|
198
|
+
SceneView sceneView,
|
|
199
|
+
out Vector3 position,
|
|
200
|
+
out Quaternion rotation)
|
|
201
|
+
{
|
|
202
|
+
rotation = sceneView.rotation;
|
|
203
|
+
position = sceneView.pivot - (rotation * Vector3.forward) * sceneView.cameraDistance;
|
|
204
|
+
}
|
|
205
|
+
|
|
216
206
|
private static Vector3? TryReadAxis(Dictionary<string, object> parameters)
|
|
217
207
|
{
|
|
218
208
|
if (parameters == null || !parameters.TryGetValue("axis", out var axisObj) || axisObj == null)
|
|
@@ -302,7 +292,7 @@ namespace UCP.Bridge
|
|
|
302
292
|
|
|
303
293
|
private static GameObject FindInHierarchy(GameObject gameObject, int instanceId)
|
|
304
294
|
{
|
|
305
|
-
if (gameObject.
|
|
295
|
+
if (gameObject.GetId() == instanceId)
|
|
306
296
|
return gameObject;
|
|
307
297
|
|
|
308
298
|
foreach (Transform child in gameObject.transform)
|