@mflrevan/ucp 0.5.1 → 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 (35) 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 +16 -3
  5. package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +26 -0
  6. package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +172 -2
  7. package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +88 -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/LogsController.cs +325 -13
  12. package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
  13. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +207 -0
  14. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs.meta +2 -0
  15. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +1 -1
  16. package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +14 -35
  17. package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +3 -3
  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 +2 -34
  22. package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs +151 -0
  23. package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs.meta +2 -0
  24. package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +304 -9
  25. package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs +322 -0
  26. package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs.meta +2 -0
  27. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +249 -0
  28. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs.meta +2 -0
  29. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +415 -0
  30. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs.meta +2 -0
  31. package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +135 -7
  32. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +252 -0
  33. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs.meta +2 -0
  34. package/bridge/com.ucp.bridge/package.json +1 -1
  35. package/package.json +1 -1
@@ -435,6 +435,78 @@ namespace UCP.Bridge.Tests
435
435
  Assert.That(Convert.ToInt32(byLevel["error"]), Is.EqualTo(1));
436
436
  }
437
437
 
438
+ [Test]
439
+ public void LogsStatus_BackfillsConsoleEntriesWhenHistoryStartsEmpty()
440
+ {
441
+ LogsController.SetConsoleBackfillProviderForTests(() => new List<LogsController.ConsoleBackfillEntry>
442
+ {
443
+ new LogsController.ConsoleBackfillEntry { Level = "warning", Message = "Compiler warning" },
444
+ new LogsController.ConsoleBackfillEntry { Level = "error", Message = "Compiler error" }
445
+ });
446
+
447
+ var response = _router.Dispatch("logs/status", 1, "{}");
448
+
449
+ Assert.That(response.error, Is.Null);
450
+
451
+ var result = (Dictionary<string, object>)response.result;
452
+ Assert.That(Convert.ToInt32(result["total"]), Is.EqualTo(2));
453
+
454
+ var byLevel = (Dictionary<string, object>)result["byLevel"];
455
+ Assert.That(Convert.ToInt32(byLevel["warning"]), Is.EqualTo(1));
456
+ Assert.That(Convert.ToInt32(byLevel["error"]), Is.EqualTo(1));
457
+ }
458
+
459
+ [Test]
460
+ public void LogsTail_BackfillIgnoresUcpBridgeNoise()
461
+ {
462
+ LogsController.SetConsoleBackfillProviderForTests(() => new List<LogsController.ConsoleBackfillEntry>
463
+ {
464
+ new LogsController.ConsoleBackfillEntry { Level = "info", Message = "[UCP] Bridge server started on port 21342" },
465
+ new LogsController.ConsoleBackfillEntry { Level = "warning", Message = "User-facing warning" }
466
+ });
467
+
468
+ var response = _router.Dispatch("logs/tail", 1, "{\"count\":20}");
469
+
470
+ Assert.That(response.error, Is.Null);
471
+
472
+ var result = (Dictionary<string, object>)response.result;
473
+ Assert.That(Convert.ToInt32(result["total"]), Is.EqualTo(1));
474
+
475
+ var logs = (List<object>)result["logs"];
476
+ var only = (Dictionary<string, object>)logs[0];
477
+ Assert.That(only["messagePreview"], Is.EqualTo("User-facing warning"));
478
+ }
479
+
480
+ [Test]
481
+ public void LogsGet_CanReadBackfilledConsoleEntries()
482
+ {
483
+ LogsController.SetConsoleBackfillProviderForTests(() => new List<LogsController.ConsoleBackfillEntry>
484
+ {
485
+ new LogsController.ConsoleBackfillEntry
486
+ {
487
+ Level = "exception",
488
+ Message = "Backfilled exception",
489
+ StackTrace = "stack line 1\nstack line 2"
490
+ }
491
+ });
492
+
493
+ var tail = _router.Dispatch("logs/tail", 1, "{\"count\":20}");
494
+ Assert.That(tail.error, Is.Null);
495
+
496
+ var tailResult = (Dictionary<string, object>)tail.result;
497
+ var logs = (List<object>)tailResult["logs"];
498
+ var first = (Dictionary<string, object>)logs[0];
499
+ var id = Convert.ToInt64(first["id"]);
500
+
501
+ var get = _router.Dispatch("logs/get", 1, "{\"id\":" + id + "}");
502
+ Assert.That(get.error, Is.Null);
503
+
504
+ var getResult = (Dictionary<string, object>)get.result;
505
+ Assert.That(getResult["level"], Is.EqualTo("exception"));
506
+ Assert.That(getResult["message"], Is.EqualTo("Backfilled exception"));
507
+ Assert.That(getResult["stackTrace"], Is.EqualTo("stack line 1\nstack line 2"));
508
+ }
509
+
438
510
  [Test]
