@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,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor's mount of a `canvas` root whose entry module DEFAULT-EXPORTS a
|
|
3
|
+
* React component — the world a game's own boot renders with `@pixi/react`:
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* export default function World() {
|
|
7
|
+
* return <pixiContainer><pixiSprite texture={Texture.WHITE} /></pixiContainer>;
|
|
8
|
+
* }
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* The world is an ordinary `@pixi/react` app. The editor renders it bare and
|
|
12
|
+
* advances Pixi's real (never-started) ticker on GAME time. A game's input is
|
|
13
|
+
* its own: the session's input door reaches it through the entry's `debug.input`
|
|
14
|
+
* export.
|
|
15
|
+
*
|
|
16
|
+
* WHOSE REACT, RECONCILER AND PIXI. The entry's hooks resolve `react`,
|
|
17
|
+
* `@pixi/react` and `pixi.js` through the project's module graph, so the root
|
|
18
|
+
* that renders it must be built with the same instances: {@link CanvasRuntime}
|
|
19
|
+
* is handed in by `canvas-entry-runtime.ts`, which takes them from the canvas
|
|
20
|
+
* doorway under the packaged runtime and loads its own otherwise. Only types
|
|
21
|
+
* are imported here, so a three-only project's editor bundle never carries
|
|
22
|
+
* the Pixi renderer.
|
|
23
|
+
*
|
|
24
|
+
* ## The loop contract
|
|
25
|
+
*
|
|
26
|
+
* `Application` is initialized with `autoStart: false` and `sharedTicker:
|
|
27
|
+
* false`, so it owns a native Pixi ticker that is NEVER started and therefore
|
|
28
|
+
* never requests an animation frame. `mounted.update(dt)` is the only thing
|
|
29
|
+
* that advances that ticker, and it feeds GAME time, not wall time — which is
|
|
30
|
+
* what makes the world freeze exactly when the host freezes it, and what makes
|
|
31
|
+
* `game.waitSimTime` work in a hidden tab (a hidden tab has no rAF; the engine
|
|
32
|
+
* loop's own catch-up still calls `update`, and Pixi has no independent clock
|
|
33
|
+
* to disagree with it).
|
|
34
|
+
*
|
|
35
|
+
* ## Component catalogue
|
|
36
|
+
*
|
|
37
|
+
* `@pixi/react` resolves `<pixiSprite>` etc. through a module-global catalogue
|
|
38
|
+
* that starts EMPTY — the first intrinsic in an un-extended tree throws
|
|
39
|
+
* "X is not part of the PIXI namespace! Did you forget to extend?". This
|
|
40
|
+
* module extends it with the whole `pixi.js` namespace once per mount
|
|
41
|
+
* (idempotent `Object.assign`). A world that adds its own classes
|
|
42
|
+
* (`@pixi/tilemap`, a custom `Container` subclass) calls `extend`/`useExtend`
|
|
43
|
+
* itself.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import type * as PixiReact from '@pixi/react';
|
|
47
|
+
import type { MountedCanvasRoot, MountedCanvasSubstrate, RootAdapter } from '@volter/editor-project/adapter';
|
|
48
|
+
import type { SystemAdapters } from '@volter/editor-project/adapter/system-adapter';
|
|
49
|
+
import { getDebugRegistry } from '../../runtime/debug-registry';
|
|
50
|
+
import type { GameCanvasHostContext } from '../../runtime/host-context';
|
|
51
|
+
import type * as PIXI from 'pixi.js';
|
|
52
|
+
import type { Application, ApplicationOptions, Container } from 'pixi.js';
|
|
53
|
+
import type * as React from 'react';
|
|
54
|
+
import type { ComponentType } from 'react';
|
|
55
|
+
|
|
56
|
+
/** The React, `@pixi/react` and `pixi.js` a canvas world is mounted with — the ones its own
|
|
57
|
+
* hooks resolve. */
|
|
58
|
+
export interface CanvasRuntime {
|
|
59
|
+
readonly createElement: typeof React.createElement;
|
|
60
|
+
readonly Fragment: typeof React.Fragment;
|
|
61
|
+
readonly useEffect: typeof React.useEffect;
|
|
62
|
+
readonly useLayoutEffect: typeof React.useLayoutEffect;
|
|
63
|
+
readonly createRoot: typeof PixiReact.createRoot;
|
|
64
|
+
readonly extend: typeof PixiReact.extend;
|
|
65
|
+
readonly pixi: typeof PIXI;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** How long `mount()` waits for the tree's first commit before failing loudly
|
|
69
|
+
* rather than hanging (and wedging every root declared after this one, since
|
|
70
|
+
* roots mount sequentially). */
|
|
71
|
+
const FIRST_COMMIT_TIMEOUT_MS = 10_000;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `@pixi/react` keys its reconciler roots off the canvas element in a
|
|
75
|
+
* module-global map, and its own unmount path (which would clear that key) is
|
|
76
|
+
* not exported. So a canvas this lane has already mounted must never be handed
|
|
77
|
+
* to a second mount: `createRoot` would silently hand back the FIRST root,
|
|
78
|
+
* whose `Application` this lane already destroyed. Every host in this repo
|
|
79
|
+
* builds a fresh canvas per mount, so this only ever fires on a genuine bug —
|
|
80
|
+
* and it fires as a named error rather than as a dead world.
|
|
81
|
+
*/
|
|
82
|
+
const MOUNTED_CANVASES = new WeakSet<HTMLCanvasElement>();
|
|
83
|
+
|
|
84
|
+
/** `@pixi/react`'s reconciler-root handle. Its `Root` type is internal (the
|
|
85
|
+
* package exports the FUNCTION, not the type), so name it off the function. */
|
|
86
|
+
type PixiReactRoot = ReturnType<typeof PixiReact.createRoot>;
|
|
87
|
+
|
|
88
|
+
interface CanvasEntryModuleExports {
|
|
89
|
+
/** The one shape: the world IS a component. */
|
|
90
|
+
readonly default?: ComponentType;
|
|
91
|
+
/** Imperative canvas substrates export their own host adapter. This is the
|
|
92
|
+
* same explicit boundary the three entry adjudicator accepts; gameplay
|
|
93
|
+
* modules remain ecosystem-native. */
|
|
94
|
+
readonly adapter?: unknown;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Pixi's substrate-narrowed mounted shape: generic canvas hosts depend on
|
|
98
|
+
* {@link MountedCanvasRoot}. */
|
|
99
|
+
export interface MountedPixiRoot extends MountedCanvasRoot {
|
|
100
|
+
readonly substrate: MountedCanvasSubstrate<Container> & { readonly name: 'pixi' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isCanvasRootAdapter(candidate: unknown): candidate is RootAdapter<'canvas'> {
|
|
104
|
+
if (candidate === null || typeof candidate !== 'object') return false;
|
|
105
|
+
const value = candidate as Record<string, unknown>;
|
|
106
|
+
return (
|
|
107
|
+
typeof value['id'] === 'string' &&
|
|
108
|
+
value['id'].length > 0 &&
|
|
109
|
+
typeof value['mount'] === 'function'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build the `RootAdapter<'canvas'>` for one default-exported Pixi world.
|
|
115
|
+
* Mounts through `@pixi/react`, gated entirely by the host's own loop and
|
|
116
|
+
* rendering into the host's own canvas — never a second canvas, never a
|
|
117
|
+
* second `requestAnimationFrame` loop.
|
|
118
|
+
*/
|
|
119
|
+
function canvasWorldAdapter(
|
|
120
|
+
id: string,
|
|
121
|
+
component: ComponentType,
|
|
122
|
+
runtime: CanvasRuntime,
|
|
123
|
+
): RootAdapter<'canvas'> {
|
|
124
|
+
const { createElement, Fragment, useEffect, useLayoutEffect, createRoot, extend } = runtime;
|
|
125
|
+
const content = createElement(component);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
id,
|
|
129
|
+
|
|
130
|
+
async mount(host: GameCanvasHostContext): Promise<MountedPixiRoot> {
|
|
131
|
+
const registry = host.game ? getDebugRegistry(host.game) : null;
|
|
132
|
+
const canvas = host.canvas;
|
|
133
|
+
if (MOUNTED_CANVASES.has(canvas)) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`canvas world "${id}": this lane has already mounted this canvas. @pixi/react keys ` +
|
|
136
|
+
'its reconciler roots off the canvas element, so re-using one would resurrect a ' +
|
|
137
|
+
'root whose Application is destroyed. Hand each mount a fresh canvas (every vgai ' +
|
|
138
|
+
'host already does).',
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
MOUNTED_CANVASES.add(canvas);
|
|
142
|
+
|
|
143
|
+
// The catalogue is module-global and starts empty; this is idempotent.
|
|
144
|
+
extend(runtime.pixi as unknown as Parameters<typeof extend>[0]);
|
|
145
|
+
|
|
146
|
+
let resolveCommitted!: () => void;
|
|
147
|
+
const committed = new Promise<void>((resolve) => {
|
|
148
|
+
resolveCommitted = resolve;
|
|
149
|
+
});
|
|
150
|
+
let resolveEffectsReady!: () => void;
|
|
151
|
+
const effectsReady = new Promise<void>((resolve) => {
|
|
152
|
+
resolveEffectsReady = resolve;
|
|
153
|
+
});
|
|
154
|
+
let resolveUnmounted!: () => void;
|
|
155
|
+
const unmounted = new Promise<void>((resolve) => {
|
|
156
|
+
resolveUnmounted = resolve;
|
|
157
|
+
});
|
|
158
|
+
let resolveEffectsUnmounted!: () => void;
|
|
159
|
+
const effectsUnmounted = new Promise<void>((resolve) => {
|
|
160
|
+
resolveEffectsUnmounted = resolve;
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* Signals the tree's FIRST COMMIT and both unmount cleanup phases. Rendered as the LAST
|
|
164
|
+
* child deliberately:
|
|
165
|
+
*
|
|
166
|
+
* - mount: layout effects run synchronously inside the commit, before
|
|
167
|
+
* any passive effect and before any tick can be scheduled;
|
|
168
|
+
* - unmount: the layout cleanup fences `useTick` and the passive cleanup fences ordinary
|
|
169
|
+
* project effects. Native destruction waits for BOTH; resolving only from layout left
|
|
170
|
+
* passive listener/resource cleanup racing an Application whose renderer was null.
|
|
171
|
+
*/
|
|
172
|
+
function Lifecycle(): null {
|
|
173
|
+
useLayoutEffect(() => {
|
|
174
|
+
resolveCommitted();
|
|
175
|
+
return resolveUnmounted;
|
|
176
|
+
}, []);
|
|
177
|
+
// This component is the last sibling. Reaching its passive effect
|
|
178
|
+
// means the initial tree's provider/command effects have registered,
|
|
179
|
+
// so mount() can truthfully back Play's ack.
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
resolveEffectsReady();
|
|
182
|
+
return resolveEffectsUnmounted;
|
|
183
|
+
}, []);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const element = createElement(Fragment, null, content, createElement(Lifecycle));
|
|
188
|
+
|
|
189
|
+
const root: PixiReactRoot = createRoot(canvas);
|
|
190
|
+
|
|
191
|
+
// The host owns size, dpr, transparency and capture cost; the loop
|
|
192
|
+
// contract owns `autoStart`/`sharedTicker`.
|
|
193
|
+
const initOptions: Partial<ApplicationOptions> = {
|
|
194
|
+
antialias: true,
|
|
195
|
+
width: host.width,
|
|
196
|
+
height: host.height,
|
|
197
|
+
resolution: host.dpr ?? globalThis.devicePixelRatio ?? 1,
|
|
198
|
+
autoDensity: true,
|
|
199
|
+
// Stacked ABOVE a sibling root -> clear with alpha 0 so the layer below
|
|
200
|
+
// shows through (COMPOSITION-DESIGN D5 §1).
|
|
201
|
+
backgroundAlpha: host.transparent ? 0 : 1,
|
|
202
|
+
preserveDrawingBuffer: host.preserveDrawingBuffer ?? false,
|
|
203
|
+
autoStart: false,
|
|
204
|
+
sharedTicker: false,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Install the reconcile-error listener BEFORE `root.render`: Pixi hands
|
|
208
|
+
// the Application back after `init()` but React can already have thrown
|
|
209
|
+
// during that same turn. Attaching after the await loses the real error
|
|
210
|
+
// and turns it into a misleading ten-second timeout.
|
|
211
|
+
let reportReconcileFailure!: (why: string) => void;
|
|
212
|
+
const reconcileFailure = new Promise<string>((resolve) => {
|
|
213
|
+
reportReconcileFailure = resolve;
|
|
214
|
+
});
|
|
215
|
+
const onError = (event: ErrorEvent): void => {
|
|
216
|
+
reportReconcileFailure(
|
|
217
|
+
`the Pixi tree crashed before its first commit — ${event.message} ` +
|
|
218
|
+
'(mount() fails loudly instead of hanging)',
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
globalThis.addEventListener?.('error', onError);
|
|
222
|
+
|
|
223
|
+
let app: Application | undefined;
|
|
224
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
225
|
+
try {
|
|
226
|
+
// `root.render` is async: it awaits `Application.init()` (which is what
|
|
227
|
+
// creates the renderer) and only then hands the tree to the reconciler.
|
|
228
|
+
// A crash inside init rejects here, which is the loud failure we want.
|
|
229
|
+
const renderedApp = await root.render(element, initOptions);
|
|
230
|
+
app = renderedApp;
|
|
231
|
+
|
|
232
|
+
// A reconcile-time crash (e.g. an un-extended component) surfaces as
|
|
233
|
+
// an uncaught window error and the first commit never lands. Race that
|
|
234
|
+
// exact signal against the commit and the last-resort ceiling.
|
|
235
|
+
const timeout = new Promise<string>((resolve) => {
|
|
236
|
+
timer = setTimeout(
|
|
237
|
+
() =>
|
|
238
|
+
resolve(
|
|
239
|
+
`no first commit within ${FIRST_COMMIT_TIMEOUT_MS / 1000}s ` +
|
|
240
|
+
'(mount() fails loudly instead of hanging)',
|
|
241
|
+
),
|
|
242
|
+
FIRST_COMMIT_TIMEOUT_MS,
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
const failure = await Promise.race([
|
|
246
|
+
Promise.all([committed, effectsReady]).then(() => null),
|
|
247
|
+
reconcileFailure,
|
|
248
|
+
timeout,
|
|
249
|
+
]);
|
|
250
|
+
if (failure !== null) {
|
|
251
|
+
throw new Error(`canvas world "${id}": ${failure}`);
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
try {
|
|
255
|
+
app?.destroy(false, { children: true });
|
|
256
|
+
} catch {
|
|
257
|
+
/* the world never fully came up; teardown failure must not mask why */
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
} finally {
|
|
261
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
262
|
+
globalThis.removeEventListener?.('error', onError);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// `app` is assigned before the first-commit race. Reaching here proves
|
|
266
|
+
// both initialization and commit succeeded, so narrow once for the live
|
|
267
|
+
// mounted-root methods below.
|
|
268
|
+
if (app === undefined) {
|
|
269
|
+
throw new Error(`canvas world "${id}": initialized without an Application`);
|
|
270
|
+
}
|
|
271
|
+
const mountedApp = app;
|
|
272
|
+
|
|
273
|
+
// Pin the ticker's clock to GAME time. `Ticker.lastTime` starts at -1, so
|
|
274
|
+
// without this the first `update()` would report a 1ms phantom delta on
|
|
275
|
+
// top of the real one. From here every `deltaMS` a `useTick` callback
|
|
276
|
+
// sees is exactly the `dt` the host handed us — no wall clock anywhere,
|
|
277
|
+
// which is what makes a paused world's resume frame ordinary rather than
|
|
278
|
+
// one giant catch-up step.
|
|
279
|
+
mountedApp.ticker.lastTime = 0;
|
|
280
|
+
let elapsedMs = 0;
|
|
281
|
+
let disposed = false;
|
|
282
|
+
// Adapter surface: the host's shared debug registry. The game's own
|
|
283
|
+
// capabilities arrive as the entry module's declared `systems`.
|
|
284
|
+
const systemAdapters: SystemAdapters = registry ? { debug: registry.adapter } : {};
|
|
285
|
+
const disposeComplete = Promise.all([unmounted, effectsUnmounted]).then(() => {
|
|
286
|
+
// React can tear the root down itself after a fatal layout/effect error. In that path the
|
|
287
|
+
// host has not called dispose() yet, but Application.destroy() still clears Pixi's ticker
|
|
288
|
+
// and renderer. Wait for both layout and passive cleanup first: project effects may own
|
|
289
|
+
// window/document listeners and can still read the Pixi Application while React drains
|
|
290
|
+
// passive deletion effects. Fence the mounted root before clearing the native slots so
|
|
291
|
+
// subsequent host frames and the eventual host teardown cannot re-enter them.
|
|
292
|
+
disposed = true;
|
|
293
|
+
mountedApp.destroy(false, { children: true });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
kind: 'canvas',
|
|
298
|
+
canvas,
|
|
299
|
+
substrate: { name: 'pixi', root: mountedApp.stage },
|
|
300
|
+
drivesOwnLoop: false,
|
|
301
|
+
systems: systemAdapters,
|
|
302
|
+
disposeComplete,
|
|
303
|
+
update(dt: number): void {
|
|
304
|
+
if (disposed) return;
|
|
305
|
+
elapsedMs += dt * 1000;
|
|
306
|
+
// The real Pixi ticker remains the one scheduler `useTick` observes;
|
|
307
|
+
// the host only gates/advances it.
|
|
308
|
+
mountedApp.ticker.update(elapsedMs);
|
|
309
|
+
},
|
|
310
|
+
// Pixi's own hit test finds only interactive objects (`eventMode` static or dynamic), so a
|
|
311
|
+
// HUD claims its buttons and lets every other point fall through to the world beneath.
|
|
312
|
+
hitTest(x: number, y: number): boolean {
|
|
313
|
+
if (disposed) return false;
|
|
314
|
+
// Pixi points its boundary at the last rendered scene inside each of its own pointer
|
|
315
|
+
// handlers; a hit test asked from outside them does the same, or finds no root.
|
|
316
|
+
const boundary = mountedApp.renderer.events.rootBoundary;
|
|
317
|
+
const rendered = mountedApp.renderer.lastObjectRendered;
|
|
318
|
+
if (!rendered) return false;
|
|
319
|
+
boundary.rootTarget = rendered;
|
|
320
|
+
return boundary.hitTest(x, y) !== null;
|
|
321
|
+
},
|
|
322
|
+
resize(width: number, height: number): void {
|
|
323
|
+
if (disposed) return;
|
|
324
|
+
// `autoDensity: true` makes Pixi re-stamp `canvas.style.width`/
|
|
325
|
+
// `.height` from this call, so the on-screen box follows the logical
|
|
326
|
+
// size with no explicit CSS write from the host.
|
|
327
|
+
mountedApp.renderer.resize(width, height);
|
|
328
|
+
},
|
|
329
|
+
dispose(): void {
|
|
330
|
+
if (disposed) return;
|
|
331
|
+
disposed = true;
|
|
332
|
+
// Pixi's EventSystem owns document.pointermove + window.pointerup
|
|
333
|
+
// (and its event ticker's global listener). React's deletion commit
|
|
334
|
+
// is asynchronous, so waiting to destroy the Application leaves
|
|
335
|
+
// those listeners live when the host audits this game realm. Detach
|
|
336
|
+
// the input target synchronously; the later Application.destroy()
|
|
337
|
+
// repeats this idempotently after every useTick cleanup has landed.
|
|
338
|
+
mountedApp.renderer.events.setTargetElement(null as unknown as HTMLElement);
|
|
339
|
+
// Rendering `null` children is the only unmount path `@pixi/react`
|
|
340
|
+
// exposes publicly (its `unmountRoot` helper is internal), and React
|
|
341
|
+
// commits that deletion on its own schedule — NOT synchronously. So
|
|
342
|
+
// the Application is destroyed on the `Lifecycle` cleanup signal
|
|
343
|
+
// instead of inline: `useTick`'s cleanup calls `ticker.remove()`, and
|
|
344
|
+
// a ticker destroyed before it lands throws inside React's commit.
|
|
345
|
+
// `removeView` stays false — the canvas belongs to the HOST.
|
|
346
|
+
void root.render(null, initOptions);
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* What a canvas entry module MEANS, in one place.
|
|
355
|
+
*
|
|
356
|
+
* The editor resolves the same entry for play mode and for the design-time
|
|
357
|
+
* layer, through this one function. When only one side knew the shape, a
|
|
358
|
+
* world played fine and showed "(no scene loaded)" in edit mode.
|
|
359
|
+
*
|
|
360
|
+
* Returns `null` when the module has no default-exported component — the
|
|
361
|
+
* editor reports a mount failure on that root's boundary node and leaves every
|
|
362
|
+
* sibling working.
|
|
363
|
+
*/
|
|
364
|
+
export function resolveCanvasEntryAdapter(
|
|
365
|
+
entryModule: unknown,
|
|
366
|
+
rootId: string,
|
|
367
|
+
runtime: CanvasRuntime,
|
|
368
|
+
): RootAdapter<'canvas'> | null {
|
|
369
|
+
const mod = entryModule as CanvasEntryModuleExports | undefined;
|
|
370
|
+
if (isCanvasRootAdapter(mod?.adapter)) return mod.adapter;
|
|
371
|
+
if (typeof mod?.default === 'function') return canvasWorldAdapter(rootId, mod.default, runtime);
|
|
372
|
+
return null;
|
|
373
|
+
}
|