@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
using System;
|
|
2
2
|
using System.Collections.Generic;
|
|
3
|
+
using System.Linq;
|
|
3
4
|
using UnityEngine;
|
|
4
5
|
using UnityEngine.SceneManagement;
|
|
5
6
|
|
|
@@ -10,6 +11,8 @@ namespace UCP.Bridge
|
|
|
10
11
|
public static void Register(CommandRouter router)
|
|
11
12
|
{
|
|
12
13
|
router.Register("snapshot", HandleSnapshot);
|
|
14
|
+
router.Register("scene/query", HandleSceneQuery);
|
|
15
|
+
router.Register("object/get-children", HandleGetChildren);
|
|
13
16
|
router.Register("objects/list", HandleListObjects);
|
|
14
17
|
router.Register("objects/components", HandleGetComponents);
|
|
15
18
|
router.Register("objects/transform", HandleGetTransform);
|
|
@@ -83,7 +86,7 @@ namespace UCP.Bridge
|
|
|
83
86
|
|
|
84
87
|
var entry = new Dictionary<string, object>
|
|
85
88
|
{
|
|
86
|
-
["instanceId"] = go.
|
|
89
|
+
["instanceId"] = go.GetId(),
|
|
87
90
|
["name"] = go.name,
|
|
88
91
|
["active"] = go.activeSelf,
|
|
89
92
|
["tag"] = go.tag,
|
|
@@ -120,20 +123,84 @@ namespace UCP.Bridge
|
|
|
120
123
|
return new Dictionary<string, object> { ["objects"] = objects };
|
|
121
124
|
}
|
|
122
125
|
|
|
123
|
-
private static
|
|
126
|
+
private static object HandleSceneQuery(string paramsJson)
|
|
124
127
|
{
|
|
125
|
-
|
|
128
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
129
|
+
if (p == null || !p.TryGetValue("query", out var queryObj) || queryObj == null)
|
|
130
|
+
throw new ArgumentException("Missing 'query' parameter");
|
|
131
|
+
|
|
132
|
+
var query = ParseSceneQuery(queryObj.ToString());
|
|
133
|
+
var fields = ParseFields(p.TryGetValue("fields", out var fieldsObj) ? fieldsObj?.ToString() : null);
|
|
134
|
+
var maxDepth = 32;
|
|
135
|
+
if (p.TryGetValue("depth", out var depthObj) && depthObj != null)
|
|
136
|
+
maxDepth = Math.Max(0, Convert.ToInt32(depthObj));
|
|
137
|
+
|
|
138
|
+
var scene = SceneManager.GetActiveScene();
|
|
139
|
+
var results = new List<object>();
|
|
140
|
+
foreach (var root in scene.GetRootGameObjects())
|
|
141
|
+
QueryHierarchy(root, query, fields, results, 0, maxDepth);
|
|
142
|
+
|
|
143
|
+
return new Dictionary<string, object>
|
|
144
|
+
{
|
|
145
|
+
["scene"] = scene.path,
|
|
146
|
+
["sceneName"] = scene.name,
|
|
147
|
+
["query"] = query.Raw,
|
|
148
|
+
["count"] = results.Count,
|
|
149
|
+
["objects"] = results
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private static object HandleGetChildren(string paramsJson)
|
|
154
|
+
{
|
|
155
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
156
|
+
if (p == null || !p.TryGetValue("instanceId", out var idObj))
|
|
157
|
+
throw new ArgumentException("Missing 'instanceId' parameter");
|
|
158
|
+
|
|
159
|
+
int instanceId = Convert.ToInt32(idObj);
|
|
160
|
+
int maxDepth = 1;
|
|
161
|
+
if (p.TryGetValue("depth", out var depthObj))
|
|
162
|
+
maxDepth = Math.Max(1, Convert.ToInt32(depthObj));
|
|
163
|
+
|
|
164
|
+
var go = FindByInstanceId(instanceId);
|
|
165
|
+
if (go == null)
|
|
166
|
+
throw new Exception($"GameObject not found: {instanceId}");
|
|
167
|
+
|
|
168
|
+
var children = new List<object>();
|
|
169
|
+
int objectCount = 0;
|
|
170
|
+
int componentCount = 0;
|
|
171
|
+
|
|
172
|
+
for (int i = 0; i < go.transform.childCount; i++)
|
|
126
173
|
{
|
|
127
|
-
|
|
174
|
+
children.Add(SerializeGameObjectTree(
|
|
175
|
+
go.transform.GetChild(i).gameObject,
|
|
176
|
+
1,
|
|
177
|
+
maxDepth,
|
|
178
|
+
ref objectCount,
|
|
179
|
+
ref componentCount));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return new Dictionary<string, object>
|
|
183
|
+
{
|
|
184
|
+
["instanceId"] = instanceId,
|
|
128
185
|
["name"] = go.name,
|
|
129
186
|
["active"] = go.activeSelf,
|
|
130
187
|
["tag"] = go.tag,
|
|
131
188
|
["layer"] = go.layer,
|
|
132
189
|
["layerName"] = LayerMask.LayerToName(go.layer),
|
|
133
190
|
["childCount"] = go.transform.childCount,
|
|
134
|
-
["
|
|
135
|
-
["
|
|
136
|
-
|
|
191
|
+
["requestedDepth"] = maxDepth,
|
|
192
|
+
["children"] = children,
|
|
193
|
+
["stats"] = new Dictionary<string, object>
|
|
194
|
+
{
|
|
195
|
+
["objectCount"] = objectCount,
|
|
196
|
+
["componentCount"] = componentCount
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private static void ListObjectsRecursive(GameObject go, List<object> list, int depth, int maxDepth)
|
|
202
|
+
{
|
|
203
|
+
list.Add(CreateGameObjectEntry(go, depth));
|
|
137
204
|
|
|
138
205
|
if (depth < maxDepth)
|
|
139
206
|
{
|
|
@@ -142,6 +209,140 @@ namespace UCP.Bridge
|
|
|
142
209
|
}
|
|
143
210
|
}
|
|
144
211
|
|
|
212
|
+
private static void QueryHierarchy(
|
|
213
|
+
GameObject go,
|
|
214
|
+
SceneQuery query,
|
|
215
|
+
HashSet<string> fields,
|
|
216
|
+
List<object> results,
|
|
217
|
+
int depth,
|
|
218
|
+
int maxDepth)
|
|
219
|
+
{
|
|
220
|
+
if (MatchesQuery(go, query))
|
|
221
|
+
results.Add(ProjectGameObject(go, fields, depth));
|
|
222
|
+
|
|
223
|
+
if (depth >= maxDepth)
|
|
224
|
+
return;
|
|
225
|
+
|
|
226
|
+
for (int i = 0; i < go.transform.childCount; i++)
|
|
227
|
+
QueryHierarchy(go.transform.GetChild(i).gameObject, query, fields, results, depth + 1, maxDepth);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private static Dictionary<string, object> ProjectGameObject(GameObject go, HashSet<string> fields, int depth)
|
|
231
|
+
{
|
|
232
|
+
var entry = new Dictionary<string, object>();
|
|
233
|
+
AddField(entry, fields, "instanceId", go.GetId());
|
|
234
|
+
AddField(entry, fields, "name", go.name);
|
|
235
|
+
AddField(entry, fields, "active", go.activeSelf);
|
|
236
|
+
AddField(entry, fields, "activeInHierarchy", go.activeInHierarchy);
|
|
237
|
+
AddField(entry, fields, "tag", go.tag);
|
|
238
|
+
AddField(entry, fields, "layer", go.layer);
|
|
239
|
+
AddField(entry, fields, "layerName", LayerMask.LayerToName(go.layer));
|
|
240
|
+
AddField(entry, fields, "depth", depth);
|
|
241
|
+
AddField(entry, fields, "childCount", go.transform.childCount);
|
|
242
|
+
AddField(entry, fields, "components", GetComponentTypes(go));
|
|
243
|
+
return entry;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private static void AddField(Dictionary<string, object> entry, HashSet<string> fields, string key, object value)
|
|
247
|
+
{
|
|
248
|
+
if (fields.Contains(key))
|
|
249
|
+
entry[key] = value;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private static HashSet<string> ParseFields(string raw)
|
|
253
|
+
{
|
|
254
|
+
var source = string.IsNullOrEmpty(raw) ? "instanceId,name,active,components" : raw;
|
|
255
|
+
return new HashSet<string>(
|
|
256
|
+
source.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
|
257
|
+
.Select(field => field.Trim()),
|
|
258
|
+
StringComparer.OrdinalIgnoreCase);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private static SceneQuery ParseSceneQuery(string raw)
|
|
262
|
+
{
|
|
263
|
+
var query = new SceneQuery { Raw = raw ?? string.Empty };
|
|
264
|
+
foreach (var token in SplitQuery(query.Raw))
|
|
265
|
+
{
|
|
266
|
+
var index = token.IndexOf('=');
|
|
267
|
+
if (index <= 0)
|
|
268
|
+
throw new ArgumentException($"Unsupported query token '{token}'. Use key=value.");
|
|
269
|
+
|
|
270
|
+
var key = token.Substring(0, index).Trim().ToLowerInvariant();
|
|
271
|
+
var value = token.Substring(index + 1).Trim().Trim('"');
|
|
272
|
+
switch (key)
|
|
273
|
+
{
|
|
274
|
+
case "name":
|
|
275
|
+
query.Name = value;
|
|
276
|
+
break;
|
|
277
|
+
case "component":
|
|
278
|
+
case "components":
|
|
279
|
+
query.Component = value;
|
|
280
|
+
break;
|
|
281
|
+
case "active":
|
|
282
|
+
query.Active = Convert.ToBoolean(value);
|
|
283
|
+
break;
|
|
284
|
+
case "tag":
|
|
285
|
+
query.Tag = value;
|
|
286
|
+
break;
|
|
287
|
+
case "layer":
|
|
288
|
+
query.Layer = value;
|
|
289
|
+
break;
|
|
290
|
+
default:
|
|
291
|
+
throw new ArgumentException($"Unsupported scene query key '{key}'");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return query;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private static IEnumerable<string> SplitQuery(string raw)
|
|
299
|
+
{
|
|
300
|
+
return (raw ?? string.Empty)
|
|
301
|
+
.Replace(" AND ", " and ")
|
|
302
|
+
.Split(new[] { " and ", "," }, StringSplitOptions.RemoveEmptyEntries)
|
|
303
|
+
.Select(token => token.Trim())
|
|
304
|
+
.Where(token => token.Length > 0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private static bool MatchesQuery(GameObject go, SceneQuery query)
|
|
308
|
+
{
|
|
309
|
+
if (!string.IsNullOrEmpty(query.Name)
|
|
310
|
+
&& !go.name.Contains(query.Name, StringComparison.OrdinalIgnoreCase))
|
|
311
|
+
return false;
|
|
312
|
+
if (!string.IsNullOrEmpty(query.Component) && !HasComponent(go, query.Component))
|
|
313
|
+
return false;
|
|
314
|
+
if (query.Active.HasValue && go.activeSelf != query.Active.Value)
|
|
315
|
+
return false;
|
|
316
|
+
if (!string.IsNullOrEmpty(query.Tag) && !string.Equals(go.tag, query.Tag, StringComparison.OrdinalIgnoreCase))
|
|
317
|
+
return false;
|
|
318
|
+
if (!string.IsNullOrEmpty(query.Layer) && !LayerMatches(go.layer, query.Layer))
|
|
319
|
+
return false;
|
|
320
|
+
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private static bool HasComponent(GameObject go, string componentName)
|
|
325
|
+
{
|
|
326
|
+
foreach (var component in go.GetComponents<Component>())
|
|
327
|
+
{
|
|
328
|
+
if (component == null)
|
|
329
|
+
continue;
|
|
330
|
+
var type = component.GetType();
|
|
331
|
+
if (type.Name.Equals(componentName, StringComparison.OrdinalIgnoreCase)
|
|
332
|
+
|| (type.FullName != null && type.FullName.Equals(componentName, StringComparison.OrdinalIgnoreCase)))
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private static bool LayerMatches(int layer, string expected)
|
|
340
|
+
{
|
|
341
|
+
if (int.TryParse(expected, out var parsed))
|
|
342
|
+
return layer == parsed;
|
|
343
|
+
return string.Equals(LayerMask.LayerToName(layer), expected, StringComparison.OrdinalIgnoreCase);
|
|
344
|
+
}
|
|
345
|
+
|
|
145
346
|
private static object HandleGetComponents(string paramsJson)
|
|
146
347
|
{
|
|
147
348
|
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
@@ -202,8 +403,92 @@ namespace UCP.Bridge
|
|
|
202
403
|
|
|
203
404
|
private static GameObject FindByInstanceId(int id)
|
|
204
405
|
{
|
|
205
|
-
var
|
|
206
|
-
|
|
406
|
+
var direct = UnityObjectCompat.ResolveByInstanceId<GameObject>(id);
|
|
407
|
+
if (direct != null)
|
|
408
|
+
return direct;
|
|
409
|
+
|
|
410
|
+
for (int i = 0; i < SceneManager.sceneCount; i++)
|
|
411
|
+
{
|
|
412
|
+
var scene = SceneManager.GetSceneAt(i);
|
|
413
|
+
if (!scene.isLoaded)
|
|
414
|
+
continue;
|
|
415
|
+
|
|
416
|
+
foreach (var root in scene.GetRootGameObjects())
|
|
417
|
+
{
|
|
418
|
+
var found = FindInHierarchy(root, id);
|
|
419
|
+
if (found != null)
|
|
420
|
+
return found;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private static GameObject FindInHierarchy(GameObject go, int instanceId)
|
|
428
|
+
{
|
|
429
|
+
if (go.GetId() == instanceId)
|
|
430
|
+
return go;
|
|
431
|
+
|
|
432
|
+
for (int i = 0; i < go.transform.childCount; i++)
|
|
433
|
+
{
|
|
434
|
+
var found = FindInHierarchy(go.transform.GetChild(i).gameObject, instanceId);
|
|
435
|
+
if (found != null)
|
|
436
|
+
return found;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private static Dictionary<string, object> SerializeGameObjectTree(
|
|
443
|
+
GameObject go,
|
|
444
|
+
int depth,
|
|
445
|
+
int maxDepth,
|
|
446
|
+
ref int objectCount,
|
|
447
|
+
ref int componentCount)
|
|
448
|
+
{
|
|
449
|
+
objectCount++;
|
|
450
|
+
|
|
451
|
+
var entry = CreateGameObjectEntry(go, depth, ref componentCount);
|
|
452
|
+
if (depth < maxDepth)
|
|
453
|
+
{
|
|
454
|
+
var children = new List<object>();
|
|
455
|
+
for (int i = 0; i < go.transform.childCount; i++)
|
|
456
|
+
{
|
|
457
|
+
children.Add(SerializeGameObjectTree(
|
|
458
|
+
go.transform.GetChild(i).gameObject,
|
|
459
|
+
depth + 1,
|
|
460
|
+
maxDepth,
|
|
461
|
+
ref objectCount,
|
|
462
|
+
ref componentCount));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (children.Count > 0)
|
|
466
|
+
entry["children"] = children;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return entry;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private static Dictionary<string, object> CreateGameObjectEntry(GameObject go, int depth)
|
|
473
|
+
{
|
|
474
|
+
int ignored = 0;
|
|
475
|
+
return CreateGameObjectEntry(go, depth, ref ignored);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private static Dictionary<string, object> CreateGameObjectEntry(GameObject go, int depth, ref int componentCount)
|
|
479
|
+
{
|
|
480
|
+
return new Dictionary<string, object>
|
|
481
|
+
{
|
|
482
|
+
["instanceId"] = go.GetId(),
|
|
483
|
+
["name"] = go.name,
|
|
484
|
+
["active"] = go.activeSelf,
|
|
485
|
+
["tag"] = go.tag,
|
|
486
|
+
["layer"] = go.layer,
|
|
487
|
+
["layerName"] = LayerMask.LayerToName(go.layer),
|
|
488
|
+
["childCount"] = go.transform.childCount,
|
|
489
|
+
["components"] = GetComponentTypes(go, ref componentCount),
|
|
490
|
+
["depth"] = depth
|
|
491
|
+
};
|
|
207
492
|
}
|
|
208
493
|
|
|
209
494
|
private static List<object> GetComponentTypes(GameObject go)
|
|
@@ -223,5 +508,15 @@ namespace UCP.Bridge
|
|
|
223
508
|
}
|
|
224
509
|
return componentTypes;
|
|
225
510
|
}
|
|
511
|
+
|
|
512
|
+
private sealed class SceneQuery
|
|
513
|
+
{
|
|
514
|
+
public string Raw;
|
|
515
|
+
public string Name;
|
|
516
|
+
public string Component;
|
|
517
|
+
public bool? Active;
|
|
518
|
+
public string Tag;
|
|
519
|
+
public string Layer;
|
|
520
|
+
}
|
|
226
521
|
}
|
|
227
522
|
}
|
|
@@ -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
|
+
}
|