@mflrevan/ucp 0.5.2 → 0.6.0
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 +10 -1
- 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/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/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 +2 -34
- 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/Tests/Editor/ControllerSmokeTests.cs +63 -7
- 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
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mflrevan/ucp
|
|
2
2
|
|
|
3
|
-
Version `0.
|
|
3
|
+
Version `0.6.0` of the Unity Control Protocol CLI.
|
|
4
4
|
|
|
5
5
|
This package installs the `ucp` command, downloads the matching published binary for your platform during `postinstall`, and ships the matching Unity bridge payload inside the npm package.
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ namespace UCP.Bridge
|
|
|
26
26
|
private const int DefaultPort = 21342;
|
|
27
27
|
private const int MaxPort = 21352;
|
|
28
28
|
private const int MaxConnections = 4;
|
|
29
|
-
private const string ProtocolVersion = "0.
|
|
29
|
+
private const string ProtocolVersion = "0.6.0";
|
|
30
30
|
|
|
31
31
|
private static TcpListener s_listener;
|
|
32
32
|
private static CancellationTokenSource s_cts;
|
|
@@ -145,6 +145,15 @@ namespace UCP.Bridge
|
|
|
145
145
|
// Hierarchy Operations
|
|
146
146
|
HierarchyController.Register(s_router);
|
|
147
147
|
|
|
148
|
+
// Transform authoring (move/rotate/scale/look-at, bulk read)
|
|
149
|
+
TransformController.Register(s_router);
|
|
150
|
+
|
|
151
|
+
// Spatial queries (raycast/overlap/bounds/ground/nearest)
|
|
152
|
+
SpatialController.Register(s_router);
|
|
153
|
+
|
|
154
|
+
// Composed visual perception (capture/isolate/orbit)
|
|
155
|
+
ViewController.Register(s_router);
|
|
156
|
+
|
|
148
157
|
// Asset Management
|
|
149
158
|
AssetController.Register(s_router);
|
|
150
159
|
ImporterController.Register(s_router);
|
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
using UnityEditor;
|
|
2
2
|
using UnityEngine;
|
|
3
|
+
using UnityEngine.SceneManagement;
|
|
3
4
|
|
|
4
5
|
namespace UCP.Bridge
|
|
5
6
|
{
|
|
6
7
|
internal static class UnityObjectCompat
|
|
7
8
|
{
|
|
9
|
+
public static int GetId(this Object obj)
|
|
10
|
+
{
|
|
11
|
+
#if UNITY_6000_5_OR_NEWER
|
|
12
|
+
return unchecked((int)EntityId.ToULong(obj.GetEntityId()));
|
|
13
|
+
#else
|
|
14
|
+
return obj.GetInstanceID();
|
|
15
|
+
#endif
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public static long GetSceneHandle(Scene scene)
|
|
19
|
+
{
|
|
20
|
+
#if UNITY_6000_5_OR_NEWER
|
|
21
|
+
return unchecked((long)scene.handle.GetRawData());
|
|
22
|
+
#else
|
|
23
|
+
// On 6000.0–6000.4, Scene.handle is a SceneHandle with implicit int and
|
|
24
|
+
// uint operators; widening straight to long is ambiguous (CS0457). Pin the
|
|
25
|
+
// int conversion explicitly — handles are 32-bit on these versions.
|
|
26
|
+
return (int)scene.handle;
|
|
27
|
+
#endif
|
|
28
|
+
}
|
|
29
|
+
|
|
8
30
|
public static Object ResolveByInstanceId(int instanceId)
|
|
9
31
|
{
|
|
32
|
+
#if UNITY_6000_5_OR_NEWER
|
|
33
|
+
return EditorUtility.EntityIdToObject(EntityId.FromULong(unchecked((ulong)instanceId)));
|
|
34
|
+
#else
|
|
10
35
|
return EditorUtility.InstanceIDToObject(instanceId);
|
|
36
|
+
#endif
|
|
11
37
|
}
|
|
12
38
|
|
|
13
39
|
public static T ResolveByInstanceId<T>(int instanceId) where T : Object
|
|
@@ -125,7 +125,7 @@ namespace UCP.Bridge
|
|
|
125
125
|
["name"] = asset.name,
|
|
126
126
|
["type"] = asset.GetType().Name,
|
|
127
127
|
["fullType"] = asset.GetType().FullName,
|
|
128
|
-
["instanceId"] = asset.
|
|
128
|
+
["instanceId"] = asset.GetId(),
|
|
129
129
|
["guid"] = AssetDatabase.AssetPathToGUID(assetPath)
|
|
130
130
|
};
|
|
131
131
|
|
|
@@ -496,7 +496,7 @@ namespace UCP.Bridge
|
|
|
496
496
|
["status"] = "ok",
|
|
497
497
|
["path"] = assetPath,
|
|
498
498
|
["type"] = soType.Name,
|
|
499
|
-
["instanceId"] = instance.
|
|
499
|
+
["instanceId"] = instance.GetId()
|
|
500
500
|
};
|
|
501
501
|
}
|
|
502
502
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
using UnityEditor;
|
|
2
|
+
using UnityEditor.SceneManagement;
|
|
3
|
+
using UnityEngine.SceneManagement;
|
|
4
|
+
|
|
5
|
+
namespace UCP.Bridge
|
|
6
|
+
{
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Centralizes editor operations that must stay non-blocking when the bridge drives
|
|
9
|
+
/// the editor headlessly.
|
|
10
|
+
///
|
|
11
|
+
/// Bridge commands run on the main thread via EditorApplication.update. Any modal
|
|
12
|
+
/// dialog (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo,
|
|
13
|
+
/// EditorUtility.DisplayDialog, file/folder panels, ...) blocks that loop with no user
|
|
14
|
+
/// present to dismiss it, hanging every queued command and timing out the CLI. Bridge
|
|
15
|
+
/// code must therefore never call a modal API. Scene saving is the most common trap, so
|
|
16
|
+
/// it lives here behind a single audited, modal-free implementation shared by every
|
|
17
|
+
/// controller instead of being copy-pasted per controller.
|
|
18
|
+
/// </summary>
|
|
19
|
+
internal static class EditorModalGuard
|
|
20
|
+
{
|
|
21
|
+
/// <summary>
|
|
22
|
+
/// Saves every loaded dirty scene without prompting. Dirty untitled scenes have no
|
|
23
|
+
/// path to save to; they are discarded (replaced with a fresh empty scene) when
|
|
24
|
+
/// <paramref name="discardUntitled"/> is true, otherwise an exception is thrown so
|
|
25
|
+
/// the caller surfaces a clear error instead of silently losing work or blocking on
|
|
26
|
+
/// a save dialog.
|
|
27
|
+
/// </summary>
|
|
28
|
+
public static void SaveOpenDirtyScenes(bool saveDirtyScenes, bool discardUntitled)
|
|
29
|
+
{
|
|
30
|
+
if (!saveDirtyScenes)
|
|
31
|
+
return;
|
|
32
|
+
|
|
33
|
+
var requiresUntitledDiscard = false;
|
|
34
|
+
|
|
35
|
+
for (var index = 0; index < SceneManager.sceneCount; index++)
|
|
36
|
+
{
|
|
37
|
+
var scene = SceneManager.GetSceneAt(index);
|
|
38
|
+
if (!scene.isLoaded || !scene.isDirty)
|
|
39
|
+
continue;
|
|
40
|
+
|
|
41
|
+
if (string.IsNullOrEmpty(scene.path))
|
|
42
|
+
{
|
|
43
|
+
if (!discardUntitled)
|
|
44
|
+
throw new System.InvalidOperationException("Dirty untitled scene cannot be auto-saved. Retry with discardUntitled=true.");
|
|
45
|
+
|
|
46
|
+
requiresUntitledDiscard = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!EditorSceneManager.SaveScene(scene))
|
|
51
|
+
throw new System.InvalidOperationException($"Failed to auto-save dirty scene: {scene.path}");
|
|
52
|
+
|
|
53
|
+
SceneChangeTracker.ClearScene(scene);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (requiresUntitledDiscard)
|
|
57
|
+
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -26,7 +26,19 @@ namespace UCP.Bridge
|
|
|
26
26
|
if (p != null && p.TryGetValue("name", out var nameObj) && nameObj != null)
|
|
27
27
|
name = nameObj.ToString();
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// Optional primitive: build a Cube/Sphere/etc. with mesh + collider in one step.
|
|
30
|
+
// Without this an agent has to hand-assemble MeshFilter + MeshRenderer and somehow
|
|
31
|
+
// reference the built-in mesh, which is not practical over the CLI.
|
|
32
|
+
GameObject go;
|
|
33
|
+
if (p != null && p.TryGetValue("primitive", out var primObj) && primObj != null)
|
|
34
|
+
{
|
|
35
|
+
go = GameObject.CreatePrimitive(ParsePrimitive(primObj.ToString()));
|
|
36
|
+
go.name = name;
|
|
37
|
+
}
|
|
38
|
+
else
|
|
39
|
+
{
|
|
40
|
+
go = new GameObject(name);
|
|
41
|
+
}
|
|
30
42
|
Undo.RegisterCreatedObjectUndo(go, "UCP Create GameObject");
|
|
31
43
|
|
|
32
44
|
// Optional parent
|
|
@@ -61,7 +73,7 @@ namespace UCP.Bridge
|
|
|
61
73
|
return new Dictionary<string, object>
|
|
62
74
|
{
|
|
63
75
|
["status"] = "ok",
|
|
64
|
-
["instanceId"] = go.
|
|
76
|
+
["instanceId"] = go.GetId(),
|
|
65
77
|
["name"] = go.name
|
|
66
78
|
};
|
|
67
79
|
}
|
|
@@ -140,7 +152,12 @@ namespace UCP.Bridge
|
|
|
140
152
|
string prefabPath = prefabObj.ToString();
|
|
141
153
|
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
|
142
154
|
if (prefab == null)
|
|
143
|
-
|
|
155
|
+
{
|
|
156
|
+
var hint = LooksLikePrimitive(prefabPath)
|
|
157
|
+
? " To create a built-in primitive, use object create with a primitive instead (e.g. `ucp object create MyCube --primitive Cube`); instantiate is only for prefab assets under Assets/."
|
|
158
|
+
: string.Empty;
|
|
159
|
+
throw new ArgumentException($"Prefab not found: {prefabPath}.{hint}");
|
|
160
|
+
}
|
|
144
161
|
source = prefab;
|
|
145
162
|
}
|
|
146
163
|
// Instantiate from existing scene object (clone)
|
|
@@ -192,7 +209,7 @@ namespace UCP.Bridge
|
|
|
192
209
|
return new Dictionary<string, object>
|
|
193
210
|
{
|
|
194
211
|
["status"] = "ok",
|
|
195
|
-
["instanceId"] = instance.
|
|
212
|
+
["instanceId"] = instance.GetId(),
|
|
196
213
|
["name"] = instance.name
|
|
197
214
|
};
|
|
198
215
|
}
|
|
@@ -267,6 +284,40 @@ namespace UCP.Bridge
|
|
|
267
284
|
};
|
|
268
285
|
}
|
|
269
286
|
|
|
287
|
+
private static bool LooksLikePrimitive(string path)
|
|
288
|
+
{
|
|
289
|
+
if (string.IsNullOrEmpty(path)) return false;
|
|
290
|
+
var token = path.Replace("PrimitiveType.", "").Trim();
|
|
291
|
+
switch (token.ToLowerInvariant())
|
|
292
|
+
{
|
|
293
|
+
case "cube":
|
|
294
|
+
case "sphere":
|
|
295
|
+
case "capsule":
|
|
296
|
+
case "cylinder":
|
|
297
|
+
case "plane":
|
|
298
|
+
case "quad":
|
|
299
|
+
return true;
|
|
300
|
+
default:
|
|
301
|
+
return path.IndexOf("PrimitiveType", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private static PrimitiveType ParsePrimitive(string value)
|
|
306
|
+
{
|
|
307
|
+
switch (value.Trim().ToLowerInvariant())
|
|
308
|
+
{
|
|
309
|
+
case "cube": return PrimitiveType.Cube;
|
|
310
|
+
case "sphere": return PrimitiveType.Sphere;
|
|
311
|
+
case "capsule": return PrimitiveType.Capsule;
|
|
312
|
+
case "cylinder": return PrimitiveType.Cylinder;
|
|
313
|
+
case "plane": return PrimitiveType.Plane;
|
|
314
|
+
case "quad": return PrimitiveType.Quad;
|
|
315
|
+
default:
|
|
316
|
+
throw new ArgumentException(
|
|
317
|
+
$"Unknown primitive '{value}'. Use one of: Cube, Sphere, Capsule, Cylinder, Plane, Quad.");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
270
321
|
private static Type ResolveComponentType(string typeName)
|
|
271
322
|
{
|
|
272
323
|
// Try exact match first
|
|
@@ -314,7 +365,7 @@ namespace UCP.Bridge
|
|
|
314
365
|
|
|
315
366
|
private static GameObject FindInHierarchy(GameObject go, int instanceId)
|
|
316
367
|
{
|
|
317
|
-
if (go.
|
|
368
|
+
if (go.GetId() == instanceId) return go;
|
|
318
369
|
for (int i = 0; i < go.transform.childCount; i++)
|
|
319
370
|
{
|
|
320
371
|
var found = FindInHierarchy(go.transform.GetChild(i).gameObject, instanceId);
|
|
@@ -50,7 +50,7 @@ namespace UCP.Bridge
|
|
|
50
50
|
["path"] = path,
|
|
51
51
|
["name"] = material.name,
|
|
52
52
|
["shader"] = shader.name,
|
|
53
|
-
["instanceId"] = material.
|
|
53
|
+
["instanceId"] = material.GetId()
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -278,7 +278,7 @@ namespace UCP.Bridge
|
|
|
278
278
|
{
|
|
279
279
|
["name"] = tex.name,
|
|
280
280
|
["path"] = texPath,
|
|
281
|
-
["instanceId"] = tex.
|
|
281
|
+
["instanceId"] = tex.GetId()
|
|
282
282
|
};
|
|
283
283
|
}
|
|
284
284
|
return null;
|
|
@@ -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
|
|
|
@@ -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}";
|