@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.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs +3 -0
  3. package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs.meta +2 -0
  4. package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +45 -5
  5. package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +26 -0
  6. package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +2 -2
  7. package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +137 -1
  8. package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs +60 -0
  9. package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs.meta +2 -0
  10. package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +56 -5
  11. package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
  12. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +207 -0
  13. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs.meta +2 -0
  14. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +1 -1
  15. package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +1 -35
  16. package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +3 -3
  17. package/bridge/com.ucp.bridge/Editor/Controllers/ProfilerController.cs +80 -5
  18. package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +1 -1
  19. package/bridge/com.ucp.bridge/Editor/Controllers/ReferenceController.cs +1 -1
  20. package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +6 -6
  21. package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +26 -36
  22. package/bridge/com.ucp.bridge/Editor/Controllers/ScriptController.cs +82 -23
  23. package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +4 -4
  24. package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs +322 -0
  25. package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs.meta +2 -0
  26. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +249 -0
  27. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs.meta +2 -0
  28. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +415 -0
  29. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs.meta +2 -0
  30. package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +409 -63
  31. package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +131 -13
  32. package/bridge/com.ucp.bridge/Tests/Editor/MiniJsonSerializerTests.cs +221 -0
  33. package/bridge/com.ucp.bridge/Tests/Editor/MiniJsonSerializerTests.cs.meta +2 -0
  34. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +252 -0
  35. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs.meta +2 -0
  36. package/bridge/com.ucp.bridge/package.json +1 -1
  37. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @mflrevan/ucp
2
2
 
3
- Version `0.5.2` of the Unity Control Protocol CLI.
3
+ Version `0.6.1` 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
 
@@ -0,0 +1,3 @@
1
+ using System.Runtime.CompilerServices;
2
+
3
+ [assembly: InternalsVisibleTo("UCP.Bridge.Editor.Tests")]
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: a1b2c3d4e5f60718293a4b5c6d7e8f90
@@ -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.5.2";
29
+ private const string ProtocolVersion = "0.6.1";
30
30
 
31
31
  private static TcpListener s_listener;
32
32
  private static CancellationTokenSource s_cts;
@@ -36,6 +36,13 @@ namespace UCP.Bridge
36
36
  private static string s_token;
37
37
  private static bool s_running;
38
38
 
39
+ // Handshake payload, captured once on the main thread at startup. `Application.dataPath`
40
+ // and friends are main-thread-only, which is the sole reason the handshake used to be
41
+ // queued behind `EditorApplication.update` -- see the socket-thread fast path in Dispatch.
42
+ private static string s_unityVersion = string.Empty;
43
+ private static string s_projectName = string.Empty;
44
+ private static string s_projectPath = string.Empty;
45
+
39
46
  // Main-thread action queue
40
47
  private static readonly ConcurrentQueue<Action> s_mainThreadQueue = new();
41
48
 
@@ -71,6 +78,7 @@ namespace UCP.Bridge
71
78
 
72
79
  try
73
80
  {
81
+ CaptureEditorIdentity();
74
82
  RegisterHandlers();
75
83
  LogsController.SeedHistoryFromConsole();
76
84
 
@@ -88,18 +96,31 @@ namespace UCP.Bridge
88
96
  }
89
97
  }
90
98
 
99
+ /// <summary>
100
+ /// Read the editor/project identity once, on the main thread, so the handshake handler is
101
+ /// pure and can run anywhere.
102
+ /// </summary>
103
+ private static void CaptureEditorIdentity()
104
+ {
105
+ s_unityVersion = Application.unityVersion;
106
+ s_projectName = Application.productName;
107
+ s_projectPath = Path.GetDirectoryName(Application.dataPath);
108
+ }
109
+
91
110
  private static void RegisterHandlers()
92
111
  {
93
112
  // Handshake
94
113
  s_router.Register("handshake", (paramsJson) =>
95
114
  {
115
+ // Touches no Unity API -- see CaptureEditorIdentity. Keep it that way: the
116
+ // socket-thread fast path in Dispatch depends on it.
96
117
  return new
97
118
  {
98
119
  serverVersion = ProtocolVersion,
99
120
  protocolVersion = ProtocolVersion,
100
- unityVersion = Application.unityVersion,
101
- projectName = Application.productName,
102
- projectPath = Path.GetDirectoryName(Application.dataPath)
121
+ unityVersion = s_unityVersion,
122
+ projectName = s_projectName,
123
+ projectPath = s_projectPath
103
124
  };
104
125
  });
105
126
 
@@ -145,6 +166,15 @@ namespace UCP.Bridge
145
166
  // Hierarchy Operations
146
167
  HierarchyController.Register(s_router);
147
168
 
169
+ // Transform authoring (move/rotate/scale/look-at, bulk read)
170
+ TransformController.Register(s_router);
171
+
172
+ // Spatial queries (raycast/overlap/bounds/ground/nearest)
173
+ SpatialController.Register(s_router);
174
+
175
+ // Composed visual perception (capture/isolate/orbit)
176
+ ViewController.Register(s_router);
177
+
148
178
  // Asset Management
149
179
  AssetController.Register(s_router);
150
180
  ImporterController.Register(s_router);
@@ -384,12 +414,22 @@ namespace UCP.Bridge
384
414
  lock (s_clientLock) { s_logSubscribers.Remove(ws); }
385
415
  }