439
511
  public void ObjectLifecycle_CreateMutateAndDelete_WorksEndToEnd()
440
512
  {
@@ -472,7 +544,7 @@ namespace UCP.Bridge.Tests
472
544
  );
473
545
  Assert.That(getPosition.error, Is.Null);
474
546
 
475
- var updated = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
547
+ var updated = UnityObjectCompat.ResolveByInstanceId(instanceId) as GameObject;
476
548
  Assert.That(updated, Is.Not.Null);
477
549
  var localPosition = updated.transform.localPosition;
478
550
  Assert.That(localPosition.x, Is.EqualTo(1f).Within(0.001f));
@@ -484,7 +556,7 @@ namespace UCP.Bridge.Tests
484
556
 
485
557
  var delete = _router.Dispatch("object/delete", 1, "{\"instanceId\":" + instanceId + "}");
486
558
  Assert.That(delete.error, Is.Null);
487
- Assert.That(EditorUtility.InstanceIDToObject(instanceId), Is.Null);
559
+ Assert.That(UnityObjectCompat.ResolveByInstanceId(instanceId), Is.Null);
488
560
  }
489
561
 
490
562
  [Test]
@@ -501,7 +573,7 @@ namespace UCP.Bridge.Tests
501
573
  var response = _router.Dispatch(
502
574
  "object/set-property",
503
575
  1,
504
- "{\"instanceId\":" + go.GetInstanceID() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"" + TempReferenceAssetPath + "\"}}"
576
+ "{\"instanceId\":" + go.GetId() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"" + TempReferenceAssetPath + "\"}}"
505
577
  );
506
578
 
507
579
  Assert.That(response.error, Is.Null);
@@ -526,7 +598,7 @@ namespace UCP.Bridge.Tests
526
598
  var response = _router.Dispatch(
527
599
  "object/set-property",
528
600
  1,
529
- "{\"instanceId\":" + cube.GetInstanceID() + ",\"component\":\"MeshRenderer\",\"property\":\"m_Materials\",\"value\":[{\"path\":\"" + TempMaterialPath + "\"}]}"
601
+ "{\"instanceId\":" + cube.GetId() + ",\"component\":\"MeshRenderer\",\"property\":\"m_Materials\",\"value\":[{\"path\":\"" + TempMaterialPath + "\"}]}"
530
602
  );
531
603
 
532
604
  Assert.That(response.error, Is.Null);
@@ -544,7 +616,7 @@ namespace UCP.Bridge.Tests
544
616
  var response = _router.Dispatch(
545
617
  "object/set-property",
546
618
  1,
547
- "{\"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\"}}"
548
620
  );
549
621
 
550
622
  Assert.That(response.error, Is.Not.Null);
@@ -918,6 +990,62 @@ namespace UCP.Bridge.Tests
918
990
  Assert.That(UnityEngine.SceneManagement.SceneManager.GetSceneByPath(TempSceneBPath).isLoaded, Is.True);
919
991
  }
920
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
+
921
1049
  [Test]
922
1050
  public void PackagesController_DependencySetInfoAndRemove_LocalFilePackage()
