@volter/editor-game 0.5.66 → 0.5.68
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.
- package/contributions/audio-unlock.service.ts +2 -2
- package/contributions/autoplay.service.ts +1 -1
- package/contributions/bridge.command.ts +3 -3
- package/contributions/build-player.document.tsx +95 -0
- package/contributions/canvas/component-board.service.ts +16 -0
- package/contributions/canvas/design-time-mount.service.ts +45 -0
- package/contributions/canvas-story-capture.service.ts +12 -0
- package/contributions/edit-mode-audio.service.ts +2 -2
- package/contributions/edit-mode-networking.service.ts +1 -1
- package/contributions/gameplay.command.ts +4 -4
- package/contributions/generation.service.ts +1 -1
- package/contributions/godot.style.ts +26 -6
- package/contributions/godot.view.ts +6 -0
- package/contributions/ingest.service.ts +2 -2
- package/contributions/instances.command.ts +1 -1
- package/contributions/navmesh.menu.ts +1 -1
- package/contributions/network-observer.service.ts +14 -0
- package/contributions/play.command.ts +10 -4
- package/contributions/react/component-board.service.ts +1 -1
- package/contributions/react/design-time-mount.service.ts +1 -1
- package/contributions/scene-document.service.ts +3 -3
- package/contributions/state-watch.menu.ts +1 -1
- package/contributions/state-watch.utility.tsx +1 -1
- package/contributions/team-playtest.service.ts +4 -4
- package/contributions/three/component-board.service.ts +1 -1
- package/contributions/three/component-verbs.command.ts +6 -6
- package/contributions/three/three-authoring.service.ts +8 -5
- package/contributions/three-story-capture.service.ts +12 -0
- package/contributions/unity.style.ts +17 -7
- package/contributions/unity.view.ts +6 -0
- package/contributions/unreal.style.ts +10 -2
- package/contributions/unreal.view.ts +5 -0
- package/package.json +21 -10
- package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
- package/src/asset-budget/asset-budget-model.ts +1 -1
- package/src/audio/AudioDebuggerPanel.tsx +1 -1
- package/src/bridge/dispatch.ts +14 -14
- package/src/bridge/live-frames.ts +1 -1
- package/src/bridge/screenshot.ts +3 -3
- package/src/build/BuildProfilesPanel.tsx +14 -3
- package/src/build/build-session.ts +35 -0
- package/src/canvas/canvas-board/CanvasBoardDocument.tsx +749 -0
- package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
- package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
- package/src/canvas/canvas-design-mount.ts +524 -0
- package/src/canvas/design-time-canvas-mount.ts +79 -0
- package/src/coverage/live-authoring-surface.ts +5 -5
- package/src/coverage/live-project-verbs.ts +1 -1
- package/src/coverage/native-system-coverage.ts +5 -5
- package/src/coverage/root-coverage.ts +1 -1
- package/src/coverage/session-coverage.ts +3 -3
- package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
- package/src/edit-mode/edit-mode-audio.ts +4 -4
- package/src/edit-mode/edit-mode-networking.ts +2 -2
- package/src/game-document/GameCaptureFrameButton.tsx +1 -1
- package/src/game-document/GameDocument.tsx +2 -2
- package/src/game-document/GamePanel.tsx +4 -4
- package/src/game-document/InstanceInspectorPicker.tsx +2 -2
- package/src/game-document/crowd-debug.ts +1 -1
- package/src/game-document/device-preview.ts +10 -11
- package/src/game-document/physics-debug.ts +1 -1
- package/src/generation/GenerationActivity.tsx +3 -3
- package/src/generation/generation-documents.tsx +5 -5
- package/src/generation/generation-jobs.ts +1 -1
- package/src/host/adapter-runtime-bindings.ts +96 -5
- package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
- package/src/host/authoring/gesture-persist.ts +1 -1
- package/src/host/authoring/ingest-data-writer.ts +1 -1
- package/src/host/authoring/ingest-source-persistence.ts +4 -4
- package/src/host/authoring/mounted-authoring.ts +4 -4
- package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
- package/src/host/authoring/pixi-authoring-adapter.ts +178 -31
- package/src/host/authoring/pixi-creatable-kinds.ts +62 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
- package/src/host/authoring/pixi-live-write-target.ts +4 -4
- package/src/host/authoring/pixi-source-identity.ts +3 -3
- package/src/host/authoring/pixi-source-write-target.ts +1709 -0
- package/src/host/authoring/pixi-still-presentation.ts +1 -1
- package/src/host/authoring/pixi-structure-history.ts +2 -2
- package/src/host/authoring/pixi-transform-channels.ts +16 -14
- package/src/host/authoring/source-persistence-backend.ts +3 -3
- package/src/host/authoring/struct-write-pipe.ts +1 -1
- package/src/host/binding-resolver.ts +8 -9
- package/src/host/browser-transpile.ts +1 -1
- package/src/host/canvas-entry-runtime.ts +58 -47
- package/src/host/canvas-preview-frames.ts +482 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
- package/src/host/components/HeaderTelemetry.tsx +4 -4
- package/src/host/components/PixiIsolationSceneContent.tsx +11 -11
- package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
- package/src/host/components/frame-debugger-model.ts +2 -2
- package/src/host/components/header-telemetry-model.ts +2 -2
- package/src/host/components/scene-document.tsx +14 -14
- package/src/host/components/utility-view-state.ts +1 -1
- package/src/host/components/world-root-stage-binding.tsx +12 -12
- package/src/host/components/world-root-stage.ts +70 -49
- package/src/host/coverage/system-adapter-coverage.ts +3 -4
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
- package/src/host/document-preview-three.ts +1 -1
- package/src/host/entry-adjudication.ts +6 -6
- package/src/host/game-css-scope-transform.ts +4 -0
- package/src/host/game-realm-page.ts +1 -1
- package/src/host/gameplay-export.ts +25 -14
- package/src/host/gameplay-recording.ts +5 -5
- package/src/host/gated-globals.ts +2 -2
- package/src/host/history/json-history-resource.ts +1 -1
- package/src/host/projection/pixi.ts +24 -2
- package/src/host/r3f-entry-runtime.ts +65 -34
- package/src/host/react-mount-runtime.ts +7 -48
- package/src/host/realm-services.ts +1 -1
- package/src/host/roots/canvas-root.tsx +373 -0
- package/src/host/roots/r3f-root.tsx +473 -0
- package/src/host/roots/react-root.ts +9 -43
- package/src/host/served-bundle-runtime-modules.ts +3 -19
- package/src/host/server-log-bridge.ts +2 -2
- package/src/host/stories/mounted-story-viewport-source.ts +1 -1
- package/src/host/stories/pixi-story-model.ts +30 -0
- package/src/host/stories/story-media-captures.ts +46 -0
- package/src/host/stories/story-media-presence.ts +3 -3
- package/src/host/stories/story-pixi-preview.ts +408 -0
- package/src/host/stories/story-three-preview.ts +806 -0
- package/src/host/stories/three-story-captures.ts +35 -0
- package/src/host/story-three-preview-runtime.ts +56 -0
- package/src/host/use-active-performance-source.ts +2 -2
- package/src/host/viewport-pose-memory.ts +1 -1
- package/src/host/viewport-root-presentation.ts +6 -5
- package/src/ingest/active-ingest.ts +1 -1
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
- package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
- package/src/ingest/deferred-ingest-play.ts +6 -6
- package/src/ingest/discovery-public-ingest.ts +2 -2
- package/src/ingest/ingest-boot-viewport.ts +2 -2
- package/src/ingest/ingest-canvas-scene-document.tsx +9 -9
- package/src/ingest/ingest-canvas-scene.ts +3 -3
- package/src/ingest/ingest-evidence-hook.ts +1 -1
- package/src/ingest/ingest-frame-snapshot.ts +1 -1
- package/src/ingest/ingest-play-control.ts +1 -1
- package/src/ingest/ingest-render-debug.ts +10 -10
- package/src/ingest/ingest-siblings.ts +13 -25
- package/src/ingest/module-mode.ts +14 -14
- package/src/ingest/mount-canvas-ingest-root.ts +21 -21
- package/src/ingest/mount-coverage.ts +2 -2
- package/src/ingest/mount-dom-ingest-root.ts +11 -11
- package/src/ingest/mount-ingest-root.ts +8 -8
- package/src/ingest/mount-three-ingest-root.ts +8 -8
- package/src/ingest/resolve-canvas.ts +1 -1
- package/src/ingest/served-html-boot.ts +1 -1
- package/src/ingest/surface-canvas.ts +1 -1
- package/src/ingest/unmount-ingest-root.ts +5 -5
- package/src/navmesh/navmesh-handler.ts +24 -16
- package/src/network/NetworkInspectorPanel.tsx +939 -37
- package/src/network/network-inspector-model.ts +20 -2
- package/src/play/play-log-events.ts +1 -1
- package/src/play/play-mode.ts +76 -133
- package/src/play/play-recording.ts +1 -1
- package/src/play/react-play-live-authoring.ts +3 -3
- package/src/play/run-selection.ts +93 -0
- package/src/play-bar/PlayBar.tsx +20 -39
- package/src/profiler/FrameDebuggerPanel.tsx +1 -1
- package/src/profiler/PerformancePanel.tsx +3 -3
- package/src/react/design-time-react-mount.ts +23 -65
- package/src/react/dom-authoring-adapter.ts +9 -9
- package/src/react/react-inspector-section.tsx +5 -5
- package/src/react/react-world-authoring-adapter.ts +11 -11
- package/src/react/story-documents/story-documents.tsx +9 -9
- package/src/react/ui-board-document.tsx +7 -7
- package/src/react/ui-component-board.ts +2 -2
- package/src/runtime/adapter/audio-meter.ts +21 -0
- package/src/runtime/adapter/first-party-audio-system.ts +230 -0
- package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
- package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
- package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
- package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
- package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
- package/src/runtime/adapter/native-debug-module.ts +498 -0
- package/src/runtime/audio/bus-mixer.ts +161 -0
- package/src/runtime/audio/pose-guard.ts +80 -0
- package/src/runtime/core/frame-pacing.ts +126 -0
- package/src/runtime/core/game-loop.ts +225 -0
- package/src/runtime/core/game-scoped-slot.ts +28 -0
- package/src/runtime/core/seeded-random.ts +162 -0
- package/src/runtime/core/sim-clock.ts +391 -0
- package/src/runtime/core/system-runner.ts +269 -0
- package/src/runtime/core/types.ts +104 -0
- package/src/runtime/create-runtime.ts +1128 -0
- package/src/runtime/debug-bridge.ts +570 -0
- package/src/runtime/debug-registry.ts +899 -0
- package/src/runtime/dev/chrome-trace.ts +153 -0
- package/src/runtime/dev/instruments.ts +403 -0
- package/src/runtime/dev/logger.ts +119 -0
- package/src/runtime/dev/performance-profiler.ts +367 -0
- package/src/runtime/dev/register-render-vitals.ts +276 -0
- package/src/runtime/dev/render-census.ts +354 -0
- package/src/runtime/dev/render-debug-adapter.ts +218 -0
- package/src/runtime/dev/render-memory.ts +226 -0
- package/src/runtime/dev/render-vitals.ts +338 -0
- package/src/runtime/dev/static-batch-advisor.ts +188 -0
- package/src/runtime/dev/webgl-frame-capture.ts +366 -0
- package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
- package/src/runtime/dev-build.ts +47 -0
- package/src/runtime/game.ts +1636 -0
- package/src/runtime/gameplay-rng-trap.ts +135 -0
- package/src/runtime/host-context.ts +64 -0
- package/src/runtime/input-router.ts +182 -0
- package/src/runtime/mount-manifest.ts +480 -0
- package/src/runtime/pixi/authoring.ts +706 -0
- package/src/runtime/pixi/ingest.ts +116 -0
- package/src/runtime/pixi/physics-registry.ts +49 -0
- package/src/runtime/pixi/render-pass-bracket.ts +117 -0
- package/src/runtime/pixi/scene-capture.ts +179 -0
- package/src/runtime/pixi/system-adapters.ts +69 -0
- package/src/runtime/playtest.ts +22 -0
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-control.ts +642 -0
- package/src/runtime/render-seed.ts +77 -0
- package/src/runtime/run-ticks-settled.ts +73 -0
- package/src/runtime/setup/setup-audio.ts +72 -0
- package/src/services/audio-pose-guard.ts +2 -2
- package/src/services/game-audio.ts +152 -0
- package/src/services/game-network.ts +767 -0
- package/src/services/game-physics.ts +334 -0
- package/src/state-watch/StateWatchPanel.tsx +1 -1
- package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
- package/src/three/authoring/constraint-inspector-section.tsx +6 -5
- package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
- package/src/three/authoring/oid-source-persistence.ts +7 -7
- package/src/three/authoring/r3f-design-session.ts +58 -54
- package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
- package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
- package/src/three/authoring/three-authoring-adapter.ts +29 -29
- package/src/three/component-verbs/extract-menu.ts +7 -6
- package/src/three/component-verbs/fork-menu.ts +7 -6
- package/src/three/component-verbs/internals-menu.ts +2 -2
- package/src/three/story-documents/three-story-documents.tsx +13 -13
- package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
- package/src/three/three-board/board-scene.ts +6 -6
- package/src/three/three-board/three-component-board.ts +2 -2
- 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
|
+
}
|