386
416
 
387
- // Dispatch on main thread
388
417
  var capturedId = id;
389
418
  var capturedMethod = method;
390
419
  var capturedParams = paramsJson;
391
420
  var capturedWs = ws;
392
421
 
422
+ // Answer the handshake straight off the socket thread. Everything it returns is
423
+ // cached, so queueing it only bought a wait for the next `EditorApplication.update`
424
+ // tick -- roughly 90 ms on an idle editor, paid by *every* CLI command before its
425
+ // real request could even be dispatched.
426
+ if (capturedMethod == "handshake")
427
+ {
428
+ SendResponse(capturedWs, s_router.Dispatch(capturedMethod, capturedId, capturedParams));
429
+ return;
430
+ }
431
+
432
+ // Dispatch on main thread
393
433
  s_mainThreadQueue.Enqueue(() =>
394
434
  {
395
435
  var response = s_router.Dispatch(capturedMethod, capturedId, capturedParams);
@@ -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.GetInstanceID(),
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.GetInstanceID()
499
+ ["instanceId"] = instance.GetId()
500
500
  };
501
501
  }
502
502
 
@@ -9,19 +9,86 @@ namespace UCP.Bridge
9
9
  {
10
10
  public static class CompilationController
11
11
  {
12
+ // SessionState survives domain reloads within one editor session. That is exactly the
13
+ // window we need: a compile that SUCCEEDS reloads the domain, a compile that FAILS keeps
14
+ // the old domain (no reload), and in both cases the captured diagnostics must still be
15
+ // readable by the CLI after the editor settles. CompileDiagnosticsTracker writes here;
16
+ // HandleDiagnostics reads it back.
17
+ internal const string DiagStateKey = "ucp.compile.diagnostics.state";
18
+ internal const string DiagRequestKey = "ucp.compile.diagnostics.requestId";
19
+ private const int MaxDiagnosticMessages = 200;
20
+
12
21
  public static void Register(CommandRouter router)
13
22
  {
14
23
  router.Register("compile", HandleCompile);
24
+ router.Register("compile/diagnostics", HandleDiagnostics);
15
25
  router.Register("refresh-assets", HandleRefresh);
16
26
  router.Register("script/doctor", HandleScriptDoctor);
17
27
  }
18
28
 
19
29
  private static object HandleCompile(string paramsJson)
20
30
  {
31
+ // Stamp a fresh request id and reset the diagnostics buffer so the CLI can tell THIS
32
+ // compile's result apart from a stale one. CompileDiagnosticsTracker fills in the
33
+ // per-assembly CompilerMessages as compilation progresses and finishes.
34
+ var requestId = SessionState.GetInt(DiagRequestKey, 0) + 1;
35
+ SessionState.SetInt(DiagRequestKey, requestId);
36
+ SessionState.SetString(DiagStateKey, MiniJson.Serialize(new Dictionary<string, object>
37
+ {
38
+ ["status"] = "requested",
39
+ ["requestId"] = requestId,
40
+ ["errorCount"] = 0,
41
+ ["warningCount"] = 0,
42
+ ["messages"] = new List<object>()
43
+ }));
44
+
21
45
  AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
22
46
  CompilationPipeline.RequestScriptCompilation();
23
47
  TrySyncSolution();
24
- return new { status = "ok", message = "Asset database refreshed and compilation requested" };
48
+ return new Dictionary<string, object>
49
+ {
50
+ ["status"] = "ok",
51
+ ["message"] = "Asset database refreshed and compilation requested",
52
+ ["requestId"] = requestId
53
+ };
54
+ }
55
+
56
+ private static object HandleDiagnostics(string paramsJson)
57
+ {
58
+ var raw = SessionState.GetString(DiagStateKey, string.Empty);
59
+ if (string.IsNullOrEmpty(raw)
60
+ || !(MiniJson.Deserialize(raw) is Dictionary<string, object> state))
61
+ {
62
+ return new Dictionary<string, object>
63
+ {
64
+ ["status"] = "idle",
65
+ ["requestId"] = SessionState.GetInt(DiagRequestKey, 0),
66
+ ["errorCount"] = 0,
67
+ ["warningCount"] = 0,
68
+ ["messages"] = new List<object>(),
69
+ ["compiling"] = EditorApplication.isCompiling
70
+ };
71
+ }
72
+
73
+ // Surface live compile state too: a "completed" buffer while isCompiling is true means
74
+ // another compilation has already started after the one the CLI asked about.
75
+ state["compiling"] = EditorApplication.isCompiling;
76
+ return state;
77
+ }
78
+
79
+ internal static void WriteDiagnosticsState(string status, int errorCount, int warningCount, List<object> messages)
80
+ {
81
+ var truncated = messages.Count > MaxDiagnosticMessages;
82
+ var trimmed = truncated ? messages.GetRange(0, MaxDiagnosticMessages) : messages;
83
+ SessionState.SetString(DiagStateKey, MiniJson.Serialize(new Dictionary<string, object>
84
+ {
85
+ ["status"] = status,
86
+ ["requestId"] = SessionState.GetInt(DiagRequestKey, 0),
87
+ ["errorCount"] = errorCount,
88
+ ["warningCount"] = warningCount,
89
+ ["truncated"] = truncated,
90
+ ["messages"] = new List<object>(trimmed)
91
+ }));
25
92
  }
26
93
 
27
94
  private static object HandleRefresh(string paramsJson)
@@ -110,4 +177,73 @@ namespace UCP.Bridge
110
177
  }
111
178
  }
