@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.
Files changed (31) 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 +10 -1
  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/EditorModalGuard.cs +60 -0
  8. package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs.meta +2 -0
  9. package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +56 -5
  10. package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
  11. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +207 -0
  12. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs.meta +2 -0
  13. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +1 -1
  14. package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +1 -35
  15. package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +3 -3
  16. package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +1 -1
  17. package/bridge/com.ucp.bridge/Editor/Controllers/ReferenceController.cs +1 -1
  18. package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +6 -6
  19. package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +2 -34
  20. package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +4 -4
  21. package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs +322 -0
  22. package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs.meta +2 -0
  23. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +249 -0
  24. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs.meta +2 -0
  25. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +415 -0
  26. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs.meta +2 -0
  27. package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +63 -7
  28. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +252 -0
  29. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs.meta +2 -0
  30. package/bridge/com.ucp.bridge/package.json +1 -1
  31. package/package.json +1 -1
@@ -0,0 +1,415 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using UnityEngine;
4
+
5
+ namespace UCP.Bridge
6
+ {
7
+ /// <summary>
8
+ /// Composed visual perception for agents. Beyond the flat `screenshot` command this adds:
9
+ /// view/capture — render from a chosen camera (by id) or a temp camera framed on a
10
+ /// target object, with an optional longest-edge cap to keep payloads small.
11
+ /// view/isolate — render a single object in isolation, auto-framed from its bounds, from
12
+ /// one or more orthographic-style directions, optionally as a composite grid.
13
+ /// view/orbit — render a ring of angles around an object as a composite grid so an LLM
14
+ /// can perceive 3D shape from one image.
15
+ ///
16
+ /// Isolation uses a temporary camera + RenderTexture and a culling layer; it works headless
17
+ /// (no Scene view required). Inactive child objects are NOT force-activated, so a fully
18
+ /// disabled target renders empty.
19
+ /// </summary>
20
+ public static class ViewController
21
+ {
22
+ // A layer reserved for isolation rendering. 31 is the conventional "free" user layer.
23
+ private const int IsolationLayer = 31;
24
+
25
+ public static void Register(CommandRouter router)
26
+ {
27
+ router.Register("view/capture", HandleCapture);
28
+ router.Register("view/isolate", HandleIsolate);
29
+ router.Register("view/orbit", HandleOrbit);
30
+ }
31
+
32
+ private static object HandleCapture(string paramsJson)
33
+ {
34
+ var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
35
+ var maxEdge = (int)ReadFloat(p, "maxEdge", 0f);
36
+
37
+ // Frame a temp camera on a target object if one was supplied.
38
+ if (p != null && (p.ContainsKey("targetId") || p.ContainsKey("targetPath") || p.ContainsKey("targetName")))
39
+ {
40
+ var target = ObjectLocator.Resolve(RemapTargetKeys(p));
41
+ ObjectLocator.TryComputeWorldBounds(target, true, out var b);
42
+ var (size, _) = Resolve(maxEdge, 1280, 720);
43
+ var fov = 50f;
44
+ var dir = (ReadVec3Optional(p, "direction") ?? new Vector3(0.4f, 0.3f, -1f)).normalized;
45
+ var (pos, rot, distance) = FitToBounds(b, fov, dir, (float)size.w / size.h);
46
+ var png = Render(pos, rot, fov, size.w, size.h, ~0, Background(p, out var transparent), transparent, distance);
47
+ return PngResult(png, size.w, size.h, new Dictionary<string, object> { ["framedOn"] = target.name });
48
+ }
49
+
50
+ // Otherwise render from an explicit camera id, or the main camera.
51
+ Camera cam = null;
52
+ if (p != null && (p.TryGetValue("camera", out var camObj)) && camObj != null)
53
+ {
54
+ var go = ObjectLocator.FindByInstanceId(Convert.ToInt32(camObj));
55
+ if (go != null) cam = go.GetComponent<Camera>();
56
+ if (cam == null) throw new ArgumentException($"No Camera component on object {camObj}");
57
+ }
58
+ cam = cam != null ? cam : (Camera.main != null ? Camera.main : UnityEngine.Object.FindAnyObjectByType<Camera>());
59
+ if (cam == null) throw new Exception("No camera available to capture");
60
+
61
+ var aspect = cam.pixelHeight > 0 ? (float)cam.pixelWidth / cam.pixelHeight : 16f / 9f;
62
+ var (csize, _) = Resolve(maxEdge, 1280, Mathf.RoundToInt(1280 / Mathf.Max(aspect, 0.01f)));
63
+ var capturePng = RenderCamera(cam, csize.w, csize.h);
64
+ return PngResult(capturePng, csize.w, csize.h, new Dictionary<string, object> { ["camera"] = cam.gameObject.name });
65
+ }
66
+
67
+ private static object HandleIsolate(string paramsJson)
68
+ {
69
+ var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
70
+ var target = ObjectLocator.Resolve(p);
71
+ var maxEdge = (int)ReadFloat(p, "maxEdge", 512f);
72
+ var edge = Mathf.Clamp(maxEdge, 64, 2048);
73
+ var bg = Background(p, out var transparent);
74
+
75
+ var views = ReadViews(p); // e.g. ["front","right","top"] or ["composite"]
76
+ var composite = views.Count == 1 && views[0] == "composite";
77
+ if (composite)
78
+ views = new List<string> { "front", "right", "back", "top" };
79
+
80
+ using (var iso = new IsolationScope(target, IsolationLayer))
81
+ {
82
+ ObjectLocator.TryComputeWorldBounds(target, true, out var bounds);
83
+ if (bounds.size == Vector3.zero)
84
+ bounds = new Bounds(target.transform.position, Vector3.one);
85
+
86
+ var rendered = new List<(string view, Texture2D tex)>();
87
+ try
88
+ {
89
+ foreach (var view in views)
90
+ {
91
+ var dir = DirectionFor(view);
92
+ var fov = 35f;
93
+ var (pos, rot, distance) = FitToBounds(bounds, fov, dir, 1f);
94
+ var tex = RenderToTexture(pos, rot, fov, edge, edge, 1 << IsolationLayer, bg, transparent, distance);
95
+ rendered.Add((view, tex));
96
+ }
97
+
98
+ if (composite)
99
+ {
100
+ var grid = ComposeGrid(rendered, edge, transparent);
101
+ var png = grid.EncodeToPNG();
102
+ var gw = grid.width;
103
+ var gh = grid.height;
104
+ UnityEngine.Object.DestroyImmediate(grid);
105
+ return PngResult(png, gw, gh,
106
+ new Dictionary<string, object> { ["target"] = target.name, ["layout"] = "composite", ["views"] = AsObjects(views) });
107
+ }
108
+
109
+ if (rendered.Count == 1)
110
+ {
111
+ var png = rendered[0].tex.EncodeToPNG();
112
+ return PngResult(png, edge, edge,
113
+ new Dictionary<string, object> { ["target"] = target.name, ["view"] = rendered[0].view });
114
+ }
115
+
116
+ var images = new List<object>();
117
+ foreach (var (view, tex) in rendered)
118
+ {
119
+ images.Add(new Dictionary<string, object>
120
+ {
121
+ ["view"] = view,
122
+ ["encoding"] = "base64",
123
+ ["format"] = "png",
124
+ ["data"] = Convert.ToBase64String(tex.EncodeToPNG())
125
+ });
126
+ }
127
+ return new Dictionary<string, object>
128
+ {
129
+ ["status"] = "ok", ["target"] = target.name, ["width"] = edge, ["height"] = edge, ["images"] = images
130
+ };
131
+ }
132
+ finally
133
+ {
134
+ foreach (var (_, tex) in rendered)
135
+ if (tex != null) UnityEngine.Object.DestroyImmediate(tex);
136
+ }
137
+ }
138
+ }
139
+
140
+ private static object HandleOrbit(string paramsJson)
141
+ {
142
+ var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
143
+ var target = ObjectLocator.Resolve(p);
144
+ var count = Mathf.Clamp((int)ReadFloat(p, "count", 4f), 1, 12);
145
+ var elevation = ReadFloat(p, "elevation", 20f);
146
+ var maxEdge = Mathf.Clamp((int)ReadFloat(p, "maxEdge", 384f), 64, 1024);
147
+ var bg = Background(p, out var transparent);
148
+
149
+ using (var iso = new IsolationScope(target, IsolationLayer))
150
+ {
151
+ ObjectLocator.TryComputeWorldBounds(target, true, out var bounds);
152
+ if (bounds.size == Vector3.zero)
153
+ bounds = new Bounds(target.transform.position, Vector3.one);
154
+
155
+ var rendered = new List<(string, Texture2D)>();
156
+ try
157
+ {
158
+ for (var i = 0; i < count; i++)
159
+ {
160
+ var yaw = 360f * i / count;
161
+ var rot = Quaternion.Euler(elevation, yaw, 0f);
162
+ var dir = rot * Vector3.forward; // camera looks along +dir toward center
163
+ var fov = 35f;
164
+ var (pos, lookRot, distance) = FitToBounds(bounds, fov, -dir, 1f);
165
+ var tex = RenderToTexture(pos, lookRot, fov, maxEdge, maxEdge, 1 << IsolationLayer, bg, transparent, distance);
166
+ rendered.Add(($"{Mathf.RoundToInt(yaw)}deg", tex));
167
+ }
168
+
169
+ var grid = ComposeGrid(rendered, maxEdge, transparent);
170
+ var png = grid.EncodeToPNG();
171
+ var w = grid.width; var h = grid.height;
172
+ UnityEngine.Object.DestroyImmediate(grid);
173
+ return PngResult(png, w, h,
174
+ new Dictionary<string, object> { ["target"] = target.name, ["layout"] = "orbit", ["angles"] = count });
175
+ }
176
+ finally
177
+ {
178
+ foreach (var (_, tex) in rendered)
179
+ if (tex != null) UnityEngine.Object.DestroyImmediate(tex);
180
+ }
181
+ }
182
+ }
183
+
184
+ // --- rendering core ------------------------------------------------
185
+
186
+ /// <summary>Place a camera so the bounds fill the frame, looking from -dir toward center.</summary>
187
+ private static (Vector3 pos, Quaternion rot, float distance) FitToBounds(Bounds bounds, float fov, Vector3 viewDir, float aspect)
188
+ {
189
+ var radius = Mathf.Max(bounds.extents.magnitude, 0.01f);
190
+ var padding = 1.25f;
191
+ var vFov = fov * Mathf.Deg2Rad;
192
+ // Account for aspect so wide/tall frames still contain the object.
193
+ var effectiveFov = aspect < 1f ? 2f * Mathf.Atan(Mathf.Tan(vFov / 2f) * aspect) : vFov;
194
+ var distance = radius * padding / Mathf.Sin(Mathf.Max(effectiveFov, 0.1f) / 2f);
195
+ var dir = viewDir.sqrMagnitude < 1e-6f ? new Vector3(0.4f, 0.3f, -1f).normalized : viewDir.normalized;
196
+ var pos = bounds.center + dir * distance;
197
+ var rot = Quaternion.LookRotation(bounds.center - pos, Vector3.up);
198
+ return (pos, rot, distance);
199
+ }
200
+
201
+ private static Texture2D RenderToTexture(Vector3 pos, Quaternion rot, float fov, int w, int h,
202
+ int cullingMask, Color bg, bool transparent, float distance)
203
+ {
204
+ var camGo = new GameObject("__ucp_view_cam") { hideFlags = HideFlags.HideAndDontSave };
205
+ var cam = camGo.AddComponent<Camera>();
206
+ var rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32) { antiAliasing = 1 };
207
+ var prevActive = RenderTexture.active;
208
+ try
209
+ {
210
+ cam.transform.position = pos;
211
+ cam.transform.rotation = rot;
212
+ cam.fieldOfView = fov;
213
+ cam.nearClipPlane = Mathf.Max(0.01f, distance * 0.01f);
214
+ cam.farClipPlane = distance * 4f + 1000f;
215
+ cam.cullingMask = cullingMask;
216
+ cam.clearFlags = CameraClearFlags.SolidColor;
217
+ cam.backgroundColor = transparent ? new Color(bg.r, bg.g, bg.b, 0f) : bg;
218
+ cam.targetTexture = rt;
219
+ cam.Render();
220
+
221
+ RenderTexture.active = rt;
222
+ var tex = new Texture2D(w, h, transparent ? TextureFormat.RGBA32 : TextureFormat.RGB24, false);
223
+ tex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
224
+ tex.Apply();
225
+ return tex;
226
+ }
227
+ finally
228
+ {
229
+ RenderTexture.active = prevActive;
230
+ cam.targetTexture = null;
231
+ UnityEngine.Object.DestroyImmediate(rt);
232
+ UnityEngine.Object.DestroyImmediate(camGo);
233
+ }
234
+ }
235
+
236
+ private static byte[] Render(Vector3 pos, Quaternion rot, float fov, int w, int h,
237
+ int cullingMask, Color bg, bool transparent, float distance)
238
+ {
239
+ var tex = RenderToTexture(pos, rot, fov, w, h, cullingMask, bg, transparent, distance);
240
+ try { return tex.EncodeToPNG(); }
241
+ finally { UnityEngine.Object.DestroyImmediate(tex); }
242
+ }
243
+
244
+ private static byte[] RenderCamera(Camera cam, int w, int h)
245
+ {
246
+ var rt = new RenderTexture(w, h, 24);
247
+ var prevTarget = cam.targetTexture;
248
+ var prevActive = RenderTexture.active;
249
+ try
250
+ {
251
+ cam.targetTexture = rt;
252
+ cam.Render();
253
+ RenderTexture.active = rt;
254
+ var tex = new Texture2D(w, h, TextureFormat.RGB24, false);
255
+ tex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
256
+ tex.Apply();
257
+ var png = tex.EncodeToPNG();
258
+ UnityEngine.Object.DestroyImmediate(tex);
259
+ return png;
260
+ }
261
+ finally
262
+ {
263
+ cam.targetTexture = prevTarget;
264
+ RenderTexture.active = prevActive;
265
+ UnityEngine.Object.DestroyImmediate(rt);
266
+ }
267
+ }
268
+
269
+ private static Texture2D ComposeGrid(List<(string view, Texture2D tex)> tiles, int tileEdge, bool transparent)
270
+ {
271
+ var cols = Mathf.CeilToInt(Mathf.Sqrt(tiles.Count));
272
+ var rows = Mathf.CeilToInt((float)tiles.Count / cols);
273
+ var grid = new Texture2D(cols * tileEdge, rows * tileEdge,
274
+ transparent ? TextureFormat.RGBA32 : TextureFormat.RGB24, false);
275
+
276
+ // Clear to transparent/black.
277
+ var clear = new Color(0, 0, 0, transparent ? 0f : 1f);
278
+ var fill = new Color[grid.width * grid.height];
279
+ for (var i = 0; i < fill.Length; i++) fill[i] = clear;
280
+ grid.SetPixels(fill);
281
+
282
+ for (var i = 0; i < tiles.Count; i++)
283
+ {
284
+ var col = i % cols;
285
+ var row = rows - 1 - (i / cols); // top-left first
286
+ grid.SetPixels(col * tileEdge, row * tileEdge, tileEdge, tileEdge, tiles[i].tex.GetPixels());
287
+ }
288
+ grid.Apply();
289
+ return grid;
290
+ }
291
+
292
+ // --- isolation scope ----------------------------------------------
293
+
294
+ /// <summary>Temporarily moves a hierarchy onto an isolation layer, restoring on dispose.</summary>
295
+ private sealed class IsolationScope : IDisposable
296
+ {
297
+ private readonly List<(GameObject go, int layer)> _saved = new();
298
+
299
+ public IsolationScope(GameObject root, int isolationLayer)
300
+ {
301
+ foreach (var t in root.GetComponentsInChildren<Transform>(true))
302
+ {
303
+ _saved.Add((t.gameObject, t.gameObject.layer));
304
+ t.gameObject.layer = isolationLayer;
305
+ }
306
+ }
307
+
308
+ public void Dispose()
309
+ {
310
+ foreach (var (go, layer) in _saved)
311
+ if (go != null) go.layer = layer;
312
+ }
313
+ }
314
+
315
+ // --- param helpers -------------------------------------------------
316
+
317
+ private static Vector3 DirectionFor(string view)
318
+ {
319
+ switch (view)
320
+ {
321
+ case "front": return new Vector3(0, 0, -1);
322
+ case "back": return new Vector3(0, 0, 1);
323
+ case "left": return new Vector3(-1, 0, 0);
324
+ case "right": return new Vector3(1, 0, 0);
325
+ case "top": return new Vector3(0, 1, -0.0001f);
326
+ case "bottom": return new Vector3(0, -1, -0.0001f);
327
+ default: return new Vector3(0.4f, 0.3f, -1f);
328
+ }
329
+ }
330
+
331
+ private static List<string> ReadViews(Dictionary<string, object> p)
332
+ {
333
+ if (p != null && p.TryGetValue("views", out var v) && v is List<object> list && list.Count > 0)
334
+ {
335
+ var views = new List<string>();
336
+ foreach (var item in list) views.Add(item.ToString().ToLowerInvariant());
337
+ return views;
338
+ }
339
+ if (p != null && p.TryGetValue("view", out var single) && single != null)
340
+ return new List<string> { single.ToString().ToLowerInvariant() };
341
+ return new List<string> { "composite" };
342
+ }
343
+
344
+ private static Color Background(Dictionary<string, object> p, out bool transparent)
345
+ {
346
+ transparent = false;
347
+ if (p == null) return new Color(0.18f, 0.18f, 0.2f, 1f);
348
+ if (p.TryGetValue("background", out var b) && b != null && b.ToString().ToLowerInvariant() == "transparent")
349
+ {
350
+ transparent = true;
351
+ return new Color(0, 0, 0, 0);
352
+ }
353
+ if (p.TryGetValue("bgColor", out var c) && c is List<object> col && col.Count >= 3)
354
+ {
355
+ return new Color(Convert.ToSingle(col[0]), Convert.ToSingle(col[1]), Convert.ToSingle(col[2]),
356
+ col.Count >= 4 ? Convert.ToSingle(col[3]) : 1f);
357
+ }
358
+ return new Color(0.18f, 0.18f, 0.2f, 1f);
359
+ }
360
+
361
+ private static ((int w, int h) size, bool capped) Resolve(int maxEdge, int defW, int defH)
362
+ {
363
+ if (maxEdge <= 0) return ((Mathf.Clamp(defW, 64, 4096), Mathf.Clamp(defH, 64, 4096)), false);
364
+ var longest = Mathf.Max(defW, defH);
365
+ var scale = (float)Mathf.Clamp(maxEdge, 64, 4096) / longest;
366
+ return ((Mathf.Max(64, Mathf.RoundToInt(defW * scale)), Mathf.Max(64, Mathf.RoundToInt(defH * scale))), true);
367
+ }
368
+
369
+ private static Dictionary<string, object> PngResult(byte[] png, int w, int h, Dictionary<string, object> extra)
370
+ {
371
+ var result = new Dictionary<string, object>
372
+ {
373
+ ["status"] = "ok",
374
+ ["width"] = w,
375
+ ["height"] = h,
376
+ ["format"] = "png",
377
+ ["encoding"] = "base64",
378
+ ["data"] = Convert.ToBase64String(png),
379
+ ["size"] = png.Length
380
+ };
381
+ if (extra != null)
382
+ foreach (var kv in extra) result[kv.Key] = kv.Value;
383
+ return result;
384
+ }
385
+
386
+ private static List<object> AsObjects(List<string> items)
387
+ {
388
+ var list = new List<object>();
389
+ foreach (var s in items) list.Add(s);
390
+ return list;
391
+ }
392
+
393
+ private static Dictionary<string, object> RemapTargetKeys(Dictionary<string, object> p)
394
+ {
395
+ var remapped = new Dictionary<string, object>();
396
+ if (p.TryGetValue("targetId", out var id)) remapped["instanceId"] = id;
397
+ if (p.TryGetValue("targetPath", out var path)) remapped["path"] = path;
398
+ if (p.TryGetValue("targetName", out var name)) remapped["name"] = name;
399
+ return remapped;
400
+ }
401
+
402
+ private static Vector3? ReadVec3Optional(Dictionary<string, object> p, string key)
403
+ {
404
+ if (p == null || !p.TryGetValue(key, out var v) || v == null) return null;
405
+ if (v is not List<object> list || list.Count < 3) return null;
406
+ return new Vector3(Convert.ToSingle(list[0]), Convert.ToSingle(list[1]), Convert.ToSingle(list[2]));
407
+ }
408
+
409
+ private static float ReadFloat(Dictionary<string, object> p, string key, float dflt)
410
+ {
411
+ if (p != null && p.TryGetValue(key, out var v) && v != null) return Convert.ToSingle(v);
412
+ return dflt;
413
+ }
414
+ }
415
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 3a4b5c6d7e8f9001a2b3c4d5e6f70819
@@ -544,7 +544,7 @@ namespace UCP.Bridge.Tests
544
544
  );
