@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,457 @@
1
+ /**
2
+ * Audio debugger tab (W3c, F3) — the read-only introspection surface over
3
+ * `SystemAdapters.AudioAdapter`:
4
+ *
5
+ * - active-node GRAPH (live `graphSnapshot()` poll — engine buses + every
6
+ * Tone node the game routed into the master bus);
7
+ * - TRANSPORT strip (`transportState()`: state/position/bpm; `null` renders
8
+ * the honest "no musical transport in this world");
9
+ * - per-bus METERS (`acquireMeters()` handle, held only while this panel is
10
+ * mounted with a live adapter and ALWAYS disposed — level updates write
11
+ * straight to DOM style via refs on a 100 ms interval, so the node
12
+ * tree/log never re-render at meter rate);
13
+ * - EVENT LOG (`audioEvents` seq-fenced ring → `AudioEventLogModel`;
14
+ * pausable).
15
+ *
16
+ * Degradation ladder rendered honestly (W3a contract): no adapter means the
17
+ * editor has not mounted a project audio graph yet; an adapter
18
+ * missing an optional capability marks that section "not provided"; nothing
19
+ * is ever fabricated. Editor code never imports Tone here — only the
20
+ * `AudioAdapter` interface (the networking rule's audio mirror). The full
21
+ * MIXER (sends/effects/gain editing) is SQ-4-gated and deliberately absent.
22
+ */
23
+
24
+ import { editorHost } from '@volter/editor-sdk/host';
25
+ import { Button, fontSizeVar, spaceVar, themeVars } from '@volter/editor-sdk/widgets';
26
+ import { meterPercent } from '@volter/game-runtime/adapter/audio-meter';
27
+ import type { AudioAdapter, AudioDebugEvent, AudioMeterFrame } from '@volter/editor-project/adapter';
28
+ import { useEffect, useReducer, useRef, useState } from 'react';
29
+ import {
30
+ AudioEventLogModel,
31
+ deriveAudioCapabilities,
32
+ formatTransportSeconds,
33
+ } from './audio-debugger-model';
34
+
35
+ const MONO: React.CSSProperties = {
36
+ fontFamily: themeVars.typography.mono,
37
+ fontSize: fontSizeVar.base,
38
+ };
39
+
40
+ function AbsentNote({ children, testId }: { children: React.ReactNode; testId?: string }) {
41
+ return (
42
+ <div
43
+ data-testid={testId}
44
+ style={{
45
+ padding: spaceVar[4],
46
+ color: themeVars.content.muted,
47
+ fontSize: fontSizeVar.base,
48
+ fontStyle: 'italic',
49
+ }}
50
+ >
51
+ {children}
52
+ </div>
53
+ );
54
+ }
55
+
56
+ function ResumeAudioButton({
57
+ adapter,
58
+ onResumed,
59
+ }: {
60
+ adapter: AudioAdapter;
61
+ onResumed: () => void;
62
+ }) {
63
+ if (!adapter.resume) return null;
64
+ return (
65
+ <Button
66
+ type="button"
67
+ variant="secondary"
68
+ data-testid="audio-resume"
69
+ onClick={() => {
70
+ adapter.resume?.();
71
+ onResumed();
72
+ }}
73
+ >
74
+ Enable Audio
75
+ </Button>
76
+ );
77
+ }
78
+
79
+ // --- Transport strip --------------------------------------------------------
80
+
81
+ const TRANSPORT_COLORS: Record<string, string> = {
82
+ started: themeVars.semantic.success,
83
+ paused: themeVars.semantic.warning,
84
+ stopped: themeVars.content.muted,
85
+ };
86
+
87
+ function TransportStrip({ adapter }: { adapter: AudioAdapter }) {
88
+ const transport = adapter.transportState?.() ?? null;
89
+ if (!transport) {
90
+ return (
91
+ <AbsentNote testId="audio-transport-none">
92
+ No musical transport in this world (the game has not bridged Tone onto its audio context).
93
+ </AbsentNote>
94
+ );
95
+ }
96
+ return (
97
+ <div
98
+ data-testid="audio-transport"
99
+ data-state={transport.state}
100
+ style={{
101
+ display: 'flex',
102
+ gap: spaceVar[6],
103
+ alignItems: 'center',
104
+ padding: `${spaceVar[2]} ${spaceVar[4]}`,
105
+ }}
106
+ >
107
+ <span style={{ color: TRANSPORT_COLORS[transport.state] ?? themeVars.content.primary }}>
108
+ ● {transport.state}
109
+ </span>
110
+ <span
111
+ data-testid="audio-transport-seconds"
112
+ style={{ ...MONO, color: themeVars.content.primary }}
113
+ >
114
+ {formatTransportSeconds(transport.seconds)}
115
+ </span>
116
+ {transport.position !== undefined && (
117
+ <span style={{ ...MONO, color: themeVars.content.muted }}>{transport.position}</span>
118
+ )}
119
+ <span style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>
120
+ bpm{' '}
121
+ <span style={{ ...MONO, color: themeVars.content.primary }}>
122
+ {transport.bpm.toFixed(0)}
123
+ </span>
124
+ </span>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ // --- Meters -----------------------------------------------------------------
130
+
131
+ /**
132
+ * Owns one metering session for `adapter`'s lifetime in this panel: acquires
133
+ * the handle on mount / adapter change, disposes it on cleanup (Stop tears
134
+ * the whole panel state down through the adapter-identity poll, so no
135
+ * analyser tap survives a session). Level updates bypass React entirely —
136
+ * a 100 ms interval writes bar width/`data-level` straight onto the DOM.
137
+ */
138
+ function MeterBars({ adapter }: { adapter: AudioAdapter }) {
139
+ const [buses, setBuses] = useState<AudioMeterFrame[] | null | undefined>(undefined);
140
+ const fillRefs = useRef(new Map<string, HTMLDivElement>());
141
+
142
+ useEffect(() => {
143
+ const handle = adapter.acquireMeters?.() ?? null;
144
+ if (!handle) {
145
+ setBuses(null);
146
+ return;
147
+ }
148
+ setBuses(handle.read()); // bus ids/labels — the one React render
149
+ const id = setInterval(() => {
150
+ for (const frame of handle.read()) {
151
+ const el = fillRefs.current.get(frame.id);
152
+ if (!el) continue;
153
+ el.style.width = `${meterPercent(frame.level)}%`;
154
+ el.dataset['level'] = frame.level.toFixed(4);
155
+ }
156
+ }, 100);
157
+ return () => {
158
+ clearInterval(id);
159
+ handle.dispose();
160
+ };
161
+ }, [adapter]);
162
+
163
+ if (buses === undefined) return null; // first effect pass hasn't run yet
164
+ if (buses === null) {
165
+ return (
166
+ <AbsentNote testId="audio-meters-unavailable">
167
+ Metering unavailable in this environment (no real audio context).
168
+ </AbsentNote>
169
+ );
170
+ }
171
+ return (
172
+ <div
173
+ data-testid="audio-meters"
174
+ style={{
175
+ display: 'flex',
176
+ gap: 14,
177
+ alignItems: 'center',
178
+ padding: `${spaceVar[2]} ${spaceVar[4]}`,
179
+ }}
180
+ >
181
+ {buses.map((bus) => (
182
+ <div key={bus.id} style={{ display: 'flex', alignItems: 'center', gap: spaceVar[2] }}>
183
+ <span style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>
184
+ {bus.label}
185
+ </span>
186
+ <div
187
+ style={{
188
+ width: 96,
189
+ height: 8,
190
+ background: themeVars.surface.inset,
191
+ borderRadius: themeVars.shape.small,
192
+ overflow: 'hidden',
193
+ }}
194
+ >
195
+ <div
196
+ data-testid="audio-meter-fill"
197
+ data-meter-id={bus.id}
198
+ data-level="0.0000"
199
+ ref={(el) => {
200
+ if (el) fillRefs.current.set(bus.id, el);
201
+ else fillRefs.current.delete(bus.id);
202
+ }}
203
+ style={{
204
+ width: '0%',
205
+ height: '100%',
206
+ background: themeVars.semantic.success,
207
+ transition: 'width 80ms linear',
208
+ }}
209
+ />
210
+ </div>
211
+ </div>
212
+ ))}
213
+ </div>
214
+ );
215
+ }
216
+
217
+ // --- Node graph -------------------------------------------------------------
218
+
219
+ function NodeRow({
220
+ node,
221
+ }: {
222
+ node: { id: string; type: string; label?: string; outputs: string[]; state?: string };
223
+ }) {
224
+ return (
225
+ <div
226
+ data-testid="audio-node-row"
227
+ data-node-id={node.id}
228
+ style={{ ...MONO, lineHeight: '18px', padding: `0 ${spaceVar[4]}`, whiteSpace: 'nowrap' }}
229
+ >
230
+ <span style={{ color: themeVars.content.primary }}>{node.label ?? node.id}</span>
231
+ <span style={{ color: themeVars.content.dim }}> {node.type}</span>
232
+ {node.state !== undefined && (
233
+ <span style={{ color: themeVars.semantic.warning }}> [{node.state}]</span>
234
+ )}
235
+ {node.outputs.length > 0 && (
236
+ <span style={{ color: themeVars.content.muted }}> → {node.outputs.join(', ')}</span>
237
+ )}
238
+ </div>
239
+ );
240
+ }
241
+
242
+ // --- Event log --------------------------------------------------------------
243
+
244
+ function eventColor(kind: AudioDebugEvent['kind']): string {
245
+ if (kind === 'error') return themeVars.semantic.danger;
246
+ if (kind === 'mute' || kind === 'unmute') return themeVars.semantic.warning;
247
+ return themeVars.accent.default;
248
+ }
249
+
250
+ function EventRow({ event }: { event: AudioDebugEvent }) {
251
+ return (
252
+ <div
253
+ data-testid="audio-log-row"
254
+ data-kind={event.kind}
255
+ style={{
256
+ ...MONO,
257
+ display: 'grid',
258
+ gridTemplateColumns: '64px 1fr',
259
+ gap: spaceVar[2],
260
+ lineHeight: '17px',
261
+ padding: `0 ${spaceVar[4]}`,
262
+ }}
263
+ >
264
+ <span style={{ color: themeVars.content.dim }}>{(event.time / 1000).toFixed(2)}s</span>
265
+ <span style={{ color: eventColor(event.kind) }}>
266
+ {event.kind}
267
+ {event.detail !== undefined ? (
268
+ <span style={{ color: themeVars.content.muted }}> {event.detail}</span>
269
+ ) : null}
270
+ </span>
271
+ </div>
272
+ );
273
+ }
274
+
275
+ // --- The panel --------------------------------------------------------------
276
+
277
+ export function AudioDebuggerPanel() {
278
+ const [, force] = useReducer((c: number) => c + 1, 0);
279
+ const logRef = useRef(new AudioEventLogModel());
280
+ const adapterRef = useRef<AudioAdapter | null>(null);
281
+ const [paused, setPaused] = useState(false);
282
+
283
+ useEffect(() => {
284
+ const syncAdapter = () => {
285
+ const adapter = editorHost().systems.inspectedAudio() ?? null;
286
+ if (adapter !== adapterRef.current) {
287
+ // Session started/stopped (or a different game mounted): the old
288
+ // session's event log is stale, and MeterBars' `adapter`-keyed effect
289
+ // re-acquires/disposes its handle off this same identity change.
290
+ adapterRef.current = adapter;
291
+ logRef.current = new AudioEventLogModel();
292
+ logRef.current.paused = paused;
293
+ }
294
+ return adapter;
295
+ };
296
+ // 250 ms consume/render poll — the NetworkInspectorPanel idiom; also how
297
+ // the panel notices play-mode start/stop (adapter identity).
298
+ const tick = () => {
299
+ logRef.current.pull(syncAdapter());
300
+ force();
301
+ };
302
+ tick();
303
+ const pollId = setInterval(tick, 250);
304
+ return () => clearInterval(pollId);
305
+ }, [paused]);
306
+
307
+ const adapter = adapterRef.current ?? editorHost().systems.inspectedAudio() ?? null;
308
+ const caps = deriveAudioCapabilities(adapter);
309
+
310
+ if (!adapter || !caps) {
311
+ return (
312
+ <div
313
+ data-testid="audio-debugger-empty"
314
+ style={{
315
+ padding: spaceVar[8],
316
+ color: themeVars.content.muted,
317
+ fontSize: fontSizeVar.md,
318
+ lineHeight: '18px',
319
+ }}
320
+ >
321
+ <div style={{ color: themeVars.content.primary, marginBottom: spaceVar[2] }}>
322
+ Audio is still initializing.
323
+ </div>
324
+ The editor registers a first-party <code style={MONO}>AudioAdapter</code> in edit mode; Play
325
+ temporarily replaces it with the running game's adapter.
326
+ </div>
327
+ );
328
+ }
329
+
330
+ const nodes = caps.graph ? (adapter.graphSnapshot?.() ?? []) : null;
331
+ const events = logRef.current.all;
332
+
333
+ return (
334
+ <div
335
+ data-testid="audio-debugger"
336
+ style={{
337
+ flex: 1,
338
+ minHeight: 0,
339
+ display: 'flex',
340
+ flexDirection: 'column',
341
+ fontSize: fontSizeVar.md,
342
+ }}
343
+ >
344
+ {/* Header: mute state + transport strip + meters */}
345
+ <div
346
+ style={{
347
+ display: 'flex',
348
+ flexWrap: 'wrap',
349
+ gap: spaceVar[4],
350
+ alignItems: 'center',
351
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
352
+ }}
353
+ >
354
+ <ResumeAudioButton adapter={adapter} onResumed={force} />
355
+ <span
356
+ data-testid="audio-mute-state"
357
+ style={{
358
+ padding: `${spaceVar[2]} ${spaceVar[4]}`,
359
+ color: adapter.isMuted() ? themeVars.semantic.warning : themeVars.semantic.success,
360
+ }}
361
+ >
362
+ {adapter.isMuted() ? 'muted' : 'audible'}
363
+ </span>
364
+ {caps.transport ? (
365
+ <TransportStrip adapter={adapter} />
366
+ ) : (
367
+ <AbsentNote testId="audio-transport-absent">
368
+ Transport not provided by this adapter.
369
+ </AbsentNote>
370
+ )}
371
+ {caps.meters ? (
372
+ <MeterBars adapter={adapter} />
373
+ ) : (
374
+ <AbsentNote testId="audio-meters-absent">Meters not provided by this adapter.</AbsentNote>
375
+ )}
376
+ </div>
377
+
378
+ {/* Body: node graph | event log */}
379
+ <div style={{ flex: 1, minHeight: 0, display: 'flex' }}>
380
+ <div
381
+ data-testid="audio-graph"
382
+ style={{
383
+ flex: 1,
384
+ minWidth: 0,
385
+ overflow: 'auto',
386
+ borderRight: `1px solid ${themeVars.boundary.default}`,
387
+ padding: `${spaceVar[2]} 0`,
388
+ }}
389
+ >
390
+ {nodes ? (
391
+ nodes.map((node) => <NodeRow key={node.id} node={node} />)
392
+ ) : (
393
+ <AbsentNote testId="audio-graph-absent">
394
+ Audio graph not provided by this adapter.
395
+ </AbsentNote>
396
+ )}
397
+ </div>
398
+
399
+ <div
400
+ data-testid="audio-event-log"
401
+ style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}
402
+ >
403
+ {caps.events ? (
404
+ <>
405
+ <div
406
+ style={{
407
+ display: 'flex',
408
+ gap: spaceVar[3],
409
+ alignItems: 'center',
410
+ padding: spaceVar[2],
411
+ }}
412
+ >
413
+ <Button
414
+ type="button"
415
+ variant="ghost"
416
+ data-testid="audio-log-pause"
417
+ onClick={() => {
418
+ const next = !paused;
419
+ logRef.current.paused = next;
420
+ setPaused(next);
421
+ }}
422
+ >
423
+ {paused ? 'Resume' : 'Pause'}
424
+ </Button>
425
+ <Button
426
+ type="button"
427
+ variant="ghost"
428
+ data-testid="audio-log-clear"
429
+ onClick={() => {
430
+ logRef.current.clear();
431
+ force();
432
+ }}
433
+ >
434
+ Clear
435
+ </Button>
436
+ <span style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>
437
+ {events.length} events
438
+ </span>
439
+ </div>
440
+ <div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
441
+ {events.length === 0 ? (
442
+ <AbsentNote>No audio events yet.</AbsentNote>
443
+ ) : (
444
+ events.map((event) => <EventRow key={event.seq} event={event} />)
445
+ )}
446
+ </div>
447
+ </>
448
+ ) : (
449
+ <AbsentNote testId="audio-log-absent">
450
+ Event log not provided by this adapter.
451
+ </AbsentNote>
452
+ )}
453
+ </div>
454
+ </div>
455
+ </div>
456
+ );
457
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Pure data-path logic for the Audio debugger tab (W3c) — no React/DOM, so
3
+ * the consumer side of the `AudioAdapter` introspection capabilities is
4
+ * headless-testable. The rendering component (`AudioDebuggerPanel.tsx`) owns
5
+ * only JSX; capability detection, the seq-fenced event-log ring with pause,
6
+ * and the meter geometry math live here (the W3b
7
+ * `network-inspector-model.ts` split, applied to audio).
8
+ *
9
+ * Editor code never imports Tone — only the `AudioAdapter` interface (the
10
+ * networking rule's audio mirror). Read-only debugger: the full mixer is
11
+ * SQ-4-gated and has no surface here.
12
+ */
13
+
14
+ import type { AudioAdapter, AudioDebugEvent } from '@volter/editor-project/adapter';
15
+
16
+ /** Which optional introspection capabilities the active adapter provides —
17
+ * the degradation ladder's per-section verdict. `null` input (no adapter =
18
+ * no running session) stays `null`: the panel renders the honest no-session
19
+ * notice instead of a fabricated all-false map. Mute (`setMuted`/`isMuted`)
20
+ * is a REQUIRED `AudioAdapter` member, so it needs no entry. */
21
+ export interface AudioCapabilityMap {
22
+ graph: boolean;
23
+ transport: boolean;
24
+ meters: boolean;
25
+ events: boolean;
26
+ }
27
+
28
+ export function deriveAudioCapabilities(
29
+ adapter: AudioAdapter | null | undefined,
30
+ ): AudioCapabilityMap | null {
31
+ if (!adapter) return null;
32
+ return {
33
+ graph: typeof adapter.graphSnapshot === 'function',
34
+ transport: typeof adapter.transportState === 'function',
35
+ meters: typeof adapter.acquireMeters === 'function',
36
+ events: typeof adapter.audioEvents === 'function',
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Consumer-side event log over `AudioAdapter.audioEvents` — the
42
+ * `MessageLogModel` idiom: pulls seq-fenced events from the adapter's ring
43
+ * into a bounded local ring. PAUSE stops consuming with the fence frozen, so
44
+ * resume picks up whatever the adapter's ring still holds (a long pause
45
+ * loses only what the adapter itself evicted; nothing fills the gap).
46
+ */
47
+ export class AudioEventLogModel {
48
+ private entries: AudioDebugEvent[] = [];
49
+ private lastSeq = 0;
50
+ paused = false;
51
+
52
+ constructor(private readonly capacity = 500) {}
53
+
54
+ /** Consume new events (`seq > fence`). Returns how many were appended
55
+ * (0 while paused or when the capability is absent). */
56
+ pull(adapter: AudioAdapter | null | undefined): number {
57
+ if (this.paused || !adapter?.audioEvents) return 0;
58
+ const fresh = adapter.audioEvents(this.lastSeq);
59
+ if (fresh.length === 0) return 0;
60
+ for (const event of fresh) {
61
+ this.entries.push(event);
62
+ if (event.seq > this.lastSeq) this.lastSeq = event.seq;
63
+ }
64
+ if (this.entries.length > this.capacity) {
65
+ this.entries.splice(0, this.entries.length - this.capacity);
66
+ }
67
+ return fresh.length;
68
+ }
69
+
70
+ get all(): readonly AudioDebugEvent[] {
71
+ return this.entries;
72
+ }
73
+
74
+ clear(): void {
75
+ // Fence deliberately NOT reset: clear empties the view; already-observed
76
+ // events don't come back.
77
+ this.entries = [];
78
+ }
79
+ }
80
+
81
+ /** Transport seconds → `m:ss.t` (e.g. `1:03.4`) for the transport strip. */
82
+ export function formatTransportSeconds(seconds: number): string {
83
+ const clamped = Math.max(0, seconds);
84
+ const minutes = Math.floor(clamped / 60);
85
+ const rest = clamped - minutes * 60;
86
+ return `${minutes}:${rest < 10 ? '0' : ''}${rest.toFixed(1)}`;
87
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `bridge-call` — the handler behind the verb, moved out of the editor's
3
+ * `command-listener.ts` with it (WORK.md §The workbench). The method table it
4
+ * dispatches through is `./dispatch.ts`; play mode, the ingest latch and the
5
+ * editor console are reached through the `@editor/*` alias until those
6
+ * modules move too. Nothing here is host API.
7
+ */
8
+
9
+ import type { EditorCommandMessage, EditorCommandResult } from '@volter/editor-sdk/commands';
10
+ import { notPlayingResult, structuredErrorResult } from '../command-results';
11
+ import { isIngestActive } from '../ingest/active-ingest';
12
+ import { isPlayModeActive } from '../play/play-mode';
13
+ import { dispatchBridgeMethod, hasLiveDebugPlane } from './dispatch';
14
+
15
+ /** `bridge-call` relay op handler — an unreachable debug seam is the one
16
+ * precondition checked up front (mirrors every other debug-seam verb of
17
+ * this package); everything else is `dispatchBridgeMethod`'s own structured
18
+ * failure, caught here and shipped back via `structuredErrorResult` so
19
+ * `code`/`data` survive the relay round trip byte-equivalent to the
20
+ * page-transport's `unwrap()` path (`packages/vgai-live/src/game-client/client.ts`). */
21
+ export async function handleBridgeCall(cmd: EditorCommandMessage): Promise<EditorCommandResult> {
22
+ // The precondition is "no plane at all", not "not playing" — see
23
+ // `hasLiveDebugPlane`. The per-method preconditions inside
24
+ // `dispatchBridgeMethod` are unchanged and still refuse honestly: the
25
+ // run-shaped methods (`runTicks`, `input.*`, `holdFor`) resolve through
26
+ // `getPlayRuntimeAccess()`, which is null outside play, so they answer
27
+ // `RUN_TICKS_UNAVAILABLE`/`DEBUG_INPUT_UNAVAILABLE` rather than pretending
28
+ // the still edit world can be driven.
29
+ if (!isPlayModeActive() && !hasLiveDebugPlane()) {
30
+ // A LIVE INGEST that publishes no plane is a third state, and answering it
31
+ // with "not in play mode" sends the reader to `vgai play` over a game that
32
+ // is already running and visible. MEASURED on the `flappy` canvas ingest:
33
+ // `game.waitSimTime(...)` refused with the play sentence while the mount's
34
+ // own play state read `playing`. The mount is what is missing a plane, so
35
+ // the refusal says so — and deliberately stays a REFUSAL, because the
36
+ // alternative (letting the call through to answer `[]`) would report "this
37
+ // game registered no commands", which is a different and equally false
38
+ // claim.
39
+ return isIngestActive()
40
+ ? {
41
+ ok: false,
42
+ error:
43
+ 'this ingest mount is live, but it publishes no debug plane — the mount installs no ' +
44
+ 'SystemAdapters.debug, so there is nothing to enumerate, read or drive through the ' +
45
+ 'game seam. Starting play will not change that; the game itself must declare the ' +
46
+ 'vgai game contract (or carry a contract shim beside it) for these calls to have an ' +
47
+ 'answer.',
48
+ }
49
+ : notPlayingResult();
50
+ }
51
+ const method = cmd['method'] as string;
52
+ const callArgs = (cmd['callArgs'] as unknown[] | undefined) ?? [];
53
+ try {
54
+ const instance = cmd['instance'] as string | undefined;
55
+ const result = await dispatchBridgeMethod(method, callArgs, instance);
56
+ return { ok: true, data: { result } };
57
+ } catch (err) {
58
+ return structuredErrorResult(err);
59
+ }
60
+ }