@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.
- 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 +16 -3
- package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +26 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +172 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +88 -1
- 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/LogsController.cs +325 -13
- 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 +14 -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/ShaderController.cs +151 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +304 -9
- 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 +135 -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,249 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using UnityEditor;
|
|
4
|
+
using UnityEditor.SceneManagement;
|
|
5
|
+
using UnityEngine;
|
|
6
|
+
using UnityEngine.SceneManagement;
|
|
7
|
+
|
|
8
|
+
namespace UCP.Bridge
|
|
9
|
+
{
|
|
10
|
+
/// <summary>
|
|
11
|
+
/// First-class transform authoring: move / rotate / scale / look-at with world|local
|
|
12
|
+
/// space and absolute|relative semantics, plus a bulk transform read. Rotations are
|
|
13
|
+
/// expressed as Euler angles (degrees) on the wire — quaternions are an internal detail.
|
|
14
|
+
///
|
|
15
|
+
/// Every mutation registers Undo, marks the scene dirty, and records a change digest,
|
|
16
|
+
/// matching the rest of the bridge. No modal APIs are touched.
|
|
17
|
+
/// </summary>
|
|
18
|
+
public static class TransformController
|
|
19
|
+
{
|
|
20
|
+
public static void Register(CommandRouter router)
|
|
21
|
+
{
|
|
22
|
+
router.Register("transform/move", HandleMove);
|
|
23
|
+
router.Register("transform/rotate", HandleRotate);
|
|
24
|
+
router.Register("transform/scale", HandleScale);
|
|
25
|
+
router.Register("transform/look-at", HandleLookAt);
|
|
26
|
+
router.Register("transform/get", HandleGet);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private static object HandleMove(string paramsJson)
|
|
30
|
+
{
|
|
31
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
32
|
+
var go = ObjectLocator.Resolve(p);
|
|
33
|
+
var value = ReadVec3(p, "position", required: true).Value;
|
|
34
|
+
var space = ReadSpace(p);
|
|
35
|
+
var relative = ReadBool(p, "relative", false);
|
|
36
|
+
|
|
37
|
+
Undo.RecordObject(go.transform, "UCP Move");
|
|
38
|
+
|
|
39
|
+
if (space == Space.World)
|
|
40
|
+
{
|
|
41
|
+
go.transform.position = relative ? go.transform.position + value : value;
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
{
|
|
45
|
+
go.transform.localPosition = relative ? go.transform.localPosition + value : value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return Commit(go, "Transform", ExtraTransform(go));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private static object HandleRotate(string paramsJson)
|
|
52
|
+
{
|
|
53
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
54
|
+
var go = ObjectLocator.Resolve(p);
|
|
55
|
+
var euler = ReadVec3(p, "euler", required: true).Value;
|
|
56
|
+
var space = ReadSpace(p);
|
|
57
|
+
var relative = ReadBool(p, "relative", false);
|
|
58
|
+
|
|
59
|
+
Undo.RecordObject(go.transform, "UCP Rotate");
|
|
60
|
+
|
|
61
|
+
if (relative)
|
|
62
|
+
{
|
|
63
|
+
go.transform.Rotate(euler, space == Space.World ? Space.World : Space.Self);
|
|
64
|
+
}
|
|
65
|
+
else if (space == Space.World)
|
|
66
|
+
{
|
|
67
|
+
go.transform.eulerAngles = euler;
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
{
|
|
71
|
+
go.transform.localEulerAngles = euler;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Commit(go, "Transform", ExtraTransform(go));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static object HandleScale(string paramsJson)
|
|
78
|
+
{
|
|
79
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
80
|
+
var go = ObjectLocator.Resolve(p);
|
|
81
|
+
var relative = ReadBool(p, "relative", false);
|
|
82
|
+
|
|
83
|
+
Vector3 scale;
|
|
84
|
+
if (p != null && p.TryGetValue("uniform", out var uniObj) && uniObj != null)
|
|
85
|
+
{
|
|
86
|
+
var u = Convert.ToSingle(uniObj);
|
|
87
|
+
scale = new Vector3(u, u, u);
|
|
88
|
+
}
|
|
89
|
+
else
|
|
90
|
+
{
|
|
91
|
+
scale = ReadVec3(p, "scale", required: true).Value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Undo.RecordObject(go.transform, "UCP Scale");
|
|
95
|
+
|
|
96
|
+
if (relative)
|
|
97
|
+
{
|
|
98
|
+
var cur = go.transform.localScale;
|
|
99
|
+
go.transform.localScale = new Vector3(cur.x * scale.x, cur.y * scale.y, cur.z * scale.z);
|
|
100
|
+
}
|
|
101
|
+
else
|
|
102
|
+
{
|
|
103
|
+
go.transform.localScale = scale;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return Commit(go, "Transform", ExtraTransform(go));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private static object HandleLookAt(string paramsJson)
|
|
110
|
+
{
|
|
111
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
112
|
+
var go = ObjectLocator.Resolve(p);
|
|
113
|
+
|
|
114
|
+
// Target is either an explicit world point or another object.
|
|
115
|
+
Vector3 targetPoint;
|
|
116
|
+
var explicitPoint = ReadVec3(p, "target", required: false);
|
|
117
|
+
if (explicitPoint.HasValue)
|
|
118
|
+
{
|
|
119
|
+
targetPoint = explicitPoint.Value;
|
|
120
|
+
}
|
|
121
|
+
else if (p != null && (p.ContainsKey("targetId") || p.ContainsKey("targetPath") || p.ContainsKey("targetName")))
|
|
122
|
+
{
|
|
123
|
+
var targetGo = ObjectLocator.Resolve(RemapTargetKeys(p));
|
|
124
|
+
targetPoint = targetGo.transform.position;
|
|
125
|
+
}
|
|
126
|
+
else
|
|
127
|
+
{
|
|
128
|
+
throw new ArgumentException("look-at requires 'target' ([x,y,z]) or 'targetId'/'targetPath'/'targetName'");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
var up = ReadVec3(p, "up", required: false) ?? Vector3.up;
|
|
132
|
+
|
|
133
|
+
Undo.RecordObject(go.transform, "UCP LookAt");
|
|
134
|
+
go.transform.LookAt(targetPoint, up);
|
|
135
|
+
|
|
136
|
+
return Commit(go, "Transform", ExtraTransform(go));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private static object HandleGet(string paramsJson)
|
|
140
|
+
{
|
|
141
|
+
// Reported bounds read collider.bounds, which lag transform edits until synced.
|
|
142
|
+
Physics.SyncTransforms();
|
|
143
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
144
|
+
|
|
145
|
+
// Bulk: 'ids' array → one entry per object, skipping unresolved ids.
|
|
146
|
+
if (p != null && p.TryGetValue("ids", out var idsObj) && idsObj is List<object> ids)
|
|
147
|
+
{
|
|
148
|
+
var list = new List<object>();
|
|
149
|
+
foreach (var idObj in ids)
|
|
150
|
+
{
|
|
151
|
+
var go = ObjectLocator.FindByInstanceId(Convert.ToInt32(idObj));
|
|
152
|
+
if (go == null) continue;
|
|
153
|
+
list.Add(DescribeTransform(go));
|
|
154
|
+
}
|
|
155
|
+
return new Dictionary<string, object> { ["transforms"] = list, ["count"] = list.Count };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Single target.
|
|
159
|
+
var single = ObjectLocator.Resolve(p);
|
|
160
|
+
return DescribeTransform(single);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private static Dictionary<string, object> DescribeTransform(GameObject go)
|
|
164
|
+
{
|
|
165
|
+
var t = go.transform;
|
|
166
|
+
var entry = new Dictionary<string, object>
|
|
167
|
+
{
|
|
168
|
+
["instanceId"] = go.GetId(),
|
|
169
|
+
["name"] = go.name,
|
|
170
|
+
["position"] = ObjectLocator.Vec3(t.position),
|
|
171
|
+
["localPosition"] = ObjectLocator.Vec3(t.localPosition),
|
|
172
|
+
["eulerAngles"] = ObjectLocator.Vec3(t.eulerAngles),
|
|
173
|
+
["localEulerAngles"] = ObjectLocator.Vec3(t.localEulerAngles),
|
|
174
|
+
["localScale"] = ObjectLocator.Vec3(t.localScale),
|
|
175
|
+
["lossyScale"] = ObjectLocator.Vec3(t.lossyScale)
|
|
176
|
+
};
|
|
177
|
+
if (ObjectLocator.TryComputeWorldBounds(go, true, out var bounds))
|
|
178
|
+
{
|
|
179
|
+
entry["boundsCenter"] = ObjectLocator.Vec3(bounds.center);
|
|
180
|
+
entry["boundsExtents"] = ObjectLocator.Vec3(bounds.extents);
|
|
181
|
+
}
|
|
182
|
+
return entry;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private static Dictionary<string, object> ExtraTransform(GameObject go)
|
|
186
|
+
{
|
|
187
|
+
var t = go.transform;
|
|
188
|
+
return new Dictionary<string, object>
|
|
189
|
+
{
|
|
190
|
+
["instanceId"] = go.GetId(),
|
|
191
|
+
["name"] = go.name,
|
|
192
|
+
["position"] = ObjectLocator.Vec3(t.position),
|
|
193
|
+
["localPosition"] = ObjectLocator.Vec3(t.localPosition),
|
|
194
|
+
["eulerAngles"] = ObjectLocator.Vec3(t.eulerAngles),
|
|
195
|
+
["localEulerAngles"] = ObjectLocator.Vec3(t.localEulerAngles),
|
|
196
|
+
["localScale"] = ObjectLocator.Vec3(t.localScale)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private static object Commit(GameObject go, string label, Dictionary<string, object> extra)
|
|
201
|
+
{
|
|
202
|
+
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
|
|
203
|
+
SceneChangeTracker.RecordGameObjectChange(go, label);
|
|
204
|
+
extra["status"] = "ok";
|
|
205
|
+
return extra;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- param helpers -------------------------------------------------
|
|
209
|
+
|
|
210
|
+
private static Vector3? ReadVec3(Dictionary<string, object> p, string key, bool required)
|
|
211
|
+
{
|
|
212
|
+
if (p == null || !p.TryGetValue(key, out var v) || v == null)
|
|
213
|
+
{
|
|
214
|
+
if (required) throw new ArgumentException($"Missing '{key}' ([x,y,z]) parameter");
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (v is not List<object> list || list.Count < 3)
|
|
218
|
+
throw new ArgumentException($"'{key}' must be an array of three numbers");
|
|
219
|
+
return new Vector3(Convert.ToSingle(list[0]), Convert.ToSingle(list[1]), Convert.ToSingle(list[2]));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private static Space ReadSpace(Dictionary<string, object> p)
|
|
223
|
+
{
|
|
224
|
+
if (p != null && p.TryGetValue("space", out var s) && s != null)
|
|
225
|
+
{
|
|
226
|
+
var str = s.ToString().ToLowerInvariant();
|
|
227
|
+
if (str == "local") return Space.Self;
|
|
228
|
+
if (str == "world") return Space.World;
|
|
229
|
+
throw new ArgumentException("'space' must be 'world' or 'local'");
|
|
230
|
+
}
|
|
231
|
+
return Space.World;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private static bool ReadBool(Dictionary<string, object> p, string key, bool dflt)
|
|
235
|
+
{
|
|
236
|
+
if (p != null && p.TryGetValue(key, out var v) && v is bool b) return b;
|
|
237
|
+
return dflt;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private static Dictionary<string, object> RemapTargetKeys(Dictionary<string, object> p)
|
|
241
|
+
{
|
|
242
|
+
var remapped = new Dictionary<string, object>();
|
|
243
|
+
if (p.TryGetValue("targetId", out var id)) remapped["instanceId"] = id;
|
|
244
|
+
if (p.TryGetValue("targetPath", out var path)) remapped["path"] = path;
|
|
245
|
+
if (p.TryGetValue("targetName", out var name)) remapped["name"] = name;
|
|
246
|
+
return remapped;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -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
|
+
}
|