@mflrevan/ucp 0.6.2 → 0.6.3

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 (51) hide show
  1. package/README.md +1 -1
  2. package/bridge/com.ucp.bridge/CHANGELOG.md +48 -0
  3. package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +55 -6
  4. package/bridge/com.ucp.bridge/Editor/Bridge/EditorStateSummary.cs +165 -0
  5. package/bridge/com.ucp.bridge/Editor/Bridge/EditorStateSummary.cs.meta +11 -0
  6. package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +22 -5
  7. package/bridge/com.ucp.bridge/Editor/Controllers/EditorController.cs +1 -0
  8. package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +10 -10
  9. package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +71 -0
  10. package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
  11. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +4 -4
  12. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +3 -3
  13. package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +6 -6
  14. package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +7 -7
  15. package/bridge/com.ucp.bridge/Editor/Controllers/RecordingController.cs +8 -0
  16. package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +3 -3
  17. package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +3 -3
  18. package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +5 -5
  19. package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +1 -1
  20. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +1 -1
  21. package/bridge/com.ucp.bridge/Editor/Controllers/UiController.cs +70 -0
  22. package/bridge/com.ucp.bridge/Editor/Controllers/UiController.cs.meta +11 -0
  23. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +1 -1
  24. package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +1 -1
  25. package/bridge/com.ucp.bridge/Editor/Ui/UiHost.cs +468 -0
  26. package/bridge/com.ucp.bridge/Editor/Ui/UiHost.cs.meta +11 -0
  27. package/bridge/com.ucp.bridge/Editor/Ui/UiLintService.cs +554 -0
  28. package/bridge/com.ucp.bridge/Editor/Ui/UiLintService.cs.meta +11 -0
  29. package/bridge/com.ucp.bridge/Editor/Ui/UiOperationManager.cs +870 -0
  30. package/bridge/com.ucp.bridge/Editor/Ui/UiOperationManager.cs.meta +11 -0
  31. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioApplier.cs +496 -0
  32. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioApplier.cs.meta +11 -0
  33. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioLoader.cs +854 -0
  34. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioLoader.cs.meta +11 -0
  35. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioModels.cs +374 -0
  36. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioModels.cs.meta +11 -0
  37. package/bridge/com.ucp.bridge/Editor/Ui/UiVisualTreeInspector.cs +690 -0
  38. package/bridge/com.ucp.bridge/Editor/Ui/UiVisualTreeInspector.cs.meta +11 -0
  39. package/bridge/com.ucp.bridge/{Runtime.meta → Editor/Ui.meta} +1 -1
  40. package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +79 -4
  41. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +2 -2
  42. package/bridge/com.ucp.bridge/Tests/Editor/UiControllerTests.cs +547 -0
  43. package/bridge/com.ucp.bridge/Tests/Editor/UiControllerTests.cs.meta +11 -0
  44. package/bridge/com.ucp.bridge/Tests/Editor/UiLintServiceTests.cs +253 -0
  45. package/bridge/com.ucp.bridge/Tests/Editor/UiLintServiceTests.cs.meta +11 -0
  46. package/bridge/com.ucp.bridge/Tests/Editor/UiScenarioTests.cs +587 -0
  47. package/bridge/com.ucp.bridge/Tests/Editor/UiScenarioTests.cs.meta +11 -0
  48. package/bridge/com.ucp.bridge/package.json +1 -1
  49. package/package.json +1 -1
  50. package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef +0 -14
  51. package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef.meta +0 -7
