@volter/editor-game 0.5.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/NOTICE +23 -0
- package/contributions/asset-budget-asset.menu.ts +26 -0
- package/contributions/asset-budget.action.ts +18 -0
- package/contributions/asset-budget.document.tsx +21 -0
- package/contributions/asset-budget.menu.ts +22 -0
- package/contributions/audio-unlock.service.ts +17 -0
- package/contributions/audio.utility.tsx +15 -0
- package/contributions/autoplay.service.ts +48 -0
- package/contributions/bridge.command.ts +172 -0
- package/contributions/build-profiles.document.tsx +21 -0
- package/contributions/build-progress.status.tsx +45 -0
- package/contributions/build.action.ts +26 -0
- package/contributions/build.command.ts +27 -0
- package/contributions/build.header.tsx +37 -0
- package/contributions/build.menu.ts +31 -0
- package/contributions/build.service.ts +31 -0
- package/contributions/connection.status.tsx +43 -0
- package/contributions/coverage.service.ts +118 -0
- package/contributions/edit-mode-audio.service.ts +23 -0
- package/contributions/edit-mode-networking.service.ts +30 -0
- package/contributions/game-document.service.ts +25 -0
- package/contributions/game-eval.command.ts +210 -0
- package/contributions/game.layout.ts +19 -0
- package/contributions/gameplay.command.ts +255 -0
- package/contributions/generation.service.ts +96 -0
- package/contributions/generations.status.tsx +53 -0
- package/contributions/ingest.service.ts +80 -0
- package/contributions/instances.command.ts +75 -0
- package/contributions/navmesh.menu.ts +39 -0
- package/contributions/navmesh.service.ts +19 -0
- package/contributions/network.utility.tsx +17 -0
- package/contributions/play.command.ts +355 -0
- package/contributions/profiler.action.ts +16 -0
- package/contributions/profiler.menu.ts +22 -0
- package/contributions/profiler.utility.tsx +17 -0
- package/contributions/react/component-board.service.ts +21 -0
- package/contributions/react/design-time-mount.service.ts +60 -0
- package/contributions/react/pasteboard.action.ts +41 -0
- package/contributions/react/react-inspector.service.ts +85 -0
- package/contributions/react/story-documents.service.ts +44 -0
- package/contributions/scene-document.service.ts +32 -0
- package/contributions/state-watch.action.ts +17 -0
- package/contributions/state-watch.menu.ts +23 -0
- package/contributions/state-watch.utility.tsx +20 -0
- package/contributions/team-playtest.service.ts +124 -0
- package/contributions/three/camera-runtime.inspector.tsx +34 -0
- package/contributions/three/component-board.service.ts +24 -0
- package/contributions/three/component-verbs.command.ts +110 -0
- package/contributions/three/component-verbs.service.ts +92 -0
- package/contributions/three/constraints.inspector.tsx +34 -0
- package/contributions/three/model-asset-sections.service.ts +26 -0
- package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
- package/contributions/three/story-documents.service.ts +31 -0
- package/contributions/three/three-authoring.service.ts +66 -0
- package/contributions/transport.header.tsx +19 -0
- package/contributions/xstate-behavior.action.ts +42 -0
- package/contributions/xstate-behavior.document.tsx +73 -0
- package/contributions/xstate-behavior.inspector.tsx +28 -0
- package/contributions/xstate-behavior.menu.ts +23 -0
- package/package.json +144 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
- package/src/asset-budget/asset-budget-model.ts +799 -0
- package/src/asset-budget/basis-encoder.ts +165 -0
- package/src/asset-budget/gltf-io.ts +154 -0
- package/src/asset-budget/gltf-optimize.ts +384 -0
- package/src/asset-budget/image-dims.ts +78 -0
- package/src/asset-budget/optimize-apply.ts +221 -0
- package/src/audio/AudioDebuggerPanel.tsx +457 -0
- package/src/audio/audio-debugger-model.ts +87 -0
- package/src/bridge/call.ts +60 -0
- package/src/bridge/dispatch.ts +459 -0
- package/src/bridge/live-frames.ts +25 -0
- package/src/bridge/screenshot.ts +316 -0
- package/src/build/BuildProfilesPanel.tsx +476 -0
- package/src/build/build-session.ts +247 -0
- package/src/build/format-bytes.ts +14 -0
- package/src/command-results.ts +36 -0
- package/src/coverage/live-authoring-surface.ts +30 -0
- package/src/coverage/live-project-verbs.ts +162 -0
- package/src/coverage/native-system-coverage.ts +143 -0
- package/src/coverage/root-coverage.ts +79 -0
- package/src/coverage/session-coverage.ts +193 -0
- package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
- package/src/edit-mode/edit-mode-audio.ts +56 -0
- package/src/edit-mode/edit-mode-networking.ts +112 -0
- package/src/game-document/DevicePresetPicker.tsx +84 -0
- package/src/game-document/GameCaptureFrameButton.tsx +45 -0
- package/src/game-document/GameDocument.tsx +215 -0
- package/src/game-document/GamePanel.tsx +662 -0
- package/src/game-document/InstanceInspectorPicker.tsx +183 -0
- package/src/game-document/crowd-debug.ts +183 -0
- package/src/game-document/device-preview.ts +336 -0
- package/src/game-document/game-view-store.ts +107 -0
- package/src/game-document/physics-debug.ts +187 -0
- package/src/generation/GenerationActivity.tsx +421 -0
- package/src/generation/GenerationGallery.css +231 -0
- package/src/generation/generation-documents.tsx +338 -0
- package/src/generation/generation-jobs.ts +128 -0
- package/src/generation/generation-presentation.ts +257 -0
- package/src/host/adapter-reach.ts +445 -0
- package/src/host/adapter-runtime-bindings.ts +303 -0
- package/src/host/after-paint.ts +112 -0
- package/src/host/api/configurations.ts +66 -0
- package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
- package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
- package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
- package/src/host/authoring/contract-scenes-stories.ts +204 -0
- package/src/host/authoring/creation-site-related.ts +55 -0
- package/src/host/authoring/ephemeral-persistence.ts +29 -0
- package/src/host/authoring/gesture-persist.ts +84 -0
- package/src/host/authoring/ingest-data-writer.ts +232 -0
- package/src/host/authoring/ingest-source-persistence.ts +826 -0
- package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
- package/src/host/authoring/mounted-authoring.ts +41 -0
- package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
- package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
- package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
- package/src/host/authoring/pixi-isolation-assets.ts +25 -0
- package/src/host/authoring/pixi-live-write-target.ts +979 -0
- package/src/host/authoring/pixi-source-identity.ts +141 -0
- package/src/host/authoring/pixi-still-presentation.ts +71 -0
- package/src/host/authoring/pixi-structure-history.ts +237 -0
- package/src/host/authoring/pixi-transform-channels.ts +205 -0
- package/src/host/authoring/selection-remount-handoff.ts +23 -0
- package/src/host/authoring/source-persistence-backend.ts +373 -0
- package/src/host/authoring/source-refresh-revisions.ts +81 -0
- package/src/host/authoring/struct-write-pipe.ts +143 -0
- package/src/host/auto-frame-window.ts +89 -0
- package/src/host/binding-resolver.ts +393 -0
- package/src/host/browser-transpile.ts +631 -0
- package/src/host/canvas-entry-runtime.ts +95 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
- package/src/host/components/HeaderTelemetry.tsx +341 -0
- package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
- package/src/host/components/ResolutionPicker.tsx +89 -0
- package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
- package/src/host/components/frame-debugger-model.ts +579 -0
- package/src/host/components/header-telemetry-model.ts +74 -0
- package/src/host/components/scene-document.tsx +447 -0
- package/src/host/components/utility-view-state.ts +87 -0
- package/src/host/components/world-root-stage-binding.tsx +133 -0
- package/src/host/components/world-root-stage.ts +1046 -0
- package/src/host/coverage/authoring-read-probe.ts +583 -0
- package/src/host/coverage/capability-coverage.ts +1587 -0
- package/src/host/coverage/coverage-accounting.ts +261 -0
- package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
- package/src/host/coverage/project-verb-coverage.ts +148 -0
- package/src/host/coverage/system-adapter-coverage.ts +373 -0
- package/src/host/design-system-stories/StoryLayout.tsx +104 -0
- package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
- package/src/host/document-preview-three.ts +110 -0
- package/src/host/entry-adjudication.ts +89 -0
- package/src/host/game-location-guard.ts +150 -0
- package/src/host/game-module-access.ts +196 -0
- package/src/host/game-realm-page.ts +358 -0
- package/src/host/game-realm-reclaim.ts +55 -0
- package/src/host/game-realm-storage.ts +104 -0
- package/src/host/gameplay-export.ts +288 -0
- package/src/host/gameplay-recording.ts +717 -0
- package/src/host/gated-globals.ts +1511 -0
- package/src/host/history/json-history-resource.ts +254 -0
- package/src/host/ingest/registry.ts +261 -0
- package/src/host/instance-extract-actions.ts +120 -0
- package/src/host/instance-fork-actions.ts +120 -0
- package/src/host/play-control-hook.ts +26 -0
- package/src/host/playwright-shim.ts +479 -0
- package/src/host/projection/dom.ts +295 -0
- package/src/host/projection/pixi.ts +288 -0
- package/src/host/r3f-entry-runtime.ts +77 -0
- package/src/host/react-mount-runtime.ts +162 -0
- package/src/host/realm-services.ts +148 -0
- package/src/host/recording-preview.ts +106 -0
- package/src/host/roots/module-root.ts +203 -0
- package/src/host/roots/react-root.ts +181 -0
- package/src/host/same-realm-loop-gate.ts +544 -0
- package/src/host/scene-view-drawability.ts +69 -0
- package/src/host/sdk/tools.ts +31 -0
- package/src/host/served-bundle-runtime-modules.ts +331 -0
- package/src/host/server-log-bridge.ts +60 -0
- package/src/host/staged-projects.ts +24 -0
- package/src/host/stories/mounted-story-viewport-source.ts +64 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +152 -0
- package/src/host/surface-content.ts +87 -0
- package/src/host/take-named-export.ts +23 -0
- package/src/host/three-ingest-runtime.ts +76 -0
- package/src/host/types-fastnoise-lite.d.ts +7 -0
- package/src/host/types-mikktspace.d.ts +20 -0
- package/src/host/types-troika-three-text.d.ts +7 -0
- package/src/host/use-active-performance-source.ts +53 -0
- package/src/host/viewport-pose-memory.ts +48 -0
- package/src/host/viewport-root-presentation.ts +40 -0
- package/src/ingest/active-ingest.ts +251 -0
- package/src/ingest/active-scene-navigation.ts +43 -0
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
- package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
- package/src/ingest/capture-wait-report.ts +122 -0
- package/src/ingest/deferred-ingest-play.ts +216 -0
- package/src/ingest/deferred-ingest-session.ts +23 -0
- package/src/ingest/discovery-public-ingest.ts +242 -0
- package/src/ingest/dom-stub-mark.ts +7 -0
- package/src/ingest/entry-load.ts +56 -0
- package/src/ingest/game-contract-realm.ts +34 -0
- package/src/ingest/game-pointer-lock.ts +89 -0
- package/src/ingest/held-scene-repaint.ts +73 -0
- package/src/ingest/host-surface-box.ts +174 -0
- package/src/ingest/ingest-boot-viewport.ts +54 -0
- package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
- package/src/ingest/ingest-canvas-scene.ts +50 -0
- package/src/ingest/ingest-evidence-hook.ts +131 -0
- package/src/ingest/ingest-frame-snapshot.ts +183 -0
- package/src/ingest/ingest-play-commands.ts +134 -0
- package/src/ingest/ingest-play-control.ts +299 -0
- package/src/ingest/ingest-render-debug.ts +320 -0
- package/src/ingest/ingest-siblings.ts +487 -0
- package/src/ingest/ingest-status.ts +62 -0
- package/src/ingest/live-ingest-facet.ts +46 -0
- package/src/ingest/module-mode.ts +229 -0
- package/src/ingest/mount-canvas-ingest-root.ts +899 -0
- package/src/ingest/mount-coverage.ts +296 -0
- package/src/ingest/mount-dom-ingest-root.ts +282 -0
- package/src/ingest/mount-ingest-root.ts +744 -0
- package/src/ingest/mount-three-ingest-root.ts +366 -0
- package/src/ingest/resolve-canvas.ts +47 -0
- package/src/ingest/resolve-three.ts +123 -0
- package/src/ingest/served-bundle.ts +109 -0
- package/src/ingest/served-html-boot.ts +292 -0
- package/src/ingest/surface-canvas.ts +84 -0
- package/src/ingest/surface-dom.ts +84 -0
- package/src/ingest/surface-three.ts +128 -0
- package/src/ingest/types.ts +76 -0
- package/src/ingest/unmount-ingest-root.ts +224 -0
- package/src/navmesh/navmesh-actions.ts +27 -0
- package/src/navmesh/navmesh-handler.ts +237 -0
- package/src/navmesh/navmesh-workflow-store.ts +78 -0
- package/src/network/NetworkInspectorPanel.tsx +644 -0
- package/src/network/network-inspector-model.ts +225 -0
- package/src/play/play-boot-stall.ts +118 -0
- package/src/play/play-log-events.ts +26 -0
- package/src/play/play-mode.ts +2553 -0
- package/src/play/play-recording.ts +335 -0
- package/src/play/react-play-live-authoring.ts +165 -0
- package/src/play-bar/PlayBar.tsx +488 -0
- package/src/play-bar/PlayerCountPicker.tsx +100 -0
- package/src/profiler/FrameDebuggerPanel.tsx +458 -0
- package/src/profiler/PerformancePanel.tsx +1068 -0
- package/src/profiler/ProfilerPanel.tsx +53 -0
- package/src/profiler/frame-debugger-store.ts +97 -0
- package/src/profiler/main-thread-busy.ts +90 -0
- package/src/react/design-time-react-mount.ts +782 -0
- package/src/react/dom-authoring-adapter.ts +764 -0
- package/src/react/pasteboard-materialize.ts +154 -0
- package/src/react/react-inspector-section.tsx +2496 -0
- package/src/react/react-world-authoring-adapter.ts +3795 -0
- package/src/react/story-documents/story-args-section.ts +19 -0
- package/src/react/story-documents/story-documents.tsx +1380 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +101 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +72 -0
- package/src/services/audio-pose-guard.ts +81 -0
- package/src/services/game-audio-unlock.ts +48 -0
- package/src/state-watch/StateWatchPanel.tsx +535 -0
- package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
- package/src/three/authoring/constraint-inspector-section.tsx +189 -0
- package/src/three/authoring/design-time-renderer.ts +189 -0
- package/src/three/authoring/model-asset-inspector-section.css +41 -0
- package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
- package/src/three/authoring/oid-source-persistence.ts +557 -0
- package/src/three/authoring/r3f-design-session.ts +1174 -0
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
- package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
- package/src/three/authoring/spatial-audio-handles.ts +215 -0
- package/src/three/authoring/spatial-collider-handles.ts +229 -0
- package/src/three/authoring/spatial-joint-handles.ts +114 -0
- package/src/three/authoring/spatial-light-handles.ts +178 -0
- package/src/three/authoring/spatial-lod-handles.ts +93 -0
- package/src/three/authoring/spatial-particle-handles.ts +368 -0
- package/src/three/authoring/three-authoring-adapter.ts +1953 -0
- package/src/three/authoring/three-scene-identity.ts +19 -0
- package/src/three/authoring/three-spatial-handles.ts +161 -0
- package/src/three/authoring/typed-three-inspector.ts +528 -0
- package/src/three/component-verbs/extract-menu.ts +71 -0
- package/src/three/component-verbs/fork-menu.ts +78 -0
- package/src/three/component-verbs/internals-menu.ts +91 -0
- package/src/three/story-documents/three-story-documents.tsx +607 -0
- package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
- package/src/three/three-board/board-framing.ts +412 -0
- package/src/three/three-board/board-layout.ts +401 -0
- package/src/three/three-board/board-scene.ts +901 -0
- package/src/three/three-board/three-component-board.ts +62 -0
- package/src/xstate/XStateBehaviorSection.tsx +130 -0
- package/src/xstate/XStateMachineInspector.tsx +566 -0
- package/src/xstate/character-animation-machine.fixture.ts +74 -0
- package/src/xstate/live-behaviors.ts +106 -0
- package/src/xstate/use-live-actor-state.ts +44 -0
- package/src/xstate/xstate-graph.ts +235 -0
- package/src/xstate/xstate-layout.ts +76 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { gameRealmForMountId } from '../host/gated-globals';
|
|
2
|
+
import {
|
|
3
|
+
readGameContract,
|
|
4
|
+
type VgaiGameContract,
|
|
5
|
+
} from '@volter/editor-project/adapter/ingest/game-contract';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The browser realm used by the singleton ingest routes.
|
|
9
|
+
*
|
|
10
|
+
* Vendored ingest entry graphs deliberately carry no `?vgai-mount=` query:
|
|
11
|
+
* they share the editor's Pixi/Three packages and run in the default gated
|
|
12
|
+
* realm. Realm hardening keeps game-owned expandos on that realm's window
|
|
13
|
+
* proxy, so host code must read the contract from the same proxy rather than
|
|
14
|
+
* from the editor's real `window`.
|
|
15
|
+
*/
|
|
16
|
+
export function ingestGameRealmWindow(): Window {
|
|
17
|
+
return gameRealmForMountId().window;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read the declaration from the realm in which ingest modules publish it.
|
|
22
|
+
*
|
|
23
|
+
* No DOM means no realm to build — `gameRealmForMountId` proxies the real
|
|
24
|
+
* `window`/`document`, so calling it in a DOM-less process is a bare `window`
|
|
25
|
+
* reference, not a null read. Answering `null` there is the same headless
|
|
26
|
+
* default `readGameContract` itself carries, and it is what keeps the
|
|
27
|
+
* DOM-free callers of this (the command relay's `recordContractSystemUse`,
|
|
28
|
+
* reached by every `bridge-call`) degrading instead of throwing
|
|
29
|
+
* `window is not defined` at them.
|
|
30
|
+
*/
|
|
31
|
+
export function readIngestGameContract(): VgaiGameContract | null {
|
|
32
|
+
if (typeof window === 'undefined') return null;
|
|
33
|
+
return readGameContract(ingestGameRealmWindow());
|
|
34
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POINTER LOCK IS A PAGE-WIDE SINGLETON, AND TWO THINGS WANT IT.
|
|
3
|
+
*
|
|
4
|
+
* The editor viewport takes pointer lock for its own camera drag
|
|
5
|
+
* (`editor-viewport.ts`). An ingested first-person game takes it the moment
|
|
6
|
+
* its canvas is clicked — unconditionally, from a listener it installed on its
|
|
7
|
+
* OWN element. Only one element can hold the lock, so in-realm those
|
|
8
|
+
* two fight: click the game canvas while merely INSPECTING it in Edit and the
|
|
9
|
+
* mouse disappears into a game that is not even playing.
|
|
10
|
+
*
|
|
11
|
+
* `gated-globals.ts` cannot see this. Its whole mechanism is a scoped
|
|
12
|
+
* `window`/`document`, and `requestPointerLock` is called on neither — it is
|
|
13
|
+
* an `Element` method invoked on the game's own canvas. So the gate goes on
|
|
14
|
+
* the method, ONCE, and is SCOPED BY ELEMENT: a request from inside a
|
|
15
|
+
* registered game surface (`gated-globals.ts`'s `gameSurfaces`) obeys that
|
|
16
|
+
* realm's input gate — the same predicate that decides whether the game's
|
|
17
|
+
* keydown listeners fire, so pointer lock and input can never disagree.
|
|
18
|
+
* A request from anywhere else — the editor's own viewport canvas, which is
|
|
19
|
+
* not inside any game surface — passes through untouched.
|
|
20
|
+
*
|
|
21
|
+
* Refused, never silent: the console line names the element, the gate and the
|
|
22
|
+
* one thing that makes it open (play running, Game tab active), because a
|
|
23
|
+
* mouse that silently does not capture reads as a broken game.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { gameSurfaces } from '../host/gated-globals';
|
|
27
|
+
|
|
28
|
+
/** The real method, captured at install time so a refusal can still call
|
|
29
|
+
* through and a double install is impossible. */
|
|
30
|
+
let realRequestPointerLock: ((this: Element, options?: unknown) => unknown) | null = null;
|
|
31
|
+
|
|
32
|
+
/** The surface a pointer-lock request came from, or null when the element
|
|
33
|
+
* belongs to the editor rather than to any mounted game. */
|
|
34
|
+
function gameSurfaceOf(el: Element): { surface: HTMLElement; gate: () => boolean } | null {
|
|
35
|
+
for (const entry of gameSurfaces()) {
|
|
36
|
+
if (entry.surface.contains(el)) return entry;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Install the gate. Idempotent and process-wide — there is ONE
|
|
43
|
+
* `Element.prototype`, and scoping is by element, not by installation.
|
|
44
|
+
* Called by the ingest mount; never uninstalled, because the predicate it
|
|
45
|
+
* consults (`gameSurfaces()`) is empty when no game is mounted, which makes
|
|
46
|
+
* the patch a pure pass-through rather than something to unwind.
|
|
47
|
+
*/
|
|
48
|
+
export function installGamePointerLockGate(): void {
|
|
49
|
+
if (realRequestPointerLock !== null) return;
|
|
50
|
+
if (typeof Element === 'undefined') return;
|
|
51
|
+
const proto = Element.prototype as unknown as {
|
|
52
|
+
requestPointerLock?: (this: Element, options?: unknown) => unknown;
|
|
53
|
+
};
|
|
54
|
+
const real = proto.requestPointerLock;
|
|
55
|
+
if (typeof real !== 'function') return;
|
|
56
|
+
realRequestPointerLock = real;
|
|
57
|
+
proto.requestPointerLock = function (this: Element, options?: unknown): unknown {
|
|
58
|
+
const entry = gameSurfaceOf(this);
|
|
59
|
+
if (entry && !entry.gate()) {
|
|
60
|
+
// A NEW native console.error site, suppressed to keep this task's diff at
|
|
61
|
+
// zero new lint warnings — and native on purpose: this fires from a game's
|
|
62
|
+
// own click handler in the page, and the page console is what `vgai
|
|
63
|
+
// status` reads.
|
|
64
|
+
// biome-ignore lint/suspicious/noConsole: see comment above
|
|
65
|
+
console.error(
|
|
66
|
+
'Ingested game requested pointer lock while its input gate is closed — refused. ' +
|
|
67
|
+
'A game only captures the mouse while play is running AND the Game tab is active ' +
|
|
68
|
+
'(the same predicate that gates its keyboard listeners); the editor viewport owns ' +
|
|
69
|
+
'pointer lock otherwise. See packages/editor/src/ingest/game-pointer-lock.ts.',
|
|
70
|
+
);
|
|
71
|
+
// Match the spec's failure shape rather than throwing: the browser
|
|
72
|
+
// rejects a disallowed request, and a throw would abort the game's own
|
|
73
|
+
// click handler mid-way.
|
|
74
|
+
return Promise.reject(new Error('pointer lock refused: game input gate is closed'));
|
|
75
|
+
}
|
|
76
|
+
return real.call(this, options);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Drop pointer lock if a GAME element holds it. Called when a mount disposes —
|
|
82
|
+
* a lock outliving the game that took it leaves the editor with a captured,
|
|
83
|
+
* invisible cursor and nothing left to release it.
|
|
84
|
+
*/
|
|
85
|
+
export function releaseGamePointerLock(): void {
|
|
86
|
+
if (typeof document === 'undefined') return;
|
|
87
|
+
const locked = document.pointerLockElement;
|
|
88
|
+
if (locked && gameSurfaceOf(locked)) document.exitPointerLock();
|
|
89
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRESENTATION repaints for a scene switch performed while the mount is HELD.
|
|
3
|
+
*
|
|
4
|
+
* The defect this closes: a canvas ingest ends held in Edit
|
|
5
|
+
* (`holdIngestContentTimeForMode`) by stopping the game's own ticker — which is
|
|
6
|
+
* also what drives its rendering. The game's navigation runs on its own
|
|
7
|
+
* machinery (promises, gsap's own ticker), so picking a scene in the story
|
|
8
|
+
* picker genuinely moves `current()` — measured live on bubbo-bubbo — while the
|
|
9
|
+
* canvas keeps showing the OLD scene's last frame until ▶. The editor claims
|
|
10
|
+
* "you are looking at scene X" while showing scene Y.
|
|
11
|
+
*
|
|
12
|
+
* The line this walks is the `edit-mode-static` invariant's own: CONTENT time
|
|
13
|
+
* stays held (the ticker is never started, no game update runs), while
|
|
14
|
+
* PRESENTATION may move. `render()` draws the stage as it is; the scene
|
|
15
|
+
* transition's tweens advance on gsap's clock whether or not anyone draws, so
|
|
16
|
+
* drawing during the switch is showing the truth, not advancing it.
|
|
17
|
+
*
|
|
18
|
+
* Bounded by construction: frames are requested only between `apply` and
|
|
19
|
+
* SETTLE (the game's own `current()` reporting the requested scene) plus one
|
|
20
|
+
* grace window for the transition's tail, with a hard deadline for a scene
|
|
21
|
+
* that never arrives. No interval survives past that; an unheld mount renders
|
|
22
|
+
* nothing here (the running ticker already draws).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** One frame-scheduler tick: `schedule(cb)` runs `cb(nowMs)` on the next
|
|
26
|
+
* presentation frame. Injectable so the loop is testable without wall-clock
|
|
27
|
+
* or rAF; production passes `requestAnimationFrame`. */
|
|
28
|
+
export type FrameScheduler = (cb: (nowMs: number) => void) => void;
|
|
29
|
+
|
|
30
|
+
export interface HeldSceneRepaintSeams {
|
|
31
|
+
/** Is content time currently held? (An unheld mount needs no help drawing.) */
|
|
32
|
+
readonly isHeld: () => boolean;
|
|
33
|
+
/** The game's own answer for which scene is up. */
|
|
34
|
+
readonly currentScene: () => string | null;
|
|
35
|
+
/** Draw one presentation frame of the stage as it currently is. */
|
|
36
|
+
readonly render: () => void;
|
|
37
|
+
readonly schedule: FrameScheduler;
|
|
38
|
+
/** Keep drawing this long after the scene settles, for the transition tail. */
|
|
39
|
+
readonly settleGraceMs?: number;
|
|
40
|
+
/** Give up on a scene that never arrives (a failed asset load refuses
|
|
41
|
+
* upstream and is reported there; this just stops drawing). */
|
|
42
|
+
readonly deadlineMs?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_GRACE_MS = 1500;
|
|
46
|
+
const DEFAULT_DEADLINE_MS = 10000;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Begin repainting toward `requestedSceneId`. Returns immediately; frames run
|
|
50
|
+
* on the scheduler. Re-entrant calls are safe: each call is its own bounded
|
|
51
|
+
* loop, and drawing the same stage twice in a frame is idempotent.
|
|
52
|
+
*/
|
|
53
|
+
export function repaintHeldSceneSwitch(
|
|
54
|
+
requestedSceneId: string,
|
|
55
|
+
seams: HeldSceneRepaintSeams,
|
|
56
|
+
): void {
|
|
57
|
+
const grace = seams.settleGraceMs ?? DEFAULT_GRACE_MS;
|
|
58
|
+
const deadline = seams.deadlineMs ?? DEFAULT_DEADLINE_MS;
|
|
59
|
+
let startMs: number | null = null;
|
|
60
|
+
let settledAtMs: number | null = null;
|
|
61
|
+
const tick = (nowMs: number): void => {
|
|
62
|
+
if (!seams.isHeld()) return; // released — the ticker draws from here on
|
|
63
|
+
startMs ??= nowMs;
|
|
64
|
+
if (settledAtMs === null && seams.currentScene() === requestedSceneId) {
|
|
65
|
+
settledAtMs = nowMs;
|
|
66
|
+
}
|
|
67
|
+
seams.render();
|
|
68
|
+
if (settledAtMs !== null && nowMs - settledAtMs >= grace) return;
|
|
69
|
+
if (settledAtMs === null && nowMs - startMs >= deadline) return;
|
|
70
|
+
seams.schedule(tick);
|
|
71
|
+
};
|
|
72
|
+
seams.schedule(tick);
|
|
73
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who owns the CSS box of an adopted ingest surface — S-4 (the SimCity ingest
|
|
3
|
+
* ledger).
|
|
4
|
+
*
|
|
5
|
+
* THE DEFECT. The host resized an ingested game with
|
|
6
|
+
* `renderer.setSize(w, h, false)`. `updateStyle: false` means "touch the
|
|
7
|
+
* backing store, never the style", so the canvas' CSS box keeps whatever
|
|
8
|
+
* three's OWN construction-time `setSize` (whose `updateStyle` defaults to
|
|
9
|
+
* `true`) stamped on it in literal pixels while the game still believed it
|
|
10
|
+
* owned the window. Adopt that canvas into a docked tab that happens to be
|
|
11
|
+
* 0-height at mount and it is frozen there: revealing the tab fires the host's
|
|
12
|
+
* ResizeObserver, the backing store is resized correctly — and the picture is
|
|
13
|
+
* still a black 0-height box, because nothing ever un-stamped the style.
|
|
14
|
+
* `updateStyle: false` cannot un-stamp a previous call; only a later write can.
|
|
15
|
+
*
|
|
16
|
+
* THE RULE, and it is the same one `engine/src/runtime/create-runtime.ts`
|
|
17
|
+
* already applies to first-party roots (see its E4.R1 comment): the HOST owns
|
|
18
|
+
* the adopted element's CSS box, the GAME owns only its backing resolution.
|
|
19
|
+
* Make the two fully independent — box asserted as "fill the pane", buffer
|
|
20
|
+
* driven by `setSize(w, h, false)` — and both a 0-height mount and a later
|
|
21
|
+
* pane resize are correct by construction, with no ordering to get right.
|
|
22
|
+
*
|
|
23
|
+
* Kept as its own module (rather than four inline style writes) because the
|
|
24
|
+
* rule now has three call sites across two mount paths, and because the box it
|
|
25
|
+
* asserts is a contract worth one test rather than three transcripts.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The CSS the host stamps on the element it adopted. `position`/`inset` place
|
|
30
|
+
* it against the pane (`hostEl` is `position:absolute;inset:0`), and the
|
|
31
|
+
* percentage `width`/`height` are what actually override a pixel stamp — a
|
|
32
|
+
* `width:1187px` left over from the game's own `setSize` survives `inset:0`
|
|
33
|
+
* alone, which is exactly how S-4 hid for so long.
|
|
34
|
+
*
|
|
35
|
+
* `zIndex` is cleared: a game that stacked itself over a whole page has no
|
|
36
|
+
* business stacking itself over editor chrome once adopted.
|
|
37
|
+
*
|
|
38
|
+
* `transform` is cleared for the same reason, and it is the OTHER half of the
|
|
39
|
+
* pixel stamp S-4 found. A game that centres its own canvas in the window
|
|
40
|
+
* writes `left:50%;top:50%;transform:translate(-50%,-50%)`.
|
|
41
|
+
* `inset:0` un-does the `left`/`top` half — they are
|
|
42
|
+
* the same longhands — but nothing un-does the transform, so the adopted
|
|
43
|
+
* canvas rendered half its own width up and to the left of the pane, with a
|
|
44
|
+
* hard black L down two edges. Like `width:100%`, this is a property the HOST
|
|
45
|
+
* now owns: the game's centring was correct while it owned the window and is
|
|
46
|
+
* simply not its decision any more.
|
|
47
|
+
*/
|
|
48
|
+
export const HOST_ADOPTED_SURFACE_CSS = {
|
|
49
|
+
position: 'absolute',
|
|
50
|
+
inset: '0',
|
|
51
|
+
width: '100%',
|
|
52
|
+
height: '100%',
|
|
53
|
+
zIndex: '',
|
|
54
|
+
transform: '',
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The box for an adopted surface whose RESOLUTION THE HOST DOES NOT DRIVE.
|
|
59
|
+
*
|
|
60
|
+
* {@link HOST_ADOPTED_SURFACE_CSS} fills the pane, and that is right for the
|
|
61
|
+
* three lane precisely because the host also resizes the game's backing store
|
|
62
|
+
* to the pane on every change — box and buffer share an aspect by
|
|
63
|
+
* construction, so "fill" never stretches anything.
|
|
64
|
+
*
|
|
65
|
+
* A canvas ingest has no such half. A Pixi/Phaser/Babylon game lays itself out
|
|
66
|
+
* with imperative formulas against the resolution IT chose (`flappy` declares
|
|
67
|
+
* 480x640 and registers no resize listener), so the host has no licence to
|
|
68
|
+
* re-resolve it and filling the pane would simply report a picture the game
|
|
69
|
+
* does not draw — MEASURED, sighted: 480x640 stretched into a 1107x780 pane
|
|
70
|
+
* flattened the bird to an oval and squashed the HUD.
|
|
71
|
+
*
|
|
72
|
+
* `width`/`height: auto` with percentage maxima is the CSS that says exactly
|
|
73
|
+
* that. For a REPLACED element (a canvas is one) the max-constraint algorithm
|
|
74
|
+
* is defined to preserve the intrinsic ratio, so the used box is the game's own
|
|
75
|
+
* resolution, scaled DOWN only far enough to fit, never up and never
|
|
76
|
+
* distorted; `inset: 0` + `margin: auto` centres it in the pane.
|
|
77
|
+
*
|
|
78
|
+
* `object-fit` is the rejected alternative and it is worth naming, because it
|
|
79
|
+
* looks equivalent and is not: it letterboxes the PAINT inside a box that stays
|
|
80
|
+
* pane-sized, so the element's `getBoundingClientRect()` no longer describes
|
|
81
|
+
* what is on screen — and that rect is exactly what `composite-screenshot.ts`
|
|
82
|
+
* draws each canvas into. The screen would letterbox while every capture of it
|
|
83
|
+
* stretched, which is worse than either alone.
|
|
84
|
+
*/
|
|
85
|
+
export const HOST_ADOPTED_UNRESIZED_SURFACE_CSS = {
|
|
86
|
+
position: 'absolute',
|
|
87
|
+
inset: '0',
|
|
88
|
+
margin: 'auto',
|
|
89
|
+
width: 'auto',
|
|
90
|
+
height: 'auto',
|
|
91
|
+
maxWidth: '100%',
|
|
92
|
+
maxHeight: '100%',
|
|
93
|
+
zIndex: '',
|
|
94
|
+
transform: '',
|
|
95
|
+
} as const;
|
|
96
|
+
|
|
97
|
+
/** The subset of `CSSStyleDeclaration` {@link claimHostSurfaceBox} and
|
|
98
|
+
* {@link claimUnresizedSurfaceBox} write — named explicitly (rather than as an
|
|
99
|
+
* index signature) so a real element's `.style` satisfies it structurally and
|
|
100
|
+
* a unit test can still pass a plain object. */
|
|
101
|
+
export interface MutableBoxStyle {
|
|
102
|
+
position: string;
|
|
103
|
+
inset: string;
|
|
104
|
+
margin: string;
|
|
105
|
+
width: string;
|
|
106
|
+
height: string;
|
|
107
|
+
maxWidth: string;
|
|
108
|
+
maxHeight: string;
|
|
109
|
+
zIndex: string;
|
|
110
|
+
transform: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Assert host ownership of `el`'s CSS box. Idempotent, and safe to re-run on
|
|
115
|
+
* every resize — which is deliberate: a game that installs its own
|
|
116
|
+
* `window.resize` handler re-stamps pixels whenever the editor window changes,
|
|
117
|
+
* so the host re-asserting on each pane resize is what keeps the box correct
|
|
118
|
+
* over a session rather than only at adoption.
|
|
119
|
+
*/
|
|
120
|
+
export function claimHostSurfaceBox(el: { style: MutableBoxStyle }): void {
|
|
121
|
+
el.style.position = HOST_ADOPTED_SURFACE_CSS.position;
|
|
122
|
+
el.style.inset = HOST_ADOPTED_SURFACE_CSS.inset;
|
|
123
|
+
el.style.width = HOST_ADOPTED_SURFACE_CSS.width;
|
|
124
|
+
el.style.height = HOST_ADOPTED_SURFACE_CSS.height;
|
|
125
|
+
el.style.zIndex = HOST_ADOPTED_SURFACE_CSS.zIndex;
|
|
126
|
+
el.style.transform = HOST_ADOPTED_SURFACE_CSS.transform;
|
|
127
|
+
// The two policies write an overlapping property set, so each must clear what
|
|
128
|
+
// the other sets — otherwise re-adopting one surface after the other leaves a
|
|
129
|
+
// stale `margin: auto` / `max-width` behind and the box is neither policy.
|
|
130
|
+
el.style.margin = '';
|
|
131
|
+
el.style.maxWidth = '';
|
|
132
|
+
el.style.maxHeight = '';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Assert host ownership of `el`'s CSS box for a surface the host does NOT
|
|
137
|
+
* resize — see {@link HOST_ADOPTED_UNRESIZED_SURFACE_CSS}. Same idempotence and
|
|
138
|
+
* the same re-assertable contract as {@link claimHostSurfaceBox}.
|
|
139
|
+
*/
|
|
140
|
+
export function claimUnresizedSurfaceBox(el: { style: MutableBoxStyle }): void {
|
|
141
|
+
el.style.position = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.position;
|
|
142
|
+
el.style.inset = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.inset;
|
|
143
|
+
el.style.margin = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.margin;
|
|
144
|
+
el.style.width = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.width;
|
|
145
|
+
el.style.height = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.height;
|
|
146
|
+
el.style.maxWidth = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.maxWidth;
|
|
147
|
+
el.style.maxHeight = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.maxHeight;
|
|
148
|
+
el.style.zIndex = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.zIndex;
|
|
149
|
+
el.style.transform = HOST_ADOPTED_UNRESIZED_SURFACE_CSS.transform;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The backing-store size to hand `renderer.setSize`. A host pane is measured
|
|
154
|
+
* with `getBoundingClientRect()`, which is fractional, and can legitimately
|
|
155
|
+
* report a collapsed dimension mid-transition; three multiplies by the pixel
|
|
156
|
+
* ratio and floors, so feeding it 0 yields a 0-wide framebuffer (a black
|
|
157
|
+
* canvas that no later correct resize necessarily repairs, since a WebGL
|
|
158
|
+
* context can be lost on a zero-size drawing buffer).
|
|
159
|
+
*
|
|
160
|
+
* Floor to whole CSS pixels and clamp to at least 1: the smallest size that is
|
|
161
|
+
* still a renderable surface. The CSS box is unaffected — it is `100%` and
|
|
162
|
+
* follows the pane exactly, so clamping the buffer never shows up as layout.
|
|
163
|
+
*/
|
|
164
|
+
export function hostSurfaceBackingSize(
|
|
165
|
+
width: number,
|
|
166
|
+
height: number,
|
|
167
|
+
): { width: number; height: number } {
|
|
168
|
+
return { width: clampDimension(width), height: clampDimension(height) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function clampDimension(value: number): number {
|
|
172
|
+
if (!Number.isFinite(value)) return 1;
|
|
173
|
+
return Math.max(1, Math.floor(value));
|
|
174
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHERE A BOOT-TIME INGEST BECOMES EDITABLE.
|
|
3
|
+
*
|
|
4
|
+
* Game is a play-time document (`game-document.ts`). A boot-time ingest
|
|
5
|
+
* still acquires Game — that is the host the runtime mounts into — but the
|
|
6
|
+
* authoring surface is Scene. Three already landed on Edit; canvas and DOM
|
|
7
|
+
* forced the play tab, so opening Bubbo put Game in front with nothing
|
|
8
|
+
* having pressed ▶.
|
|
9
|
+
*
|
|
10
|
+
* This is also the ONE cold-landing boundary. Every successful ingest mount
|
|
11
|
+
* publishes its required pause surface before calling here, so hold content
|
|
12
|
+
* time BEFORE exposing Edit. Keeping the hold here prevents each surface
|
|
13
|
+
* adapter from independently remembering (or forgetting) the same invariant.
|
|
14
|
+
* A play-time ingest mount (`deferred-ingest-play.ts`) crosses this boundary
|
|
15
|
+
* while it constructs, then explicitly releases the hold and flips to Game.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { editorConsole } from '@volter/editor-core/editor-console';
|
|
19
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
20
|
+
import { projectAdapterFacet } from '@volter/editor-core/project-adapter';
|
|
21
|
+
import { sceneDocumentId } from '@volter/editor-core/scene-document-plan';
|
|
22
|
+
import { CANVAS_SCENE_DOCUMENT_ID, SCENE_DOCUMENT_ID } from '@volter/editor-core/workspace-document-ids';
|
|
23
|
+
import {
|
|
24
|
+
activateWorkspaceDocument,
|
|
25
|
+
openWorkspaceDocuments,
|
|
26
|
+
} from '@volter/editor-core/workspace-document-registry';
|
|
27
|
+
import { activeIngest } from './active-ingest';
|
|
28
|
+
import { activeIngestPauseGap, holdIngestContentTimeForMode } from './ingest-play-control';
|
|
29
|
+
|
|
30
|
+
export function landIngestBootInEdit(store: EditorShellStore): void {
|
|
31
|
+
const active = activeIngest();
|
|
32
|
+
if (active) {
|
|
33
|
+
const held = holdIngestContentTimeForMode('edit');
|
|
34
|
+
if (!held) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Ingest "${active.worldId}" mounted without the required pause capability; refusing ` +
|
|
37
|
+
`to expose a running game in Edit — ${activeIngestPauseGap() ?? 'no lifecycle control was bound'}.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
editorConsole.log(
|
|
41
|
+
`Ingest "${active.worldId}" is HELD — content time does not advance in Edit until ▶`,
|
|
42
|
+
'ingest',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
store.setActiveViewportTab('edit');
|
|
46
|
+
const open = openWorkspaceDocuments();
|
|
47
|
+
const defaultId = projectAdapterFacet()?.scenes.default;
|
|
48
|
+
const isolationId = defaultId ? sceneDocumentId(defaultId) : undefined;
|
|
49
|
+
const scene =
|
|
50
|
+
(isolationId ? open.find((d) => d.descriptor.id === isolationId) : undefined) ??
|
|
51
|
+
open.find((d) => d.descriptor.id === CANVAS_SCENE_DOCUMENT_ID) ??
|
|
52
|
+
open.find((d) => d.descriptor.id === SCENE_DOCUMENT_ID);
|
|
53
|
+
if (scene) activateWorkspaceDocument(scene.descriptor.id);
|
|
54
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE Scene tab for a canvas ingest.
|
|
3
|
+
*
|
|
4
|
+
* First-party canvas worlds get `workspace:canvas-scene` from
|
|
5
|
+
* `installRootDocuments`, which reads design-time layer candidates. An
|
|
6
|
+
* `{ ingest }` root is excluded from that list on purpose — remounting the
|
|
7
|
+
* game as a first-party `@pixi/react` world is a category error. The result
|
|
8
|
+
* was a workspace with only `workspace:game`: the live tree existed, and
|
|
9
|
+
* nothing presented it as a Scene.
|
|
10
|
+
*
|
|
11
|
+
* This module is the ingest half of that document. It opens the same pinned
|
|
12
|
+
* Scene id, shows the already-mounted game through the Scene chrome (editor
|
|
13
|
+
* camera backdrop + selection), and reparents the realm host into the Scene
|
|
14
|
+
* pane while that tab is in front. Game keeps the host the rest of the time.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
getMountFailureReports,
|
|
19
|
+
subscribeToMountFailures,
|
|
20
|
+
} from '@volter/editor-core/authoring/mount-failure-report';
|
|
21
|
+
import { installCanvasSceneNavigation } from '@volter/editor-core/authoring/react-canvas-navigation';
|
|
22
|
+
import { createRootViewController } from '@volter/editor-core/authoring/world-pan-state';
|
|
23
|
+
import {
|
|
24
|
+
CANVAS_SCENE_BACKGROUND,
|
|
25
|
+
CanvasSceneBackdrop,
|
|
26
|
+
CanvasSceneControls,
|
|
27
|
+
} from '@volter/editor-core/components/CanvasSceneViewport';
|
|
28
|
+
import { RootSelectionOverlay } from '@volter/editor-core/components/RootSelectionOverlay';
|
|
29
|
+
import { SurfaceStateOverlay } from '@volter/editor-core/components/SurfaceStateOverlay';
|
|
30
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
31
|
+
import { liveDocumentContainer } from '@volter/editor-core/live-document';
|
|
32
|
+
import { projectAdapterFacet } from '@volter/editor-core/project-adapter';
|
|
33
|
+
import { readinessFacet, subscribeRootReadiness } from '@volter/editor-core/readiness';
|
|
34
|
+
import { explainSurface } from '@volter/editor-core/surface-state';
|
|
35
|
+
import {
|
|
36
|
+
registerWorkspaceDocumentSelection,
|
|
37
|
+
type WorkspaceDocumentContentProps,
|
|
38
|
+
} from '@volter/editor-core/workspace-document-registry';
|
|
39
|
+
import type { AuthoringAdapter } from '@volter/editor-project/adapter';
|
|
40
|
+
import { useEffect, useRef, useSyncExternalStore } from 'react';
|
|
41
|
+
import { activeIngest } from './active-ingest';
|
|
42
|
+
import { activeContractScenes } from './active-scene-navigation';
|
|
43
|
+
import { installIngestCanvasSceneDocument } from './ingest-canvas-scene';
|
|
44
|
+
|
|
45
|
+
/** The boot screen's contract id: the live `list()` entry whose label is the
|
|
46
|
+
* table default's own class name (`TitleScreen` → `title`). */
|
|
47
|
+
function defaultContractSceneId(): string | undefined {
|
|
48
|
+
const table = projectAdapterFacet()?.scenes;
|
|
49
|
+
if (!table?.default) return undefined;
|
|
50
|
+
const def = table.entries.find((entry) => entry.id === table.default);
|
|
51
|
+
const name = def?.source?.export ?? def?.id;
|
|
52
|
+
if (!name) return undefined;
|
|
53
|
+
const listed = activeContractScenes()?.storiesFor('') ?? [];
|
|
54
|
+
return listed.find((scene) => scene.label === name || scene.id === name)?.id;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function IngestCanvasSceneContent({
|
|
58
|
+
active,
|
|
59
|
+
adapter,
|
|
60
|
+
documentId,
|
|
61
|
+
store,
|
|
62
|
+
worldId,
|
|
63
|
+
contractSceneId,
|
|
64
|
+
}: WorkspaceDocumentContentProps & {
|
|
65
|
+
adapter: AuthoringAdapter;
|
|
66
|
+
store: EditorShellStore;
|
|
67
|
+
worldId: string;
|
|
68
|
+
/** When set, activating this tab navigates the held ingest to that screen. */
|
|
69
|
+
contractSceneId?: string;
|
|
70
|
+
}) {
|
|
71
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
72
|
+
const viewRef = useRef(createRootViewController());
|
|
73
|
+
const view = viewRef.current;
|
|
74
|
+
useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
75
|
+
const rootReadiness = useSyncExternalStore(subscribeRootReadiness, readinessFacet);
|
|
76
|
+
const mountFailures = useSyncExternalStore(subscribeToMountFailures, getMountFailureReports);
|
|
77
|
+
const live = activeIngest();
|
|
78
|
+
const roots = adapter.hierarchy.roots();
|
|
79
|
+
const emptyRootIds = new Set(adapter.rects?.emptyContainers?.().map((entry) => entry.id) ?? []);
|
|
80
|
+
const hasRenderableContent = roots.some((root) => !emptyRootIds.has(root.id));
|
|
81
|
+
const surfaceExplanation = explainSurface({
|
|
82
|
+
surface: 'Ingest scene',
|
|
83
|
+
rootIds: [worldId],
|
|
84
|
+
phase:
|
|
85
|
+
live?.kind === 'canvas' && live.worldId === worldId && live.session.hostEl
|
|
86
|
+
? 'ready'
|
|
87
|
+
: 'loading',
|
|
88
|
+
content:
|
|
89
|
+
live?.kind !== 'canvas' || live.worldId !== worldId || !live.session.hostEl
|
|
90
|
+
? 'unknown'
|
|
91
|
+
: hasRenderableContent
|
|
92
|
+
? 'present'
|
|
93
|
+
: 'empty',
|
|
94
|
+
readiness: rootReadiness,
|
|
95
|
+
failures: mountFailures,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
return registerWorkspaceDocumentSelection(documentId, () => ({
|
|
100
|
+
adapter,
|
|
101
|
+
nodeId: [...store.selectedEntityIds].find((id) => adapter.hierarchy.node(id)) ?? null,
|
|
102
|
+
}));
|
|
103
|
+
}, [adapter, documentId, store]);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const container = containerRef.current;
|
|
107
|
+
if (!container) return;
|
|
108
|
+
return installCanvasSceneNavigation(container, view);
|
|
109
|
+
}, [view]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!active) return;
|
|
113
|
+
const container = containerRef.current;
|
|
114
|
+
const live = activeIngest();
|
|
115
|
+
const hostEl = live?.kind === 'canvas' ? live.session.hostEl : undefined;
|
|
116
|
+
if (!container || !hostEl) return;
|
|
117
|
+
container.appendChild(hostEl);
|
|
118
|
+
return () => {
|
|
119
|
+
const gameContainer = liveDocumentContainer();
|
|
120
|
+
if (gameContainer && hostEl.parentElement === container) {
|
|
121
|
+
gameContainer.appendChild(hostEl);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}, [active]);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!active) return;
|
|
128
|
+
const sceneId = contractSceneId ?? defaultContractSceneId();
|
|
129
|
+
if (!sceneId) return;
|
|
130
|
+
const scenes = activeContractScenes();
|
|
131
|
+
if (!scenes) return;
|
|
132
|
+
void scenes.goToScene(sceneId);
|
|
133
|
+
}, [active, contractSceneId]);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div
|
|
137
|
+
ref={containerRef}
|
|
138
|
+
data-testid="world-document:ingest-canvas-scene"
|
|
139
|
+
data-vgai-backdrop-color={active ? CANVAS_SCENE_BACKGROUND : undefined}
|
|
140
|
+
data-vgai-backdrop-policy={active ? 'dark-frost' : undefined}
|
|
141
|
+
style={{
|
|
142
|
+
position: 'absolute',
|
|
143
|
+
inset: 0,
|
|
144
|
+
overflow: 'hidden',
|
|
145
|
+
pointerEvents: 'auto',
|
|
146
|
+
backgroundColor: CANVAS_SCENE_BACKGROUND,
|
|
147
|
+
touchAction: 'none',
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<CanvasSceneBackdrop view={view} />
|
|
151
|
+
<RootSelectionOverlay adapter={adapter} view={view} transformModeAware />
|
|
152
|
+
<SurfaceStateOverlay
|
|
153
|
+
explanation={surfaceExplanation}
|
|
154
|
+
testId="ingest-canvas-surface-status"
|
|
155
|
+
style={{ zIndex: 20 }}
|
|
156
|
+
/>
|
|
157
|
+
<CanvasSceneControls
|
|
158
|
+
active={active}
|
|
159
|
+
adapter={adapter}
|
|
160
|
+
containerRef={containerRef}
|
|
161
|
+
view={view}
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Open the pinned Scene tab over the live canvas ingest, or no-op. */
|
|
168
|
+
export function installIngestCanvasScene(store: EditorShellStore): () => void {
|
|
169
|
+
const live = activeIngest();
|
|
170
|
+
if (live?.kind !== 'canvas' || !live.session.adapter) return () => {};
|
|
171
|
+
const adapter = live.session.adapter;
|
|
172
|
+
const worldId = live.worldId;
|
|
173
|
+
const Content = (props: WorkspaceDocumentContentProps) => (
|
|
174
|
+
<IngestCanvasSceneContent {...props} adapter={adapter} store={store} worldId={worldId} />
|
|
175
|
+
);
|
|
176
|
+
return installIngestCanvasSceneDocument(store, Content);
|
|
177
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open/close the pinned canvas Scene document for a live canvas ingest.
|
|
3
|
+
* The React body lives in `ingest-canvas-scene-document.tsx` so this
|
|
4
|
+
* installer can be unit-tested without pulling Pixi/Three into jsdom.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
8
|
+
import { projectAdapterFacet } from '@volter/editor-core/project-adapter';
|
|
9
|
+
import { isolationTabsReplaceGenericScene } from '@volter/editor-core/scene-document-plan';
|
|
10
|
+
import { CANVAS_SCENE_DOCUMENT_ID } from '@volter/editor-core/workspace-document-ids';
|
|
11
|
+
import {
|
|
12
|
+
activateWorkspaceDocument,
|
|
13
|
+
closeWorkspaceDocument,
|
|
14
|
+
openWorkspaceDocument,
|
|
15
|
+
type WorkspaceDocumentContentProps,
|
|
16
|
+
} from '@volter/editor-core/workspace-document-registry';
|
|
17
|
+
import { registerRootDocumentRoute } from '@volter/editor-core/world-document-routing';
|
|
18
|
+
import type { ReactNode } from 'react';
|
|
19
|
+
import { activeIngest } from './active-ingest';
|
|
20
|
+
|
|
21
|
+
export function installIngestCanvasSceneDocument(
|
|
22
|
+
store: EditorShellStore,
|
|
23
|
+
Content: (props: WorkspaceDocumentContentProps) => ReactNode,
|
|
24
|
+
): () => void {
|
|
25
|
+
const live = activeIngest();
|
|
26
|
+
if (live?.kind !== 'canvas' || !live.session.adapter) return () => {};
|
|
27
|
+
const table = projectAdapterFacet()?.scenes;
|
|
28
|
+
// Isolation tabs ARE Edit. The live host is Play's Game document.
|
|
29
|
+
if (table && isolationTabsReplaceGenericScene(table)) return () => {};
|
|
30
|
+
const worldId = live.worldId;
|
|
31
|
+
|
|
32
|
+
openWorkspaceDocument({
|
|
33
|
+
id: CANVAS_SCENE_DOCUMENT_ID,
|
|
34
|
+
title: 'Scene',
|
|
35
|
+
kind: 'world',
|
|
36
|
+
workspaceRole: 'authored-subject',
|
|
37
|
+
provenance: { rootId: worldId },
|
|
38
|
+
Content,
|
|
39
|
+
closeable: false,
|
|
40
|
+
presentation: () => ({ kind: 'world', id: worldId }),
|
|
41
|
+
onActivate: () => store.setActiveViewportTab('edit'),
|
|
42
|
+
});
|
|
43
|
+
activateWorkspaceDocument(CANVAS_SCENE_DOCUMENT_ID);
|
|
44
|
+
const unregisterRoute = registerRootDocumentRoute(worldId, CANVAS_SCENE_DOCUMENT_ID);
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
unregisterRoute();
|
|
48
|
+
closeWorkspaceDocument(CANVAS_SCENE_DOCUMENT_ID, { discardDirty: true });
|
|
49
|
+
};
|
|
50
|
+
}
|