112
179
  }
180
+
181
+ /// <summary>
182
+ /// Captures per-assembly compiler messages so `compile` can report whether the build actually
183
+ /// succeeded instead of always claiming success. Subscribes once per domain load; accumulates
184
+ /// messages for the in-flight compilation in static fields (safe because a domain reload only
185
+ /// happens AFTER compilationFinished) and flushes them to SessionState, which persists across
186
+ /// the reload for the CLI to read once the editor settles.
187
+ /// </summary>
188
+ [InitializeOnLoad]
189
+ internal static class CompileDiagnosticsTracker
190
+ {
191
+ private static readonly List<object> s_messages = new List<object>();
192
+ private static int s_errorCount;
193
+ private static int s_warningCount;
194
+
195
+ static CompileDiagnosticsTracker()
196
+ {
197
+ CompilationPipeline.compilationStarted += OnCompilationStarted;
198
+ CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished;
199
+ CompilationPipeline.compilationFinished += OnCompilationFinished;
200
+ }
201
+
202
+ private static void OnCompilationStarted(object context)
203
+ {
204
+ s_messages.Clear();
205
+ s_errorCount = 0;
206
+ s_warningCount = 0;
207
+ CompilationController.WriteDiagnosticsState("compiling", 0, 0, s_messages);
208
+ }
209
+
210
+ private static void OnAssemblyCompilationFinished(string assemblyPath, CompilerMessage[] messages)
211
+ {
212
+ if (messages == null) return;
213
+ var assembly = Path.GetFileNameWithoutExtension(assemblyPath);
214
+ foreach (var message in messages)
215
+ {
216
+ string type;
217
+ switch (message.type)
218
+ {
219
+ case CompilerMessageType.Error:
220
+ type = "error";
221
+ s_errorCount++;
222
+ break;
223
+ case CompilerMessageType.Warning:
224
+ type = "warning";
225
+ s_warningCount++;
226
+ break;
227
+ default:
228
+ type = "info";
229
+ break;
230
+ }
231
+
232
+ s_messages.Add(new Dictionary<string, object>
233
+ {
234
+ ["assembly"] = assembly,
235
+ ["type"] = type,
236
+ ["message"] = message.message ?? string.Empty,
237
+ ["file"] = message.file ?? string.Empty,
238
+ ["line"] = message.line,
239
+ ["column"] = message.column
240
+ });
241
+ }
242
+ }
243
+
244
+ private static void OnCompilationFinished(object context)
245
+ {
246
+ CompilationController.WriteDiagnosticsState("completed", s_errorCount, s_warningCount, s_messages);
247
+ }
248
+ }
113
249
  }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6
@@ -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
- var go = new GameObject(name);
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.GetInstanceID(),
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
- throw new ArgumentException($"Prefab not found: {prefabPath}");
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.GetInstanceID(),
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.GetInstanceID() == instanceId) return 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.GetInstanceID()
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.GetInstanceID()
281
+ ["instanceId"] = tex.GetId()
282
282
  };
283
283
  }
284
284
  return null;