545
545
  Assert.That(getPosition.error, Is.Null);
546
546
 
547
- var updated = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
547
+ var updated = UnityObjectCompat.ResolveByInstanceId(instanceId) as GameObject;
548
548
  Assert.That(updated, Is.Not.Null);
549
549
  var localPosition = updated.transform.localPosition;
550
550
  Assert.That(localPosition.x, Is.EqualTo(1f).Within(0.001f));
@@ -556,7 +556,7 @@ namespace UCP.Bridge.Tests
556
556
 
557
557
  var delete = _router.Dispatch("object/delete", 1, "{\"instanceId\":" + instanceId + "}");
558
558
  Assert.That(delete.error, Is.Null);
559
- Assert.That(EditorUtility.InstanceIDToObject(instanceId), Is.Null);
559
+ Assert.That(UnityObjectCompat.ResolveByInstanceId(instanceId), Is.Null);
560
560
  }
561
561
 
562
562
  [Test]
@@ -573,7 +573,7 @@ namespace UCP.Bridge.Tests
573
573
  var response = _router.Dispatch(
574
574
  "object/set-property",
575
575
  1,
576
- "{\"instanceId\":" + go.GetInstanceID() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"" + TempReferenceAssetPath + "\"}}"
576
+ "{\"instanceId\":" + go.GetId() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"" + TempReferenceAssetPath + "\"}}"
577
577
  );
