@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,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev-mode `Math.random` phase trap (enforcer 3, T-D15.3). A project that
|
|
3
|
+
* declares `determinism.seededRandom` is claiming ALL gameplay RNG flows
|
|
4
|
+
* through `ctx.random` — the burn-down scan (`test/gameplay-rng-ban.test.ts`)
|
|
5
|
+
* catches raw `Math.random()`/`Date.now()`/`performance.now()` call sites it
|
|
6
|
+
* can SEE in source; this trap catches what the scan structurally can't: a
|
|
7
|
+
* TRANSITIVE draw made by a third-party library the game calls into during a
|
|
8
|
+
* gameplay tick.
|
|
9
|
+
*
|
|
10
|
+
* Scope is exactly one fixed-step frame (`GameInternal.runFrame`'s whole
|
|
11
|
+
* body — see `game.ts`'s `runFrameImpl`, which brackets its entire call with
|
|
12
|
+
* `profiler.beginFrame()`/`endFrame()`; this trap's `enable()`/`disable()`
|
|
13
|
+
* are called at those exact two points) — code that runs OUTSIDE a frame
|
|
14
|
+
* (module-load-time library init, an unrelated timer callback) never trips
|
|
15
|
+
* it. Warn-once PER CALL SITE (not once ever, not once per call) — a
|
|
16
|
+
* third-party library that draws from several different internal call sites
|
|
17
|
+
* gets one warning per site, so nothing is silently swallowed after the
|
|
18
|
+
* first hit, but a hot per-frame draw from the SAME site doesn't spam.
|
|
19
|
+
* Never throws: a cosmetic third-party draw is legal, just outside the
|
|
20
|
+
* contract (the design doc is explicit about this — "warn, never throw").
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createGameScopedSlot } from './core/game-scoped-slot';
|
|
24
|
+
|
|
25
|
+
export interface GameplayRngTrap {
|
|
26
|
+
/** Wrap `Math.random` for the duration of one frame — call at frame start. */
|
|
27
|
+
enable(): void;
|
|
28
|
+
/** Restore whatever `Math.random` was immediately before `enable()` — call
|
|
29
|
+
* at frame end. A no-op if something else already replaced `Math.random`
|
|
30
|
+
* out from under this trap between `enable()` and `disable()` (defensive;
|
|
31
|
+
* should not happen in practice). */
|
|
32
|
+
disable(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pull a single representative "call site" line out of a captured stack —
|
|
36
|
+
* the warn-once dedupe key. `stack[0]` is the `Error:` header line,
|
|
37
|
+
* `stack[1]` is this trap's OWN wrapper frame — the caller's frame is the
|
|
38
|
+
* next one down. Falls back to a fixed string when `Error().stack` isn't
|
|
39
|
+
* populated (some non-V8 engines) so the trap still functions, just with
|
|
40
|
+
* coarser (single-bucket) deduping. */
|
|
41
|
+
function callSiteFromStack(stack: string | undefined): string {
|
|
42
|
+
if (!stack) return '<unknown call site — no stack captured>';
|
|
43
|
+
const lines = stack.split('\n');
|
|
44
|
+
return (lines[2] ?? lines[1] ?? lines[0] ?? '<unknown call site>').trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Construct a fresh trap. `warn` defaults to `console.warn`; tests (and any
|
|
49
|
+
* caller that wants to assert on the message) can override it to a plain
|
|
50
|
+
* capturing function instead of spying on the global console.
|
|
51
|
+
*/
|
|
52
|
+
export function createGameplayRngTrap(
|
|
53
|
+
warn: (message: string) => void = (message: string) => {
|
|
54
|
+
// biome-ignore lint/suspicious/noConsole: the trap's entire purpose is a structured, greppable dev-mode warning (D15) — this IS the console sink, not incidental debug output.
|
|
55
|
+
console.warn(message);
|
|
56
|
+
},
|
|
57
|
+
): GameplayRngTrap {
|
|
58
|
+
const warnedSites = new Set<string>();
|
|
59
|
+
let previous: (() => number) | undefined;
|
|
60
|
+
|
|
61
|
+
function trapped(): number {
|
|
62
|
+
const site = callSiteFromStack(new Error().stack);
|
|
63
|
+
if (!warnedSites.has(site)) {
|
|
64
|
+
warnedSites.add(site);
|
|
65
|
+
warn(
|
|
66
|
+
// Deliberately NOT the literal substring "Math.random(" (the phrasing
|
|
67
|
+
// below reads fine without it) — this engine repo's OWN gameplay-rng-
|
|
68
|
+
// ban scan (`test/gameplay-rng-ban.test.ts`) greps engine src/ for
|
|
69
|
+
// that exact pattern, and a plain-text mention of the call inside
|
|
70
|
+
// this warning STRING would otherwise false-positive against itself.
|
|
71
|
+
'[determinism] the global Math.random generator was drawn from during a gameplay frame ' +
|
|
72
|
+
'while this project declares determinism.seededRandom — use ctx.random() / ' +
|
|
73
|
+
`ctx.random.stream(name) instead. Call site:\n${site}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
// `previous` is always set by the time `trapped` can run — `enable()`
|
|
77
|
+
// assigns it before installing `trapped` as `Math.random`, and nothing
|
|
78
|
+
// else can invoke this function reference.
|
|
79
|
+
return previous!();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
enable() {
|
|
84
|
+
previous = Math.random;
|
|
85
|
+
Math.random = trapped;
|
|
86
|
+
},
|
|
87
|
+
disable() {
|
|
88
|
+
if (Math.random === trapped && previous) {
|
|
89
|
+
Math.random = previous;
|
|
90
|
+
previous = undefined;
|
|
91
|
+
}
|
|
92
|
+
// else: something else replaced `Math.random` out from under this trap
|
|
93
|
+
// between `enable()` and `disable()` — leave `previous` untouched.
|
|
94
|
+
// `trapped` may still be reachable via a chain built on top of it (e.g.
|
|
95
|
+
// a third party did `const wrapped = Math.random; Math.random = () =>
|
|
96
|
+
// { ...; return wrapped(); }` while `wrapped` was `trapped`) — nulling
|
|
97
|
+
// `previous` here would make that still-chained call to `trapped()`
|
|
98
|
+
// throw (`previous!()` on undefined) instead of quietly forwarding to
|
|
99
|
+
// the real original generator.
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Game-scoped registry — same slot-on-owner-object pattern as
|
|
106
|
+
// `core/seeded-random.ts`'s `registerSeededRandom`/`getSeededRandom` (and
|
|
107
|
+
// `debug-registry.ts`'s `registerDebugRegistry`/`getDebugRegistry`): the
|
|
108
|
+
// trap's enable/disable calls live inside `game.ts`'s `runFrameImpl`, gated
|
|
109
|
+
// on a boolean only the manifest-aware boot path (`mount-manifest.ts`) knows
|
|
110
|
+
// how to set — `createGame` constructs the trap unconditionally (cheap, does
|
|
111
|
+
// nothing while disabled) and registers a control surface for that boot path
|
|
112
|
+
// to flip once it has resolved `manifest.determinism?.seededRandom`.
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
export interface GameplayRngTrapControl {
|
|
116
|
+
setEnabled(enabled: boolean): void;
|
|
117
|
+
readonly enabled: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const controlByOwner = createGameScopedSlot<GameplayRngTrapControl>('gameplay-rng-trap');
|
|
121
|
+
|
|
122
|
+
/** Called once by `createGame`, alongside `registerSeededRandom`/
|
|
123
|
+
* `registerDebugRegistry`. */
|
|
124
|
+
export function registerGameplayRngTrapControl(
|
|
125
|
+
owner: object,
|
|
126
|
+
control: GameplayRngTrapControl,
|
|
127
|
+
): void {
|
|
128
|
+
controlByOwner.set(owner, control);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** `null` for an owner built without one — same absence precedent as
|
|
132
|
+
* `getSeededRandom`/`getDebugRegistry`. */
|
|
133
|
+
export function getGameplayRngTrapControl(owner: object): GameplayRngTrapControl | null {
|
|
134
|
+
return controlByOwner.get(owner) ?? null;
|
|
135
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The game runtime's extension of the shared host contexts: the `game` handle
|
|
3
|
+
* a host hands a root it mounts. `@volter/editor-project`'s host contexts carry
|
|
4
|
+
* only surfaces; this product-specific execution handle belongs here, beside
|
|
5
|
+
* the `Game` it names, so the shared project contract never imports this
|
|
6
|
+
* runtime.
|
|
7
|
+
*/
|
|
8
|
+
import type {
|
|
9
|
+
CanvasHostContext,
|
|
10
|
+
DomHostContext,
|
|
11
|
+
ThreeHostContext,
|
|
12
|
+
} from '@volter/editor-project/adapter/host-context';
|
|
13
|
+
import type { AssetCache } from '@volter/threejs-runtime/assets';
|
|
14
|
+
import type * as THREE from 'three';
|
|
15
|
+
import type { Game } from './game';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* THE DOORS a mounting root actually opens on the Game — and the whole
|
|
19
|
+
* reason `three`/`canvas` host contexts no longer name `GameInternal`.
|
|
20
|
+
*
|
|
21
|
+
* `GameInternal` is the HOST's own control surface: `runFrame`,
|
|
22
|
+
* `runRenderFrame`, `runTicks`, `registerRoot`, `notifySystemAdaptersChanged`,
|
|
23
|
+
* `dispose`. Handing it to an adapter's `mount()` published the loop driver to
|
|
24
|
+
* the thing being driven — nothing read those members through `host.game`, but
|
|
25
|
+
* the type said they could, so "what may a root do to its Game?" had no
|
|
26
|
+
* answer short of "everything".
|
|
27
|
+
*
|
|
28
|
+
* The two doors, each with its verified consumer:
|
|
29
|
+
*
|
|
30
|
+
* 1. **Debug-registry access** — `getDebugRegistry(host.game)`
|
|
31
|
+
* (`r3f-root.tsx` and `canvas-root.tsx` in the editor); the registry is keyed by
|
|
32
|
+
* Game IDENTITY, which is why this handle is the Game and not a projection
|
|
33
|
+
* of it.
|
|
34
|
+
* 2. **The profiler toggle** — `host.game.profiler` (`r3f-root.tsx`).
|
|
35
|
+
*
|
|
36
|
+
* Both are on the PUBLIC {@link Game}, and a `GameInternal` satisfies it.
|
|
37
|
+
*/
|
|
38
|
+
export type HostGameHandle = Game;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A `three`-surface host context carrying the Game root. The host constructs
|
|
42
|
+
* the Game shell BEFORE mounting an adapter and hands it down here so an
|
|
43
|
+
* adapter can expose `ctx.game`/`ctx.roots` to the game it mounts. Absent in
|
|
44
|
+
* headless harnesses and foreign hosts — everything must keep working when
|
|
45
|
+
* this is undefined.
|
|
46
|
+
*/
|
|
47
|
+
export interface GameThreeHostContext extends ThreeHostContext<typeof THREE, THREE.WebGLRenderer, AssetCache> {
|
|
48
|
+
readonly game?: HostGameHandle | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The `canvas`-surface counterpart of {@link GameThreeHostContext}. */
|
|
52
|
+
export interface GameCanvasHostContext extends CanvasHostContext {
|
|
53
|
+
/** Shared Game root when mounted by the universal multi-root host. */
|
|
54
|
+
readonly game?: HostGameHandle | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The `dom`-surface counterpart, typed as the PUBLIC `Game` shell because that
|
|
59
|
+
* is the weakest promise this surface makes, and deliberately optional: a
|
|
60
|
+
* sibling mounted beside an ingest root has no native `Game` to hand it.
|
|
61
|
+
*/
|
|
62
|
+
export interface GameDomHostContext extends DomHostContext {
|
|
63
|
+
readonly game?: Game | undefined;
|
|
64
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The delegating input router (D5 §2a) —
|
|
3
|
+
* `createGameRuntime`'s roots path (T6.1 slice 1) uses this to route
|
|
4
|
+
* pointer input across N stacked world canvases with exactly ONE root
|
|
5
|
+
* listener.
|
|
6
|
+
*
|
|
7
|
+
* Split into two layers so the decision logic is unit-testable with plain
|
|
8
|
+
* fake roots — no real DOM/canvas needed:
|
|
9
|
+
*
|
|
10
|
+
* - `stackOrder` / `resolveClaimingRoot`: pure functions over
|
|
11
|
+
* `{id, zOrder, hitTest?}` — "who claims this point". They live in
|
|
12
|
+
* `adapter/root-stacking.ts`, because the EDITOR asks the same question
|
|
13
|
+
* at design time with no game running and may not import the runtime.
|
|
14
|
+
* - `applyPointerEventsStacking` / `createInputRouter`: this file, the DOM wiring —
|
|
15
|
+
* sets `pointer-events` CSS per D5 §2a and forwards a claimed event to
|
|
16
|
+
* the claiming world's own canvas.
|
|
17
|
+
*
|
|
18
|
+
* Per-event fall-through (D5 §2b — the sanctioned mechanism for an
|
|
19
|
+
* authoring surface's own interior drag affordances, e.g. a canvas
|
|
20
|
+
* editor's gizmos) is explicitly NOT this file's job — it stays inside
|
|
21
|
+
* whatever authoring surface needs it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
type ClaimEntry,
|
|
26
|
+
resolveClaimingRoot,
|
|
27
|
+
stackOrder,
|
|
28
|
+
} from '@volter/editor-project/adapter/root-stacking';
|
|
29
|
+
|
|
30
|
+
/** The DOM-wiring inputs — one real canvas per world. */
|
|
31
|
+
export interface RouterAdapterRoot extends ClaimEntry {
|
|
32
|
+
readonly canvas: HTMLCanvasElement;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Sets `canvas.style.pointerEvents` per D5 §1/§2a: the bottom (lowest
|
|
37
|
+
* zOrder) world's canvas is `'auto'` (real user input DOM-hit-tests it by
|
|
38
|
+
* default); every canvas above it is `'none'` (real user input never
|
|
39
|
+
* DOM-hit-tests it — the router's JS-level `hitTest` is what still lets it
|
|
40
|
+
* claim a point via `createInputRouter`'s forwarding).
|
|
41
|
+
*/
|
|
42
|
+
export function applyPointerEventsStacking(entries: readonly RouterAdapterRoot[]): void {
|
|
43
|
+
const order = stackOrder(entries);
|
|
44
|
+
order.forEach((entry, i) => {
|
|
45
|
+
entry.canvas.style.pointerEvents = i === 0 ? 'auto' : 'none';
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ROUTED_EVENT_TYPES = ['pointerdown', 'pointerup', 'pointermove', 'click', 'wheel'] as const;
|
|
50
|
+
|
|
51
|
+
/** Common pointer/mouse/wheel event fields this router knows how to carry
|
|
52
|
+
* over when forwarding a claimed event to a different canvas (see
|
|
53
|
+
* `forwardEvent` below) — not an exhaustive Event surface, just the fields
|
|
54
|
+
* games/libraries actually read for hit-testing and input state. */
|
|
55
|
+
function pickEventInit(original: Event): Record<string, unknown> {
|
|
56
|
+
const src = original as unknown as Record<string, unknown>;
|
|
57
|
+
const keys = [
|
|
58
|
+
'clientX',
|
|
59
|
+
'clientY',
|
|
60
|
+
'screenX',
|
|
61
|
+
'screenY',
|
|
62
|
+
'offsetX',
|
|
63
|
+
'offsetY',
|
|
64
|
+
'button',
|
|
65
|
+
'buttons',
|
|
66
|
+
'ctrlKey',
|
|
67
|
+
'altKey',
|
|
68
|
+
'shiftKey',
|
|
69
|
+
'metaKey',
|
|
70
|
+
'pointerId',
|
|
71
|
+
'pointerType',
|
|
72
|
+
'pressure',
|
|
73
|
+
'isPrimary',
|
|
74
|
+
'deltaX',
|
|
75
|
+
'deltaY',
|
|
76
|
+
'deltaZ',
|
|
77
|
+
'deltaMode',
|
|
78
|
+
];
|
|
79
|
+
const init: Record<string, unknown> = { bubbles: true, cancelable: true, composed: true };
|
|
80
|
+
for (const key of keys) if (key in src) init[key] = src[key];
|
|
81
|
+
return init;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Forward `original` onto `canvas` as a NEW event of the same event-type
|
|
86
|
+
* family — so a canvas with `pointer-events:none` (never a native DOM
|
|
87
|
+
* hit-test target) still receives the interaction its own event pipeline
|
|
88
|
+
* (Pixi's `EventSystem`, a raycast-driven three controller, …) listens for.
|
|
89
|
+
* The clone BUBBLES, because Pixi hears `pointermove` on the document and
|
|
90
|
+
* `pointerup` on the window, not on its canvas; the container's capture
|
|
91
|
+
* listener sees every clone on the way down (capture runs whether or not an
|
|
92
|
+
* event bubbles), and {@link forwarded} is what lets it pass rather than be
|
|
93
|
+
* routed again. Re-dispatching the SAME event object is not an option (the
|
|
94
|
+
* DOM forbids re-dispatching an event still being dispatched).
|
|
95
|
+
*
|
|
96
|
+
* Best-effort: environments with no `PointerEvent`/`MouseEvent`/
|
|
97
|
+
* `WheelEvent` global (Node unit tests) skip forwarding silently — those
|
|
98
|
+
* tests exercise the pure claim-resolution logic above instead, which needs
|
|
99
|
+
* no real `Event` objects at all.
|
|
100
|
+
*/
|
|
101
|
+
/** The clones this router dispatched: they bubble through the container's capture listener, which
|
|
102
|
+
* lets them pass instead of routing them again. */
|
|
103
|
+
const forwarded = new WeakSet<Event>();
|
|
104
|
+
|
|
105
|
+
function forwardEvent(canvas: HTMLCanvasElement, original: Event): void {
|
|
106
|
+
const g = globalThis as unknown as {
|
|
107
|
+
PointerEvent?: new (type: string, init?: unknown) => Event;
|
|
108
|
+
MouseEvent?: new (type: string, init?: unknown) => Event;
|
|
109
|
+
WheelEvent?: new (type: string, init?: unknown) => Event;
|
|
110
|
+
};
|
|
111
|
+
const Ctor =
|
|
112
|
+
original.type === 'wheel'
|
|
113
|
+
? g.WheelEvent
|
|
114
|
+
: original.type.startsWith('pointer')
|
|
115
|
+
? g.PointerEvent
|
|
116
|
+
: (g.MouseEvent ?? g.PointerEvent);
|
|
117
|
+
if (!Ctor || typeof canvas.dispatchEvent !== 'function') return;
|
|
118
|
+
try {
|
|
119
|
+
const clone = new Ctor(original.type, pickEventInit(original));
|
|
120
|
+
forwarded.add(clone);
|
|
121
|
+
canvas.dispatchEvent(clone);
|
|
122
|
+
} catch {
|
|
123
|
+
// Best-effort forwarding — a construction failure here must never break
|
|
124
|
+
// the (already-delivered) original event's own handling.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface InputRouterHandle {
|
|
129
|
+
/** Resolve which world claims a container-relative point right now — the
|
|
130
|
+
* pure decision, exposed for callers/tests that don't want to simulate a
|
|
131
|
+
* real DOM event. */
|
|
132
|
+
resolveClaim(x: number, y: number): string | null;
|
|
133
|
+
/** Detach the container listener(s). Idempotent. */
|
|
134
|
+
dispose(): void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Wire the delegating router onto `container` for `entries` (D5 §2a): apply
|
|
139
|
+
* the pointer-events stacking, then attach ONE capture-phase listener per
|
|
140
|
+
* routed event type that resolves the claiming world and forwards the event
|
|
141
|
+
* to ITS canvas when that world isn't already the natural DOM target (the
|
|
142
|
+
* bottom world, whose canvas is the only one with `pointer-events:auto`).
|
|
143
|
+
*/
|
|
144
|
+
export function createInputRouter(
|
|
145
|
+
container: HTMLElement,
|
|
146
|
+
entries: readonly RouterAdapterRoot[],
|
|
147
|
+
): InputRouterHandle {
|
|
148
|
+
applyPointerEventsStacking(entries);
|
|
149
|
+
const byId = new Map(entries.map((e) => [e.id, e] as const));
|
|
150
|
+
const bottomId = stackOrder(entries)[0]?.id;
|
|
151
|
+
|
|
152
|
+
const handler = (evt: Event) => {
|
|
153
|
+
if (forwarded.has(evt)) return;
|
|
154
|
+
const rect = container.getBoundingClientRect?.() ?? { left: 0, top: 0 };
|
|
155
|
+
const clientX = (evt as unknown as { clientX?: number }).clientX ?? 0;
|
|
156
|
+
const clientY = (evt as unknown as { clientY?: number }).clientY ?? 0;
|
|
157
|
+
const x = clientX - rect.left;
|
|
158
|
+
const y = clientY - rect.top;
|
|
159
|
+
const claimant = resolveClaimingRoot(entries, x, y);
|
|
160
|
+
if (claimant && claimant !== bottomId) {
|
|
161
|
+
const entry = byId.get(claimant);
|
|
162
|
+
if (entry) {
|
|
163
|
+
// The claim is exclusive: the world beneath never hears a press its HUD took.
|
|
164
|
+
evt.stopPropagation();
|
|
165
|
+
forwardEvent(entry.canvas, evt);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
for (const type of ROUTED_EVENT_TYPES) {
|
|
171
|
+
container.addEventListener?.(type, handler, true);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
resolveClaim: (x, y) => resolveClaimingRoot(entries, x, y),
|
|
176
|
+
dispose() {
|
|
177
|
+
for (const type of ROUTED_EVENT_TYPES) {
|
|
178
|
+
container.removeEventListener?.(type, handler, true);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|