@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
@@ -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,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 1a2b3c4d5e6f708192a3b4c5d6e7f809
@@ -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