578
578
 
579
579
  Assert.That(response.error, Is.Null);
@@ -598,7 +598,7 @@ namespace UCP.Bridge.Tests
598
598
  var response = _router.Dispatch(
599
599
  "object/set-property",
600
600
  1,
601
- "{\"instanceId\":" + cube.GetInstanceID() + ",\"component\":\"MeshRenderer\",\"property\":\"m_Materials\",\"value\":[{\"path\":\"" + TempMaterialPath + "\"}]}"
601
+ "{\"instanceId\":" + cube.GetId() + ",\"component\":\"MeshRenderer\",\"property\":\"m_Materials\",\"value\":[{\"path\":\"" + TempMaterialPath + "\"}]}"
602
602
  );
603
603
 
604
604
  Assert.That(response.error, Is.Null);
@@ -616,7 +616,7 @@ namespace UCP.Bridge.Tests
616
616
  var response = _router.Dispatch(
617
617
  "object/set-property",
618
618
  1,
619
- "{\"instanceId\":" + go.GetInstanceID() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"Assets/Missing.asset\"}}"
619
+ "{\"instanceId\":" + go.GetId() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"Assets/Missing.asset\"}}"
620
620
  );
621
621
 
622
622
  Assert.That(response.error, Is.Not.Null);
@@ -990,6 +990,62 @@ namespace UCP.Bridge.Tests
990
990
  Assert.That(UnityEngine.SceneManagement.SceneManager.GetSceneByPath(TempSceneBPath).isLoaded, Is.True);
