@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,95 @@
1
+ /**
2
+ * Resolve the canvas entry adapter from the same module graph as project
3
+ * worlds — the `@pixi/react` sibling of `r3f-entry-runtime.ts`.
4
+ *
5
+ * The packaged editor's UI is prebuilt with its own React, `@pixi/react` and
6
+ * `pixi.js`, while project modules are transformed by a separate,
7
+ * project-rooted Vite server. Building a project component's adapter with the
8
+ * UI bundle's factory crosses those graphs: the reconciler's hooks and the
9
+ * component's hooks come from two Reacts, the tree never commits, and
10
+ * the canvas mount fails its own first-commit ceiling ten seconds
11
+ * later. See `../vite-plugin-module-doorways.ts` for the measured failure.
12
+ *
13
+ * `@volter/game-runtime/canvas-react` is dynamic-imported on BOTH branches, deliberately: a
14
+ * static import would put `pixi.js` and the Pixi reconciler in every editor
15
+ * bundle, including one opening a three-only project — the same reason the
16
+ * call sites this replaces were already dynamic.
17
+ */
18
+
19
+ import type { resolveCanvasEntryAdapter } from '@volter/game-runtime/canvas-react';
20
+ import type { RootAdapter } from '@volter/editor-project/adapter';
21
+ import type * as PIXI from 'pixi.js';
22
+ import { CANVAS_RUNTIME_PATH } from '@volter/editor-sdk/host';
23
+ import { isPackagedRuntime } from '@volter/editor-core/packaged-runtime';
24
+
25
+ type CanvasEntryResolver = typeof resolveCanvasEntryAdapter;
26
+
27
+ /**
28
+ * The `pixi.js` module namespace a canvas surface constructs and class-checks
29
+ * with. Every editor-side collaborator of one mounted canvas world takes THIS,
30
+ * never a static import — see `../vite-plugin-module-doorways.ts` for the four
31
+ * measured failures a second namespace causes.
32
+ */
33
+ export type CanvasPixiNamespace = typeof PIXI;
34
+
35
+ interface PackagedCanvasRuntime {
36
+ readonly resolve: CanvasEntryResolver;
37
+ readonly projectPixi: CanvasPixiNamespace;
38
+ }
39
+
40
+ let cachedPackagedRuntime: Promise<PackagedCanvasRuntime> | null = null;
41
+
42
+ async function packagedRuntime(): Promise<PackagedCanvasRuntime> {
43
+ const mod = (await import(/* @vite-ignore */ CANVAS_RUNTIME_PATH)) as {
44
+ resolveCanvasEntryAdapter?: unknown;
45
+ projectPixi?: unknown;
46
+ };
47
+ if (typeof mod.resolveCanvasEntryAdapter !== 'function') {
48
+ throw new Error(
49
+ "The packaged runtime's synthetic canvas module did not export " +
50
+ '`resolveCanvasEntryAdapter` — see vite-plugin-module-doorways.ts.',
51
+ );
52
+ }
53
+ if (typeof mod.projectPixi !== 'object' || mod.projectPixi === null) {
54
+ throw new Error(
55
+ "The packaged runtime's synthetic canvas module did not export " +
56
+ '`projectPixi` — see vite-plugin-module-doorways.ts.',
57
+ );
58
+ }
59
+ return {
60
+ resolve: mod.resolveCanvasEntryAdapter as CanvasEntryResolver,
61
+ projectPixi: mod.projectPixi as CanvasPixiNamespace,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * THE `pixi.js` OF THE GRAPH A CANVAS WORLD MOUNTS IN — the project's under the
67
+ * packaged runtime, this bundle's own copy otherwise.
68
+ *
69
+ * Dynamic on BOTH branches for the reason this module's header gives: a static
70
+ * `pixi.js` import here would put the whole renderer in every editor bundle,
71
+ * including one opening a three-only project.
72
+ *
73
+ * A mount site resolves this ONCE and hands it to everything it builds for that
74
+ * world. Nothing further down calls it per node — a namespace that could differ
75
+ * between two collaborators of the same surface is the bug, not the seam.
76
+ */
77
+ export async function resolveCanvasPixiForEditor(): Promise<CanvasPixiNamespace> {
78
+ if (!(await isPackagedRuntime())) return import('pixi.js');
79
+ if (!cachedPackagedRuntime) cachedPackagedRuntime = packagedRuntime();
80
+ return (await cachedPackagedRuntime).projectPixi;
81
+ }
82
+
83
+ /** The adjudicator `resolveCanvasEntryAdapter` — from the PROJECT's graph
84
+ * under the packaged runtime, from this bundle's own engine copy otherwise. */
85
+ export async function resolveCanvasEntryAdapterForEditor(
86
+ entryModule: unknown,
87
+ rootId: string,
88
+ ): Promise<RootAdapter<'canvas'> | null> {
89
+ if (!(await isPackagedRuntime())) {
90
+ const { resolveCanvasEntryAdapter } = await import('@volter/game-runtime/canvas-react');
91
+ return resolveCanvasEntryAdapter(entryModule, rootId);
92
+ }
93
+ if (!cachedPackagedRuntime) cachedPackagedRuntime = packagedRuntime();
94
+ return (await cachedPackagedRuntime).resolve(entryModule, rootId);
95
+ }
@@ -0,0 +1,216 @@
1
+ import {
2
+ faCamera,
3
+ faEllipsis,
4
+ faGripLines,
5
+ faThumbtack,
6
+ faXmark,
7
+ } from '@fortawesome/free-solid-svg-icons';
8
+ import {
9
+ Button,
10
+ EditorBadge,
11
+ EditorIcon,
12
+ EditorPopover,
13
+ EditorSurface,
14
+ IconButton,
15
+ Inline,
16
+ Text,
17
+ Tooltip,
18
+ } from '@volter/editor-sdk/widgets';
19
+ import { type RefObject, useEffect, useRef, useState, useSyncExternalStore } from 'react';
20
+ import type * as THREE from 'three';
21
+ import {
22
+ alignCameraToViewport,
23
+ cameraAuthoringPresentation,
24
+ cameraAuthoringVersion,
25
+ leaveCameraView,
26
+ pilotCamera,
27
+ subscribeCameraAuthoring,
28
+ toggleCameraCompositionGuides,
29
+ toggleCameraPreviewPin,
30
+ viewThroughCamera,
31
+ } from '@volter/editor-core/camera-authoring';
32
+
33
+ export interface CameraAuthoringOverlayProps {
34
+ readonly previewRef: RefObject<HTMLDivElement | null>;
35
+ }
36
+
37
+ function CameraPreview({ previewRef }: CameraAuthoringOverlayProps) {
38
+ const { preview, previewPinned } = cameraAuthoringPresentation();
39
+ const [menuOpen, setMenuOpen] = useState(false);
40
+ const menuRef = useRef<HTMLDivElement>(null);
41
+
42
+ useEffect(() => {
43
+ if (!menuOpen) return;
44
+ const dismiss = (event: PointerEvent) => {
45
+ if (!menuRef.current?.contains(event.target as Node)) setMenuOpen(false);
46
+ };
47
+ document.addEventListener('pointerdown', dismiss);
48
+ return () => document.removeEventListener('pointerdown', dismiss);
49
+ }, [menuOpen]);
50
+
51
+ if (!preview) return null;
52
+ const aspect =
53
+ (preview.camera as THREE.PerspectiveCamera).isPerspectiveCamera &&
54
+ Number.isFinite((preview.camera as THREE.PerspectiveCamera).aspect)
55
+ ? (preview.camera as THREE.PerspectiveCamera).aspect
56
+ : 16 / 9;
57
+
58
+ return (
59
+ <div className="vgai-camera-preview" data-testid="camera-preview">
60
+ <Inline className="vgai-camera-preview-header" gap={4} align="center">
61
+ <EditorIcon icon={faCamera} size="sm" />
62
+ <Text
63
+ variant="label"
64
+ truncate
65
+ className="vgai-camera-preview-title"
66
+ title={`${preview.name} · ${preview.lens}`}
67
+ >
68
+ {preview.name}
69
+ </Text>
70
+ <Tooltip text={previewPinned ? 'Unpin camera preview' : 'Pin camera preview'}>
71
+ <IconButton
72
+ aria-label={previewPinned ? 'Unpin camera preview' : 'Pin camera preview'}
73
+ aria-pressed={previewPinned}
74
+ size="compact"
75
+ onClick={toggleCameraPreviewPin}
76
+ >
77
+ <EditorIcon icon={faThumbtack} size="sm" />
78
+ </IconButton>
79
+ </Tooltip>
80
+ <div ref={menuRef} className="vgai-camera-preview-menu-anchor">
81
+ <Tooltip text="Camera actions">
82
+ <IconButton
83
+ aria-label="Camera actions"
84
+ aria-expanded={menuOpen}
85
+ size="compact"
86
+ onClick={() => setMenuOpen((open) => !open)}
87
+ >
88
+ <EditorIcon icon={faEllipsis} size="sm" />
89
+ </IconButton>
90
+ </Tooltip>
91
+ {menuOpen ? (
92
+ <EditorPopover className="vgai-camera-preview-menu">
93
+ <Button
94
+ variant="ghost"
95
+ size="compact"
96
+ disabled={!preview.canAuthorPose}
97
+ title={preview.poseRefusal}
98
+ onClick={() => {
99
+ setMenuOpen(false);
100
+ pilotCamera(preview);
101
+ }}
102
+ >
103
+ Pilot Camera
104
+ </Button>
105
+ <Button
106
+ variant="ghost"
107
+ size="compact"
108
+ onClick={() => {
109
+ setMenuOpen(false);
110
+ viewThroughCamera(preview);
111
+ }}
112
+ >
113
+ View Through Camera
114
+ </Button>
115
+ <Button
116
+ variant="ghost"
117
+ size="compact"
118
+ disabled={!preview.canAuthorPose}
119
+ title={preview.poseRefusal}
120
+ onClick={() => {
121
+ setMenuOpen(false);
122
+ alignCameraToViewport(preview);
123
+ }}
124
+ >
125
+ Align Camera to Current View
126
+ </Button>
127
+ </EditorPopover>
128
+ ) : null}
129
+ </div>
130
+ </Inline>
131
+ <div
132
+ ref={previewRef}
133
+ className="vgai-camera-preview-image"
134
+ style={{ aspectRatio: String(Math.max(0.5, Math.min(3, aspect))) }}
135
+ role="img"
136
+ aria-label={`Live view through ${preview.name}`}
137
+ onDoubleClick={() => viewThroughCamera(preview)}
138
+ />
139
+ </div>
140
+ );
141
+ }
142
+
143
+ function CompositionGuides() {
144
+ return (
145
+ <div className="vgai-camera-guides" aria-hidden="true">
146
+ <div className="vgai-camera-guide vgai-camera-guide-v1" />
147
+ <div className="vgai-camera-guide vgai-camera-guide-v2" />
148
+ <div className="vgai-camera-guide vgai-camera-guide-h1" />
149
+ <div className="vgai-camera-guide vgai-camera-guide-h2" />
150
+ <div className="vgai-camera-safe-frame" />
151
+ <div className="vgai-camera-center-mark" />
152
+ </div>
153
+ );
154
+ }
155
+
156
+ function CameraViewBanner() {
157
+ const { view, guidesVisible } = cameraAuthoringPresentation();
158
+ if (!view) return null;
159
+ const piloting = view.mode === 'pilot';
160
+ return (
161
+ <>
162
+ {guidesVisible ? <CompositionGuides /> : null}
163
+ <EditorSurface
164
+ variant="overlay"
165
+ border
166
+ className="vgai-camera-view-banner vgai-chrome-island vgai-glass-island"
167
+ data-testid="camera-view-banner"
168
+ >
169
+ <Inline gap={6} align="center">
170
+ <EditorBadge>{piloting ? 'PILOTING' : 'CAMERA VIEW'}</EditorBadge>
171
+ <Text variant="label">{view.subject.name}</Text>
172
+ <Text variant="caption" tone="muted">
173
+ Exact camera view
174
+ </Text>
175
+ <Tooltip text={guidesVisible ? 'Hide composition guides' : 'Show composition guides'}>
176
+ <IconButton
177
+ aria-label={guidesVisible ? 'Hide composition guides' : 'Show composition guides'}
178
+ aria-pressed={guidesVisible}
179
+ size="compact"
180
+ onClick={toggleCameraCompositionGuides}
181
+ >
182
+ <EditorIcon icon={faGripLines} size="sm" />
183
+ </IconButton>
184
+ </Tooltip>
185
+ {!piloting ? (
186
+ <Button
187
+ size="compact"
188
+ variant="primary"
189
+ disabled={!view.subject.canAuthorPose}
190
+ title={view.subject.poseRefusal}
191
+ onClick={() => pilotCamera(view.subject)}
192
+ >
193
+ Pilot
194
+ </Button>
195
+ ) : null}
196
+ <Tooltip text="Exit camera view">
197
+ <IconButton aria-label="Exit camera view" size="compact" onClick={leaveCameraView}>
198
+ <EditorIcon icon={faXmark} size="sm" />
199
+ </IconButton>
200
+ </Tooltip>
201
+ </Inline>
202
+ </EditorSurface>
203
+ </>
204
+ );
205
+ }
206
+
207
+ /** Selected-camera preview and view/pilot presentation for the Scene viewport. */
208
+ export function CameraAuthoringOverlay({ previewRef }: CameraAuthoringOverlayProps) {
209
+ useSyncExternalStore(subscribeCameraAuthoring, cameraAuthoringVersion, cameraAuthoringVersion);
210
+ return (
211
+ <>
212
+ <CameraPreview previewRef={previewRef} />
213
+ <CameraViewBanner />
214
+ </>
215
+ );
216
+ }
@@ -0,0 +1,341 @@
1
+ import { faVolumeHigh, faVolumeXmark } from '@fortawesome/free-solid-svg-icons';
2
+ import { AnchoredMenu, Button, EditorIcon, Inline, MenuItem } from '@volter/editor-sdk/widgets';
3
+ import type { PerformanceSnapshot } from '@volter/game-runtime/dev/performance-profiler';
4
+ import type { AudioAdapter, AudioMeterFrame } from '@volter/editor-project/adapter';
5
+ import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
6
+ import {
7
+ activeAudioVersion,
8
+ getAllActiveAudio,
9
+ getInspectedAudio,
10
+ subscribeActiveAudio,
11
+ } from '@volter/editor-core/authoring/active-systems';
12
+ import { useEditorStore } from '@volter/editor-core/editor-runtime';
13
+ import { useActivePerformanceSource } from '../use-active-performance-source';
14
+ import {
15
+ activeChromeRegions,
16
+ chromeRegionsKey,
17
+ subscribeChromeRegions,
18
+ } from '@volter/editor-core/workspace-regions';
19
+ import {
20
+ AUDIO_METER_SEGMENTS,
21
+ audioMeterSegmentCount,
22
+ FRAME_BUDGET_60_FPS_MS,
23
+ frameSparklinePoints,
24
+ PERFORMANCE_SPARKLINE_SAMPLES,
25
+ performanceTelemetryTone,
26
+ sampleFrameInterval,
27
+ } from './header-telemetry-model';
28
+ import { profilerView } from './utility-view-state';
29
+
30
+ const MUTE_KEY = 'editor:muted';
31
+ const EMPTY_PERFORMANCE: PerformanceSnapshot = {
32
+ enabled: false,
33
+ recording: false,
34
+ fps: 0,
35
+ cpuMs: 0,
36
+ p95Ms: 0,
37
+ p99Ms: 0,
38
+ frames: [],
39
+ phases: [],
40
+ systems: [],
41
+ components: [],
42
+ render: {
43
+ gpuMs: null,
44
+ drawCalls: 0,
45
+ triangles: 0,
46
+ geometries: 0,
47
+ textures: 0,
48
+ renderPasses: 0,
49
+ },
50
+ };
51
+
52
+ function storedMutePreference(): boolean {
53
+ try {
54
+ return localStorage.getItem(MUTE_KEY) === '1';
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ function persistMutePreference(muted: boolean): void {
61
+ try {
62
+ localStorage.setItem(MUTE_KEY, muted ? '1' : '0');
63
+ } catch {
64
+ // Storage can be unavailable in an embedded/locked-down host. Muting the
65
+ // live adapters still succeeds; only the next-session preference is lost.
66
+ }
67
+ }
68
+
69
+ function audioDisplayLevel(adapter: AudioAdapter, frames: readonly AudioMeterFrame[]): number {
70
+ const master = frames.find((frame) => frame.id === 'master')?.level;
71
+ if (!adapter.isMuted() && master !== undefined) return master;
72
+ // The first-party master tap is post-gain, so it truthfully flatlines while
73
+ // muted. The bus taps are pre-master: their maximum is an ACTIVITY reading,
74
+ // not a fabricated mixed output level, and lets grey bars say "signal is
75
+ // present but silenced". External adapters that expose only master degrade
76
+ // to that honest (possibly zero) value.
77
+ const upstream = frames.filter((frame) => frame.id !== 'master').map((frame) => frame.level);
78
+ return upstream.length > 0 ? Math.max(...upstream) : (master ?? 0);
79
+ }
80
+
81
+ /**
82
+ * AUTHORING IS SILENT: the host's own policy, not each game's manners.
83
+ *
84
+ * In edit mode a mounted world's system adapters are live (the design session
85
+ * publishes `game.systemAdapters`), so a game whose audio starts with its
86
+ * scene sounds at the person EDITING it. The engine only mutes for pause
87
+ * (`setRootGates`), which is a runtime concern; nothing muted a world for
88
+ * being authored rather than played. So the effective mute for every active
89
+ * adapter is `userMuted || !playing`, applied here, with the user's own
90
+ * preference left untouched: the toggle keeps meaning "the user wants mute",
91
+ * and while stopped it decides what happens once play starts.
92
+ *
93
+ * Only `'playing'` counts as audible so the two gates agree instead of
94
+ * fighting: `'paused'` is already muted by the engine, which restores the
95
+ * pre-pause value on resume — which is exactly this effective value, and the
96
+ * play-state subscription re-applies it on that transition anyway.
97
+ */
98
+ function AudioTelemetry() {
99
+ const store = useEditorStore();
100
+ useSyncExternalStore(store.subscribe, store.getShellSnapshot ?? store.getSnapshot);
101
+ const playing = store.playState === 'playing';
102
+ const [menuOpen, setMenuOpen] = useState(false);
103
+ const [level, setLevel] = useState(0);
104
+ const [muted, setMuted] = useState(false);
105
+ const [meterAvailable, setMeterAvailable] = useState(false);
106
+ // The user's own wish, which the authoring gate above never clobbers — the
107
+ // menu names THIS (what the next click changes), while the icon and the
108
+ // state label report the effective, audible-or-not truth.
109
+ const [userMuted, setUserMuted] = useState(storedMutePreference);
110
+ const desiredMuteRef = useRef(storedMutePreference());
111
+ const appliedRef = useRef(new WeakMap<object, boolean>());
112
+ const buttonRef = useRef<HTMLButtonElement>(null);
113
+ const audioVersion = useSyncExternalStore(
114
+ subscribeActiveAudio,
115
+ activeAudioVersion,
116
+ activeAudioVersion,
117
+ );
118
+ const adapter = getInspectedAudio();
119
+
120
+ useEffect(() => {
121
+ void audioVersion;
122
+ const desired = desiredMuteRef.current || !playing;
123
+ for (const active of getAllActiveAudio()) {
124
+ if (appliedRef.current.get(active) === desired) continue;
125
+ active.setMuted(desired);
126
+ appliedRef.current.set(active, desired);
127
+ }
128
+ }, [audioVersion, playing]);
129
+
130
+ useEffect(() => {
131
+ if (!adapter) {
132
+ setLevel(0);
133
+ setMuted(false);
134
+ setMeterAvailable(false);
135
+ return;
136
+ }
137
+ const handle = adapter.acquireMeters?.() ?? null;
138
+ setMeterAvailable(handle !== null);
139
+ const sample = () => {
140
+ setMuted(adapter.isMuted());
141
+ setLevel(handle ? audioDisplayLevel(adapter, handle.read()) : 0);
142
+ };
143
+ sample();
144
+ const id = setInterval(sample, 100);
145
+ return () => {
146
+ clearInterval(id);
147
+ handle?.dispose();
148
+ };
149
+ }, [adapter]);
150
+
151
+ // The button toggles the USER's preference, never the effective state:
152
+ // reading it back off the adapters would make "unmute" while stopped (where
153
+ // everything is muted by the authoring gate) unmute the world at the person
154
+ // editing it, and would lose the preference the next time play starts.
155
+ const toggleMute = () => {
156
+ const nextPreference = !desiredMuteRef.current;
157
+ const effective = nextPreference || !playing;
158
+ for (const item of getAllActiveAudio()) {
159
+ item.resume?.();
160
+ item.setMuted(effective);
161
+ appliedRef.current.set(item, effective);
162
+ }
163
+ desiredMuteRef.current = nextPreference;
164
+ setUserMuted(nextPreference);
165
+ persistMutePreference(nextPreference);
166
+ setMuted(effective);
167
+ };
168
+
169
+ const filled = audioMeterSegmentCount(level);
170
+ // Muted-because-not-playing is the one state a person cannot explain from
171
+ // the icon alone; it rides in the existing tooltip rather than new chrome.
172
+ const muteReason = adapter && muted && !userMuted && !playing ? ' (not playing)' : '';
173
+ const stateLabel = !adapter
174
+ ? 'Audio unavailable'
175
+ : muted
176
+ ? 'Audio muted'
177
+ : level > 0
178
+ ? 'Audio playing'
179
+ : 'Audio quiet';
180
+
181
+ return (
182
+ <div className="vgai-header-audio" data-muted={muted || undefined} data-available={!!adapter}>
183
+ <Button
184
+ ref={buttonRef}
185
+ type="button"
186
+ variant="ghost"
187
+ size="compact"
188
+ className="vgai-header-telemetry-button"
189
+ data-testid="header-audio-telemetry"
190
+ aria-label={`${stateLabel}; open audio controls`}
191
+ aria-haspopup="menu"
192
+ aria-expanded={menuOpen}
193
+ title={`${stateLabel}${muteReason}${meterAvailable ? '' : ' · level metering unavailable'}`}
194
+ onClick={() => setMenuOpen((open) => !open)}
195
+ >
196
+ <EditorIcon icon={muted ? faVolumeXmark : faVolumeHigh} size="sm" />
197
+ <span className="vgai-audio-vu" aria-hidden="true">
198
+ {Array.from({ length: AUDIO_METER_SEGMENTS }, (_, index) => (
199
+ <i key={index} data-filled={index < filled || undefined} />
200
+ ))}
201
+ </span>
202
+ </Button>
203
+ {menuOpen && (
204
+ <AnchoredMenu anchorRef={buttonRef} onDismiss={() => setMenuOpen(false)} gap={3}>
205
+ <MenuItem
206
+ onClick={() => {
207
+ toggleMute();
208
+ setMenuOpen(false);
209
+ }}
210
+ >
211
+ {userMuted ? 'Unmute audio' : 'Mute audio'}
212
+ </MenuItem>
213
+ </AnchoredMenu>
214
+ )}
215
+ </div>
216
+ );
217
+ }
218
+
219
+ function PerformanceTelemetry() {
220
+ const source = useActivePerformanceSource();
221
+ const profiler = source?.profiler ?? null;
222
+ const [snapshot, setSnapshot] = useState<PerformanceSnapshot>(EMPTY_PERFORMANCE);
223
+ const [frameHistory, setFrameHistory] = useState<number[]>([]);
224
+
225
+ useEffect(() => {
226
+ if (!profiler) {
227
+ setSnapshot(EMPTY_PERFORMANCE);
228
+ setFrameHistory([]);
229
+ return;
230
+ }
231
+ const telemetryWasEnabled = profiler.telemetryEnabled;
232
+ profiler.telemetryEnabled = true;
233
+ const initial = profiler.getSnapshot();
234
+ const initialSample = sampleFrameInterval(initial.frames, 0);
235
+ let sampledThroughFrameId = initialSample.frameId;
236
+ setSnapshot(initial);
237
+ setFrameHistory(initialSample.intervalMs === null ? [] : [initialSample.intervalMs]);
238
+ let timer: ReturnType<typeof setTimeout> | null = null;
239
+ const publish = () => {
240
+ timer = null;
241
+ const next = profiler.getSnapshot();
242
+ const sample = sampleFrameInterval(next.frames, sampledThroughFrameId);
243
+ sampledThroughFrameId = sample.frameId;
244
+ setSnapshot(next);
245
+ const interval = sample.intervalMs;
246
+ if (interval !== null) {
247
+ setFrameHistory((history) => [...history, interval].slice(-PERFORMANCE_SPARKLINE_SAMPLES));
248
+ }
249
+ };
250
+ const unsubscribe = profiler.subscribe(() => {
251
+ if (timer === null) timer = setTimeout(publish, 500);
252
+ });
253
+ return () => {
254
+ unsubscribe();
255
+ if (timer !== null) clearTimeout(timer);
256
+ if (!telemetryWasEnabled) profiler.telemetryEnabled = false;
257
+ };
258
+ }, [profiler]);
259
+
260
+ const latestFrameMs = frameHistory.at(-1) ?? null;
261
+ const points = frameSparklinePoints(frameHistory, 72, 18);
262
+ const fps = latestFrameMs
263
+ ? Math.round(1000 / latestFrameMs)
264
+ : snapshot.fps > 0
265
+ ? Math.round(snapshot.fps)
266
+ : null;
267
+ const frameMs = latestFrameMs ?? (snapshot.fps > 0 ? 1000 / snapshot.fps : null);
268
+ const tone = performanceTelemetryTone(frameHistory);
269
+ const title = source
270
+ ? `${source.label}: ${fps ?? '—'} FPS · ${frameMs?.toFixed(1) ?? '—'} ms frame time · open Profiler`
271
+ : 'The active document has no live render loop to profile';
272
+
273
+ return (
274
+ <Button
275
+ type="button"
276
+ variant="ghost"
277
+ size="compact"
278
+ className="vgai-header-telemetry-button vgai-header-performance"
279
+ data-testid="header-performance-telemetry"
280
+ data-performance-tone={tone}
281
+ disabled={!source}
282
+ aria-label={title}
283
+ title={title}
284
+ onClick={() => profilerView.open('profiler')}
285
+ >
286
+ <svg
287
+ className="vgai-performance-graph"
288
+ aria-hidden="true"
289
+ viewBox="0 0 72 18"
290
+ width="72"
291
+ height="18"
292
+ >
293
+ <line
294
+ className="vgai-performance-budget"
295
+ x1="0"
296
+ x2="72"
297
+ y1={18 - (FRAME_BUDGET_60_FPS_MS / 50) * 18}
298
+ y2={18 - (FRAME_BUDGET_60_FPS_MS / 50) * 18}
299
+ />
300
+ {points && <polyline className="vgai-performance-sparkline" points={points} />}
301
+ </svg>
302
+ <span className="vgai-performance-readout">
303
+ <strong className="vgai-header-fps">{fps ?? '—'} FPS</strong>
304
+ <span className="vgai-header-frame-ms">{frameMs?.toFixed(1) ?? '—'} ms</span>
305
+ </span>
306
+ </Button>
307
+ );
308
+ }
309
+
310
+ /**
311
+ * Persistent, glanceable runtime telemetry. The audio VU is an instantaneous
312
+ * segmented magnitude; performance is a historical line, so the two readings
313
+ * remain distinguishable without color.
314
+ *
315
+ * IT IS A CHROME REGION (`telemetry`, `workspace-regions.ts`), the way the
316
+ * workspace tab strip is: measured against Blender's Modeling frame, this
317
+ * cluster is the brightest, highest-chroma ink in a window whose brightest
318
+ * chrome ink is 216 — a yellow readout, an orange sparkline and a lit meter
319
+ * where Blender's corner carries two quiet wells (Scene, ViewLayer) — and it
320
+ * is the first thing the eye lands on. A style that wants that corner quiet
321
+ * declares `telemetry: 'hidden'`; absent means shown, so every existing skin
322
+ * is unchanged. Only these three readings are behind the region: Invite and
323
+ * Account are product identity, not look, and they are already quiet. The
324
+ * Profiler utility remains the door to the same numbers, which is what the
325
+ * readout's own tooltip says.
326
+ */
327
+ export function HeaderTelemetry() {
328
+ useSyncExternalStore(subscribeChromeRegions, chromeRegionsKey, chromeRegionsKey);
329
+ if (activeChromeRegions().telemetry === 'hidden') return null;
330
+ return (
331
+ <Inline
332
+ className="vgai-header-telemetry vgai-chrome-island vgai-glass-island"
333
+ data-island-scale="compact"
334
+ align="center"
335
+ gap={1}
336
+ >
337
+ <AudioTelemetry />
338
+ <PerformanceTelemetry />
339
+ </Inline>
340
+ );
341
+ }