@@ -0,0 +1,690 @@
1
+ #if UNITY_6000_0_OR_NEWER
2
+ using System;
3
+ using System.Collections;
4
+ using System.Collections.Generic;
5
+ using System.Linq;
6
+ using System.Reflection;
7
+ using UnityEngine;
8
+ using UnityEngine.UIElements;
9
+
10
+ namespace UCP.Bridge
11
+ {
12
+ internal sealed class UiInspectOptions
13
+ {
14
+ private const int DefaultDepth = 12;
15
+ private const int DefaultMaxElements = 500;
16
+
17
+ internal string Query { get; private set; }
18
+ internal int MaxDepth { get; private set; }
19
+ internal int MaxElements { get; private set; }
20
+ internal string Detail { get; private set; }
21
+
22
+ internal static UiInspectOptions Parse(Dictionary<string, object> parameters)
23
+ {
24
+ parameters ??= new Dictionary<string, object>();
25
+ var options = new UiInspectOptions
26
+ {
27
+ Query = ReadString(parameters, "query"),
28
+ MaxDepth = ReadInt(parameters, "depth", DefaultDepth, 0, 64),
29
+ MaxElements = ReadInt(parameters, "max", DefaultMaxElements, 1, 5000),
30
+ Detail = ReadString(parameters, "detail") ?? "normal"
31
+ };
32
+
33
+ options.Detail = options.Detail.ToLowerInvariant();
34
+ if (options.Detail != "summary" && options.Detail != "normal" && options.Detail != "verbose")
35
+ throw new ArgumentException("'detail' must be one of: summary, normal, verbose");
36
+
37
+ UiSimpleQuery.Validate(options.Query);
38
+ return options;
39
+ }
40
+
41
+ private static string ReadString(Dictionary<string, object> parameters, string key)
42
+ {
43
+ return parameters.TryGetValue(key, out var value) && value != null
44
+ ? value.ToString()
45
+ : null;
46
+ }
47
+
48
+ private static int ReadInt(
49
+ Dictionary<string, object> parameters,
50
+ string key,
51
+ int defaultValue,
52
+ int min,
53
+ int max)
54
+ {
55
+ if (!parameters.TryGetValue(key, out var value) || value == null)
56
+ return defaultValue;
57
+
58
+ try
59
+ {
60
+ var result = Convert.ToInt32(value);
61
+ if (result < min || result > max)
62
+ throw new ArgumentException($"'{key}' must be between {min} and {max}");
63
+ return result;
64
+ }
65
+ catch (ArgumentException)
66
+ {
67
+ throw;
68
+ }
69
+ catch (Exception)
70
+ {
71
+ throw new ArgumentException($"'{key}' must be an integer between {min} and {max}");
72
+ }
73
+ }
74
+ }
75
+
76
+ internal static class UiVisualTreeInspector
77
+ {
78
+ internal static Dictionary<string, object> Inspect(
79
+ VisualElement root,
80
+ UiInspectOptions options,
81
+ IReadOnlyList<UiCollectionMetadata> collectionMetadata)
82
+ {
83
+ if (root == null)
84
+ throw new ArgumentException("The resolved UXML did not create a visual tree");
85
+
86
+ var selectedRoots = Select(root, options.Query);
87
+ var context = new SnapshotContext(options);
88
+ var roots = new List<object>();
89
+ foreach (var selectedRoot in selectedRoots)
90
+ {
91
+ if (context.Truncated)
92
+ break;
93
+ var node = BuildNode(selectedRoot, 0, context);
94
+ if (node != null)
95
+ roots.Add(node);
96
+ }
97
+
98
+ var collections = new List<object>();
99
+ if (collectionMetadata != null)
100
+ {
101
+ foreach (var metadata in collectionMetadata)
102
+ collections.Add(metadata.ToDictionary());
103
+ }
104
+
105
+ return new Dictionary<string, object>
106
+ {
107
+ ["query"] = options.Query,
108
+ ["matchCount"] = selectedRoots.Count,
109
+ ["returnedElementCount"] = context.ReturnedCount,
110
+ ["truncated"] = context.Truncated,
111
+ ["maxElements"] = options.MaxElements,
112
+ ["maxDepth"] = options.MaxDepth,
113
+ ["detail"] = options.Detail,
114
+ ["roots"] = roots,
115
+ ["collections"] = collections
116
+ };
117
+ }
118
+
119
+ private static List<VisualElement> Select(VisualElement root, string query)
120
+ {
121
+ if (string.IsNullOrWhiteSpace(query))
122
+ return new List<VisualElement> { root };
123
+
124
+ var result = new List<VisualElement>();
125
+ Visit(root, element =>
126
+ {
127
+ if (UiSimpleQuery.Matches(element, query))
128
+ result.Add(element);
129
+ });
130
+ return result;
131
+ }
132
+
133
+ private static Dictionary<string, object> BuildNode(
134
+ VisualElement element,
135
+ int depth,
136
+ SnapshotContext context)
137
+ {
138
+ if (context.ReturnedCount >= context.Options.MaxElements)
139
+ {
140
+ context.Truncated = true;
141
+ return null;
142
+ }
143
+
144
+ context.ReturnedCount++;
145
+ var node = new Dictionary<string, object>
146
+ {
147
+ ["ref"] = $"e{context.ReturnedCount}",
148
+ ["type"] = element.GetType().Name,
149
+ ["name"] = string.IsNullOrEmpty(element.name) ? null : element.name,
150
+ ["classes"] = element.GetClasses().Cast<object>().ToList()
151
+ };
152
+
153
+ if (element is TextElement textElement && !string.IsNullOrEmpty(textElement.text))
154
+ node["text"] = textElement.text;
155
+
156
+ var value = ReadValue(element);
157
+ if (value.Found)
158
+ node["value"] = value.Value;
159
+
160
+ if (context.Options.Detail != "summary")
161
+ {
162
+ var displayed = IsDisplayed(element);
163
+ node["enabled"] = element.enabledInHierarchy;
164
+ node["focusable"] = element.focusable;
165
+ node["focused"] = element.panel?.focusController?.focusedElement == element;
166
+ node["visible"] = displayed && element.resolvedStyle.visibility == Visibility.Visible;
167
+ node["layout"] = RectDictionary(element.layout);
168
+ node["worldBound"] = RectDictionary(element.worldBound);
169
+ node["resolvedStyle"] = StyleDictionary(element.resolvedStyle);
170
+ node["bindings"] = BindingDictionaries(element);
171
+ }
172
+
173
+ if (context.Options.Detail == "verbose")
174
+ {
175
+ node["fullType"] = element.GetType().FullName;
176
+ node["pickingMode"] = element.pickingMode.ToString();
177
+ node["tooltip"] = string.IsNullOrEmpty(element.tooltip) ? null : element.tooltip;
178
+ }
179
+
180
+ var children = new List<object>();
181
+ if (depth < context.Options.MaxDepth)
182
+ {
183
+ foreach (var child in element.hierarchy.Children())
184
+ {
185
+ if (context.ReturnedCount >= context.Options.MaxElements)
186
+ {
187
+ context.Truncated = true;
188
+ break;
189
+ }
190
+
191
+ var childNode = BuildNode(child, depth + 1, context);
192
+ if (childNode != null)
193
+ children.Add(childNode);
194
+ }
195
+ }
196
+ else if (element.hierarchy.childCount > 0)
197
+ {
198
+ context.Truncated = true;
199
+ }
200
+
201
+ if (children.Count > 0)
202
+ node["children"] = children;
203
+ return node;
204
+ }
205
+
206
+ internal static List<object> BindingDictionaries(VisualElement element)
207
+ {
208
+ var bindings = new List<object>();
209
+ foreach (var info in element.GetBindingInfos())
210
+ {
211
+ var bindingId = info.bindingId;
212
+ var item = new Dictionary<string, object>
213
+ {
214
+ ["property"] = bindingId.ToString(),
215
+ ["bindingType"] = info.binding?.GetType().Name
216
+ };
217
+
218
+ if (element.TryGetLastBindingToUIResult(bindingId, out var toUi))
219
+ {
220
+ item["toUi"] = new Dictionary<string, object>
221
+ {
222
+ ["status"] = toUi.status.ToString(),
223
+ ["message"] = string.IsNullOrEmpty(toUi.message) ? null : toUi.message
224
+ };
225
+ }
226
+
227
+ if (element.TryGetLastBindingToSourceResult(bindingId, out var toSource))
228
+ {
229
+ item["toSource"] = new Dictionary<string, object>
230
+ {
231
+ ["status"] = toSource.status.ToString(),
232
+ ["message"] = string.IsNullOrEmpty(toSource.message) ? null : toSource.message
233
+ };
234
+ }
235
+
236
+ bindings.Add(item);
237
+ }
238
+ return bindings;
239
+ }
240
+
241
+ private static Dictionary<string, object> RectDictionary(Rect value)
242
+ {
243
+ var finite = UiValue.IsFinite(value.x) && UiValue.IsFinite(value.y) &&
244
+ UiValue.IsFinite(value.width) && UiValue.IsFinite(value.height);
245
+ return new Dictionary<string, object>
246
+ {
247
+ ["x"] = UiValue.FiniteOrNull(value.x),
248
+ ["y"] = UiValue.FiniteOrNull(value.y),
249
+ ["width"] = UiValue.FiniteOrNull(value.width),
250
+ ["height"] = UiValue.FiniteOrNull(value.height),
251
+ ["finite"] = finite
252
+ };
253
+ }
254
+
255
+ private static Dictionary<string, object> StyleDictionary(IResolvedStyle style)
256
+ {
257
+ return new Dictionary<string, object>
258
+ {
259
+ ["display"] = style.display.ToString(),
260
+ ["visibility"] = style.visibility.ToString(),
261
+ ["position"] = style.position.ToString(),
262
+ ["flexDirection"] = style.flexDirection.ToString(),
263
+ ["justifyContent"] = style.justifyContent.ToString(),
264
+ ["alignItems"] = style.alignItems.ToString(),
265
+ ["alignSelf"] = style.alignSelf.ToString(),
266
+ ["flexGrow"] = UiValue.FiniteOrNull(style.flexGrow),
267
+ ["flexShrink"] = UiValue.FiniteOrNull(style.flexShrink),
268
+ ["width"] = UiValue.FiniteOrNull(style.width),
269
+ ["height"] = UiValue.FiniteOrNull(style.height),
270
+ ["opacity"] = UiValue.FiniteOrNull(style.opacity),
271
+ ["color"] = ColorDictionary(style.color),
272
+ ["backgroundColor"] = ColorDictionary(style.backgroundColor),
273
+ ["fontSize"] = UiValue.FiniteOrNull(style.fontSize),
274
+ ["whiteSpace"] = style.whiteSpace.ToString()
275
+ };
276
+ }
277
+
278
+ private static Dictionary<string, object> ColorDictionary(Color value)
279
+ {
280
+ return new Dictionary<string, object>
281
+ {
282
+ ["r"] = UiValue.FiniteOrNull(value.r),
283
+ ["g"] = UiValue.FiniteOrNull(value.g),
284
+ ["b"] = UiValue.FiniteOrNull(value.b),
285
+ ["a"] = UiValue.FiniteOrNull(value.a)
286
+ };
287
+ }
288
+
289
+ private static ValueRead ReadValue(VisualElement element)
290
+ {
291
+ try
292
+ {
293
+ var property = element.GetType().GetProperty(
294
+ "value",
295
+ BindingFlags.Instance | BindingFlags.Public);
296
+ if (property == null || !property.CanRead || property.GetIndexParameters().Length != 0)
297
+ return default;
298
+
299
+ return new ValueRead(true, NormalizeValue(property.GetValue(element)));
300
+ }
301
+ catch
302
+ {
303
+ return default;
304
+ }
305
+ }
306
+
307
+ private static object NormalizeValue(object value)
308
+ {
309
+ if (value == null || value is string || value is bool || value is char ||
310
+ value is byte || value is sbyte || value is short || value is ushort ||
311
+ value is int || value is uint || value is long || value is ulong || value is decimal)
312
+ return value;
313
+ if (value is float floatValue)
314
+ return UiValue.FiniteOrNull(floatValue);
315
+ if (value is double doubleValue)
316
+ return UiValue.FiniteOrNull(doubleValue);
317
+ if (value is Enum enumValue)
318
+ return enumValue.ToString();
319
+ if (value is Color colorValue)
320
+ return ColorDictionary(colorValue);
321
+ if (value is UnityEngine.Object objectValue)
322
+ return objectValue != null ? objectValue.name : null;
323
+ return value.ToString();
324
+ }
325
+
326
+ private static bool IsDisplayed(VisualElement element)
327
+ {
328
+ for (var current = element; current != null; current = current.parent)
329
+ {
330
+ if (current.resolvedStyle.display == DisplayStyle.None)
331
+ return false;
332
+ }
333
+ return true;
334
+ }
335
+
336
+ internal static void Visit(VisualElement root, Action<VisualElement> action)
337
+ {
338
+ if (root == null)
339
+ return;
340
+ action(root);
341
+ // Collection controls can have a null contentContainer while their
342
+ // realized rows still exist in the physical visual hierarchy.
343
+ foreach (var child in root.hierarchy.Children())
344
+ Visit(child, action);
345
+ }
346
+
347
+ private sealed class SnapshotContext
348
+ {
349
+ internal SnapshotContext(UiInspectOptions options)
350
+ {
351
+ Options = options;
352
+ }
353
+
354
+ internal UiInspectOptions Options { get; }
355
+ internal int ReturnedCount { get; set; }
356
+ internal bool Truncated { get; set; }
357
+ }
358
+
359
+ private readonly struct ValueRead
360
+ {
361
+ internal ValueRead(bool found, object value)
362
+ {
363
+ Found = found;
364
+ Value = value;
365
+ }
366
+
367
+ internal bool Found { get; }
368
+ internal object Value { get; }
369
+ }
370
+ }
371
+
372
+ internal static class UiSimpleQuery
373
+ {
374
+ internal static void Validate(string query)
375
+ {
376
+ if (string.IsNullOrWhiteSpace(query))
377
+ return;
378
+ query = query.Trim();
379
+ if (query == "#" || query == ".")
380
+ throw new ArgumentException("'query' must contain a name, class, or element type");
381
+ if (query.IndexOfAny(new[] { ' ', '>', '+', '~', '[', ']', ':', ',' }) >= 0)
382
+ {
383
+ throw new ArgumentException(
384
+ "'query' supports one simple selector only: #name, .class, or element type");
385
+ }
386
+
387
+ if (query[0] != '#' && query[0] != '.')
388
+ return;
389
+
390
+ // A second '#' or '.' would be read as part of the name and match nothing.
391
+ for (var i = 1; i < query.Length; i++)
392
+ {
393
+ var c = query[i];
394
+ if (!(char.IsLetterOrDigit(c) || c == '_' || c == '-'))
395
+ {
396
+ throw new ArgumentException(
397
+ "'query' #name and .class selectors may contain only letters, digits, '_' and '-'; " +
398
+ "compound selectors such as '.a.b' are not supported");
399
+ }
400
+ }
401
+ }
402
+
403
+ internal static bool Matches(VisualElement element, string query)
404
+ {
405
+ if (element == null || string.IsNullOrWhiteSpace(query))
406
+ return false;
407
+ query = query.Trim();
408
+ if (query[0] == '#')
409
+ return string.Equals(element.name, query.Substring(1), StringComparison.Ordinal);
410
+ if (query[0] == '.')
411
+ return element.ClassListContains(query.Substring(1));
412
+ return string.Equals(element.GetType().Name, query, StringComparison.OrdinalIgnoreCase) ||
413
+ string.Equals(element.GetType().FullName, query, StringComparison.OrdinalIgnoreCase);
414
+ }
415
+ }
416
+
417
+ internal static class UiAudit
418
+ {
419
+ private const int MaxDiagnostics = 200;
420
+
421
+ internal static Dictionary<string, object> Run(
422
+ VisualElement root,
423
+ UiApplyReport applyReport,
424
+ IReadOnlyList<UiCollectionMetadata> collectionMetadata)
425
+ {
426
+ var diagnostics = new DiagnosticCollector();
427
+ var collectionRoots = FindCollectionRoots(root, collectionMetadata);
428
+ var namedElements = new Dictionary<string, List<VisualElement>>(StringComparer.Ordinal);
429
+
430
+ if (applyReport != null)
431
+ {
432
+ foreach (var diagnostic in applyReport.Diagnostics)
433
+ {
434
+ var severity = diagnostic.TryGetValue("severity", out var value)
435
+ ? value?.ToString()
436
+ : null;
437
+ diagnostics.Add(severity == "error", diagnostic);
438
+ }
439
+ }
440
+
441
+ UiVisualTreeInspector.Visit(root, element =>
442
+ {
443
+ var insideDynamicCollection = HasAncestor(element, collectionRoots);
444
+ if (!insideDynamicCollection && IsUserElementName(element.name))
445
+ {
446
+ if (!namedElements.TryGetValue(element.name, out var matches))
447
+ {
448
+ matches = new List<VisualElement>();
449
+ namedElements.Add(element.name, matches);
450
+ }
451
+ matches.Add(element);
452
+ }
453
+
454
+ if (element.focusable && !IsUnityInternalName(element.name) && IsVisibleThroughAncestors(element))
455
+ {
456
+ var rect = element.worldBound;
457
+ if (UiValue.IsFinite(rect.width) && UiValue.IsFinite(rect.height) &&
458
+ (rect.width <= 0f || rect.height <= 0f))
459
+ {
460
+ diagnostics.Add(false, Diagnostic(
461
+ "focusable_zero_size",
462
+ "Focusable element has zero rendered width or height",
463
+ element));
464
+ }
465
+ else if (!insideDynamicCollection && !HasScrollViewAncestor(element) &&
466
+ IsFinitePositive(rect) && IsFinitePositive(root.worldBound))
467
+ {
468
+ var viewport = root.worldBound;
469
+ var intersection = Intersect(rect, viewport);
470
+ if (intersection.width <= 0f || intersection.height <= 0f)
471
+ {
472
+ diagnostics.Add(false, Diagnostic(
473
+ "focusable_outside_viewport",
474
+ "Focusable element is fully outside the UI viewport",
475
+ element));
476
+ }
477
+ else if (intersection.width + 0.5f < rect.width ||
478
+ intersection.height + 0.5f < rect.height)
479
+ {
480
+ diagnostics.Add(false, Diagnostic(
481
+ "focusable_partially_outside_viewport",
482
+ "Focusable element is partially outside the UI viewport",
483
+ element));
484
+ }
485
+ }
486
+ }
487
+
488
+ foreach (var bindingObject in UiVisualTreeInspector.BindingDictionaries(element))
489
+ {
490
+ if (bindingObject is not Dictionary<string, object> binding)
491
+ continue;
492
+ CheckBindingResult(
493
+ binding,
494
+ "toUi",
495
+ element,
496
+ diagnostics);
497
+ CheckBindingResult(
498
+ binding,
499
+ "toSource",
500
+ element,
501
+ diagnostics);
502
+ }
503
+ });
504
+
505
+ foreach (var pair in namedElements)
506
+ {
507
+ if (pair.Value.Count < 2)
508
+ continue;
509
+ diagnostics.Add(false, new Dictionary<string, object>
510
+ {
511
+ ["code"] = "duplicate_name",
512
+ ["message"] = $"Element name '{pair.Key}' is used {pair.Value.Count} times outside dynamic collections",
513
+ ["element"] = new Dictionary<string, object>
514
+ {
515
+ ["type"] = pair.Value[0].GetType().Name,
516
+ ["name"] = pair.Key
517
+ }
518
+ });
519
+ }
520
+
521
+ return new Dictionary<string, object>
522
+ {
523
+ ["passed"] = diagnostics.ErrorCount == 0,
524
+ ["errorCount"] = diagnostics.ErrorCount,
525
+ ["warningCount"] = diagnostics.WarningCount,
526
+ ["diagnosticsTruncated"] = diagnostics.Truncated,
527
+ ["errors"] = diagnostics.Errors,
528
+ ["warnings"] = diagnostics.Warnings
529
+ };
530
+ }
531
+
532
+ private static void CheckBindingResult(
533
+ Dictionary<string, object> binding,
534
+ string direction,
535
+ VisualElement element,
536
+ DiagnosticCollector diagnostics)
537
+ {
538
+ if (!binding.TryGetValue(direction, out var resultObject) ||
539
+ resultObject is not Dictionary<string, object> result ||
540
+ !result.TryGetValue("status", out var statusObject))
541
+ return;
542
+
543
+ var status = statusObject?.ToString();
544
+ if (string.Equals(status, "Success", StringComparison.OrdinalIgnoreCase))
545
+ return;
546
+
547
+ var message = result.TryGetValue("message", out var messageObject)
548
+ ? messageObject?.ToString()
549
+ : null;
550
+ var property = binding.TryGetValue("property", out var propertyObject)
551
+ ? propertyObject?.ToString()
552
+ : null;
553
+ var diagnostic = Diagnostic(
554
+ "binding_" + (status ?? "unknown").ToLowerInvariant(),
555
+ $"Binding {direction} for '{property}' reported {status}: {message}",
556
+ element);
557
+ diagnostics.Add(string.Equals(status, "Failure", StringComparison.OrdinalIgnoreCase), diagnostic);
558
+ }
559
+
560
+ private static Dictionary<string, object> Diagnostic(
561
+ string code,
562
+ string message,
563
+ VisualElement element)
564
+ {
565
+ return new Dictionary<string, object>
566
+ {
567
+ ["code"] = code,
568
+ ["message"] = message,
569
+ ["element"] = new Dictionary<string, object>
570
+ {
571
+ ["type"] = element.GetType().Name,
572
+ ["name"] = string.IsNullOrEmpty(element.name) ? null : element.name
573
+ }
574
+ };
575
+ }
576
+
577
+ private static HashSet<VisualElement> FindCollectionRoots(
578
+ VisualElement root,
579
+ IReadOnlyList<UiCollectionMetadata> metadata)
580
+ {
581
+ var result = new HashSet<VisualElement>();
582
+ if (metadata == null)
583
+ return result;
584
+ foreach (var collection in metadata)
585
+ {
586
+ if (collection.Selector == ":root")
587
+ {
588
+ result.Add(root);
589
+ continue;
590
+ }
591
+ UiVisualTreeInspector.Visit(root, element =>
592
+ {
593
+ if (UiSimpleQuery.Matches(element, collection.Selector))
594
+ result.Add(element);
595
+ });
596
+ }
597
+ return result;
598
+ }
599
+
600
+ private static bool HasAncestor(VisualElement element, HashSet<VisualElement> candidates)
601
+ {
602
+ for (var current = element; current != null; current = current.parent)
603
+ {
604
+ if (candidates.Contains(current))
605
+ return true;
606
+ }
607
+ return false;
608
+ }
609
+
610
+ private static bool HasScrollViewAncestor(VisualElement element)
611
+ {
612
+ for (var current = element.parent; current != null; current = current.parent)
613
+ {
614
+ if (current is ScrollView)
615
+ return true;
616
+ }
617
+ return false;
618
+ }
619
+
620
+ private static bool IsUserElementName(string name)
621
+ {
622
+ return !string.IsNullOrEmpty(name) &&
623
+ !IsUnityInternalName(name);
624
+ }
625
+
626
+ private static bool IsUnityInternalName(string name)
627
+ {
628
+ return name != null && name.StartsWith("unity-", StringComparison.Ordinal);
629
+ }
630
+
631
+ private static bool IsVisibleThroughAncestors(VisualElement element)
632
+ {
633
+ for (var current = element; current != null; current = current.parent)
634
+ {
635
+ if (current.resolvedStyle.display == DisplayStyle.None ||
636
+ current.resolvedStyle.visibility == Visibility.Hidden)
637
+ {
638
+ return false;
639
+ }
640
+ }
641
+ return true;
642
+ }
643
+
644
+ private static bool IsFinitePositive(Rect rect)
645
+ {
646
+ return UiValue.IsFinite(rect.x) && UiValue.IsFinite(rect.y) &&
647
+ UiValue.IsFinite(rect.width) && UiValue.IsFinite(rect.height) &&
648
+ rect.width > 0f && rect.height > 0f;
649
+ }
650
+
651
+ private static Rect Intersect(Rect left, Rect right)
652
+ {
653
+ var xMin = Mathf.Max(left.xMin, right.xMin);
654
+ var yMin = Mathf.Max(left.yMin, right.yMin);
655
+ var xMax = Mathf.Min(left.xMax, right.xMax);
656
+ var yMax = Mathf.Min(left.yMax, right.yMax);
657
+ return Rect.MinMaxRect(xMin, yMin, Mathf.Max(xMin, xMax), Mathf.Max(yMin, yMax));
658
+ }
659
+
660
+ private sealed class DiagnosticCollector
661
+ {
662
+ internal List<object> Errors { get; } = new List<object>();
663
+ internal List<object> Warnings { get; } = new List<object>();
664
+ internal int ErrorCount { get; private set; }
665
+ internal int WarningCount { get; private set; }
666
+ internal bool Truncated { get; private set; }
667
+
668
+ internal void Add(bool isError, object diagnostic)
669
+ {
670
+ // Bound the response, but keep counting every diagnostic so
671
+ // truncation cannot hide a failing audit or warning policy.
672
+ if (isError)
673
+ ErrorCount++;
674
+ else
675
+ WarningCount++;
676
+
677
+ if (Errors.Count + Warnings.Count >= MaxDiagnostics)
678
+ {
679
+ Truncated = true;
680
+ if (!isError || Warnings.Count == 0)
681
+ return;
682
+ // Preserve the cause of failure even when warnings arrived first.
683
+ Warnings.RemoveAt(Warnings.Count - 1);
684
+ }
685
+ (isError ? Errors : Warnings).Add(diagnostic);
686
+ }
687
+ }
688
+ }
689
+ }
690
+ #endif
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: bfab24ff939e46f189b02d0255fc7c1a
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant: