@volter/editor-game 0.5.66 → 0.5.67

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 (236) hide show
  1. package/contributions/audio-unlock.service.ts +2 -2
  2. package/contributions/autoplay.service.ts +1 -1
  3. package/contributions/bridge.command.ts +3 -3
  4. package/contributions/canvas/component-board.service.ts +16 -0
  5. package/contributions/canvas/design-time-mount.service.ts +45 -0
  6. package/contributions/canvas-story-capture.service.ts +12 -0
  7. package/contributions/edit-mode-audio.service.ts +2 -2
  8. package/contributions/edit-mode-networking.service.ts +1 -1
  9. package/contributions/gameplay.command.ts +4 -4
  10. package/contributions/generation.service.ts +1 -1
  11. package/contributions/godot.style.ts +26 -6
  12. package/contributions/godot.view.ts +6 -0
  13. package/contributions/ingest.service.ts +2 -2
  14. package/contributions/instances.command.ts +1 -1
  15. package/contributions/navmesh.menu.ts +1 -1
  16. package/contributions/network-observer.service.ts +14 -0
  17. package/contributions/play.command.ts +10 -4
  18. package/contributions/react/component-board.service.ts +1 -1
  19. package/contributions/react/design-time-mount.service.ts +1 -1
  20. package/contributions/scene-document.service.ts +3 -3
  21. package/contributions/state-watch.menu.ts +1 -1
  22. package/contributions/state-watch.utility.tsx +1 -1
  23. package/contributions/team-playtest.service.ts +4 -4
  24. package/contributions/three/component-board.service.ts +1 -1
  25. package/contributions/three/component-verbs.command.ts +6 -6
  26. package/contributions/three/three-authoring.service.ts +8 -5
  27. package/contributions/three-story-capture.service.ts +12 -0
  28. package/contributions/unity.style.ts +17 -7
  29. package/contributions/unity.view.ts +6 -0
  30. package/contributions/unreal.style.ts +10 -2
  31. package/contributions/unreal.view.ts +5 -0
  32. package/package.json +20 -10
  33. package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
  34. package/src/asset-budget/asset-budget-model.ts +1 -1
  35. package/src/audio/AudioDebuggerPanel.tsx +1 -1
  36. package/src/bridge/dispatch.ts +14 -14
  37. package/src/bridge/live-frames.ts +1 -1
  38. package/src/bridge/screenshot.ts +3 -3
  39. package/src/build/BuildProfilesPanel.tsx +3 -3
  40. package/src/canvas/canvas-board/CanvasBoardDocument.tsx +676 -0
  41. package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
  42. package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
  43. package/src/canvas/canvas-design-mount.ts +524 -0
  44. package/src/canvas/design-time-canvas-mount.ts +79 -0
  45. package/src/coverage/live-authoring-surface.ts +5 -5
  46. package/src/coverage/live-project-verbs.ts +1 -1
  47. package/src/coverage/native-system-coverage.ts +5 -5
  48. package/src/coverage/root-coverage.ts +1 -1
  49. package/src/coverage/session-coverage.ts +3 -3
  50. package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
  51. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
  52. package/src/edit-mode/edit-mode-audio.ts +4 -4
  53. package/src/edit-mode/edit-mode-networking.ts +2 -2
  54. package/src/game-document/GameCaptureFrameButton.tsx +1 -1
  55. package/src/game-document/GameDocument.tsx +2 -2
  56. package/src/game-document/GamePanel.tsx +4 -4
  57. package/src/game-document/InstanceInspectorPicker.tsx +2 -2
  58. package/src/game-document/crowd-debug.ts +1 -1
  59. package/src/game-document/device-preview.ts +10 -11
  60. package/src/game-document/physics-debug.ts +1 -1
  61. package/src/generation/GenerationActivity.tsx +3 -3
  62. package/src/generation/generation-documents.tsx +5 -5
  63. package/src/generation/generation-jobs.ts +1 -1
  64. package/src/host/adapter-runtime-bindings.ts +96 -5
  65. package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
  66. package/src/host/authoring/gesture-persist.ts +1 -1
  67. package/src/host/authoring/ingest-data-writer.ts +1 -1
  68. package/src/host/authoring/ingest-source-persistence.ts +4 -4
  69. package/src/host/authoring/mounted-authoring.ts +4 -4
  70. package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
  71. package/src/host/authoring/pixi-authoring-adapter.ts +40 -15
  72. package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
  73. package/src/host/authoring/pixi-live-write-target.ts +4 -4
  74. package/src/host/authoring/pixi-source-identity.ts +3 -3
  75. package/src/host/authoring/pixi-source-write-target.ts +1614 -0
  76. package/src/host/authoring/pixi-still-presentation.ts +1 -1
  77. package/src/host/authoring/pixi-structure-history.ts +2 -2
  78. package/src/host/authoring/pixi-transform-channels.ts +16 -14
  79. package/src/host/authoring/source-persistence-backend.ts +3 -3
  80. package/src/host/authoring/struct-write-pipe.ts +1 -1
  81. package/src/host/binding-resolver.ts +8 -9
  82. package/src/host/browser-transpile.ts +1 -1
  83. package/src/host/canvas-entry-runtime.ts +58 -47
  84. package/src/host/canvas-preview-frames.ts +482 -0
  85. package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
  86. package/src/host/components/HeaderTelemetry.tsx +4 -4
  87. package/src/host/components/PixiIsolationSceneContent.tsx +10 -10
  88. package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
  89. package/src/host/components/frame-debugger-model.ts +2 -2
  90. package/src/host/components/header-telemetry-model.ts +2 -2
  91. package/src/host/components/scene-document.tsx +14 -14
  92. package/src/host/components/utility-view-state.ts +1 -1
  93. package/src/host/components/world-root-stage-binding.tsx +12 -12
  94. package/src/host/components/world-root-stage.ts +70 -49
  95. package/src/host/coverage/system-adapter-coverage.ts +3 -4
  96. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
  97. package/src/host/document-preview-three.ts +1 -1
  98. package/src/host/entry-adjudication.ts +6 -6
  99. package/src/host/game-css-scope-transform.ts +4 -0
  100. package/src/host/game-realm-page.ts +1 -1
  101. package/src/host/gameplay-export.ts +25 -14
  102. package/src/host/gameplay-recording.ts +5 -5
  103. package/src/host/gated-globals.ts +2 -2
  104. package/src/host/history/json-history-resource.ts +1 -1
  105. package/src/host/projection/pixi.ts +24 -2
  106. package/src/host/r3f-entry-runtime.ts +65 -34
  107. package/src/host/react-mount-runtime.ts +7 -48
  108. package/src/host/realm-services.ts +1 -1
  109. package/src/host/roots/canvas-root.tsx +361 -0
  110. package/src/host/roots/r3f-root.tsx +473 -0
  111. package/src/host/roots/react-root.ts +9 -43
  112. package/src/host/served-bundle-runtime-modules.ts +1 -19
  113. package/src/host/server-log-bridge.ts +2 -2
  114. package/src/host/stories/mounted-story-viewport-source.ts +1 -1
  115. package/src/host/stories/pixi-story-model.ts +30 -0
  116. package/src/host/stories/story-media-captures.ts +46 -0
  117. package/src/host/stories/story-media-presence.ts +3 -3
  118. package/src/host/stories/story-pixi-preview.ts +408 -0
  119. package/src/host/stories/story-three-preview.ts +806 -0
  120. package/src/host/stories/three-story-captures.ts +35 -0
  121. package/src/host/story-three-preview-runtime.ts +56 -0
  122. package/src/host/use-active-performance-source.ts +2 -2
  123. package/src/host/viewport-pose-memory.ts +1 -1
  124. package/src/host/viewport-root-presentation.ts +6 -5
  125. package/src/ingest/active-ingest.ts +1 -1
  126. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
  127. package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
  128. package/src/ingest/deferred-ingest-play.ts +6 -6
  129. package/src/ingest/discovery-public-ingest.ts +2 -2
  130. package/src/ingest/ingest-boot-viewport.ts +2 -2
  131. package/src/ingest/ingest-canvas-scene-document.tsx +8 -8
  132. package/src/ingest/ingest-canvas-scene.ts +3 -3
  133. package/src/ingest/ingest-evidence-hook.ts +1 -1
  134. package/src/ingest/ingest-frame-snapshot.ts +1 -1
  135. package/src/ingest/ingest-play-control.ts +1 -1
  136. package/src/ingest/ingest-render-debug.ts +10 -10
  137. package/src/ingest/ingest-siblings.ts +13 -25
  138. package/src/ingest/module-mode.ts +14 -14
  139. package/src/ingest/mount-canvas-ingest-root.ts +21 -21
  140. package/src/ingest/mount-coverage.ts +2 -2
  141. package/src/ingest/mount-dom-ingest-root.ts +11 -11
  142. package/src/ingest/mount-ingest-root.ts +8 -8
  143. package/src/ingest/mount-three-ingest-root.ts +8 -8
  144. package/src/ingest/resolve-canvas.ts +1 -1
  145. package/src/ingest/served-html-boot.ts +1 -1
  146. package/src/ingest/surface-canvas.ts +1 -1
  147. package/src/ingest/unmount-ingest-root.ts +5 -5
  148. package/src/navmesh/navmesh-handler.ts +24 -16
  149. package/src/network/NetworkInspectorPanel.tsx +449 -14
  150. package/src/network/network-inspector-model.ts +14 -2
  151. package/src/play/play-log-events.ts +1 -1
  152. package/src/play/play-mode.ts +76 -133
  153. package/src/play/play-recording.ts +1 -1
  154. package/src/play/react-play-live-authoring.ts +3 -3
  155. package/src/play/run-selection.ts +93 -0
  156. package/src/play-bar/PlayBar.tsx +20 -39
  157. package/src/profiler/FrameDebuggerPanel.tsx +1 -1
  158. package/src/profiler/PerformancePanel.tsx +3 -3
  159. package/src/react/design-time-react-mount.ts +23 -65
  160. package/src/react/dom-authoring-adapter.ts +9 -9
  161. package/src/react/react-inspector-section.tsx +5 -5
  162. package/src/react/react-world-authoring-adapter.ts +11 -11
  163. package/src/react/story-documents/story-documents.tsx +9 -9
  164. package/src/react/ui-board-document.tsx +7 -7
  165. package/src/react/ui-component-board.ts +2 -2
  166. package/src/runtime/adapter/audio-meter.ts +21 -0
  167. package/src/runtime/adapter/first-party-audio-system.ts +230 -0
  168. package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
  169. package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
  170. package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
  171. package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
  172. package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
  173. package/src/runtime/adapter/native-debug-module.ts +498 -0
  174. package/src/runtime/audio/bus-mixer.ts +161 -0
  175. package/src/runtime/audio/pose-guard.ts +80 -0
  176. package/src/runtime/core/frame-pacing.ts +126 -0
  177. package/src/runtime/core/game-loop.ts +225 -0
  178. package/src/runtime/core/game-scoped-slot.ts +28 -0
  179. package/src/runtime/core/seeded-random.ts +162 -0
  180. package/src/runtime/core/sim-clock.ts +391 -0
  181. package/src/runtime/core/system-runner.ts +269 -0
  182. package/src/runtime/core/types.ts +104 -0
  183. package/src/runtime/create-runtime.ts +1128 -0
  184. package/src/runtime/debug-bridge.ts +570 -0
  185. package/src/runtime/debug-registry.ts +899 -0
  186. package/src/runtime/dev/chrome-trace.ts +153 -0
  187. package/src/runtime/dev/instruments.ts +403 -0
  188. package/src/runtime/dev/logger.ts +119 -0
  189. package/src/runtime/dev/performance-profiler.ts +367 -0
  190. package/src/runtime/dev/register-render-vitals.ts +276 -0
  191. package/src/runtime/dev/render-census.ts +354 -0
  192. package/src/runtime/dev/render-debug-adapter.ts +218 -0
  193. package/src/runtime/dev/render-memory.ts +226 -0
  194. package/src/runtime/dev/render-vitals.ts +338 -0
  195. package/src/runtime/dev/static-batch-advisor.ts +188 -0
  196. package/src/runtime/dev/webgl-frame-capture.ts +366 -0
  197. package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
  198. package/src/runtime/dev-build.ts +47 -0
  199. package/src/runtime/game.ts +1636 -0
  200. package/src/runtime/gameplay-rng-trap.ts +135 -0
  201. package/src/runtime/host-context.ts +64 -0
  202. package/src/runtime/input-router.ts +169 -0
  203. package/src/runtime/mount-manifest.ts +480 -0
  204. package/src/runtime/pixi/authoring.ts +706 -0
  205. package/src/runtime/pixi/ingest.ts +116 -0
  206. package/src/runtime/pixi/physics-registry.ts +49 -0
  207. package/src/runtime/pixi/render-pass-bracket.ts +117 -0
  208. package/src/runtime/pixi/scene-capture.ts +179 -0
  209. package/src/runtime/pixi/system-adapters.ts +69 -0
  210. package/src/runtime/playtest.ts +22 -0
  211. package/src/runtime/presentation.ts +141 -0
  212. package/src/runtime/render-control.ts +642 -0
  213. package/src/runtime/render-seed.ts +77 -0
  214. package/src/runtime/run-ticks-settled.ts +73 -0
  215. package/src/runtime/setup/setup-audio.ts +72 -0
  216. package/src/services/audio-pose-guard.ts +2 -2
  217. package/src/services/game-audio.ts +152 -0
  218. package/src/services/game-network.ts +657 -0
  219. package/src/services/game-physics.ts +334 -0
  220. package/src/state-watch/StateWatchPanel.tsx +1 -1
  221. package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
  222. package/src/three/authoring/constraint-inspector-section.tsx +6 -5
  223. package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
  224. package/src/three/authoring/oid-source-persistence.ts +7 -7
  225. package/src/three/authoring/r3f-design-session.ts +58 -54
  226. package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
  227. package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
  228. package/src/three/authoring/three-authoring-adapter.ts +29 -29
  229. package/src/three/component-verbs/extract-menu.ts +7 -6
  230. package/src/three/component-verbs/fork-menu.ts +7 -6
  231. package/src/three/component-verbs/internals-menu.ts +2 -2
  232. package/src/three/story-documents/three-story-documents.tsx +13 -13
  233. package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
  234. package/src/three/three-board/board-scene.ts +6 -6
  235. package/src/three/three-board/three-component-board.ts +2 -2
  236. package/src/services/game-audio-unlock.ts +0 -48