991
991
  }
992
992
 
993
+ [Test]
994
+ public void ModalGuard_AutoSavesDirtyTitledScene_WithoutPrompting()
995
+ {
996
+ var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
997
+ Assert.That(EditorSceneManager.SaveScene(scene, TempScenePath), Is.True);
998
+ new GameObject("ModalGuardDirtyMaker");
999
+ EditorSceneManager.MarkSceneDirty(scene);
1000
+ Assert.That(scene.isDirty, Is.True);
1001
+
1002
+ EditorModalGuard.SaveOpenDirtyScenes(true, true);
1003
+
1004
+ Assert.That(scene.isDirty, Is.False);
1005
+ Assert.That(string.IsNullOrEmpty(scene.path), Is.False);
1006
+ }
1007
+
1008
+ [Test]
1009
+ public void ModalGuard_DiscardsDirtyUntitledScene_WhenDiscardAllowed()
1010
+ {
1011
+ var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
1012
+ new GameObject("ModalGuardUntitledDirtyMaker");
1013
+ EditorSceneManager.MarkSceneDirty(scene);
1014
+ Assert.That(string.IsNullOrEmpty(scene.path), Is.True);
1015
+ Assert.That(scene.isDirty, Is.True);
1016
+
1017
+ EditorModalGuard.SaveOpenDirtyScenes(true, true);
1018
+
1019
+ var active = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
1020
+ Assert.That(string.IsNullOrEmpty(active.path), Is.True);
1021
+ Assert.That(active.isDirty, Is.False);
1022
+ Assert.That(GameObject.Find("ModalGuardUntitledDirtyMaker"), Is.Null);
1023
+ }
1024
+
1025
+ [Test]
1026
+ public void ModalGuard_ThrowsOnDirtyUntitledScene_WhenDiscardDisallowed()
1027
+ {
1028
+ var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
1029
+ new GameObject("ModalGuardUntitledDirtyMaker");
1030
+ EditorSceneManager.MarkSceneDirty(scene);
1031
+
1032
+ Assert.That(
1033
+ () => EditorModalGuard.SaveOpenDirtyScenes(true, false),
1034
+ Throws.TypeOf<System.InvalidOperationException>());
1035
+ }
1036
+
1037
+ [Test]
1038
+ public void ModalGuard_LeavesSceneUntouched_WhenSavingDisabled()
1039
+ {
1040
+ var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
1041
+ new GameObject("ModalGuardUntitledDirtyMaker");
1042
+ EditorSceneManager.MarkSceneDirty(scene);
1043
+ Assert.That(scene.isDirty, Is.True);
1044
+
1045
+ Assert.That(() => EditorModalGuard.SaveOpenDirtyScenes(false, false), Throws.Nothing);
1046
+ Assert.That(UnityEngine.SceneManagement.SceneManager.GetActiveScene().isDirty, Is.True);
1047
+ }
1048
+
993
1049
  [Test]
994
1050
  public void PackagesController_DependencySetInfoAndRemove_LocalFilePackage()
995
1051
  {
@@ -1071,7 +1127,7 @@ namespace UCP.Bridge.Tests
1071
1127
  var response = _router.Dispatch(
1072
1128
  "scene/focus",
1073
1129
  1,
1074
- "{\"instanceId\":" + cube.GetInstanceID() + ",\"axis\":[1,0,1]}"
1130
+ "{\"instanceId\":" + cube.GetId() + ",\"axis\":[1,0,1]}"
1075
1131
  );
1076
1132
 
1077
1133
  Assert.That(response.error, Is.Null);
@@ -1102,7 +1158,7 @@ namespace UCP.Bridge.Tests
1102
1158
  var response = _router.Dispatch(
1103
1159
  "scene/focus",
1104
1160
  1,
1105
- "{\"instanceId\":" + cube.GetInstanceID() + ",\"axis\":[0,0,0]}"
1161
+ "{\"instanceId\":" + cube.GetId() + ",\"axis\":[0,0,0]}"
1106
1162
  );
1107
1163
 
1108
1164
  Assert.That(response.error, Is.Not.Null);