@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,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SCENE DOCUMENT (`workspace:scene`): the active Three scene, titled
|
|
3
|
+
* `Scene` (the subject, not its entry file — see SCENE_DOCUMENT_TITLE),
|
|
4
|
+
* closeable and discoverable in Content, content = the world root's stage.
|
|
5
|
+
*
|
|
6
|
+
* Transcribed from the Scene half of vgai's `components/CenterDocuments.tsx`.
|
|
7
|
+
* The kit keeps the other half (`@volter/editor-core/components/CenterDocuments`
|
|
8
|
+
* `useCenterDocuments`): it binds the Game document's tab, reports the
|
|
9
|
+
* live/stopped edge and closes every document on session teardown. What moved
|
|
10
|
+
* here is only what the kit no longer does — synthesize the Scene document
|
|
11
|
+
* the kit still routes every three root's edit document to, and mirror the
|
|
12
|
+
* store's `activeViewportTab` into registry activation for the Scene/Game pair.
|
|
13
|
+
*
|
|
14
|
+
* THE INPUT-GATE CONTRACT (CLAUDE.md T6.3): `store.activeViewportTab`
|
|
15
|
+
* remains the single source of truth that `play-mode.ts`/`ingest/mount-ingest-root.ts`
|
|
16
|
+
* read (`playState === 'playing' && activeViewportTab === 'play'`) — this
|
|
17
|
+
* module NEVER replaces that predicate, it mirrors registry activation into
|
|
18
|
+
* it (descriptor `onActivate` → `setActiveViewportTab`) and store changes
|
|
19
|
+
* back into registry activation (`syncSceneDocument`, run on every store
|
|
20
|
+
* notify).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
activeDocumentSourcePath,
|
|
25
|
+
activeSaveDestination,
|
|
26
|
+
activeSaveState,
|
|
27
|
+
} from '@volter/editor-core/authoring/shell-document-ops';
|
|
28
|
+
import { Object3DDocumentViewport } from '@volter/editor-core/components/Object3DDocumentViewport';
|
|
29
|
+
import type { DocumentPreviewSource } from '@volter/editor-core/document-preview-source';
|
|
30
|
+
import { editorConsole } from '@volter/editor-core/editor-console';
|
|
31
|
+
import type { EditorShellStore, ViewportTab } from '@volter/editor-core/editor-shell-store';
|
|
32
|
+
import { projectAdapterFacet, subscribeProjectAdapter } from '@volter/editor-core/project-adapter';
|
|
33
|
+
import { getCurrentProject } from '@volter/editor-core/project-manager';
|
|
34
|
+
import { isolationTabsReplaceGenericScene, sceneTabRow } from '@volter/editor-core/scene-document-plan';
|
|
35
|
+
import {
|
|
36
|
+
registerAvailableWorkspaceDocument,
|
|
37
|
+
unregisterAvailableWorkspaceDocument,
|
|
38
|
+
} from '@volter/editor-core/workspace-available-documents';
|
|
39
|
+
import { GAME_DOCUMENT_ID, SCENE_DOCUMENT_ID } from '@volter/editor-core/workspace-document-ids';
|
|
40
|
+
import {
|
|
41
|
+
activateWorkspaceDocument,
|
|
42
|
+
activeWorkspaceDocumentId,
|
|
43
|
+
openWorkspaceDocuments,
|
|
44
|
+
setWorkspaceDocumentDirty,
|
|
45
|
+
type WorkspaceDocumentContentProps,
|
|
46
|
+
type WorkspaceDocumentDescriptor,
|
|
47
|
+
} from '@volter/editor-core/workspace-document-registry';
|
|
48
|
+
import { Button, themeVars } from '@volter/editor-sdk/widgets';
|
|
49
|
+
import { Component, type ErrorInfo, type ReactNode, useEffect, useState } from 'react';
|
|
50
|
+
import { scheduleAfterPaint } from '../after-paint';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The scene document's tab title. It is the WORD `Scene`, not the entry file's
|
|
54
|
+
* name: this document is the project's one three world, and a strip that reads
|
|
55
|
+
* `Scene · 3D · UI` names four subjects the same way instead of leaking one
|
|
56
|
+
* root's implementation file (`world.tsx`) into a rail of plain nouns. The
|
|
57
|
+
* authored source path is still the document's `provenance.sourcePath`, and
|
|
58
|
+
* the unsaved-changes dot is registry state (`dirty`) rather than title text,
|
|
59
|
+
* so both survive a fixed title.
|
|
60
|
+
*/
|
|
61
|
+
export const SCENE_DOCUMENT_TITLE = 'Scene';
|
|
62
|
+
|
|
63
|
+
/** The narrow store surface this module reads/writes — kept minimal so the
|
|
64
|
+
* bridge reads against a plain fake (no full EditorStore). */
|
|
65
|
+
export interface CenterDocumentsStore {
|
|
66
|
+
subscribe(listener: () => void): () => void;
|
|
67
|
+
readonly playState: 'stopped' | 'playing' | 'paused';
|
|
68
|
+
readonly activeViewportTab: ViewportTab;
|
|
69
|
+
/** Where the ACTIVE authoring adapter's edits persist (its
|
|
70
|
+
* `PersistenceProvider.destination`), or null when it persists nothing. */
|
|
71
|
+
readonly savePath: string | null;
|
|
72
|
+
/** Project-relative source path for an immediate-write document whose
|
|
73
|
+
* adapter intentionally exposes no PersistenceProvider. */
|
|
74
|
+
readonly sourcePath?: string | null;
|
|
75
|
+
/** Whether the active authoring adapter has unsaved edits. */
|
|
76
|
+
readonly isDirty: boolean;
|
|
77
|
+
setActiveViewportTab(tab: ViewportTab): void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The root Scene's preview stays available after its tab closes. Its source
|
|
81
|
+
* identity is read lazily because the three authoring adapter arrives after
|
|
82
|
+
* the workspace document is registered on a cold boot. */
|
|
83
|
+
function rootScenePreviewSource(store: CenterDocumentsStore): DocumentPreviewSource {
|
|
84
|
+
const source = (): { path: string; exportName?: string } | null => {
|
|
85
|
+
const table = projectAdapterFacet()?.scenes;
|
|
86
|
+
const row = table
|
|
87
|
+
? sceneTabRow(table).find((item) => item.plan.kind === 'root-document')
|
|
88
|
+
: undefined;
|
|
89
|
+
const declared = row?.entry.source;
|
|
90
|
+
const path = declared?.path ?? store.sourcePath ?? store.savePath;
|
|
91
|
+
return path
|
|
92
|
+
? { path, ...(declared?.export === undefined ? {} : { exportName: declared.export }) }
|
|
93
|
+
: null;
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
revision: () => {
|
|
97
|
+
const current = source();
|
|
98
|
+
return current ? `${current.path}:${current.exportName ?? 'default'}` : 'unavailable';
|
|
99
|
+
},
|
|
100
|
+
subscribe: (listener) => {
|
|
101
|
+
let identity = JSON.stringify(source());
|
|
102
|
+
const changed = () => {
|
|
103
|
+
const next = JSON.stringify(source());
|
|
104
|
+
if (next === identity) return;
|
|
105
|
+
identity = next;
|
|
106
|
+
listener();
|
|
107
|
+
};
|
|
108
|
+
const stopStore = store.subscribe(changed);
|
|
109
|
+
const stopAdapter = subscribeProjectAdapter(changed);
|
|
110
|
+
return () => {
|
|
111
|
+
stopStore();
|
|
112
|
+
stopAdapter();
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
capture: async (request) => {
|
|
116
|
+
const current = source();
|
|
117
|
+
if (!current) throw new Error('The Scene has no declared source module to preview.');
|
|
118
|
+
const owner = await import('./ThreeIsolationSceneContent');
|
|
119
|
+
return owner.captureThreeIsolationScenePreview(
|
|
120
|
+
{
|
|
121
|
+
path: current.path,
|
|
122
|
+
exportName: current.exportName,
|
|
123
|
+
label: SCENE_DOCUMENT_TITLE,
|
|
124
|
+
},
|
|
125
|
+
request,
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface ViewportErrorBoundaryState {
|
|
132
|
+
error: Error | null;
|
|
133
|
+
/** Whether an automatic retry is currently scheduled — tracked as state
|
|
134
|
+
* (not inferred from the timer field) so the "retrying…" copy is part of
|
|
135
|
+
* the same render pass `componentDidCatch` triggers when it schedules one. */
|
|
136
|
+
autoRetryPending: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** THREE's context-creation failure message for the transient
|
|
140
|
+
* Chromium/SwiftShader GPU-channel race that occasionally loses at editor
|
|
141
|
+
* boot — distinct from a genuinely dead/unsupported renderer, which fails
|
|
142
|
+
* differently. */
|
|
143
|
+
const TRANSIENT_RENDERER_STARTUP_FAILURE_PATTERN = /creating WebGL context/i;
|
|
144
|
+
|
|
145
|
+
/** The SAME transient class in its other spelling: the context was created but
|
|
146
|
+
* came back already dead, so THREE's capability probe reads `precision` off
|
|
147
|
+
* the `null` that `getShaderPrecisionFormat` returns on a lost context. Seen
|
|
148
|
+
* live while a project's thumbnail sweep was churning short-lived contexts —
|
|
149
|
+
* the same renderer boots fine on the retry, exactly like the class above. */
|
|
150
|
+
const TRANSIENT_DEAD_CONTEXT_PATTERN = /null \(reading 'precision'\)/;
|
|
151
|
+
|
|
152
|
+
/** True when `error` matches the transient renderer-startup failure class. */
|
|
153
|
+
export function isTransientRendererStartupFailure(error: Error): boolean {
|
|
154
|
+
return (
|
|
155
|
+
TRANSIENT_RENDERER_STARTUP_FAILURE_PATTERN.test(error.message) ||
|
|
156
|
+
TRANSIENT_DEAD_CONTEXT_PATTERN.test(error.message)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Bound on automatic retries per boundary instance — see `componentDidCatch`. */
|
|
161
|
+
const MAX_AUTO_RETRIES = 3;
|
|
162
|
+
|
|
163
|
+
/** A renderer failure must degrade the Scene document, not unmount the whole
|
|
164
|
+
* workspace. The surrounding hierarchy, Inspector, Console, menus,
|
|
165
|
+
* and project resources remain usable, with an explicit retry after the old
|
|
166
|
+
* renderer/context has been torn down.
|
|
167
|
+
*
|
|
168
|
+
* A transient WebGL context-creation failure at boot would otherwise kill the
|
|
169
|
+
* viewport permanently even though the SAME renderer boots fine moments
|
|
170
|
+
* later. For that specific, recognizable failure class the boundary retries
|
|
171
|
+
* automatically with backoff BEFORE surfacing the manual button — bounded to
|
|
172
|
+
* 3 attempts per boundary instance so a genuinely dead renderer still ends up
|
|
173
|
+
* in front of the user with the manual UI. */
|
|
174
|
+
export class ViewportErrorBoundary extends Component<
|
|
175
|
+
{ children: ReactNode },
|
|
176
|
+
ViewportErrorBoundaryState
|
|
177
|
+
> {
|
|
178
|
+
override state: ViewportErrorBoundaryState = { error: null, autoRetryPending: false };
|
|
179
|
+
|
|
180
|
+
/** Automatic retries already used by THIS boundary instance — never reset
|
|
181
|
+
* by a manual retry click, so a manual click after the auto-budget is
|
|
182
|
+
* exhausted does not re-open the budget. */
|
|
183
|
+
private autoRetryCount = 0;
|
|
184
|
+
// `window.setTimeout` (DOM lib) returns `number`; typed explicitly rather
|
|
185
|
+
// than via `ReturnType<typeof setTimeout>` because @types/node's ambient
|
|
186
|
+
// overload would otherwise mis-widen this to `Timeout`.
|
|
187
|
+
private retryTimer: number | null = null;
|
|
188
|
+
|
|
189
|
+
static getDerivedStateFromError(error: Error): ViewportErrorBoundaryState {
|
|
190
|
+
return { error, autoRetryPending: false };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private clearPendingRetry(): void {
|
|
194
|
+
if (this.retryTimer !== null) {
|
|
195
|
+
window.clearTimeout(this.retryTimer);
|
|
196
|
+
this.retryTimer = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
override componentDidCatch(error: Error, info: ErrorInfo): void {
|
|
201
|
+
editorConsole.error(`Viewport failed to start: ${error.message}`, 'editor');
|
|
202
|
+
// Keep the component stack in the browser console for developer diagnosis.
|
|
203
|
+
// biome-ignore lint/suspicious/noConsole: renderer startup failures need their React component stack in developer tools as well as the user-facing editor Console entry
|
|
204
|
+
console.error('[vgai] Viewport failed to start', error, info.componentStack);
|
|
205
|
+
|
|
206
|
+
if (isTransientRendererStartupFailure(error) && this.autoRetryCount < MAX_AUTO_RETRIES) {
|
|
207
|
+
this.clearPendingRetry();
|
|
208
|
+
const delayMs = 500 * 2 ** this.autoRetryCount;
|
|
209
|
+
this.autoRetryCount++;
|
|
210
|
+
this.retryTimer = window.setTimeout(() => {
|
|
211
|
+
this.retryTimer = null;
|
|
212
|
+
this.setState({ error: null, autoRetryPending: false });
|
|
213
|
+
}, delayMs);
|
|
214
|
+
this.setState({ autoRetryPending: true });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
override componentWillUnmount(): void {
|
|
219
|
+
this.clearPendingRetry();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private handleManualRetry = (): void => {
|
|
223
|
+
// A manual click also cancels any pending auto-retry (the boundary must
|
|
224
|
+
// not race a scheduled retry against the user's own), but deliberately
|
|
225
|
+
// does NOT reset `autoRetryCount` — the auto-retry budget is per
|
|
226
|
+
// boundary instance, not per error occurrence.
|
|
227
|
+
this.clearPendingRetry();
|
|
228
|
+
this.setState({ error: null, autoRetryPending: false });
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
override render() {
|
|
232
|
+
if (!this.state.error) return this.props.children;
|
|
233
|
+
const retryPending = this.state.autoRetryPending;
|
|
234
|
+
return (
|
|
235
|
+
<div
|
|
236
|
+
data-testid="viewport-startup-error"
|
|
237
|
+
role="alert"
|
|
238
|
+
style={{
|
|
239
|
+
position: 'absolute',
|
|
240
|
+
inset: 0,
|
|
241
|
+
display: 'flex',
|
|
242
|
+
flexDirection: 'column',
|
|
243
|
+
alignItems: 'center',
|
|
244
|
+
justifyContent: 'center',
|
|
245
|
+
gap: 8,
|
|
246
|
+
padding: 24,
|
|
247
|
+
background: themeVars.surface.shell,
|
|
248
|
+
color: themeVars.content.muted,
|
|
249
|
+
textAlign: 'center',
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
<div style={{ color: themeVars.content.primary, fontSize: 14 }}>
|
|
253
|
+
{retryPending ? 'Viewport unavailable — retrying…' : 'Viewport unavailable'}
|
|
254
|
+
</div>
|
|
255
|
+
<div style={{ maxWidth: 520, color: themeVars.content.dim, fontSize: 11 }}>
|
|
256
|
+
{this.state.error.message}
|
|
257
|
+
</div>
|
|
258
|
+
<Button
|
|
259
|
+
type="button"
|
|
260
|
+
className="vgai-btn"
|
|
261
|
+
data-testid="retry-viewport"
|
|
262
|
+
onClick={this.handleManualRetry}
|
|
263
|
+
>
|
|
264
|
+
Retry Viewport
|
|
265
|
+
</Button>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The Scene document's content: the kit's ONE 3D STAGE HOST, showing the
|
|
273
|
+
* world root. It is a document like any other — same host, same furniture,
|
|
274
|
+
* same overlays, same per-stage door — and the ONE thing it says that a
|
|
275
|
+
* prefab's stage does not is WHAT it shows (`content`): the world, through
|
|
276
|
+
* this package's world-root binding, loaded only when a Scene stage mounts.
|
|
277
|
+
*
|
|
278
|
+
* The after-paint gate owns the mount of everything below it — the viewport
|
|
279
|
+
* door's binding included — so it may never be scheduled on a bare rAF chain.
|
|
280
|
+
* A hidden tab fires no frames at all and the editor would boot to a
|
|
281
|
+
* heartbeating shell that can never start play.
|
|
282
|
+
*/
|
|
283
|
+
const SCENE_CONTENT = {
|
|
284
|
+
kind: 'world-root',
|
|
285
|
+
load: () => import('./world-root-stage-binding').then((module) => module.worldRootStageBinding),
|
|
286
|
+
} as const;
|
|
287
|
+
|
|
288
|
+
function SceneDocumentContent({ documentId, active }: WorkspaceDocumentContentProps) {
|
|
289
|
+
const [ready, setReady] = useState(false);
|
|
290
|
+
useEffect(() => scheduleAfterPaint(() => setReady(true)), []);
|
|
291
|
+
return (
|
|
292
|
+
<ViewportErrorBoundary>
|
|
293
|
+
{ready ? (
|
|
294
|
+
<Object3DDocumentViewport
|
|
295
|
+
documentId={documentId}
|
|
296
|
+
active={active}
|
|
297
|
+
content={SCENE_CONTENT}
|
|
298
|
+
displayName={SCENE_DOCUMENT_TITLE}
|
|
299
|
+
/>
|
|
300
|
+
) : null}
|
|
301
|
+
</ViewportErrorBoundary>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Build the Scene document descriptor bound to a store. `onActivate` is the
|
|
306
|
+
* registry→store half of the bridge: activating the tab (click, open,
|
|
307
|
+
* close-neighbor fallback) writes the store's `activeViewportTab`, which is
|
|
308
|
+
* what the T6.3 input gate reads. */
|
|
309
|
+
export function createCenterDocumentDescriptors(store: CenterDocumentsStore): {
|
|
310
|
+
scene: WorkspaceDocumentDescriptor;
|
|
311
|
+
} {
|
|
312
|
+
const documentPath = store.sourcePath ?? store.savePath;
|
|
313
|
+
const scene: WorkspaceDocumentDescriptor = {
|
|
314
|
+
id: SCENE_DOCUMENT_ID,
|
|
315
|
+
title: SCENE_DOCUMENT_TITLE,
|
|
316
|
+
kind: 'scene',
|
|
317
|
+
workspaceRole: 'authored-subject',
|
|
318
|
+
...(documentPath ? { provenance: { sourcePath: documentPath } } : {}),
|
|
319
|
+
Content: SceneDocumentContent,
|
|
320
|
+
preview: rootScenePreviewSource(store),
|
|
321
|
+
presentation: () =>
|
|
322
|
+
store.savePath
|
|
323
|
+
? { kind: 'scene', path: store.savePath }
|
|
324
|
+
: { kind: 'workspace', id: SCENE_DOCUMENT_ID },
|
|
325
|
+
onActivate: () => store.setActiveViewportTab('edit'),
|
|
326
|
+
};
|
|
327
|
+
return { scene };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The store→registry half of the bridge for the Scene/Game pair. Idempotent —
|
|
332
|
+
* safe (and designed) to run on EVERY store notify; re-entrant calls triggered
|
|
333
|
+
* by its own `onActivate` → `setActiveViewportTab` → notify chain settle as
|
|
334
|
+
* no-ops. The Game document's live/stopped edge is the kit's
|
|
335
|
+
* (`useCenterDocuments`), not repeated here.
|
|
336
|
+
*
|
|
337
|
+
* 1. the scene document is available when the manifest has a Three root;
|
|
338
|
+
* 2. its tab's dirty dot tracks `isDirty` live (the title is fixed);
|
|
339
|
+
* 3. the active document mirrors `activeViewportTab` (auto-switch on
|
|
340
|
+
* play/stop, `set-viewport-tab` control commands, the ingest mount lane's
|
|
341
|
+
* direct writes). A `play` tab value with no Game document open (stopped,
|
|
342
|
+
* or a transient store state) activates the scene if its tab is open.
|
|
343
|
+
*/
|
|
344
|
+
export function syncCenterDocuments(
|
|
345
|
+
store: CenterDocumentsStore,
|
|
346
|
+
docs: { scene: WorkspaceDocumentDescriptor },
|
|
347
|
+
hasThreeRoot = true,
|
|
348
|
+
): void {
|
|
349
|
+
const isOpen = (id: string) => openWorkspaceDocuments().some((d) => d.descriptor.id === id);
|
|
350
|
+
// Opening a registry document activates it. Capture the caller's requested
|
|
351
|
+
// viewport before touching the open set, then restore that intent below.
|
|
352
|
+
const requestedViewportTab = store.activeViewportTab;
|
|
353
|
+
|
|
354
|
+
const table = projectAdapterFacet()?.scenes;
|
|
355
|
+
if (hasThreeRoot && !(table && isolationTabsReplaceGenericScene(table))) {
|
|
356
|
+
const row = table
|
|
357
|
+
? sceneTabRow(table).find((item) => item.plan.kind === 'root-document')
|
|
358
|
+
: undefined;
|
|
359
|
+
registerAvailableWorkspaceDocument(
|
|
360
|
+
{
|
|
361
|
+
...docs.scene,
|
|
362
|
+
title: row?.plan.kind === 'root-document' ? row.plan.title : docs.scene.title,
|
|
363
|
+
provenance: {
|
|
364
|
+
...docs.scene.provenance,
|
|
365
|
+
...(row?.entry.source?.path ? { sourcePath: row.entry.source.path } : {}),
|
|
366
|
+
...(row?.plan.kind === 'root-document' ? { rootId: row.plan.regionId } : {}),
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
category: 'scene',
|
|
371
|
+
rootId: row?.plan.kind === 'root-document' ? row.plan.regionId : undefined,
|
|
372
|
+
default: table !== undefined && (row?.isDefault ?? table.entries.length === 0),
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
setWorkspaceDocumentDirty(SCENE_DOCUMENT_ID, store.isDirty);
|
|
376
|
+
} else {
|
|
377
|
+
unregisterAvailableWorkspaceDocument(SCENE_DOCUMENT_ID);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Mirror the store tab into registry activation. A THIRD-party document
|
|
381
|
+
// being active is a legitimate state (its own onActivate already asserted
|
|
382
|
+
// the store's tab, so the T6.3 gate is off). The mirror therefore only
|
|
383
|
+
// corrects genuine disagreements about the SCENE/GAME pair:
|
|
384
|
+
// - tab says 'play' (play auto-switch, `set-viewport-tab play`) → the
|
|
385
|
+
// Game document must be active;
|
|
386
|
+
// - tab says 'edit' while the GAME document is active (external
|
|
387
|
+
// `set-viewport-tab edit` during play) → back to the scene document;
|
|
388
|
+
// - no active document at all (first run) → the scene document.
|
|
389
|
+
// An 'edit' tab with an asset/story document active is CONSISTENT and must
|
|
390
|
+
// not be "corrected".
|
|
391
|
+
const activeId = activeWorkspaceDocumentId();
|
|
392
|
+
if (requestedViewportTab === 'play' && isOpen(GAME_DOCUMENT_ID)) {
|
|
393
|
+
if (activeId !== GAME_DOCUMENT_ID) activateWorkspaceDocument(GAME_DOCUMENT_ID);
|
|
394
|
+
} else if (hasThreeRoot && (activeId === GAME_DOCUMENT_ID || activeId === null)) {
|
|
395
|
+
activateWorkspaceDocument(SCENE_DOCUMENT_ID);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Installs the Scene document for one session store and keeps it synced. The
|
|
401
|
+
* returned teardown stops syncing; the service withdraws the Scene document, and the kit's
|
|
402
|
+
* `useCenterDocuments` closes every open document on session teardown.
|
|
403
|
+
*/
|
|
404
|
+
export function bindSceneDocument(store: EditorShellStore): () => void {
|
|
405
|
+
// `savePath`/`isDirty` are the ACTIVE authoring adapter's, read live
|
|
406
|
+
// through getters rather than copied — the shell owns neither.
|
|
407
|
+
// Keep the authored source identity across Edit → Play: Play deliberately
|
|
408
|
+
// swaps in a live adapter with no source document, but the Scene
|
|
409
|
+
// tab still represents the same authored TSX document.
|
|
410
|
+
let lastSourcePath: string | null = null;
|
|
411
|
+
const view: CenterDocumentsStore = {
|
|
412
|
+
subscribe: (listener) => store.subscribe(listener),
|
|
413
|
+
get playState() {
|
|
414
|
+
return store.playState;
|
|
415
|
+
},
|
|
416
|
+
get activeViewportTab() {
|
|
417
|
+
return store.activeViewportTab;
|
|
418
|
+
},
|
|
419
|
+
get savePath() {
|
|
420
|
+
return activeSaveDestination(store);
|
|
421
|
+
},
|
|
422
|
+
get sourcePath() {
|
|
423
|
+
const current = activeDocumentSourcePath(store);
|
|
424
|
+
if (current) lastSourcePath = current;
|
|
425
|
+
return lastSourcePath;
|
|
426
|
+
},
|
|
427
|
+
get isDirty() {
|
|
428
|
+
return activeSaveState(store) === 'unsaved';
|
|
429
|
+
},
|
|
430
|
+
setActiveViewportTab: (tab) => store.setActiveViewportTab(tab),
|
|
431
|
+
};
|
|
432
|
+
const docs = createCenterDocumentDescriptors(view);
|
|
433
|
+
// Missing is the legacy-safe default: manifests read by current editor
|
|
434
|
+
// versions always carry this derived flag.
|
|
435
|
+
const run = () =>
|
|
436
|
+
syncCenterDocuments(view, docs, getCurrentProject()?.config.hasThreeRoot !== false);
|
|
437
|
+
run();
|
|
438
|
+
const unsubscribe = store.subscribe(run);
|
|
439
|
+
const unsubscribeAdapter = subscribeProjectAdapter(run);
|
|
440
|
+
// Unbinding follows the session's store and leaves the document registered:
|
|
441
|
+
// a store arriving again at boot must not close a restored Scene tab. The
|
|
442
|
+
// service withdraws the document when it stops.
|
|
443
|
+
return () => {
|
|
444
|
+
unsubscribe();
|
|
445
|
+
unsubscribeAdapter();
|
|
446
|
+
};
|
|
447
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selected view for each bottom-drawer utility that hosts more than one
|
|
3
|
+
* instrument.
|
|
4
|
+
*
|
|
5
|
+
* Every store is the usual `useSyncExternalStore` triple (subscribe, version,
|
|
6
|
+
* active) plus `open`, which selects the view AND reveals its own utility
|
|
7
|
+
* through the semantic dock command. Callers name the utility they mean:
|
|
8
|
+
* `profilerView` is the engine instrument bench (Profiler/Frame).
|
|
9
|
+
*
|
|
10
|
+
* A game's own faces are deliberately not among them. They are PROJECT
|
|
11
|
+
* contributions — the game's own vendored source, mounted through the
|
|
12
|
+
* contribution registries — and a contribution owns whatever state it needs
|
|
13
|
+
* inside its own module, so there is nothing for this editor-owned store to
|
|
14
|
+
* hold on its behalf. This store survives on the host side only because HOST
|
|
15
|
+
* chrome reads it: the header heartbeat and the Game toolbar's Capture frame
|
|
16
|
+
* button both select a view of a bench that now ships in `@volter/editor-game`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { showWorkspaceUtility } from '@volter/editor-core/workspace-host-commands';
|
|
20
|
+
|
|
21
|
+
export type ProfilerViewId = 'profiler' | 'frame';
|
|
22
|
+
|
|
23
|
+
export interface UtilityViewState<T extends string> {
|
|
24
|
+
subscribe(listener: () => void): () => void;
|
|
25
|
+
/** Monotonic change counter — the `getSnapshot` for `useSyncExternalStore`. */
|
|
26
|
+
version(): number;
|
|
27
|
+
active(): T;
|
|
28
|
+
select(view: T): void;
|
|
29
|
+
/** Select the view and reveal its utility in the bottom region. */
|
|
30
|
+
open(view: T): void;
|
|
31
|
+
/** Re-publish without changing the view (state this store carries beside
|
|
32
|
+
* the selected view, e.g. the selected machine, notifies through here). */
|
|
33
|
+
notify(): void;
|
|
34
|
+
reset(): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createUtilityViewState<T extends string>(
|
|
38
|
+
utilityId: string,
|
|
39
|
+
initial: T,
|
|
40
|
+
): UtilityViewState<T> {
|
|
41
|
+
let active = initial;
|
|
42
|
+
let version = 0;
|
|
43
|
+
const listeners = new Set<() => void>();
|
|
44
|
+
const notify = () => {
|
|
45
|
+
version++;
|
|
46
|
+
for (const listener of listeners) listener();
|
|
47
|
+
};
|
|
48
|
+
const select = (view: T) => {
|
|
49
|
+
if (active === view) return;
|
|
50
|
+
active = view;
|
|
51
|
+
notify();
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
subscribe(listener) {
|
|
55
|
+
listeners.add(listener);
|
|
56
|
+
return () => {
|
|
57
|
+
listeners.delete(listener);
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
version: () => version,
|
|
61
|
+
active: () => active,
|
|
62
|
+
select,
|
|
63
|
+
open(view) {
|
|
64
|
+
select(view);
|
|
65
|
+
showWorkspaceUtility(utilityId);
|
|
66
|
+
},
|
|
67
|
+
notify,
|
|
68
|
+
reset() {
|
|
69
|
+
active = initial;
|
|
70
|
+
notify();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Engine instruments: the Profiler and the single-frame draw-call capture. */
|
|
76
|
+
export const profilerView = createUtilityViewState<ProfilerViewId>(
|
|
77
|
+
// The bench is `@volter/editor-game`'s `profiler.utility` contribution, so its
|
|
78
|
+
// registered id carries the host's `tool:` namespace. This store stays on
|
|
79
|
+
// the host side because host chrome reads it (`HeaderTelemetry`'s heartbeat,
|
|
80
|
+
// the Game toolbar's Capture frame button).
|
|
81
|
+
'tool:profiler.utility',
|
|
82
|
+
'profiler',
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
export function __resetUtilityViewStateForTest(): void {
|
|
86
|
+
profilerView.reset();
|
|
87
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE GAME'S WORLD-ROOT BINDING for the kit's stage host
|
|
3
|
+
* (`@volter/editor-core/components/world-root-binding`): the world's surface,
|
|
4
|
+
* its stage, and the overlays the stage shows only over a world.
|
|
5
|
+
*
|
|
6
|
+
* `Overlays` is the world-root half of vgai's `stage-overlay-set.tsx` — every
|
|
7
|
+
* part it rendered only when `worldRoot` held (the root selection layer, the
|
|
8
|
+
* camera-authoring pin, the controls hint while the world mounts, the scene's
|
|
9
|
+
* surface-state card). The rest of that overlay set is the kit's
|
|
10
|
+
* `StageOverlays`, which the host renders beside this.
|
|
11
|
+
*/
|
|
12
|
+
import { EDITOR_LAYER } from '@volter/editor-threejs/viewport/editor-layers';
|
|
13
|
+
import { activeAuthoringVersion, subscribeActiveAuthoring } from '@volter/editor-core/authoring/active-adapter';
|
|
14
|
+
import {
|
|
15
|
+
getMountFailureReports,
|
|
16
|
+
subscribeToMountFailures,
|
|
17
|
+
} from '@volter/editor-core/authoring/mount-failure-report';
|
|
18
|
+
import {
|
|
19
|
+
object3DDocumentSessionsVersion,
|
|
20
|
+
subscribeObject3DDocumentSessions,
|
|
21
|
+
} from '@volter/editor-core/authoring/object3d-document-session-registry';
|
|
22
|
+
import { RootSelectionOverlay } from '@volter/editor-core/components/RootSelectionOverlay';
|
|
23
|
+
import { SurfaceStateOverlay } from '@volter/editor-core/components/SurfaceStateOverlay';
|
|
24
|
+
import { ViewportControlsHint } from '@volter/editor-core/components/ViewportControlsHint';
|
|
25
|
+
import type {
|
|
26
|
+
WorldRootOverlayProps,
|
|
27
|
+
WorldRootStageBinding,
|
|
28
|
+
} from '@volter/editor-core/components/world-root-binding';
|
|
29
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
30
|
+
import { readinessFacet, subscribeRootReadiness } from '@volter/editor-core/readiness';
|
|
31
|
+
import { documentStageContext, threeSelectionToolsApply } from '@volter/editor-core/stage-context';
|
|
32
|
+
import { explainSurface } from '@volter/editor-core/surface-state';
|
|
33
|
+
import {
|
|
34
|
+
subscribeWorkspaceDocuments,
|
|
35
|
+
workspaceDocumentRegistryVersion,
|
|
36
|
+
} from '@volter/editor-core/workspace-document-registry';
|
|
37
|
+
import { useMemo, useSyncExternalStore } from 'react';
|
|
38
|
+
import type * as THREE from 'three';
|
|
39
|
+
import { threeSceneHasRenderableContent } from '../surface-content';
|
|
40
|
+
import { CameraAuthoringOverlay } from './CameraAuthoringOverlay';
|
|
41
|
+
import { installWorldRootStage, mountWorldRootSurface } from './world-root-stage';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The scene-status overlay is the sole viewport child whose truth can change
|
|
45
|
+
* from object-map membership alone (an empty live scene may gain its first
|
|
46
|
+
* renderable object). Keep that exact broad subscription here so runtime
|
|
47
|
+
* structure does not rerender the canvas, chrome, or collaboration surface.
|
|
48
|
+
*/
|
|
49
|
+
function SceneViewportStateOverlay({
|
|
50
|
+
store,
|
|
51
|
+
rootIds,
|
|
52
|
+
mountStatus,
|
|
53
|
+
}: {
|
|
54
|
+
store: EditorShellStore;
|
|
55
|
+
rootIds: readonly string[];
|
|
56
|
+
mountStatus: 'mounting' | 'ready';
|
|
57
|
+
}) {
|
|
58
|
+
useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
59
|
+
const rootReadiness = useSyncExternalStore(subscribeRootReadiness, readinessFacet);
|
|
60
|
+
const mountFailures = useSyncExternalStore(subscribeToMountFailures, getMountFailureReports);
|
|
61
|
+
const sceneBackground = store.scene?.background as THREE.Color | null | undefined;
|
|
62
|
+
const sceneBackgroundIsAuthored =
|
|
63
|
+
sceneBackground !== null &&
|
|
64
|
+
!(sceneBackground?.isColor === true && sceneBackground.getHex() === 0xaaaaaa);
|
|
65
|
+
const sceneHasRenderableObjects = useMemo(
|
|
66
|
+
() =>
|
|
67
|
+
Boolean(
|
|
68
|
+
store.scene &&
|
|
69
|
+
threeSceneHasRenderableContent(store.scene, {
|
|
70
|
+
includeBackground: false,
|
|
71
|
+
ignoredLayer: EDITOR_LAYER,
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
[store.scene, store.contentVersion],
|
|
75
|
+
);
|
|
76
|
+
const sceneHasContent = Boolean(
|
|
77
|
+
store.scene && (sceneBackgroundIsAuthored || sceneHasRenderableObjects),
|
|
78
|
+
);
|
|
79
|
+
const explanation = explainSurface({
|
|
80
|
+
surface: 'Scene',
|
|
81
|
+
rootIds,
|
|
82
|
+
phase: mountStatus === 'mounting' ? 'loading' : 'ready',
|
|
83
|
+
content: sceneHasContent ? 'present' : mountStatus === 'mounting' ? 'unknown' : 'empty',
|
|
84
|
+
readiness: rootReadiness,
|
|
85
|
+
failures: mountFailures,
|
|
86
|
+
});
|
|
87
|
+
return <SurfaceStateOverlay explanation={explanation} testId="scene-viewport-status" />;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function WorldRootOverlays({
|
|
91
|
+
store,
|
|
92
|
+
documentId,
|
|
93
|
+
cameraPreviewRef,
|
|
94
|
+
mountStatus,
|
|
95
|
+
rootIds,
|
|
96
|
+
}: WorldRootOverlayProps) {
|
|
97
|
+
useSyncExternalStore(store.subscribe, store.getShellSnapshot ?? store.getSnapshot);
|
|
98
|
+
useSyncExternalStore(subscribeActiveAuthoring, activeAuthoringVersion);
|
|
99
|
+
useSyncExternalStore(subscribeObject3DDocumentSessions, object3DDocumentSessionsVersion);
|
|
100
|
+
useSyncExternalStore(subscribeWorkspaceDocuments, workspaceDocumentRegistryVersion);
|
|
101
|
+
// The world root's stage is the Scene document's: the kit's overlay props
|
|
102
|
+
// carry no document id, and the Scene document is the one world-root stage.
|
|
103
|
+
const ctx = documentStageContext(store, documentId, 'document');
|
|
104
|
+
const showsSelectionTools = threeSelectionToolsApply(ctx);
|
|
105
|
+
return (
|
|
106
|
+
<>
|
|
107
|
+
{/* Three-scene selection overlay. React/Pixi documents mount their own
|
|
108
|
+
co-located overlay beside the isolated world host, and the worlds it
|
|
109
|
+
draws over are the manifest's — so its condition is the stage showing
|
|
110
|
+
them, the same fact the surface-state card reads. */}
|
|
111
|
+
{ctx.surface !== null ? <RootSelectionOverlay /> : null}
|
|
112
|
+
{/* A pinned camera preview survives deselection. The camera-authoring
|
|
113
|
+
host is installed by the world root's own binding
|
|
114
|
+
(`world-root-stage.ts`), so the pin belongs to the stage that
|
|
115
|
+
installed it. */}
|
|
116
|
+
{ctx.surface === 'three' ? <CameraAuthoringOverlay previewRef={cameraPreviewRef} /> : null}
|
|
117
|
+
{/* The hint also shows WHILE the world is still mounting: a new user
|
|
118
|
+
reads the screen during those boot seconds and looks for the camera
|
|
119
|
+
controls exactly then. The kit's overlay set shows it on a three
|
|
120
|
+
surface; this adds only the mounting case it does not cover. */}
|
|
121
|
+
{!showsSelectionTools && ctx.surface !== 'three' && mountStatus === 'mounting' ? (
|
|
122
|
+
<ViewportControlsHint />
|
|
123
|
+
) : null}
|
|
124
|
+
<SceneViewportStateOverlay store={store} rootIds={rootIds} mountStatus={mountStatus} />
|
|
125
|
+
</>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const worldRootStageBinding: WorldRootStageBinding = {
|
|
130
|
+
mountWorldRootSurface,
|
|
131
|
+
installWorldRootStage,
|
|
132
|
+
Overlays: WorldRootOverlays,
|
|
133
|
+
};
|