@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,468 @@
1
+ #if UNITY_6000_0_OR_NEWER
2
+ using System;
3
+ using System.Collections.Generic;
4
+ using System.Globalization;
5
+ using System.IO;
6
+ using System.Security.Cryptography;
7
+ using System.Text;
8
+ using UnityEditor;
9
+ using UnityEditorInternal;
10
+ using UnityEngine;
11
+ using UnityEngine.Rendering;
12
+ using UnityEngine.UIElements;
13
+
14
+ namespace UCP.Bridge
15
+ {
16
+ /// <summary>
17
+ /// Transient editor-only host for deterministic UI Toolkit inspection and capture.
18
+ /// The compositor compensates for CaptureEditorWindow treating logical window points
19
+ /// as physical pixels on high-DPI displays.
20
+ /// </summary>
21
+ internal sealed class UiHostWindow : EditorWindow
22
+ {
23
+ private int _viewportWidth;
24
+ private int _viewportHeight;
25
+ private float _compositorScale = 1f;
26
+
27
+ internal VisualElement ContentRoot { get; private set; }
28
+
29
+ internal static UiHostWindow Open(int width, int height)
30
+ {
31
+ var window = CreateInstance<UiHostWindow>();
32
+ try
33
+ {
34
+ window.hideFlags = HideFlags.HideAndDontSave;
35
+ window._viewportWidth = width;
36
+ window._viewportHeight = height;
37
+ window._compositorScale = 1f / Mathf.Max(1f, EditorGUIUtility.pixelsPerPoint);
38
+ window.titleContent = new GUIContent("UCP UI Harness");
39
+ window.minSize = new Vector2(width, height);
40
+ window.maxSize = new Vector2(width, height);
41
+ window.position = new Rect(80f, 80f, width, height);
42
+ window.ShowUtility();
43
+ window.EnsureContent();
44
+ window.Focus();
45
+ return window;
46
+ }
47
+ catch
48
+ {
49
+ try
50
+ {
51
+ CloseAndDestroy(window);
52
+ }
53
+ catch
54
+ {
55
+ // Preserve the exception that prevented the host from opening.
56
+ }
57
+ throw;
58
+ }
59
+ }
60
+
61
+ internal static void CloseAndDestroy(UiHostWindow window)
62
+ {
63
+ if (window == null)
64
+ return;
65
+
66
+ var error = UiCleanup.RunAll(
67
+ () => window.Close(),
68
+ () =>
69
+ {
70
+ if (window != null)
71
+ UnityEngine.Object.DestroyImmediate(window);
72
+ });
73
+ if (error != null)
74
+ throw error;
75
+ }
76
+
77
+ public void CreateGUI()
78
+ {
79
+ EnsureContent();
80
+ }
81
+
82
+ internal void EnsureContent()
83
+ {
84
+ if (ContentRoot != null)
85
+ return;
86
+
87
+ rootVisualElement.Clear();
88
+ rootVisualElement.style.overflow = Overflow.Hidden;
89
+
90
+ ContentRoot = new VisualElement
91
+ {
92
+ name = "ucp-ui-content-root",
93
+ pickingMode = PickingMode.Position
94
+ };
95
+ ContentRoot.style.position = Position.Absolute;
96
+ ContentRoot.style.left = 0f;
97
+ ContentRoot.style.bottom = 0f;
98
+ ContentRoot.style.width = _viewportWidth;
99
+ ContentRoot.style.height = _viewportHeight;
100
+ ContentRoot.style.overflow = Overflow.Hidden;
101
+ ContentRoot.style.transformOrigin = new TransformOrigin(Length.Percent(0f), Length.Percent(100f));
102
+ ContentRoot.style.scale = new Scale(new Vector3(_compositorScale, _compositorScale, 1f));
103
+ rootVisualElement.Add(ContentRoot);
104
+ }
105
+
106
+ internal void Pump()
107
+ {
108
+ EnsureContent();
109
+ Focus();
110
+ Repaint();
111
+ EditorApplication.QueuePlayerLoopUpdate();
112
+ }
113
+ }
114
+
115
+ internal readonly struct UiGeometrySample
116
+ {
117
+ internal UiGeometrySample(
118
+ string hash,
119
+ int elementCount,
120
+ int renderedElementCount,
121
+ int invalidGeometryCount,
122
+ bool panelAttached)
123
+ {
124
+ Hash = hash;
125
+ ElementCount = elementCount;
126
+ RenderedElementCount = renderedElementCount;
127
+ InvalidGeometryCount = invalidGeometryCount;
128
+ PanelAttached = panelAttached;
129
+ }
130
+
131
+ internal string Hash { get; }
132
+ internal int ElementCount { get; }
133
+ internal int RenderedElementCount { get; }
134
+ internal int InvalidGeometryCount { get; }
135
+ internal bool PanelAttached { get; }
136
+ internal bool IsValid => PanelAttached && RenderedElementCount > 0 && InvalidGeometryCount == 0;
137
+
138
+ internal Dictionary<string, object> ToDictionary(int stableFrames)
139
+ {
140
+ return new Dictionary<string, object>
141
+ {
142
+ ["panelAttached"] = PanelAttached,
143
+ ["elementCount"] = ElementCount,
144
+ ["renderedElementCount"] = RenderedElementCount,
145
+ ["invalidGeometryCount"] = InvalidGeometryCount,
146
+ ["stableFrames"] = stableFrames,
147
+ ["geometryHash"] = Hash
148
+ };
149
+ }
150
+ }
151
+
152
+ internal static class UiGeometrySampler
153
+ {
154
+ internal static UiGeometrySample Measure(VisualElement root)
155
+ {
156
+ var hash = 1469598103934665603UL;
157
+ var elementCount = 0;
158
+ var renderedCount = 0;
159
+ var invalidCount = 0;
160
+ Visit(root, true, ref hash, ref elementCount, ref renderedCount, ref invalidCount);
161
+ return new UiGeometrySample(
162
+ hash.ToString("x16", CultureInfo.InvariantCulture),
163
+ elementCount,
164
+ renderedCount,
165
+ invalidCount,
166
+ root?.panel != null);
167
+ }
168
+
169
+ private static void Visit(
170
+ VisualElement element,
171
+ bool ancestorsDisplayed,
172
+ ref ulong hash,
173
+ ref int elementCount,
174
+ ref int renderedCount,
175
+ ref int invalidCount)
176
+ {
177
+ if (element == null)
178
+ return;
179
+
180
+ elementCount++;
181
+ var displayed = ancestorsDisplayed && element.resolvedStyle.display != DisplayStyle.None;
182
+ Add(ref hash, element.GetType().FullName);
183
+ Add(ref hash, element.name);
184
+ Add(ref hash, displayed ? "displayed" : "not-displayed");
185
+ Add(ref hash, element.hierarchy.childCount.ToString(CultureInfo.InvariantCulture));
186
+ if (element is TextElement textElement)
187
+ Add(ref hash, textElement.text);
188
+
189
+ if (displayed)
190
+ {
191
+ renderedCount++;
192
+ AddRect(ref hash, element.layout, ref invalidCount);
193
+ AddRect(ref hash, element.worldBound, ref invalidCount);
194
+ Add(ref hash, element.resolvedStyle.visibility.ToString());
195
+ }
196
+
197
+ foreach (var child in element.hierarchy.Children())
198
+ Visit(child, displayed, ref hash, ref elementCount, ref renderedCount, ref invalidCount);
199
+ }
200
+
201
+ private static void AddRect(ref ulong hash, Rect rect, ref int invalidCount)
202
+ {
203
+ if (!UiValue.IsFinite(rect.x) || !UiValue.IsFinite(rect.y) ||
204
+ !UiValue.IsFinite(rect.width) || !UiValue.IsFinite(rect.height))
205
+ {
206
+ invalidCount++;
207
+ Add(ref hash, "<non-finite>");
208
+ return;
209
+ }
210
+
211
+ Add(ref hash, Math.Round(rect.x, 3).ToString(CultureInfo.InvariantCulture));
212
+ Add(ref hash, Math.Round(rect.y, 3).ToString(CultureInfo.InvariantCulture));
213
+ Add(ref hash, Math.Round(rect.width, 3).ToString(CultureInfo.InvariantCulture));
214
+ Add(ref hash, Math.Round(rect.height, 3).ToString(CultureInfo.InvariantCulture));
215
+ }
216
+
217
+ private static void Add(ref ulong hash, string value)
218
+ {
219
+ value ??= "<null>";
220
+ for (var i = 0; i < value.Length; i++)
221
+ {
222
+ hash ^= value[i];
223
+ hash *= 1099511628211UL;
224
+ }
225
+
226
+ hash ^= 0xff;
227
+ hash *= 1099511628211UL;
228
+ }
229
+ }
230
+
231
+ internal sealed class UiCaptureSample
232
+ {
233
+ internal byte[] Png { get; set; }
234
+ internal string PixelHash { get; set; }
235
+ internal int Width { get; set; }
236
+ internal int Height { get; set; }
237
+ internal int SampledDistinctColors { get; set; }
238
+ internal int NonTransparentPixels { get; set; }
239
+ }
240
+
241
+ internal sealed class UiCaptureSurface : IDisposable
242
+ {
243
+ private RenderTexture _texture;
244
+
245
+ internal UiCaptureSurface(int width, int height)
246
+ {
247
+ _texture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB)
248
+ {
249
+ name = "UCP UI Capture",
250
+ antiAliasing = 1,
251
+ hideFlags = HideFlags.HideAndDontSave
252
+ };
253
+ _texture.Create();
254
+ }
255
+
256
+ internal UiCaptureSample Capture(UiHostWindow window)
257
+ {
258
+ if (window == null || _texture == null || !_texture.IsCreated())
259
+ throw new InvalidOperationException("The UI capture surface is unavailable");
260
+
261
+ _texture.DiscardContents();
262
+ if (!InternalEditorUtility.CaptureEditorWindow(window, _texture))
263
+ throw new InvalidOperationException("Unity could not capture the UI harness window");
264
+
265
+ var previous = RenderTexture.active;
266
+ Texture2D texture = null;
267
+ try
268
+ {
269
+ RenderTexture.active = _texture;
270
+ texture = new Texture2D(_texture.width, _texture.height, TextureFormat.RGBA32, false, false)
271
+ {
272
+ hideFlags = HideFlags.HideAndDontSave
273
+ };
274
+ texture.ReadPixels(new Rect(0f, 0f, _texture.width, _texture.height), 0, 0, false);
275
+ texture.Apply(false, false);
276
+
277
+ var pixels = texture.GetPixels32();
278
+ var distinct = new HashSet<uint>();
279
+ var nonTransparent = 0;
280
+ var stride = Mathf.Max(1, pixels.Length / 8192);
281
+ for (var i = 0; i < pixels.Length; i++)
282
+ {
283
+ var pixel = pixels[i];
284
+ if (pixel.a > 0)
285
+ nonTransparent++;
286
+ if (i % stride == 0)
287
+ {
288
+ distinct.Add((uint)(pixel.r << 24 | pixel.g << 16 | pixel.b << 8 | pixel.a));
289
+ }
290
+ }
291
+
292
+ var png = texture.EncodeToPNG();
293
+ return new UiCaptureSample
294
+ {
295
+ Png = png,
296
+ PixelHash = Sha256(png),
297
+ Width = _texture.width,
298
+ Height = _texture.height,
299
+ SampledDistinctColors = distinct.Count,
300
+ NonTransparentPixels = nonTransparent
301
+ };
302
+ }
303
+ finally
304
+ {
305
+ RenderTexture.active = previous;
306
+ if (texture != null)
307
+ UnityEngine.Object.DestroyImmediate(texture);
308
+ }
309
+ }
310
+
311
+ public void Dispose()
312
+ {
313
+ var texture = _texture;
314
+ _texture = null;
315
+ if (texture == null)
316
+ return;
317
+
318
+ var error = UiCleanup.RunAll(
319
+ () =>
320
+ {
321
+ if (RenderTexture.active == texture)
322
+ RenderTexture.active = null;
323
+ },
324
+ () => texture.Release(),
325
+ () =>
326
+ {
327
+ if (texture != null)
328
+ UnityEngine.Object.DestroyImmediate(texture);
329
+ });
330
+ if (error != null)
331
+ throw error;
332
+ }
333
+
334
+ private static string Sha256(byte[] value)
335
+ {
336
+ using (var sha = SHA256.Create())
337
+ {
338
+ var bytes = sha.ComputeHash(value);
339
+ var result = new StringBuilder(bytes.Length * 2);
340
+ foreach (var item in bytes)
341
+ result.Append(item.ToString("x2", CultureInfo.InvariantCulture));
342
+ return result.ToString();
343
+ }
344
+ }
345
+ }
346
+
347
+ internal static class UiHostCapabilities
348
+ {
349
+ internal static void EnsureAvailable(string operation)
350
+ {
351
+ if (Application.isBatchMode)
352
+ {
353
+ throw new UiOperationException(
354
+ "capability_unavailable",
355
+ $"ui/{operation} requires an interactive Unity Editor; batch mode supports ui/lint only",
356
+ Describe());
357
+ }
358
+
359
+ if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null)
360
+ {
361
+ throw new UiOperationException(
362
+ "capability_unavailable",
363
+ $"ui/{operation} requires a graphics device; Unity is using the Null graphics device",
364
+ Describe());
365
+ }
366
+ }
367
+
368
+ internal static Dictionary<string, object> Describe()
369
+ {
370
+ return new Dictionary<string, object>
371
+ {
372
+ ["batchMode"] = Application.isBatchMode,
373
+ ["graphicsDeviceType"] = SystemInfo.graphicsDeviceType.ToString(),
374
+ ["graphicsDeviceName"] = SystemInfo.graphicsDeviceName,
375
+ ["pixelsPerPoint"] = UiValue.FiniteOrNull(EditorGUIUtility.pixelsPerPoint),
376
+ ["editorHost"] = !Application.isBatchMode && SystemInfo.graphicsDeviceType != GraphicsDeviceType.Null,
377
+ ["lint"] = true,
378
+ ["layout"] = !Application.isBatchMode && SystemInfo.graphicsDeviceType != GraphicsDeviceType.Null,
379
+ ["screenshot"] = !Application.isBatchMode && SystemInfo.graphicsDeviceType != GraphicsDeviceType.Null
380
+ };
381
+ }
382
+ }
383
+
384
+ internal static class UiCleanup
385
+ {
386
+ internal static Exception RunAll(params Action[] actions)
387
+ {
388
+ Exception firstError = null;
389
+ if (actions == null)
390
+ return null;
391
+
392
+ foreach (var action in actions)
393
+ {
394
+ if (action == null)
395
+ continue;
396
+ try
397
+ {
398
+ action();
399
+ }
400
+ catch (Exception exception)
401
+ {
402
+ firstError ??= exception;
403
+ }
404
+ }
405
+ return firstError;
406
+ }
407
+ }
408
+
409
+ internal sealed class UiOperationException : Exception
410
+ {
411
+ internal UiOperationException(string code, string message, object details = null)
412
+ : base(message)
413
+ {
414
+ Code = code;
415
+ Details = details;
416
+ }
417
+
418
+ internal string Code { get; }
419
+ internal object Details { get; }
420
+ }
421
+
422
+ internal static class UiValue
423
+ {
424
+ internal static bool IsFinite(float value)
425
+ {
426
+ return !float.IsNaN(value) && !float.IsInfinity(value);
427
+ }
428
+
429
+ internal static bool IsFinite(double value)
430
+ {
431
+ return !double.IsNaN(value) && !double.IsInfinity(value);
432
+ }
433
+
434
+ internal static object FiniteOrNull(float value)
435
+ {
436
+ return IsFinite(value) ? (object)value : null;
437
+ }
438
+
439
+ internal static object FiniteOrNull(double value)
440
+ {
441
+ return IsFinite(value) ? (object)value : null;
442
+ }
443
+
444
+ internal static string NormalizePath(string path)
445
+ {
446
+ return path?.Replace('\\', '/');
447
+ }
448
+
449
+ internal static string SafeFileName(string value)
450
+ {
451
+ if (string.IsNullOrWhiteSpace(value))
452
+ return "ui";
453
+
454
+ var invalid = Path.GetInvalidFileNameChars();
455
+ var builder = new StringBuilder(value.Length);
456
+ foreach (var character in value)
457
+ {
458
+ builder.Append(Array.IndexOf(invalid, character) >= 0 || character == '/' || character == '\\'
459
+ ? '-'
460
+ : character);
461
+ }
462
+
463
+ var result = builder.ToString().Trim('.', ' ', '-');
464
+ return string.IsNullOrEmpty(result) ? "ui" : result;
465
+ }
466
+ }
467
+ }
468
+ #endif
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 3a6968eabe884a34afea71fd6c099669
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant: