@volter/editor-game 0.5.65

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 (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,535 @@
1
+ /**
2
+ * Runtime State Watch — the standard debugger tree/watch surface over the
3
+ * game's native `DebugAdapter` providers.
4
+ *
5
+ * It deliberately authors nothing: providers stay game-owned, values are read
6
+ * through the same adapter `@volter/editor-live` uses, and pins are per-user editor
7
+ * convenience in the host's project-local document
8
+ * (`editorHost().projectLocalState`), not a project sidecar or a new format.
9
+ */
10
+
11
+ import { getActiveDebug } from '@volter/editor-core/authoring/active-systems';
12
+ import { editorHost } from '@volter/editor-sdk/host';
13
+ import { Button, DisclosureIcon, TextInput, themeVars } from '@volter/editor-sdk/widgets';
14
+ import type { DebugAdapter } from '@volter/editor-project/adapter/system-adapter';
15
+ import { useCallback, useEffect, useMemo, useState } from 'react';
16
+
17
+ const POLL_MS = 250;
18
+ const MAX_VISIBLE_CHILDREN = 200;
19
+ const MAX_FILTERED_LEAVES = 500;
20
+ /** The section of the project-local document this panel owns. */
21
+ const SECTION = 'stateWatchPins';
22
+
23
+ interface WatchPin {
24
+ provider: string;
25
+ path: string[];
26
+ }
27
+
28
+ interface WatchLeaf extends WatchPin {
29
+ value: unknown;
30
+ }
31
+
32
+ interface ResolvedWatchValue {
33
+ available: boolean;
34
+ value: unknown;
35
+ }
36
+
37
+ function pinKey(pin: WatchPin): string {
38
+ return JSON.stringify([pin.provider, ...pin.path]);
39
+ }
40
+
41
+ function pathLabel(pin: WatchPin): string {
42
+ return [pin.provider, ...pin.path].join('.');
43
+ }
44
+
45
+ function projectId(): string {
46
+ return editorHost().projectLocalState.projectRootPath() ?? '__unscoped__';
47
+ }
48
+
49
+ function readPins(_id: string): WatchPin[] {
50
+ const pins = editorHost().projectLocalState.read<WatchPin[]>(SECTION);
51
+ if (!Array.isArray(pins)) return [];
52
+ return pins.filter(
53
+ (pin) =>
54
+ typeof pin?.provider === 'string' &&
55
+ Array.isArray(pin.path) &&
56
+ pin.path.every((part) => typeof part === 'string'),
57
+ );
58
+ }
59
+
60
+ function writePins(_id: string, pins: readonly WatchPin[]): void {
61
+ editorHost().projectLocalState.write(
62
+ SECTION,
63
+ pins.map((pin) => ({ provider: pin.provider, path: [...pin.path] })),
64
+ );
65
+ }
66
+
67
+ function isBranch(value: unknown): value is object {
68
+ return value !== null && typeof value === 'object';
69
+ }
70
+
71
+ function formatValue(value: unknown): string {
72
+ if (typeof value === 'string') return `“${value}”`;
73
+ if (typeof value === 'number') return Number.isInteger(value) ? String(value) : value.toFixed(3);
74
+ if (value === undefined) return 'undefined';
75
+ if (value === null) return 'null';
76
+ if (typeof value === 'bigint') return `${value}n`;
77
+ if (typeof value === 'function') return '[Function]';
78
+ if (isBranch(value)) {
79
+ try {
80
+ return Array.isArray(value)
81
+ ? `Array(${value.length})`
82
+ : `Object(${Object.keys(value).length})`;
83
+ } catch {
84
+ return '[Unreadable object]';
85
+ }
86
+ }
87
+ return String(value);
88
+ }
89
+
90
+ function entriesOf(value: object): [string, unknown][] {
91
+ try {
92
+ return Object.entries(value as Record<string, unknown>);
93
+ } catch {
94
+ return [];
95
+ }
96
+ }
97
+
98
+ function valueAt(snapshot: Record<string, unknown>, pin: WatchPin): ResolvedWatchValue {
99
+ try {
100
+ if (!Object.hasOwn(snapshot, pin.provider)) return { available: false, value: undefined };
101
+ let value = snapshot[pin.provider];
102
+ for (const part of pin.path) {
103
+ if (!isBranch(value) || !Object.hasOwn(value, part)) {
104
+ return { available: false, value: undefined };
105
+ }
106
+ value = (value as Record<string, unknown>)[part];
107
+ }
108
+ return { available: true, value };
109
+ } catch {
110
+ return { available: false, value: undefined };
111
+ }
112
+ }
113
+
114
+ function collectLeaves(
115
+ provider: string,
116
+ value: unknown,
117
+ path: string[],
118
+ query: string,
119
+ output: WatchLeaf[],
120
+ ancestors: readonly object[],
121
+ ): void {
122
+ if (output.length >= MAX_FILTERED_LEAVES) return;
123
+ const circular = isBranch(value) && ancestors.includes(value);
124
+ if (!isBranch(value) || circular) {
125
+ const leaf = { provider, path, value: circular ? '[Circular]' : value };
126
+ if (
127
+ pathLabel(leaf).toLowerCase().includes(query) ||
128
+ formatValue(leaf.value).toLowerCase().includes(query)
129
+ ) {
130
+ output.push(leaf);
131
+ }
132
+ return;
133
+ }
134
+ const nextAncestors = [...ancestors, value];
135
+ const entries = entriesOf(value);
136
+ if (entries.length === 0) {
137
+ const leaf = { provider, path, value };
138
+ if (pathLabel(leaf).toLowerCase().includes(query)) output.push(leaf);
139
+ return;
140
+ }
141
+ for (const [name, child] of entries) {
142
+ collectLeaves(provider, child, [...path, name], query, output, nextAncestors);
143
+ if (output.length >= MAX_FILTERED_LEAVES) return;
144
+ }
145
+ }
146
+
147
+ const MONO: React.CSSProperties = {
148
+ fontFamily: themeVars.typography.mono,
149
+ fontSize: 11,
150
+ };
151
+
152
+ function LeafRow({
153
+ leaf,
154
+ pinned,
155
+ onTogglePin,
156
+ depth = 0,
157
+ }: {
158
+ leaf: WatchLeaf;
159
+ pinned: boolean;
160
+ onTogglePin: (pin: WatchPin) => void;
161
+ depth?: number;
162
+ }) {
163
+ const label = leaf.path.at(-1) ?? leaf.provider;
164
+ return (
165
+ <div
166
+ style={{
167
+ ...MONO,
168
+ minHeight: 22,
169
+ display: 'grid',
170
+ gridTemplateColumns: 'minmax(100px, 0.65fr) minmax(120px, 1fr) auto',
171
+ alignItems: 'center',
172
+ gap: 8,
173
+ paddingLeft: 8 + depth * 14,
174
+ paddingRight: 8,
175
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
176
+ }}
177
+ title={pathLabel(leaf)}
178
+ >
179
+ <span
180
+ style={{ color: themeVars.content.primary, overflow: 'hidden', textOverflow: 'ellipsis' }}
181
+ >
182
+ {label}
183
+ </span>
184
+ <span
185
+ style={{
186
+ color: themeVars.content.muted,
187
+ overflow: 'hidden',
188
+ textOverflow: 'ellipsis',
189
+ whiteSpace: 'nowrap',
190
+ }}
191
+ >
192
+ {formatValue(leaf.value)}
193
+ </span>
194
+ <Button
195
+ type="button"
196
+ size="compact"
197
+ variant={pinned ? 'primary' : 'secondary'}
198
+ aria-pressed={pinned}
199
+ title={pinned ? `Remove ${pathLabel(leaf)} from watches` : `Watch ${pathLabel(leaf)}`}
200
+ onClick={() => onTogglePin(leaf)}
201
+ >
202
+ {pinned ? 'Pinned' : 'Pin'}
203
+ </Button>
204
+ </div>
205
+ );
206
+ }
207
+
208
+ function StateTreeNode({
209
+ provider,
210
+ name,
211
+ value,
212
+ path,
213
+ depth,
214
+ ancestors,
215
+ pinnedKeys,
216
+ onTogglePin,
217
+ }: {
218
+ provider: string;
219
+ name: string;
220
+ value: unknown;
221
+ path: string[];
222
+ depth: number;
223
+ ancestors: readonly object[];
224
+ pinnedKeys: ReadonlySet<string>;
225
+ onTogglePin: (pin: WatchPin) => void;
226
+ }) {
227
+ const circular = isBranch(value) && ancestors.includes(value);
228
+ const [expanded, setExpanded] = useState(depth === 0);
229
+ if (!isBranch(value) || circular) {
230
+ const leaf = { provider, path, value: circular ? '[Circular]' : value };
231
+ return (
232
+ <LeafRow
233
+ leaf={leaf}
234
+ depth={depth}
235
+ pinned={pinnedKeys.has(pinKey(leaf))}
236
+ onTogglePin={onTogglePin}
237
+ />
238
+ );
239
+ }
240
+
241
+ const entries = entriesOf(value);
242
+ const visible = entries.slice(0, MAX_VISIBLE_CHILDREN);
243
+ return (
244
+ <div>
245
+ <button
246
+ type="button"
247
+ onClick={() => setExpanded((current) => !current)}
248
+ style={{
249
+ ...MONO,
250
+ width: '100%',
251
+ minHeight: 22,
252
+ display: 'flex',
253
+ alignItems: 'center',
254
+ gap: 4,
255
+ padding: `0 8px 0 ${8 + depth * 14}px`,
256
+ color: themeVars.content.primary,
257
+ background: 'transparent',
258
+ border: 0,
259
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
260
+ textAlign: 'left',
261
+ cursor: 'pointer',
262
+ }}
263
+ >
264
+ <span style={{ width: 12, color: themeVars.content.muted }}>
265
+ <DisclosureIcon direction={expanded ? 'down' : 'right'} />
266
+ </span>
267
+ <span>{name}</span>
268
+ <span style={{ color: themeVars.content.dim }}>
269
+ {Array.isArray(value) ? `[${entries.length}]` : `{${entries.length}}`}
270
+ </span>
271
+ </button>
272
+ {expanded &&
273
+ visible.map(([childName, child]) => (
274
+ <StateTreeNode
275
+ key={childName}
276
+ provider={provider}
277
+ name={childName}
278
+ value={child}
279
+ path={[...path, childName]}
280
+ depth={depth + 1}
281
+ ancestors={[...ancestors, value]}
282
+ pinnedKeys={pinnedKeys}
283
+ onTogglePin={onTogglePin}
284
+ />
285
+ ))}
286
+ {expanded && entries.length > visible.length && (
287
+ <div
288
+ style={{
289
+ ...MONO,
290
+ padding: `3px 8px 3px ${8 + (depth + 1) * 14}px`,
291
+ color: themeVars.content.dim,
292
+ }}
293
+ >
294
+ {entries.length - visible.length} more values — narrow with search
295
+ </div>
296
+ )}
297
+ </div>
298
+ );
299
+ }
300
+
301
+ function readSnapshot(adapter: DebugAdapter | null | undefined): {
302
+ providers: ReturnType<DebugAdapter['providers']>;
303
+ state: Record<string, unknown>;
304
+ error: string | null;
305
+ } {
306
+ if (!adapter) return { providers: [], state: {}, error: null };
307
+ let providers: ReturnType<DebugAdapter['providers']>;
308
+ try {
309
+ providers = adapter.providers();
310
+ } catch (reason) {
311
+ return {
312
+ providers: [],
313
+ state: {},
314
+ error: reason instanceof Error ? reason.message : String(reason),
315
+ };
316
+ }
317
+ try {
318
+ return { providers, state: adapter.stateAll(), error: null };
319
+ } catch (reason) {
320
+ return {
321
+ providers,
322
+ state: {},
323
+ error: reason instanceof Error ? reason.message : String(reason),
324
+ };
325
+ }
326
+ }
327
+
328
+ export function StateWatchPanel() {
329
+ const scopedProject = projectId();
330
+ const [snapshot, setSnapshot] = useState(() => readSnapshot(getActiveDebug()));
331
+ const [paused, setPaused] = useState(false);
332
+ const [filter, setFilter] = useState('');
333
+ const [pins, setPins] = useState<WatchPin[]>(() => readPins(scopedProject));
334
+
335
+ useEffect(() => setPins(readPins(scopedProject)), [scopedProject]);
336
+
337
+ const refresh = useCallback(() => setSnapshot(readSnapshot(getActiveDebug())), []);
338
+ useEffect(() => {
339
+ refresh();
340
+ if (paused) return;
341
+ const interval = window.setInterval(refresh, POLL_MS);
342
+ return () => window.clearInterval(interval);
343
+ }, [paused, refresh]);
344
+
345
+ const pinnedKeys = useMemo(() => new Set(pins.map(pinKey)), [pins]);
346
+ const togglePin = useCallback(
347
+ (pin: WatchPin) => {
348
+ setPins((current) => {
349
+ const key = pinKey(pin);
350
+ const next = current.some((item) => pinKey(item) === key)
351
+ ? current.filter((item) => pinKey(item) !== key)
352
+ : [...current, { provider: pin.provider, path: [...pin.path] }];
353
+ writePins(scopedProject, next);
354
+ return next;
355
+ });
356
+ },
357
+ [scopedProject],
358
+ );
359
+
360
+ const filteredLeaves = useMemo(() => {
361
+ const query = filter.trim().toLowerCase();
362
+ if (!query) return [];
363
+ const matches: WatchLeaf[] = [];
364
+ for (const provider of snapshot.providers) {
365
+ collectLeaves(provider.name, snapshot.state[provider.name], [], query, matches, []);
366
+ }
367
+ return matches;
368
+ }, [filter, snapshot]);
369
+
370
+ const adapter = getActiveDebug();
371
+ if (!adapter) {
372
+ return (
373
+ <div style={{ padding: 12, color: themeVars.content.muted, fontSize: 11 }}>
374
+ State Watch is available while a game with debug providers is running.
375
+ </div>
376
+ );
377
+ }
378
+
379
+ return (
380
+ <div style={{ height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }}>
381
+ <div
382
+ style={{
383
+ minHeight: 34,
384
+ display: 'flex',
385
+ alignItems: 'center',
386
+ gap: 8,
387
+ padding: '4px 8px',
388
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
389
+ }}
390
+ >
391
+ <TextInput
392
+ value={filter}
393
+ onChange={(event) => setFilter(event.target.value)}
394
+ placeholder="Filter state paths or values…"
395
+ aria-label="Filter state watches"
396
+ style={{ width: 240 }}
397
+ />
398
+ <Button type="button" size="compact" onClick={() => setPaused((value) => !value)}>
399
+ {paused ? 'Resume' : 'Pause'}
400
+ </Button>
401
+ <Button type="button" size="compact" disabled={!paused} onClick={refresh}>
402
+ Refresh
403
+ </Button>
404
+ <span style={{ color: themeVars.content.dim, fontSize: 10 }}>
405
+ {snapshot.providers.length} provider{snapshot.providers.length === 1 ? '' : 's'} ·{' '}
406
+ {pins.length} pinned{paused ? ' · paused' : ''}
407
+ </span>
408
+ {snapshot.error && (
409
+ <span style={{ color: themeVars.semantic.danger, fontSize: 10 }}>{snapshot.error}</span>
410
+ )}
411
+ </div>
412
+
413
+ <div
414
+ style={{
415
+ flex: 1,
416
+ minHeight: 0,
417
+ display: 'grid',
418
+ gridTemplateColumns: 'minmax(280px, 1fr) minmax(240px, 0.65fr)',
419
+ }}
420
+ >
421
+ <section aria-label="Live state" style={{ minWidth: 0, overflow: 'auto' }}>
422
+ {filter.trim() ? (
423
+ filteredLeaves.length > 0 ? (
424
+ filteredLeaves.map((leaf) => (
425
+ <LeafRow
426
+ key={pinKey(leaf)}
427
+ leaf={leaf}
428
+ pinned={pinnedKeys.has(pinKey(leaf))}
429
+ onTogglePin={togglePin}
430
+ />
431
+ ))
432
+ ) : (
433
+ <div style={{ padding: 10, color: themeVars.content.dim, fontSize: 11 }}>
434
+ No state values match “{filter.trim()}”.
435
+ </div>
436
+ )
437
+ ) : (
438
+ snapshot.providers.map((provider) => (
439
+ <StateTreeNode
440
+ key={provider.name}
441
+ provider={provider.name}
442
+ name={`${provider.name} · ${provider.tier}`}
443
+ value={snapshot.state[provider.name]}
444
+ path={[]}
445
+ depth={0}
446
+ ancestors={[]}
447
+ pinnedKeys={pinnedKeys}
448
+ onTogglePin={togglePin}
449
+ />
450
+ ))
451
+ )}
452
+ {snapshot.providers.length === 0 && (
453
+ <div style={{ padding: 10, color: themeVars.content.dim, fontSize: 11 }}>
454
+ The running game has not registered any state providers.
455
+ </div>
456
+ )}
457
+ </section>
458
+
459
+ <aside
460
+ aria-label="Pinned watches"
461
+ style={{
462
+ minWidth: 0,
463
+ overflow: 'auto',
464
+ borderLeft: `1px solid ${themeVars.boundary.default}`,
465
+ }}
466
+ >
467
+ <div
468
+ style={{
469
+ position: 'sticky',
470
+ top: 0,
471
+ zIndex: 1,
472
+ padding: '6px 8px',
473
+ color: themeVars.content.muted,
474
+ background: themeVars.surface.panel,
475
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
476
+ fontSize: 10,
477
+ fontWeight: 600,
478
+ textTransform: 'uppercase',
479
+ letterSpacing: '0.04em',
480
+ }}
481
+ >
482
+ Watches
483
+ </div>
484
+ {pins.map((pin) => {
485
+ const resolved = valueAt(snapshot.state, pin);
486
+ return (
487
+ <div
488
+ key={pinKey(pin)}
489
+ style={{
490
+ ...MONO,
491
+ display: 'grid',
492
+ gridTemplateColumns: 'minmax(130px, 1fr) minmax(100px, 0.7fr) auto',
493
+ alignItems: 'center',
494
+ gap: 8,
495
+ minHeight: 24,
496
+ padding: '2px 8px',
497
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
498
+ }}
499
+ >
500
+ <span
501
+ title={pathLabel(pin)}
502
+ style={{
503
+ color: themeVars.content.primary,
504
+ overflow: 'hidden',
505
+ textOverflow: 'ellipsis',
506
+ }}
507
+ >
508
+ {pathLabel(pin)}
509
+ </span>
510
+ <span
511
+ style={{
512
+ color: resolved.available ? themeVars.content.muted : themeVars.content.dim,
513
+ overflow: 'hidden',
514
+ textOverflow: 'ellipsis',
515
+ whiteSpace: 'nowrap',
516
+ }}
517
+ >
518
+ {resolved.available ? formatValue(resolved.value) : 'unavailable'}
519
+ </span>
520
+ <Button type="button" size="compact" onClick={() => togglePin(pin)}>
521
+ Remove
522
+ </Button>
523
+ </div>
524
+ );
525
+ })}
526
+ {pins.length === 0 && (
527
+ <div style={{ padding: 10, color: themeVars.content.dim, fontSize: 11 }}>
528
+ Pin values from the live tree to keep them visible while you inspect the game.
529
+ </div>
530
+ )}
531
+ </aside>
532
+ </div>
533
+ </div>
534
+ );
535
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Runtime Camera Inspector contribution.
3
+ *
4
+ * Camera/lens/transform authoring is already the ordinary native-camera
5
+ * Inspector. This additional section appears only in Play when the game has
6
+ * explicitly registered `SystemAdapters.camera`, and projects the controller's
7
+ * own active-camera / priority / ownership / blend facts. It is deliberately
8
+ * read-only: follow, aim, collision and arbitration remain project TS/TSX, and
9
+ * the editor creates no camera graph or sidecar.
10
+ */
11
+
12
+ import { getActiveCamera } from '@volter/editor-core/authoring/active-systems';
13
+ import type { InspectorSectionProps } from '@volter/editor-core/inspector-section-registry';
14
+ import type {
15
+ AuthoringAdapter,
16
+ CameraAdapter,
17
+ CameraRuntimeCamera,
18
+ CameraRuntimeSnapshot,
19
+ EditorNode,
20
+ } from '@volter/editor-project/adapter';
21
+ import { EditorBanner, FieldGroup, FieldRow, Text } from '@volter/editor-sdk/widgets';
22
+ import { useEffect, useReducer } from 'react';
23
+ import type * as THREE from 'three';
24
+
25
+ function selectedNativeCamera(
26
+ adapter: AuthoringAdapter,
27
+ nodeId: string | null,
28
+ ): THREE.Camera | null {
29
+ const object = nodeId ? adapter.hierarchy.object3D?.(nodeId) : null;
30
+ if (!object) return null;
31
+ if ((object as THREE.Camera).isCamera) return object as THREE.Camera;
32
+ const owned = object.userData['_camera'] as THREE.Camera | undefined;
33
+ return owned?.isCamera ? owned : null;
34
+ }
35
+
36
+ export function matches(node: EditorNode | null, adapter: AuthoringAdapter): boolean {
37
+ return !!getActiveCamera() && !!selectedNativeCamera(adapter, node?.id ?? null);
38
+ }
39
+
40
+ function matchingRuntimeCamera(
41
+ selected: NonNullable<ReturnType<typeof selectedNativeCamera>>,
42
+ snapshot: CameraRuntimeSnapshot,
43
+ ): CameraRuntimeCamera | null {
44
+ return (
45
+ snapshot.cameras.find((camera) => camera.nativeId === selected.uuid) ??
46
+ snapshot.cameras.find(
47
+ (camera) => camera.id === selected.name || camera.label === selected.name,
48
+ ) ??
49
+ null
50
+ );
51
+ }
52
+
53
+ function useCameraSnapshot(camera: CameraAdapter | null): CameraRuntimeSnapshot | null {
54
+ const [, invalidate] = useReducer((value: number) => value + 1, 0);
55
+ useEffect(() => {
56
+ if (!camera) return;
57
+ return camera.subscribe(invalidate);
58
+ }, [camera]);
59
+ return camera?.snapshot() ?? null;
60
+ }
61
+
62
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one compact projection conditionally renders only facts the adapter actually supplied.
63
+ export function CameraRuntimeSection({ adapter, nodeId }: InspectorSectionProps) {
64
+ const camera = getActiveCamera();
65
+ const snapshot = useCameraSnapshot(camera);
66
+ // The contribution matched a registered adapter. A late unregistration can
67
+ // happen between composition and projection on Stop; render nothing for that
68
+ // one reconciliation instead of retaining stale runtime facts.
69
+ if (!snapshot) return null;
70
+ const selected = selectedNativeCamera(adapter, nodeId);
71
+ if (!selected) return null;
72
+ const runtimeCamera = matchingRuntimeCamera(selected, snapshot);
73
+ const isActive = runtimeCamera?.id === snapshot.activeCameraId;
74
+ const transition = snapshot.transition;
75
+ const remaining = transition
76
+ ? Math.max(0, transition.durationSeconds - transition.elapsedSeconds)
77
+ : null;
78
+
79
+ return (
80
+ <div style={{ display: 'grid', gap: 8 }}>
81
+ {!runtimeCamera ? (
82
+ <EditorBanner tone="info">
83
+ This native camera is not registered with the active runtime controller.
84
+ </EditorBanner>
85
+ ) : null}
86
+ <FieldGroup>
87
+ <FieldRow label="Role">
88
+ <Text tone={isActive ? 'success' : 'muted'}>
89
+ {isActive ? 'Active camera' : runtimeCamera ? 'Registered camera' : 'Scene camera'}
90
+ </Text>
91
+ </FieldRow>
92
+ {runtimeCamera ? (
93
+ <FieldRow label="Runtime ID">
94
+ <Text tone="muted">{runtimeCamera.id}</Text>
95
+ </FieldRow>
96
+ ) : null}
97
+ {runtimeCamera?.priority !== undefined ? (
98
+ <FieldRow label="Priority">
99
+ <Text tone="muted">{runtimeCamera.priority}</Text>
100
+ </FieldRow>
101
+ ) : null}
102
+ {runtimeCamera?.enabled !== undefined ? (
103
+ <FieldRow label="Enabled">
104
+ <Text tone={runtimeCamera.enabled ? 'success' : 'muted'}>
105
+ {runtimeCamera.enabled ? 'Yes' : 'No'}
106
+ </Text>
107
+ </FieldRow>
108
+ ) : null}
109
+ <FieldRow label="Active camera">
110
+ <Text tone="muted">{snapshot.activeCameraId ?? 'None'}</Text>
111
+ </FieldRow>
112
+ <FieldRow label="Controller">
113
+ <Text tone="muted">{snapshot.activeControllerId ?? 'Gameplay / unowned'}</Text>
114
+ </FieldRow>
115
+ <FieldRow label="Ownership depth">
116
+ <Text tone="muted">{snapshot.ownershipDepth}</Text>
117
+ </FieldRow>
118
+ {transition ? (
119
+ <>
120
+ <FieldRow label="Blend">
121
+ <Text tone="muted">
122
+ {`${transition.fromCameraId ?? 'current pose'} → ${transition.toCameraId}`}
123
+ </Text>
124
+ </FieldRow>
125
+ <FieldRow label="Progress">
126
+ <Text tone="muted">
127
+ {`${Math.round(transition.progress * 100)}% · ${remaining?.toFixed(2)}s left`}
128
+ </Text>
129
+ </FieldRow>
130
+ </>
131
+ ) : null}
132
+ </FieldGroup>
133
+ </div>
134
+ );
135
+ }