@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
|
@@ -54,7 +54,7 @@ namespace UCP.Bridge
|
|
|
54
54
|
}
|
|
55
55
|
else
|
|
56
56
|
{
|
|
57
|
-
|
|
57
|
+
EditorModalGuard.SaveOpenDirtyScenes(saveDirtyScenes, discardUntitled);
|
|
58
58
|
EditorSceneManager.OpenScene(path, additive ? OpenSceneMode.Additive : OpenSceneMode.Single);
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -74,38 +74,6 @@ namespace UCP.Bridge
|
|
|
74
74
|
return defaultValue;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
private static void SaveDirtyScenesIfRequested(bool saveDirtyScenes, bool discardUntitled)
|
|
78
|
-
{
|
|
79
|
-
if (!saveDirtyScenes)
|
|
80
|
-
return;
|
|
81
|
-
|
|
82
|
-
var requiresUntitledDiscard = false;
|
|
83
|
-
|
|
84
|
-
for (var index = 0; index < SceneManager.sceneCount; index++)
|
|
85
|
-
{
|
|
86
|
-
var scene = SceneManager.GetSceneAt(index);
|
|
87
|
-
if (!scene.isLoaded || !scene.isDirty)
|
|
88
|
-
continue;
|
|
89
|
-
|
|
90
|
-
if (string.IsNullOrEmpty(scene.path))
|
|
91
|
-
{
|
|
92
|
-
if (!discardUntitled)
|
|
93
|
-
throw new System.InvalidOperationException("Dirty untitled scene cannot be auto-saved. Retry with discardUntitled=true.");
|
|
94
|
-
|
|
95
|
-
requiresUntitledDiscard = true;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!EditorSceneManager.SaveScene(scene))
|
|
100
|
-
throw new System.InvalidOperationException($"Failed to auto-save dirty scene: {scene.path}");
|
|
101
|
-
|
|
102
|
-
SceneChangeTracker.ClearScene(scene);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (requiresUntitledDiscard)
|
|
106
|
-
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
77
|
private static object HandleSaveActive(string paramsJson)
|
|
110
78
|
{
|
|
111
79
|
var scene = SceneManager.GetActiveScene();
|
|
@@ -302,7 +270,7 @@ namespace UCP.Bridge
|
|
|
302
270
|
|
|
303
271
|
private static GameObject FindInHierarchy(GameObject gameObject, int instanceId)
|
|
304
272
|
{
|
|
305
|
-
if (gameObject.
|
|
273
|
+
if (gameObject.GetId() == instanceId)
|
|
306
274
|
return gameObject;
|
|
307
275
|
|
|
308
276
|
foreach (Transform child in gameObject.transform)
|
|
@@ -86,7 +86,7 @@ namespace UCP.Bridge
|
|
|
86
86
|
|
|
87
87
|
var entry = new Dictionary<string, object>
|
|
88
88
|
{
|
|
89
|
-
["instanceId"] = go.
|
|
89
|
+
["instanceId"] = go.GetId(),
|
|
90
90
|
["name"] = go.name,
|
|
91
91
|
["active"] = go.activeSelf,
|
|
92
92
|
["tag"] = go.tag,
|
|
@@ -230,7 +230,7 @@ namespace UCP.Bridge
|
|
|
230
230
|
private static Dictionary<string, object> ProjectGameObject(GameObject go, HashSet<string> fields, int depth)
|
|
231
231
|
{
|
|
232
232
|
var entry = new Dictionary<string, object>();
|
|
233
|
-
AddField(entry, fields, "instanceId", go.
|
|
233
|
+
AddField(entry, fields, "instanceId", go.GetId());
|
|
234
234
|
AddField(entry, fields, "name", go.name);
|
|
235
235
|
AddField(entry, fields, "active", go.activeSelf);
|
|
236
236
|
AddField(entry, fields, "activeInHierarchy", go.activeInHierarchy);
|
|
@@ -426,7 +426,7 @@ namespace UCP.Bridge
|
|
|
426
426
|
|
|
427
427
|
private static GameObject FindInHierarchy(GameObject go, int instanceId)
|
|
428
428
|
{
|
|
429
|
-
if (go.
|
|
429
|
+
if (go.GetId() == instanceId)
|
|
430
430
|
return go;
|
|
431
431
|
|
|
432
432
|
for (int i = 0; i < go.transform.childCount; i++)
|
|
@@ -479,7 +479,7 @@ namespace UCP.Bridge
|
|
|
479
479
|
{
|
|
480
480
|
return new Dictionary<string, object>
|
|
481
481
|
{
|
|
482
|
-
["instanceId"] = go.
|
|
482
|
+
["instanceId"] = go.GetId(),
|
|
483
483
|
["name"] = go.name,
|
|
484
484
|
["active"] = go.activeSelf,
|
|
485
485
|
["tag"] = go.tag,
|
|
@@ -0,0 +1,322 @@
|
|
|
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
|
+
/// Spatial reasoning primitives so an agent can answer geometric questions about a scene
|
|
12
|
+
/// instead of inferring them from raw transforms: raycast, overlap, world bounds, drop-to-
|
|
13
|
+
/// surface (ground), and nearest-object search.
|
|
14
|
+
///
|
|
15
|
+
/// Physics queries hit colliders only — objects without a Collider are invisible to
|
|
16
|
+
/// raycast/overlap/ground. 'bounds' and 'nearest' fall back to renderer bounds and so also
|
|
17
|
+
/// see render-only objects.
|
|
18
|
+
/// </summary>
|
|
19
|
+
public static class SpatialController
|
|
20
|
+
{
|
|
21
|
+
public static void Register(CommandRouter router)
|
|
22
|
+
{
|
|
23
|
+
router.Register("physics/raycast", HandleRaycast);
|
|
24
|
+
router.Register("physics/overlap", HandleOverlap);
|
|
25
|
+
router.Register("object/bounds", HandleBounds);
|
|
26
|
+
router.Register("spatial/ground", HandleGround);
|
|
27
|
+
router.Register("spatial/nearest", HandleNearest);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private static object HandleRaycast(string paramsJson)
|
|
31
|
+
{
|
|
32
|
+
// Collider positions in the edit-mode physics scene can lag transform edits made via
|
|
33
|
+
// earlier RPCs; sync so queries see the current state.
|
|
34
|
+
Physics.SyncTransforms();
|
|
35
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
36
|
+
var origin = RequireVec3(p, "origin");
|
|
37
|
+
var direction = RequireVec3(p, "direction");
|
|
38
|
+
if (direction.sqrMagnitude < 1e-8f)
|
|
39
|
+
throw new ArgumentException("'direction' must not be the zero vector");
|
|
40
|
+
|
|
41
|
+
var maxDistance = ReadFloat(p, "maxDistance", Mathf.Infinity);
|
|
42
|
+
var layerMask = ReadLayerMask(p);
|
|
43
|
+
var queryTriggers = ReadBool(p, "queryTriggers", false)
|
|
44
|
+
? QueryTriggerInteraction.Collide
|
|
45
|
+
: QueryTriggerInteraction.Ignore;
|
|
46
|
+
|
|
47
|
+
if (Physics.Raycast(new Ray(origin, direction.normalized), out var hit, maxDistance, layerMask, queryTriggers))
|
|
48
|
+
{
|
|
49
|
+
return new Dictionary<string, object>
|
|
50
|
+
{
|
|
51
|
+
["status"] = "ok",
|
|
52
|
+
["hit"] = true,
|
|
53
|
+
["point"] = ObjectLocator.Vec3(hit.point),
|
|
54
|
+
["normal"] = ObjectLocator.Vec3(hit.normal),
|
|
55
|
+
["distance"] = hit.distance,
|
|
56
|
+
["instanceId"] = hit.collider.gameObject.GetId(),
|
|
57
|
+
["gameObject"] = hit.collider.gameObject.name,
|
|
58
|
+
["collider"] = hit.collider.GetType().Name
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new Dictionary<string, object> { ["status"] = "ok", ["hit"] = false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private static object HandleOverlap(string paramsJson)
|
|
66
|
+
{
|
|
67
|
+
Physics.SyncTransforms();
|
|
68
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
69
|
+
var shape = (ReadString(p, "shape") ?? "sphere").ToLowerInvariant();
|
|
70
|
+
var center = RequireVec3(p, "center");
|
|
71
|
+
var layerMask = ReadLayerMask(p);
|
|
72
|
+
var queryTriggers = ReadBool(p, "queryTriggers", false)
|
|
73
|
+
? QueryTriggerInteraction.Collide
|
|
74
|
+
: QueryTriggerInteraction.Ignore;
|
|
75
|
+
|
|
76
|
+
Collider[] hits;
|
|
77
|
+
switch (shape)
|
|
78
|
+
{
|
|
79
|
+
case "sphere":
|
|
80
|
+
hits = Physics.OverlapSphere(center, ReadFloat(p, "radius", 1f), layerMask, queryTriggers);
|
|
81
|
+
break;
|
|
82
|
+
case "box":
|
|
83
|
+
var half = ReadVec3Optional(p, "halfExtents") ?? Vector3.one * 0.5f;
|
|
84
|
+
hits = Physics.OverlapBox(center, half, Quaternion.identity, layerMask, queryTriggers);
|
|
85
|
+
break;
|
|
86
|
+
case "capsule":
|
|
87
|
+
var end = ReadVec3Optional(p, "end") ?? center;
|
|
88
|
+
hits = Physics.OverlapCapsule(center, end, ReadFloat(p, "radius", 1f), layerMask, queryTriggers);
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
throw new ArgumentException("'shape' must be 'sphere', 'box', or 'capsule'");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
var list = new List<object>();
|
|
95
|
+
foreach (var c in hits)
|
|
96
|
+
{
|
|
97
|
+
if (c == null) continue;
|
|
98
|
+
list.Add(new Dictionary<string, object>
|
|
99
|
+
{
|
|
100
|
+
["instanceId"] = c.gameObject.GetId(),
|
|
101
|
+
["gameObject"] = c.gameObject.name,
|
|
102
|
+
["collider"] = c.GetType().Name,
|
|
103
|
+
["distance"] = Vector3.Distance(center, c.bounds.center)
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return new Dictionary<string, object> { ["status"] = "ok", ["count"] = list.Count, ["hits"] = list };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private static object HandleBounds(string paramsJson)
|
|
111
|
+
{
|
|
112
|
+
// Collider bounds lag transform edits in edit mode; sync before reading them.
|
|
113
|
+
Physics.SyncTransforms();
|
|
114
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
115
|
+
var go = ObjectLocator.Resolve(p);
|
|
116
|
+
var includeChildren = ReadBool(p, "includeChildren", true);
|
|
117
|
+
|
|
118
|
+
if (!ObjectLocator.TryComputeWorldBounds(go, includeChildren, out var bounds))
|
|
119
|
+
{
|
|
120
|
+
// No renderers/colliders: fall back to a zero-size box at the transform.
|
|
121
|
+
bounds = new Bounds(go.transform.position, Vector3.zero);
|
|
122
|
+
return new Dictionary<string, object>
|
|
123
|
+
{
|
|
124
|
+
["status"] = "ok",
|
|
125
|
+
["instanceId"] = go.GetId(),
|
|
126
|
+
["name"] = go.name,
|
|
127
|
+
["empty"] = true,
|
|
128
|
+
["center"] = ObjectLocator.Vec3(bounds.center),
|
|
129
|
+
["extents"] = ObjectLocator.Vec3(bounds.extents),
|
|
130
|
+
["size"] = ObjectLocator.Vec3(bounds.size),
|
|
131
|
+
["min"] = ObjectLocator.Vec3(bounds.min),
|
|
132
|
+
["max"] = ObjectLocator.Vec3(bounds.max)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return new Dictionary<string, object>
|
|
137
|
+
{
|
|
138
|
+
["status"] = "ok",
|
|
139
|
+
["instanceId"] = go.GetId(),
|
|
140
|
+
["name"] = go.name,
|
|
141
|
+
["empty"] = false,
|
|
142
|
+
["center"] = ObjectLocator.Vec3(bounds.center),
|
|
143
|
+
["extents"] = ObjectLocator.Vec3(bounds.extents),
|
|
144
|
+
["size"] = ObjectLocator.Vec3(bounds.size),
|
|
145
|
+
["min"] = ObjectLocator.Vec3(bounds.min),
|
|
146
|
+
["max"] = ObjectLocator.Vec3(bounds.max)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private static object HandleGround(string paramsJson)
|
|
151
|
+
{
|
|
152
|
+
Physics.SyncTransforms();
|
|
153
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
154
|
+
var direction = (ReadVec3Optional(p, "direction") ?? Vector3.down).normalized;
|
|
155
|
+
var maxDistance = ReadFloat(p, "maxDistance", 1000f);
|
|
156
|
+
var layerMask = ReadLayerMask(p);
|
|
157
|
+
var apply = ReadBool(p, "apply", true);
|
|
158
|
+
|
|
159
|
+
// Two modes: drop an object onto the surface, or just probe a point.
|
|
160
|
+
GameObject go = null;
|
|
161
|
+
Vector3 origin;
|
|
162
|
+
if (p != null && (p.ContainsKey("instanceId") || p.ContainsKey("id") || p.ContainsKey("path") || p.ContainsKey("name")))
|
|
163
|
+
{
|
|
164
|
+
go = ObjectLocator.Resolve(p);
|
|
165
|
+
origin = go.transform.position;
|
|
166
|
+
}
|
|
167
|
+
else
|
|
168
|
+
{
|
|
169
|
+
origin = RequireVec3(p, "point");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Offset the ray start slightly against the cast direction so an object already
|
|
173
|
+
// resting on / overlapping the surface still registers a hit.
|
|
174
|
+
var start = origin - direction * 0.01f;
|
|
175
|
+
if (!Physics.Raycast(start, direction, out var hit, maxDistance, layerMask, QueryTriggerInteraction.Ignore))
|
|
176
|
+
return new Dictionary<string, object> { ["status"] = "ok", ["hit"] = false };
|
|
177
|
+
|
|
178
|
+
var result = new Dictionary<string, object>
|
|
179
|
+
{
|
|
180
|
+
["status"] = "ok",
|
|
181
|
+
["hit"] = true,
|
|
182
|
+
["point"] = ObjectLocator.Vec3(hit.point),
|
|
183
|
+
["normal"] = ObjectLocator.Vec3(hit.normal),
|
|
184
|
+
["distance"] = hit.distance,
|
|
185
|
+
["surface"] = hit.collider.gameObject.name,
|
|
186
|
+
["surfaceId"] = hit.collider.gameObject.GetId()
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (go != null && apply)
|
|
190
|
+
{
|
|
191
|
+
// Rest the object's pivot on the surface, raised by the half-height of its
|
|
192
|
+
// bounds along the up axis so it sits on rather than through the surface.
|
|
193
|
+
var lift = 0f;
|
|
194
|
+
if (ObjectLocator.TryComputeWorldBounds(go, true, out var bounds))
|
|
195
|
+
{
|
|
196
|
+
var pivotToBottom = go.transform.position.y - bounds.min.y;
|
|
197
|
+
lift = Mathf.Max(pivotToBottom, 0f);
|
|
198
|
+
}
|
|
199
|
+
Undo.RecordObject(go.transform, "UCP Ground");
|
|
200
|
+
go.transform.position = hit.point + Vector3.up * lift;
|
|
201
|
+
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
|
|
202
|
+
SceneChangeTracker.RecordGameObjectChange(go, "Transform");
|
|
203
|
+
result["movedId"] = go.GetId();
|
|
204
|
+
result["restPosition"] = ObjectLocator.Vec3(go.transform.position);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private static object HandleNearest(string paramsJson)
|
|
211
|
+
{
|
|
212
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
213
|
+
|
|
214
|
+
Vector3 from;
|
|
215
|
+
GameObject self = null;
|
|
216
|
+
if (p != null && (p.ContainsKey("instanceId") || p.ContainsKey("id") || p.ContainsKey("path") || p.ContainsKey("name")))
|
|
217
|
+
{
|
|
218
|
+
self = ObjectLocator.Resolve(p);
|
|
219
|
+
from = self.transform.position;
|
|
220
|
+
}
|
|
221
|
+
else
|
|
222
|
+
{
|
|
223
|
+
from = RequireVec3(p, "point");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
var max = (int)ReadFloat(p, "max", 5f);
|
|
227
|
+
var componentFilter = ReadString(p, "component");
|
|
228
|
+
var tagFilter = ReadString(p, "tag");
|
|
229
|
+
|
|
230
|
+
var candidates = new List<(GameObject go, float dist)>();
|
|
231
|
+
for (var i = 0; i < SceneManager.sceneCount; i++)
|
|
232
|
+
{
|
|
233
|
+
var scene = SceneManager.GetSceneAt(i);
|
|
234
|
+
if (!scene.isLoaded) continue;
|
|
235
|
+
foreach (var root in scene.GetRootGameObjects())
|
|
236
|
+
CollectNearest(root, from, self, componentFilter, tagFilter, candidates);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
candidates.Sort((a, b) => a.dist.CompareTo(b.dist));
|
|
240
|
+
var list = new List<object>();
|
|
241
|
+
for (var i = 0; i < candidates.Count && i < max; i++)
|
|
242
|
+
{
|
|
243
|
+
var (go, dist) = candidates[i];
|
|
244
|
+
list.Add(new Dictionary<string, object>
|
|
245
|
+
{
|
|
246
|
+
["instanceId"] = go.GetId(),
|
|
247
|
+
["name"] = go.name,
|
|
248
|
+
["distance"] = dist,
|
|
249
|
+
["position"] = ObjectLocator.Vec3(go.transform.position)
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return new Dictionary<string, object> { ["status"] = "ok", ["count"] = list.Count, ["objects"] = list };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private static void CollectNearest(GameObject go, Vector3 from, GameObject self,
|
|
257
|
+
string componentFilter, string tagFilter, List<(GameObject, float)> outList)
|
|
258
|
+
{
|
|
259
|
+
var include = go != self;
|
|
260
|
+
if (include && !string.IsNullOrEmpty(componentFilter) && go.GetComponent(componentFilter) == null)
|
|
261
|
+
include = false;
|
|
262
|
+
if (include && !string.IsNullOrEmpty(tagFilter) && !go.CompareTag(tagFilter))
|
|
263
|
+
include = false;
|
|
264
|
+
if (include)
|
|
265
|
+
outList.Add((go, Vector3.Distance(from, go.transform.position)));
|
|
266
|
+
|
|
267
|
+
foreach (Transform child in go.transform)
|
|
268
|
+
CollectNearest(child.gameObject, from, self, componentFilter, tagFilter, outList);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- param helpers -------------------------------------------------
|
|
272
|
+
|
|
273
|
+
private static Vector3 RequireVec3(Dictionary<string, object> p, string key)
|
|
274
|
+
{
|
|
275
|
+
var v = ReadVec3Optional(p, key);
|
|
276
|
+
if (!v.HasValue) throw new ArgumentException($"Missing '{key}' ([x,y,z]) parameter");
|
|
277
|
+
return v.Value;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private static Vector3? ReadVec3Optional(Dictionary<string, object> p, string key)
|
|
281
|
+
{
|
|
282
|
+
if (p == null || !p.TryGetValue(key, out var v) || v == null) return null;
|
|
283
|
+
if (v is not List<object> list || list.Count < 3)
|
|
284
|
+
throw new ArgumentException($"'{key}' must be an array of three numbers");
|
|
285
|
+
return new Vector3(Convert.ToSingle(list[0]), Convert.ToSingle(list[1]), Convert.ToSingle(list[2]));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private static float ReadFloat(Dictionary<string, object> p, string key, float dflt)
|
|
289
|
+
{
|
|
290
|
+
if (p != null && p.TryGetValue(key, out var v) && v != null) return Convert.ToSingle(v);
|
|
291
|
+
return dflt;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private static bool ReadBool(Dictionary<string, object> p, string key, bool dflt)
|
|
295
|
+
{
|
|
296
|
+
if (p != null && p.TryGetValue(key, out var v) && v is bool b) return b;
|
|
297
|
+
return dflt;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private static string ReadString(Dictionary<string, object> p, string key)
|
|
301
|
+
{
|
|
302
|
+
if (p != null && p.TryGetValue(key, out var v) && v != null) return v.ToString();
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private static int ReadLayerMask(Dictionary<string, object> p)
|
|
307
|
+
{
|
|
308
|
+
// Accept an explicit int mask, or a single layer name, else everything.
|
|
309
|
+
if (p != null && p.TryGetValue("layerMask", out var v) && v != null)
|
|
310
|
+
{
|
|
311
|
+
if (v is string s)
|
|
312
|
+
{
|
|
313
|
+
var layer = LayerMask.NameToLayer(s);
|
|
314
|
+
if (layer < 0) throw new ArgumentException($"Unknown layer name '{s}'");
|
|
315
|
+
return 1 << layer;
|
|
316
|
+
}
|
|
317
|
+
return Convert.ToInt32(v);
|
|
318
|
+
}
|
|
319
|
+
return ~0;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -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
|
+
}
|