@@ -0,0 +1,116 @@
1
+ import type { VisibleCaptureWindow } from '@volter/threejs-runtime/adapter/ingest/visible-capture-window';
2
+ import {
3
+ type CapturedRuntime2D,
4
+ installSceneCapture2D,
5
+ type SceneCapture2DHandle,
6
+ } from './scene-capture';
7
+
8
+ /** A registered unmodified PixiJS game (the Pixi analog of `IngestGame`). */
9
+ export interface IngestGame2D {
10
+ id: string;
11
+ name: string;
12
+ description: string;
13
+ /** Import + run the unmodified game module in the host's own realm. */
14
+ load: () => Promise<unknown>;
15
+ /** How long to wait for the game's first captured frame before failing. */
16
+ captureTimeoutMs?: number;
17
+ }
18
+
19
+ /**
20
+ * The game's own modules failed to load or run — a DIFFERENT defect from
21
+ * "loaded, but the host's pixi never saw a frame", and named so a caller can
22
+ * tell them apart.
23
+ *
24
+ * Both used to arrive as a bare `Error`, so `mount-canvas-ingest-root.ts`'s
25
+ * one catch reported a module that 404'd, threw, or failed to transform in
26
+ * the capture trap's words — "the host's pixi captured no frame ... or it
27
+ * bundles its own (un-shared) copy of pixi.js". That sentence sends the
28
+ * reader to the wrong file: it describes a module-identity problem, and the
29
+ * measured cause was a JSX-bearing `.js` module the packaged host had no
30
+ * transform for.
31
+ *
32
+ * Classified by `name` ({@link INGEST_GAME_2D_LOAD_ERROR_NAME}) rather than
33
+ * `instanceof` — under the packaged runtime the editor shell and the project
34
+ * graph can hold different instances of a module, and this whole lane exists
35
+ * because of that; a discriminator that silently flips when the class object
36
+ * differs is the wrong tool for saying which failure happened.
37
+ */
38
+ export const INGEST_GAME_2D_LOAD_ERROR_NAME = 'IngestGame2DLoadError';
39
+
40
+ export class IngestGame2DLoadError extends Error {
41
+ constructor(gameId: string, cause: unknown) {
42
+ super(`pixi ingest: game "${gameId}" failed to load: ${cause}`, { cause });
43
+ this.name = INGEST_GAME_2D_LOAD_ERROR_NAME;
44
+ }
45
+ }
46
+
47
+ export interface IngestMount2D {
48
+ /** The captured live stage Container. */
49
+ stage: unknown;
50
+ capture: SceneCapture2DHandle;
51
+ setPaused(paused: boolean): void;
52
+ dispose(): void;
53
+ }
54
+
55
+ /**
56
+ * Mount an UNMODIFIED PixiJS game via the shared-instance capture path — the 2D
57
+ * analog of `mountIngestGame`. Installs the render trap on the host's pixi
58
+ * instance, runs the game's `load()`, waits for the game to render its first
59
+ * frame (capturing its live stage), and exposes loop gating + cleanup. A game
60
+ * whose pixi the trap cannot reach never captures, and the mount FAILS by name.
61
+ *
62
+ * `onWait` is the three lane's own seam (`ingest-root-adapter.ts` passes
63
+ * `setCaptureWait`): the capture window PARKS while the tab is hidden, and a
64
+ * parked wait is otherwise indistinguishable from a hung mount at every door.
65
+ * Threading it here is what lets `vgai status` say "waiting for the first
66
+ * visible frame — the tab is hidden" over a canvas ingest too.
67
+ */
68
+ export async function mountIngestGame2D(
69
+ pixiNamespace: unknown,
70
+ game: IngestGame2D,
71
+ opts: {
72
+ captureTimeoutMs?: number;
73
+ onWait?: (wait: VisibleCaptureWindow | null) => void;
74
+ acceptCapture?: (runtime: CapturedRuntime2D) => boolean;
75
+ /**
76
+ * Called after the entry has run, before spending the Pixi capture budget.
77
+ * A canvas host with other structural adapters can answer true once one of
78
+ * those runtimes is ready; `null` then means "dispatch that runtime" rather
79
+ * than "Pixi failed".
80
+ */
81
+ preferStructuralRuntime?: () => boolean | Promise<boolean>;
82
+ } = {},
83
+ ): Promise<IngestMount2D | null> {
84
+ const capture = installSceneCapture2D(pixiNamespace, {
85
+ ...(opts.acceptCapture ? { accept: opts.acceptCapture } : {}),
86
+ });
87
+ try {
88
+ await game.load();
89
+ if (await opts.preferStructuralRuntime?.()) {
90
+ capture.uninstall();
91
+ return null;
92
+ }
93
+ } catch (err) {
94
+ capture.uninstall();
95
+ throw new IngestGame2DLoadError(game.id, err);
96
+ }
97
+
98
+ const timeout = opts.captureTimeoutMs ?? game.captureTimeoutMs ?? 8000;
99
+ try {
100
+ const rt = await capture.waitForCapture({
101
+ timeoutMs: timeout,
102
+ ...(opts.onWait ? { onWait: opts.onWait } : {}),
103
+ });
104
+ return {
105
+ stage: rt.stage,
106
+ capture,
107
+ setPaused: (p) => capture.setPaused(p),
108
+ dispose: () => capture.uninstall(),
109
+ };
110
+ } catch (err) {
111
+ capture.uninstall();
112
+ throw new Error(
113
+ `pixi ingest: game "${game.id}" rendered no capturable frame within ${timeout}ms: ${err}`,
114
+ );
115
+ }
116
+ }
@@ -0,0 +1,49 @@
1
+ import type RAPIER from '@dimforge/rapier2d-compat';
2
+ import type { Container } from 'pixi.js';
3
+
4
+ /** The Rapier 2D handles owned by a single display-object entity. */
5
+ export interface Physics2DRefs {
6
+ body: RAPIER.RigidBody;
7
+ collider: RAPIER.Collider;
8
+ }
9
+
10
+ /**
11
+ * Maps PixiJS display objects to their Rapier 2D bodies/colliders and back —
12
+ * the Pixi analog of `PhysicsRegistry`. Two indexes:
13
+ * - `Container → {body, collider}` for the postPhysics transform writer.
14
+ * - `colliderHandle → Container` reverse index for collision/trigger dispatch.
15
+ */
16
+ export function createPhysics2DRegistry() {
17
+ const byObject = new Map<Container, Physics2DRefs>();
18
+ const byColliderHandle = new Map<number, Container>();
19
+
20
+ return {
21
+ add(display: Container, body: RAPIER.RigidBody, collider: RAPIER.Collider): void {
22
+ byObject.set(display, { body, collider });
23
+ byColliderHandle.set(collider.handle, display);
24
+ },
25
+ get(display: Container): Physics2DRefs | undefined {
26
+ return byObject.get(display);
27
+ },
28
+ getByColliderHandle(handle: number): Container | undefined {
29
+ return byColliderHandle.get(handle);
30
+ },
31
+ remove(display: Container): void {
32
+ const refs = byObject.get(display);
33
+ if (refs) byColliderHandle.delete(refs.collider.handle);
34
+ byObject.delete(display);
35
+ },
36
+ entries(): IterableIterator<[Container, Physics2DRefs]> {
37
+ return byObject.entries();
38
+ },
39
+ get size(): number {
40
+ return byObject.size;
41
+ },
42
+ clear(): void {
43
+ byObject.clear();
44
+ byColliderHandle.clear();
45
+ },
46
+ };
47
+ }
48
+
49
+ export type Physics2DRegistry = ReturnType<typeof createPhysics2DRegistry>;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The render-pass BRACKET on the Pixi surface — the canvas-substrate answer to
3
+ * the question the three lane answers with the render accessor-trap's
4
+ * `setRenderPassHooks` (`adapter/ingest/scene-capture.ts`): *when does this
5
+ * renderer's pass begin and end?*
6
+ *
7
+ * ## Why the renderer's own runners, and not another wrap
8
+ *
9
+ * Pixi's `AbstractRenderer.render()` emits five runners in a fixed order —
10
+ * `prerender → renderStart → render → renderEnd → postrender` (verified in the
11
+ * installed pixi.js 8.19,
12
+ * `rendering/renderers/shared/system/AbstractRenderer.mjs`). EVERY GL draw the
13
+ * pass issues happens between the first and the last: the batcher draws under
14
+ * `render`, and even the back-buffer blit, the one draw that is easy to miss,
15
+ * runs under `renderStart`/`renderEnd` (`gl/GlBackBufferSystem.mjs`). So a
16
+ * listener added to `prerender` and `postrender` brackets the whole pass using
17
+ * the renderer's OWN published seam — no instance shadowing, no prototype
18
+ * patch, no ordering assumption beyond the one Pixi itself guarantees.
19
+ *
20
+ * `SystemRunner.add(item)` binds by METHOD NAME (`item[runnerName]` must
21
+ * exist) and `remove(item)` takes it back off, so installing and uninstalling
22
+ * is symmetric and touches nothing else on the renderer.
23
+ *
24
+ * ## Honest absence
25
+ *
26
+ * A renderer that publishes no such runners is not bracketed by guesswork:
27
+ * this returns `null`, and the caller treats that exactly like a missing
28
+ * WebGL2 context — the capability is absent, never faked.
29
+ */
30
+
31
+ import type { RenderPassHooks } from '@volter/threejs-runtime/adapter/ingest/scene-capture';
32
+
33
+ /** The slice of a Pixi renderer this reads — structural, so a headless test
34
+ * hands in a plain object with two runners and nothing else. */
35
+ interface RunnerLike {
36
+ add(item: unknown): unknown;
37
+ remove(item: unknown): unknown;
38
+ }
39
+
40
+ interface RendererWithRunners {
41
+ runners?: {
42
+ prerender?: RunnerLike;
43
+ postrender?: RunnerLike;
44
+ };
45
+ }
46
+
47
+ function runnerPair(renderer: unknown): { pre: RunnerLike; post: RunnerLike } | null {
48
+ if (!renderer || typeof renderer !== 'object') return null;
49
+ const runners = (renderer as RendererWithRunners).runners;
50
+ const pre = runners?.prerender;
51
+ const post = runners?.postrender;
52
+ if (!pre || typeof pre.add !== 'function' || typeof pre.remove !== 'function') return null;
53
+ if (!post || typeof post.add !== 'function' || typeof post.remove !== 'function') return null;
54
+ return { pre, post };
55
+ }
56
+
57
+ /**
58
+ * Bracket a Pixi renderer's render pass with `hooks.before`/`hooks.after`.
59
+ *
60
+ * Returns the uninstall function, or `null` when this renderer publishes no
61
+ * `prerender`/`postrender` runners — the honest "cannot bracket this", which
62
+ * the caller reads as "no render-debug capability here".
63
+ *
64
+ * Neither hook may throw — the same contract the three lane's `RenderPassHooks`
65
+ * carries, and for a sharper reason here: these run as listeners on the
66
+ * renderer's own runners, so a throwing `before` aborts the game's frame AND
67
+ * skips the `postrender` emit that would have closed the pass. The assembly
68
+ * point (`ingest/ingest-render-debug.ts`) is where that contract is kept.
69
+ */
70
+ export function installPixiRenderPassBracket(
71
+ renderer: unknown,
72
+ hooks: RenderPassHooks,
73
+ ): (() => void) | null {
74
+ const pair = runnerPair(renderer);
75
+ if (!pair) return null;
76
+
77
+ // One object per runner: `SystemRunner.add` binds by method name, so the
78
+ // `prerender` listener and the `postrender` listener cannot be the same
79
+ // object unless it answers to both — keeping them separate makes each
80
+ // `remove()` exact.
81
+ const preListener = {
82
+ prerender() {
83
+ hooks.before();
84
+ },
85
+ };
86
+ const postListener = {
87
+ postrender() {
88
+ hooks.after();
89
+ },
90
+ };
91
+
92
+ pair.pre.add(preListener);
93
+ pair.post.add(postListener);
94
+
95
+ let uninstalled = false;
96
+ return () => {
97
+ if (uninstalled) return;
98
+ uninstalled = true;
99
+ pair.pre.remove(preListener);
100
+ pair.post.remove(postListener);
101
+ };
102
+ }
103
+
104
+ /**
105
+ * The captured Pixi renderer's real WebGL2 context, or `undefined`.
106
+ *
107
+ * `WebGLRenderer.gl` is the renderer's own published field — `GlContextSystem`
108
+ * assigns it (`this._renderer.gl = gl`,
109
+ * `rendering/renderers/gl/context/GlContextSystem.mjs`) as part of
110
+ * initialisation, and `WebGLRenderer.d.ts` declares it. A WebGPU renderer, or
111
+ * a renderer that has not initialised a context, simply has none — which is the
112
+ * answer, not an error.
113
+ */
114
+ export function pixiRenderingContext(renderer: unknown): unknown {
115
+ if (!renderer || typeof renderer !== 'object') return undefined;
116
+ return (renderer as { gl?: unknown }).gl;
117
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Scene capture for the Pixi surface — the PixiJS analog of the three.js render
3
+ * accessor-trap (`adapter/ingest/scene-capture.ts`).
4
+ *
5
+ * An unmodified PixiJS game owns its own `Application`, `stage`, renderer, and
6
+ * ticker. To inspect/host that live scene with ZERO edits to the game, we trap the
7
+ * render call and capture the game's `stage` + renderer + app on its first frame.
8
+ *
9
+ * Unlike three.js's `WebGLRenderer` (whose `render` is an own-instance property,
10
+ * forcing a prototype getter/setter), PixiJS's `Application.prototype.render` is a
11
+ * normal prototype method — the standard ticker render path (`ticker → app.render()`)
12
+ * goes through it — so a direct prototype wrap suffices.
13
+ *
14
+ * CRITICAL (same module-identity gatekeeper as the 3D path): the trap must be
15
+ * installed on the SAME `pixi.js` module instance the game uses. A bundler/dev-server
16
+ * that dedupes `pixi.js` makes an ESM game's `import 'pixi.js'` resolve to the host
17
+ * instance, so it is trapped; a game bundling its own pixi cannot be captured at
18
+ * all, and its mount fails by name.
19
+ */
20
+
21
+ import {
22
+ type CaptureWaitOptions,
23
+ documentVisibilityClock,
24
+ startVisibleCaptureWindow,
25
+ } from '@volter/threejs-runtime/adapter/ingest/visible-capture-window';
26
+
27
+ /**
28
+ * Options for {@link SceneCapture2DHandle.waitForCapture} — the WAIT's options,
29
+ * not this lane's, so they carry no `2D` suffix and are not declared here.
30
+ * They configure `visible-capture-window.ts`'s window, which both surfaces
31
+ * share; this lane used to restate them field-for-field under a `2D` name.
32
+ * The suffix marks what a surface genuinely owns
33
+ * ({@link CapturedRuntime2D} is a Pixi stage/renderer/app, and stays), and a
34
+ * budget of visible milliseconds is not that. Passing a bare number is
35
+ * `{ timeoutMs }`, which is what every existing caller does.
36
+ */
37
+ export type { CaptureWaitOptions };
38
+
39
+ /** A live Pixi runtime captured from an external PixiJS game. */
40
+ export interface CapturedRuntime2D {
41
+ /** The game's root stage Container. */
42
+ stage: unknown;
43
+ /** The game's PixiJS renderer. */
44
+ renderer: unknown;
45
+ /** The game's Application (so the host can pause/resume its ticker). */
46
+ app: unknown;
47
+ }
48
+
49
+ export interface SceneCapture2DHandle {
50
+ readonly captured: CapturedRuntime2D | null;
51
+ /**
52
+ * Resolve once the game's stage is captured.
53
+ *
54
+ * The timeout is a budget of **VISIBLE** time, not wall-clock time — the same
55
+ * rule the three lane has had since `visible-capture-window.ts`, and for the
56
+ * same measured reason: a hidden document cannot render (the browser parks
57
+ * rAF), so counting hidden time against the game counts time it was not
58
+ * allowed to use. MEASURED on the bubbo-bubbo ingest with a plain
59
+ * `setTimeout` here: the mount succeeded when the auto-opened tab happened to
60
+ * be in front and died terminally 8s later when it was behind — "canvas
61
+ * ingest: game … rendered no capturable frame", with foregrounding the tab
62
+ * afterwards changing nothing. The normal HUMAN path is the backgrounded one.
63
+ *
64
+ * So the wait PARKS while hidden, the trap stays installed, and the first
65
+ * frame the tab draws after it comes forward is trapped exactly as it would
66
+ * have been at boot. Rejects only when the window is spent while VISIBLE.
67
+ */
68
+ waitForCapture(options?: number | CaptureWaitOptions): Promise<CapturedRuntime2D>;
69
+ getDrawCount(): number;
70
+ /** Pause/resume the captured game's own ticker (loop-host gating). */
71
+ setPaused(paused: boolean): void;
72
+ uninstall(): void;
73
+ }
74
+
75
+ interface PixiLike {
76
+ Application: { prototype: Record<string, unknown> };
77
+ }
78
+
79
+ export interface SceneCapture2DOptions {
80
+ /**
81
+ * Select the runtime owned by this mount when other Pixi Applications share
82
+ * the page (for example editor prefab previews). Rejected draws still count,
83
+ * but cannot satisfy {@link SceneCapture2DHandle.waitForCapture}.
84
+ */
85
+ accept?: ((runtime: CapturedRuntime2D) => boolean) | undefined;
86
+ }
87
+
88
+ /**
89
+ * Install the render trap on `pixiNamespace.Application.prototype.render`. Pass the
90
+ * host's `pixi.js` namespace so a shared-instance game is trapped. Install once per
91
+ * ingest session; `uninstall()` on teardown.
92
+ */
93
+ export function installSceneCapture2D(
94
+ pixiNamespace: unknown,
95
+ options: SceneCapture2DOptions = {},
96
+ ): SceneCapture2DHandle {
97
+ const PIXI = pixiNamespace as PixiLike;
98
+ const proto = PIXI.Application.prototype;
99
+
100
+ let captured: CapturedRuntime2D | null = null;
101
+ let drawCount = 0;
102
+ const waiters: Array<(rt: CapturedRuntime2D) => void> = [];
103
+
104
+ const priorRender = Object.getOwnPropertyDescriptor(proto, 'render');
105
+ const realRender = proto['render'] as ((...a: unknown[]) => unknown) | undefined;
106
+
107
+ Object.defineProperty(proto, 'render', {
108
+ configurable: true,
109
+ writable: true,
110
+ value: function (this: Record<string, unknown>, ...args: unknown[]) {
111
+ drawCount++;
112
+ const stage = this['stage'];
113
+ if (!captured && stage) {
114
+ const candidate = { stage, renderer: this['renderer'], app: this };
115
+ if (options.accept?.(candidate) ?? true) {
116
+ captured = candidate;
117
+ for (const resolve of waiters.splice(0)) resolve(captured);
118
+ }
119
+ }
120
+ return realRender?.apply(this, args);
121
+ },
122
+ });
123
+
124
+ return {
125
+ get captured() {
126
+ return captured;
127
+ },
128
+ getDrawCount() {
129
+ return drawCount;
130
+ },
131
+ setPaused(paused: boolean) {
132
+ const app = captured?.app as { ticker?: { start(): void; stop(): void } } | undefined;
133
+ if (!app?.ticker) return;
134
+ if (paused) app.ticker.stop();
135
+ else app.ticker.start();
136
+ },
137
+ waitForCapture(options) {
138
+ const opts: CaptureWaitOptions =
139
+ typeof options === 'number' ? { timeoutMs: options } : (options ?? {});
140
+ const timeoutMs = opts.timeoutMs ?? 10_000;
141
+ if (captured) return Promise.resolve(captured);
142
+ return new Promise<CapturedRuntime2D>((resolve, reject) => {
143
+ const captureWindow = startVisibleCaptureWindow({
144
+ budgetMs: timeoutMs,
145
+ clock: opts.visibility ?? documentVisibilityClock(),
146
+ onExpire: () => {
147
+ const i = waiters.indexOf(wrapped);
148
+ if (i >= 0) waiters.splice(i, 1);
149
+ opts.onWait?.(null);
150
+ reject(
151
+ new Error(
152
+ `pixi scene capture timed out after ${timeoutMs}ms of VISIBLE time ` +
153
+ `(${Math.round(captureWindow.elapsedHiddenMs())}ms browser-suspended, which is ` +
154
+ 'not counted because no frame can be presented) — the game never rendered, or it bundles its own ' +
155
+ '(un-shared) copy of pixi.js.',
156
+ ),
157
+ );
158
+ },
159
+ });
160
+ const wrapped = (rt: CapturedRuntime2D) => {
161
+ captureWindow.cancel();
162
+ opts.onWait?.(null);
163
+ resolve(rt);
164
+ };
165
+ waiters.push(wrapped);
166
+ opts.onWait?.(captureWindow);
167
+ });
168
+ },
169
+ uninstall() {
170
+ if (priorRender) Object.defineProperty(proto, 'render', priorRender);
171
+ else if (realRender)
172
+ Object.defineProperty(proto, 'render', {
173
+ configurable: true,
174
+ writable: true,
175
+ value: realRender,
176
+ });
177
+ },
178
+ };
179
+ }
@@ -0,0 +1,69 @@
1
+ import type { PhysicsAdapter2D as PhysicsAdapter2DSeam } from '@volter/editor-project/adapter/physics-adapter-2d';
2
+ import type { Container } from 'pixi.js';
3
+ import type { Physics2DRegistry } from './physics-registry';
4
+
5
+ /** The canvas physics seam, keyed by Pixi's display object. */
6
+ export type PhysicsAdapter2D = PhysicsAdapter2DSeam<Container>;
7
+
8
+ export function createPhysicsAdapter2D(physics: Physics2DRegistry): PhysicsAdapter2D {
9
+ // Remember each frozen body's prior type so unfreeze restores it.
10
+ const frozen = new WeakMap<Container, number>();
11
+ return {
12
+ keyedBy: 'display',
13
+ ownerOf(display) {
14
+ const refs = physics.get(display);
15
+ if (!refs) return 'none';
16
+ return refs.body.isDynamic() || refs.body.isKinematic() ? 'physics' : 'none';
17
+ },
18
+ freeze(display) {
19
+ const refs = physics.get(display);
20
+ if (!refs) return;
21
+ frozen.set(display, refs.body.bodyType());
22
+ // Kinematic-position bodies hold a pose without being driven by forces.
23
+ refs.body.setBodyType(2 /* KinematicPositionBased */, true);
24
+ },
25
+ commit(display, position, rotation) {
26
+ const refs = physics.get(display);
27
+ if (!refs) return;
28
+ refs.body.setTranslation({ x: position[0], y: position[1] }, true);
29
+ refs.body.setRotation(rotation, true);
30
+ },
31
+ unfreeze(display) {
32
+ const refs = physics.get(display);
33
+ if (!refs) return;
34
+ const prior = frozen.get(display);
35
+ if (prior !== undefined) {
36
+ refs.body.setBodyType(prior, true);
37
+ frozen.delete(display);
38
+ }
39
+ },
40
+ };
41
+ }
42
+
43
+ /**
44
+ * ONE carrier over several, each owning a DIFFERENT set of display objects.
45
+ *
46
+ * The canvas ingest mount is the case: the host always builds a registry-backed
47
+ * carrier for objects the editor itself created, and an ingested game may ALSO
48
+ * declare one over its own simulation's objects. Neither is a superset of the
49
+ * other and neither can answer for the other's objects, so the composition is
50
+ * ownership-routed rather than layered — `ownerOf` asks each in order and the
51
+ * FIRST to claim the object handles every other call for it. A display object
52
+ * nobody claims answers `'none'` and the freeze/commit/unfreeze trio no-ops,
53
+ * which is exactly what a single unclaiming carrier already does.
54
+ *
55
+ * Order is precedence: earlier carriers win a (never-expected) double claim,
56
+ * so the caller states its own preference instead of this function inventing
57
+ * one.
58
+ */
59
+ export function composePhysicsAdapters2D(carriers: readonly PhysicsAdapter2D[]): PhysicsAdapter2D {
60
+ const ownerFor = (display: Container): PhysicsAdapter2D | null =>
61
+ carriers.find((carrier) => carrier.ownerOf(display) === 'physics') ?? null;
62
+ return {
63
+ keyedBy: 'display',
64
+ ownerOf: (display) => (ownerFor(display) ? 'physics' : 'none'),
65
+ freeze: (display) => ownerFor(display)?.freeze(display),
66
+ commit: (display, position, rotation) => ownerFor(display)?.commit(display, position, rotation),
67
+ unfreeze: (display) => ownerFor(display)?.unfreeze(display),
68
+ };
69
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * WHO IS PLAYING, and under what identity — the host's own declaration for one
3
+ * private play run or coordinated team test.
4
+ *
5
+ * Its own module because it is a PUBLISHED path: a project's Colyseus
6
+ * networking adapter (the `colyseus` capability, copied into every game) names
7
+ * this type, so it must live somewhere that survives. The engine never joins a
8
+ * room and never couples this authoring identity to a networking library —
9
+ * games may pass `roomKey` as a Colyseus filter/join option, and that is the
10
+ * whole of the coupling.
11
+ */
12
+
13
+ /** Host-supplied identity for one private play run or coordinated team test.
14
+ * Games may pass `roomKey` as a Colyseus filter/join option; the engine never
15
+ * joins a room or couples this authoring identity to a networking library. */
16
+ export interface PlaytestContext {
17
+ readonly mode: 'private' | 'team';
18
+ readonly id: string;
19
+ readonly roomKey: string;
20
+ readonly revision: number | null;
21
+ readonly participantId: string | null;
22
+ }