@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
|
@@ -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
|
+
}
|
|
@@ -544,7 +544,7 @@ namespace UCP.Bridge.Tests
|
|
|
544
544
|
);
|
|
545
545
|
Assert.That(getPosition.error, Is.Null);
|
|
546
546
|
|
|
547
|
-
var updated =
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1161
|
+
"{\"instanceId\":" + cube.GetId() + ",\"axis\":[0,0,0]}"
|
|
1106
1162
|
);
|
|
1107
1163
|
|
|
1108
1164
|
Assert.That(response.error, Is.Not.Null);
|