923
1051
  {
@@ -999,7 +1127,7 @@ namespace UCP.Bridge.Tests
999
1127
  var response = _router.Dispatch(
1000
1128
  "scene/focus",
1001
1129
  1,
1002
- "{\"instanceId\":" + cube.GetInstanceID() + ",\"axis\":[1,0,1]}"
1130
+ "{\"instanceId\":" + cube.GetId() + ",\"axis\":[1,0,1]}"
1003
1131
  );
1004
1132
 
1005
1133
  Assert.That(response.error, Is.Null);
@@ -1030,7 +1158,7 @@ namespace UCP.Bridge.Tests
1030
1158
  var response = _router.Dispatch(
1031
1159
  "scene/focus",
1032
1160
  1,
1033
- "{\"instanceId\":" + cube.GetInstanceID() + ",\"axis\":[0,0,0]}"
1161
+ "{\"instanceId\":" + cube.GetId() + ",\"axis\":[0,0,0]}"
1034
1162
  );
1035
1163
 
1036
1164
  Assert.That(response.error, Is.Not.Null);
@@ -0,0 +1,252 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using NUnit.Framework;
4
+ using UnityEditor.SceneManagement;
5
+ using UnityEngine;
6
+ using UnityEngine.Rendering;
7
+ using UnityEngine.SceneManagement;
8
+
9
+ namespace UCP.Bridge.Tests
10
+ {
11
+ /// <summary>
12
+ /// Edit-mode coverage for the spatial/visual controllers added for in-scene authoring:
13
+ /// TransformController, SpatialController, ViewController, and the shared ObjectLocator.
14
+ /// </summary>
15
+ public class SpatialVisualControllerTests
16
+ {
17
+ private CommandRouter _router;
18
+ private readonly List<GameObject> _spawned = new();
19
+
20
+ [SetUp]
21
+ public void SetUp()
22
+ {
23
+ EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
24
+ _router = new CommandRouter();
25
+ HierarchyController.Register(_router);
26
+ TransformController.Register(_router);
27
+ SpatialController.Register(_router);
28
+ ViewController.Register(_router);
29
+ _spawned.Clear();
30
+ }
31
+
32
+ [TearDown]
33
+ public void TearDown()
34
+ {
35
+ foreach (var go in _spawned)
36
+ if (go != null) UnityEngine.Object.DestroyImmediate(go);
37
+ _spawned.Clear();
38
+ }
39
+
40
+ private GameObject Spawn(PrimitiveType type, string name, Vector3 pos)
41
+ {
42
+ var go = GameObject.CreatePrimitive(type);
43
+ go.name = name;
44
+ go.transform.position = pos;
45
+ _spawned.Add(go);
46
+ return go;
47
+ }
48
+
49
+ private static Dictionary<string, object> Result(JsonRpcResponse r)
50
+ {
51
+ Assert.That(r.error, Is.Null, "RPC returned an error");
52
+ return (Dictionary<string, object>)r.result;
53
+ }
54
+
55
+ private static float F(object o) => Convert.ToSingle(o);
56
+ private static List<object> Vec(Dictionary<string, object> d, string key) => (List<object>)d[key];
57
+
58
+ // --- Primitive builder ---------------------------------------------
59
+
60
+ [Test]
61
+ public void Object_CreatePrimitive_AddsMeshAndCollider()
62
+ {
63
+ var res = Result(_router.Dispatch("object/create", 1,
64
+ "{\"name\":\"Greybox\",\"primitive\":\"Cube\"}"));
65
+ var id = Convert.ToInt32(res["instanceId"]);
66
+ var go = ObjectLocator.FindByInstanceId(id);
67
+ _spawned.Add(go);
68
+
69
+ Assert.That(go, Is.Not.Null);
70
+ Assert.That(go.name, Is.EqualTo("Greybox"));
71
+ Assert.That(go.GetComponent<MeshFilter>(), Is.Not.Null, "primitive should have a mesh");
72
+ Assert.That(go.GetComponent<MeshFilter>().sharedMesh, Is.Not.Null);
73
+ Assert.That(go.GetComponent<Collider>(), Is.Not.Null, "primitive should have a collider");
74
+ }
75
+
76
+ [Test]
77
+ public void Object_CreatePrimitive_RejectsUnknownType()
78
+ {
79
+ var res = _router.Dispatch("object/create", 1,
80
+ "{\"name\":\"Bad\",\"primitive\":\"Dodecahedron\"}");
81
+ Assert.That(res.error, Is.Not.Null, "unknown primitive must be an InvalidParams error, not a crash");
82
+ }
83
+
84
+ [Test]
85
+ public void Object_Instantiate_PrimitiveName_RedirectsToPrimitiveFlag()
86
+ {
87
+ // The exact wrong path a weak agent took: treating "PrimitiveType.Cube" as a prefab.
88
+ var res = _router.Dispatch("object/instantiate", 1,
89
+ "{\"prefab\":\"PrimitiveType.Cube\"}");
90
+ Assert.That(res.error, Is.Not.Null);
91
+ Assert.That(res.error.message, Does.Contain("--primitive"),
92
+ "the not-found error should redirect to object create --primitive");
93
+ }
94
+
95
+ // --- Transform -----------------------------------------------------
96
+
97
+ [Test]
98
+ public void Transform_Move_WorldAbsoluteAndRelative()
99
+ {
100
+ var go = Spawn(PrimitiveType.Cube, "Mover", Vector3.zero);
101
+ var id = go.GetId();
102
+
103
+ var abs = Result(_router.Dispatch("transform/move", 1,
104
+ "{\"instanceId\":" + id + ",\"position\":[5,0,0],\"space\":\"world\"}"));
105
+ Assert.That(go.transform.position.x, Is.EqualTo(5f).Within(0.001f));
106
+ Assert.That(F(Vec(abs, "position")[0]), Is.EqualTo(5f).Within(0.001f));
107
+
108
+ _router.Dispatch("transform/move", 1,
109
+ "{\"instanceId\":" + id + ",\"position\":[1,0,0],\"relative\":true}");
110
+ Assert.That(go.transform.position.x, Is.EqualTo(6f).Within(0.001f));
111
+ }
112
+
113
+ [Test]
114
+ public void Transform_Rotate_AbsoluteEulerWorld()
115
+ {
116
+ var go = Spawn(PrimitiveType.Cube, "Rotor", Vector3.zero);
117
+ _router.Dispatch("transform/rotate", 1,
118
+ "{\"instanceId\":" + go.GetId() + ",\"euler\":[0,90,0]}");
119
+ Assert.That(go.transform.eulerAngles.y, Is.EqualTo(90f).Within(0.01f));
120
+ }
121
+
122
+ [Test]
123
+ public void Transform_Scale_UniformAndNonUniform()
124
+ {
125
+ var go = Spawn(PrimitiveType.Cube, "Scaler", Vector3.zero);
126
+ _router.Dispatch("transform/scale", 1, "{\"instanceId\":" + go.GetId() + ",\"uniform\":2}");
127
+ Assert.That(go.transform.localScale, Is.EqualTo(Vector3.one * 2f));
128
+
129
+ _router.Dispatch("transform/scale", 1, "{\"instanceId\":" + go.GetId() + ",\"scale\":[1,3,1],\"relative\":true}");
130
+ Assert.That(go.transform.localScale.y, Is.EqualTo(6f).Within(0.001f));
131
+ }
132
+
133
+ [Test]
134
+ public void Transform_LookAt_FacesWorldPoint()
135
+ {
136
+ var go = Spawn(PrimitiveType.Cube, "Looker", Vector3.zero);
137
+ _router.Dispatch("transform/look-at", 1,
138
+ "{\"instanceId\":" + go.GetId() + ",\"target\":[10,0,0]}");
139
+ // forward should point along +X
140
+ Assert.That(Vector3.Dot(go.transform.forward, Vector3.right), Is.GreaterThan(0.99f));
141
+ }
142
+
143
+ [Test]
144
+ public void Transform_Get_BulkReadByIds()
145
+ {
146
+ var a = Spawn(PrimitiveType.Cube, "A", new Vector3(1, 0, 0));
147
+ var b = Spawn(PrimitiveType.Cube, "B", new Vector3(2, 0, 0));
148
+ var res = Result(_router.Dispatch("transform/get", 1,
149
+ "{\"ids\":[" + a.GetId() + "," + b.GetId() + "]}"));
150
+ Assert.That(Convert.ToInt32(res["count"]), Is.EqualTo(2));
151
+ }
152
+
153
+ // --- ObjectLocator -------------------------------------------------
154
+
155
+ [Test]
156
+ public void Locator_ResolvesByNameAndPath()
157
+ {
158
+ var root = Spawn(PrimitiveType.Cube, "Root", Vector3.zero);
159
+ var child = GameObject.CreatePrimitive(PrimitiveType.Cube);
160
+ child.name = "Child";
161
+ child.transform.SetParent(root.transform, false);
162
+ _spawned.Add(child);
163
+
164
+ _router.Dispatch("transform/move", 1, "{\"name\":\"Child\",\"position\":[0,4,0]}");
165
+ Assert.That(child.transform.position.y, Is.EqualTo(4f).Within(0.001f));
166
+
167
+ _router.Dispatch("transform/move", 1, "{\"path\":\"Root/Child\",\"position\":[0,7,0]}");
168
+ Assert.That(child.transform.position.y, Is.EqualTo(7f).Within(0.001f));
169
+ }
170
+
171
+ // --- Spatial -------------------------------------------------------
172
+
173
+ [Test]
174
+ public void Spatial_Raycast_HitsColliderBelow()
175
+ {
176
+ var ground = Spawn(PrimitiveType.Cube, "Ground", Vector3.zero);
177
+ ground.transform.localScale = new Vector3(20, 1, 20);
178
+
179
+ var res = Result(_router.Dispatch("physics/raycast", 1,
180
+ "{\"origin\":[0,5,0],\"direction\":[0,-1,0]}"));
181
+ Assert.That(Convert.ToBoolean(res["hit"]), Is.True);
182
+ Assert.That(Convert.ToInt32(res["instanceId"]), Is.EqualTo(ground.GetId()));
183
+ }
184
+
185
+ [Test]
186
+ public void Spatial_Overlap_FindsSphereOverlap()
187
+ {
188
+ var box = Spawn(PrimitiveType.Cube, "Box", Vector3.zero);
189
+ var res = Result(_router.Dispatch("physics/overlap", 1,
190
+ "{\"shape\":\"sphere\",\"center\":[0,0,0],\"radius\":2}"));
191
+ Assert.That(Convert.ToInt32(res["count"]), Is.GreaterThanOrEqualTo(1));
192
+ _ = box;
193
+ }
194
+
195
+ [Test]
196
+ public void Spatial_Bounds_ReturnsWorldAabb()
197
+ {
198
+ var go = Spawn(PrimitiveType.Cube, "Bounded", new Vector3(3, 0, 0));
199
+ var res = Result(_router.Dispatch("object/bounds", 1, "{\"instanceId\":" + go.GetId() + "}"));
200
+ Assert.That(F(Vec(res, "center")[0]), Is.EqualTo(3f).Within(0.01f));
201
+ Assert.That(Convert.ToBoolean(res["empty"]), Is.False);
202
+ }
203
+
204
+ [Test]
205
+ public void Spatial_Ground_DropsObjectOntoSurface()
206
+ {
207
+ var ground = Spawn(PrimitiveType.Cube, "Floor", Vector3.zero);
208
+ ground.transform.localScale = new Vector3(20, 1, 20); // top surface at y=0.5
209
+ var cube = Spawn(PrimitiveType.Cube, "Falling", new Vector3(0, 8, 0));
210
+
211
+ var res = Result(_router.Dispatch("spatial/ground", 1,
212
+ "{\"instanceId\":" + cube.GetId() + ",\"apply\":true}"));
213
+ Assert.That(Convert.ToBoolean(res["hit"]), Is.True);
214
+ // Cube (half-height 0.5) should rest with its centre at surface(0.5) + 0.5 = 1.0
215
+ Assert.That(cube.transform.position.y, Is.EqualTo(1f).Within(0.05f));
216
+ }
217
+
218
+ [Test]
219
+ public void Spatial_Nearest_SortsByDistance()
220
+ {
221
+ Spawn(PrimitiveType.Cube, "Near", new Vector3(1, 0, 0));
222
+ Spawn(PrimitiveType.Cube, "Mid", new Vector3(5, 0, 0));
223
+ Spawn(PrimitiveType.Cube, "Far", new Vector3(20, 0, 0));
224
+
225
+ var res = Result(_router.Dispatch("spatial/nearest", 1,
226
+ "{\"point\":[0,0,0],\"max\":2}"));
227
+ var objects = (List<object>)res["objects"];
228
+ Assert.That(objects.Count, Is.EqualTo(2));
229
+ var first = (Dictionary<string, object>)objects[0];
230
+ Assert.That(first["name"].ToString(), Is.EqualTo("Near"));
231
+ }
232
+
233
+ // --- View ----------------------------------------------------------
234
+
235
+ [Test]
236
+ public void View_Isolate_ProducesPngForSingleView()
237
+ {
238
+ if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null)
239
+ Assert.Ignore("No graphics device (headless -nographics); skipping render test.");
240
+
241
+ var go = Spawn(PrimitiveType.Cube, "Hero", Vector3.zero);
242
+ var res = Result(_router.Dispatch("view/isolate", 1,
243
+ "{\"instanceId\":" + go.GetId() + ",\"views\":[\"front\"],\"maxEdge\":128}"));
244
+
245
+ Assert.That(res["encoding"].ToString(), Is.EqualTo("base64"));
246
+ Assert.That(res["data"].ToString().Length, Is.GreaterThan(0));
247
+ Assert.That(Convert.ToInt32(res["width"]), Is.EqualTo(128));
248
+ // Isolation must restore the object's original layer.
249
+ Assert.That(go.layer, Is.EqualTo(0));
250
+ }
251
+ }
252
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 7a8b9c0d1e2f3041a2b3c4d5e6f70819
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.ucp.bridge",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "displayName": "Unity Control Protocol Bridge",
5
5
  "description": "WebSocket bridge for programmatic Unity Editor control via CLI and AI agents.",
6
6
  "unity": "2021.3",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mflrevan/ucp",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Unity Control Protocol - CLI for programmatic Unity Editor control",
5
5
  "license": "MIT",
6
6
  "repository": {