@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,1046 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE WORLD ROOT'S CONTENT BINDING — what the one stage host shows when its
|
|
3
|
+
* `content` is `{ kind: 'world-root' }` (ARCHITECTURE-CORE §One stage).
|
|
4
|
+
*
|
|
5
|
+
* This is the scene panel's whole body, moved: the design session that mounts
|
|
6
|
+
* the manifest's three roots, the composer that draws them (the world's own
|
|
7
|
+
* image pipeline, the shading modes, the soft-particle depth pass, the
|
|
8
|
+
* adopted-image config), the camera-authoring host, the viewport action bus,
|
|
9
|
+
* the auto-frame window, and the presenter Play adopts through
|
|
10
|
+
* (`viewport-root-presentation.ts`). A `build` binding shows one Object3D the
|
|
11
|
+
* project constructed; THIS one shows the project's world, and Play adoption
|
|
12
|
+
* is the condition on it — a build binding presents nothing.
|
|
13
|
+
*
|
|
14
|
+
* WHY IT IS A SEPARATE MODULE, and loaded by dynamic import:
|
|
15
|
+
* `scripts/validate-editor-closure.mjs` pins the host because a BOUNDED HOST
|
|
16
|
+
* (`@volter/editor-blender`'s Model document) mounts it with no shell above it. Everything below reaches the shell's own estate — its transport
|
|
17
|
+
* (`editor-api.ts`), the collaboration client, the server-log bridge, the
|
|
18
|
+
* command listener, the world-adapter install. Static imports would have put
|
|
19
|
+
* all of that into every stage's closure, which is exactly the erosion that
|
|
20
|
+
* ratchet exists to stop. The condition that selects this content gates its
|
|
21
|
+
* LOAD too.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ViewportPresentation, ViewportRoot } from '@volter/editor-sdk/host';
|
|
25
|
+
import { themeVars } from '@volter/editor-sdk/widgets';
|
|
26
|
+
import { createPerformanceProfiler } from '@volter/game-runtime/dev/performance-profiler';
|
|
27
|
+
import { createWebGLGpuTimer } from '@volter/game-runtime/dev/webgl-gpu-timer';
|
|
28
|
+
import type { AuthoringAdapter, Transform } from '@volter/editor-project/adapter';
|
|
29
|
+
import { markHostRenderer } from '@volter/editor-threejs/viewport/renderer-ownership';
|
|
30
|
+
import { applyWorldRendererConfig } from '@volter/threejs-runtime/adapter/renderer-config';
|
|
31
|
+
import { getUserData } from '@volter/threejs-runtime/ecs/user-data';
|
|
32
|
+
import { detectKtx2Support } from '@volter/threejs-runtime/loader';
|
|
33
|
+
import { resolveRenderSettings } from '@volter/threejs-runtime/render/render-settings';
|
|
34
|
+
import { createSoftParticleDepthPass } from '@volter/threejs-runtime/render/soft-particle-depth';
|
|
35
|
+
import { ViewportShadingRenderer } from '@volter/threejs-runtime/render/viewport-shading';
|
|
36
|
+
import {
|
|
37
|
+
applyRendererSettings,
|
|
38
|
+
applySceneRenderPipeline,
|
|
39
|
+
} from '@volter/threejs-runtime/setup/setup-renderer';
|
|
40
|
+
import {
|
|
41
|
+
BlendFunction,
|
|
42
|
+
EffectComposer,
|
|
43
|
+
EffectPass,
|
|
44
|
+
KernelSize,
|
|
45
|
+
OutlineEffect,
|
|
46
|
+
type Pass,
|
|
47
|
+
} from 'postprocessing';
|
|
48
|
+
import * as THREE from 'three';
|
|
49
|
+
import { getActiveAuthoring } from '@volter/editor-core/authoring/active-adapter';
|
|
50
|
+
import type { CompositeAuthoringAdapter } from '@volter/editor-core/authoring/composite-authoring-adapter';
|
|
51
|
+
import {
|
|
52
|
+
applyAuthoringTransform,
|
|
53
|
+
beginAuthoringTransformEdit,
|
|
54
|
+
endAuthoringTransformEdit,
|
|
55
|
+
} from '@volter/editor-core/authoring/consumer-actions';
|
|
56
|
+
import { designTimeMountFor } from '@volter/editor-core/authoring/design-time-mount-registry';
|
|
57
|
+
import { attachProjectAuthoringStage } from '@volter/editor-core/authoring/project-authoring-session';
|
|
58
|
+
import {
|
|
59
|
+
isThreejsSurfaceVisible,
|
|
60
|
+
resolveThreeViewportRootId,
|
|
61
|
+
} from '@volter/editor-core/authoring/world-hidden-viewport';
|
|
62
|
+
import { AutoFrameWindow } from '../auto-frame-window';
|
|
63
|
+
import {
|
|
64
|
+
type CameraAuthoringSubject,
|
|
65
|
+
type CameraViewMode,
|
|
66
|
+
cameraAuthoringPresentation,
|
|
67
|
+
installCameraAuthoringHost,
|
|
68
|
+
} from '@volter/editor-core/camera-authoring';
|
|
69
|
+
import { registerPresentedCanvasFrame } from '@volter/editor-core/canvas-preview-frames';
|
|
70
|
+
import { collectState } from '@volter/editor-core/command-listener';
|
|
71
|
+
import { getDownloadedAssetPath, getOnlineAssetFiles, reportEditorState } from '@volter/editor-core/editor-api';
|
|
72
|
+
import { editorConsole } from '@volter/editor-core/editor-console';
|
|
73
|
+
import { isEditorPresentationActive } from '@volter/editor-core/editor-presentation-activity';
|
|
74
|
+
import type { EditorStats } from '@volter/editor-core/editor-runtime';
|
|
75
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
76
|
+
import { EditorViewport, type OnlineAssetDrop } from '@volter/editor-core/editor-viewport';
|
|
77
|
+
import { entityObject3D } from '@volter/editor-core/entity-object';
|
|
78
|
+
import {
|
|
79
|
+
nativeSelectionColors,
|
|
80
|
+
nativeViewportLook,
|
|
81
|
+
subscribeNativeSelectionTheme,
|
|
82
|
+
} from '@volter/editor-core/native-selection-style';
|
|
83
|
+
import { registerPerformanceSource } from '@volter/editor-core/performance-sources';
|
|
84
|
+
import { drawSceneUnlessRefused } from '../scene-view-drawability';
|
|
85
|
+
import { withSceneFogNeutralized } from '@volter/editor-core/scene-view-fog';
|
|
86
|
+
import { connectServerLogs } from '../server-log-bridge';
|
|
87
|
+
import { focusedStageStore } from '@volter/editor-core/stage-context';
|
|
88
|
+
import {
|
|
89
|
+
createThreeSelectionOutline,
|
|
90
|
+
setThreeSelectionOutlineColors,
|
|
91
|
+
syncThreeSelectionOutline,
|
|
92
|
+
} from '@volter/editor-core/three-viewport/selection-outline';
|
|
93
|
+
import {
|
|
94
|
+
setThreeViewportProjection,
|
|
95
|
+
subscribeThreeViewportPresentation,
|
|
96
|
+
threeViewportPresentation,
|
|
97
|
+
} from '@volter/editor-core/three-viewport-presentation';
|
|
98
|
+
import { recordViewportFirstFrame } from '@volter/editor-core/viewport-activation-timings';
|
|
99
|
+
import { savedViewportPose, saveViewportPose } from '../viewport-pose-memory';
|
|
100
|
+
import { presentThreeRoots } from '../viewport-root-presentation';
|
|
101
|
+
import { isEditorViewportShadingTarget } from '@volter/editor-core/viewport-shading-boundary';
|
|
102
|
+
import { downloadOnlineAssetWithHistory } from '@volter/editor-core/components/asset-editor-persistence';
|
|
103
|
+
import { bindStagePresenceMarkers } from '@volter/editor-core/components/stage-presence-markers';
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The world root's renderer, built to the ENGINE's own render settings —
|
|
107
|
+
* opaque, high-performance, PCF soft shadows, KTX2 support taught to the
|
|
108
|
+
* shared loader. The host's `build` lane constructs a small alpha renderer for
|
|
109
|
+
* a studio stage instead; a stage's drawing surface belongs to what it draws.
|
|
110
|
+
*/
|
|
111
|
+
export function mountWorldRootSurface(
|
|
112
|
+
canvasHost: HTMLDivElement,
|
|
113
|
+
container: HTMLDivElement,
|
|
114
|
+
displayName: string,
|
|
115
|
+
): { canvas: HTMLCanvasElement; renderer: THREE.WebGLRenderer; lease: null } {
|
|
116
|
+
const canvas = document.createElement('canvas');
|
|
117
|
+
canvas.setAttribute('aria-label', `${displayName} authoring viewport`);
|
|
118
|
+
canvas.setAttribute('data-testid', 'editor-canvas');
|
|
119
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
120
|
+
canvas.tabIndex = -1;
|
|
121
|
+
canvas.style.width = '100%';
|
|
122
|
+
canvas.style.height = '100%';
|
|
123
|
+
canvas.style.display = 'block';
|
|
124
|
+
canvas.style.pointerEvents = 'auto';
|
|
125
|
+
canvasHost.appendChild(canvas);
|
|
126
|
+
// Construct WebGL at the viewport's real backing size. The old 300x150
|
|
127
|
+
// canvas default made the driver allocate once during context creation and
|
|
128
|
+
// again immediately below; under SwiftShader that second cold allocation
|
|
129
|
+
// alone is visible in the startup trace.
|
|
130
|
+
const initialWidth = Math.max(1, container.clientWidth);
|
|
131
|
+
const initialHeight = Math.max(1, container.clientHeight);
|
|
132
|
+
const initialPixelRatio = Math.min(window.devicePixelRatio || 1, 2);
|
|
133
|
+
canvas.width = Math.ceil(initialWidth * initialPixelRatio);
|
|
134
|
+
canvas.height = Math.ceil(initialHeight * initialPixelRatio);
|
|
135
|
+
const renderer = markHostRenderer(
|
|
136
|
+
new THREE.WebGLRenderer({
|
|
137
|
+
canvas,
|
|
138
|
+
antialias: Boolean(resolveRenderSettings(undefined)['antialias']),
|
|
139
|
+
powerPreference: 'high-performance',
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
// Cap at 2, matching the runtime's own policy (create-runtime.ts) — an
|
|
143
|
+
// uncapped 3x display renders 2.25x the pixels the game itself would.
|
|
144
|
+
applyRendererSettings(renderer, undefined, initialPixelRatio);
|
|
145
|
+
renderer.setSize(initialWidth, initialHeight);
|
|
146
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
147
|
+
renderer.info.autoReset = false;
|
|
148
|
+
// Teach the shared KTX2Loader this GPU's compressed-format support, so a
|
|
149
|
+
// KHR_texture_basisu GLB (including the Asset Budget's own KTX2 output)
|
|
150
|
+
// loads in EDIT mode too, not only once a game runtime has booted.
|
|
151
|
+
// Idempotent; see `detectKtx2Support` in the engine's loader.
|
|
152
|
+
detectKtx2Support(renderer);
|
|
153
|
+
return { canvas, renderer, lease: null };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface WorldRootStageOptions {
|
|
157
|
+
/** The SHELL's store — the world root's stage runs on it until unit 4. */
|
|
158
|
+
readonly store: EditorShellStore;
|
|
159
|
+
readonly documentId: string;
|
|
160
|
+
readonly container: HTMLDivElement;
|
|
161
|
+
readonly canvas: HTMLCanvasElement;
|
|
162
|
+
readonly renderer: THREE.WebGLRenderer;
|
|
163
|
+
readonly stats: EditorStats;
|
|
164
|
+
/** The camera-authoring preview pane, read per frame (a React ref). */
|
|
165
|
+
readonly cameraPreview: () => HTMLDivElement | null;
|
|
166
|
+
readonly onMountStatus: (status: 'mounting' | 'ready') => void;
|
|
167
|
+
readonly onRootIds: (rootIds: readonly string[]) => void;
|
|
168
|
+
/** This stage's frame hook on the per-stage viewport door — the host owns
|
|
169
|
+
* the door binding, so it hands the stage the call rather than the id. */
|
|
170
|
+
readonly runFrame: (deltaSeconds: number) => void;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface WorldRootStage {
|
|
174
|
+
readonly scene: THREE.Scene;
|
|
175
|
+
readonly viewport: EditorViewport;
|
|
176
|
+
frame(timeMs: number, resumed: boolean): void;
|
|
177
|
+
present(roots: readonly ViewportRoot[]): ViewportPresentation | null;
|
|
178
|
+
setHelper(kind: string, object: THREE.Object3D | null): void;
|
|
179
|
+
dispose(): void;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* THE SHELL'S HALF of a viewport online-asset drop — installed as the gizmo
|
|
184
|
+
* viewport's `onlineAssetResolver` so the viewport itself imports neither the
|
|
185
|
+
* editor's transport (`editor-api.ts`) nor the project's download history
|
|
186
|
+
* (`asset-editor-persistence.ts`). See `OnlineAssetResolver` in
|
|
187
|
+
* `editor-viewport.ts` for the contract; this is its one production
|
|
188
|
+
* implementation.
|
|
189
|
+
*
|
|
190
|
+
* Answers a PROJECT-LOCAL path, or `null` after reporting on the console why
|
|
191
|
+
* it could not — the viewport stays quiet on `null` precisely because this
|
|
192
|
+
* already spoke. A cached asset answers with the cache's own path; a fresh
|
|
193
|
+
* download answers with the served, leading-slash form.
|
|
194
|
+
*/
|
|
195
|
+
async function resolveDroppedOnlineAsset(online: OnlineAssetDrop): Promise<string | null> {
|
|
196
|
+
const cachedPath = getDownloadedAssetPath(online.source, online.id);
|
|
197
|
+
if (cachedPath) return cachedPath;
|
|
198
|
+
|
|
199
|
+
let url = online.url;
|
|
200
|
+
let format = online.format ?? 'gltf';
|
|
201
|
+
let includes = online.includes;
|
|
202
|
+
if (!url) {
|
|
203
|
+
// No URL in the drag payload: ask the source for its file options and take
|
|
204
|
+
// the first, exactly as the asset browser's own import does.
|
|
205
|
+
const file = (await getOnlineAssetFiles(online.source, online.id))[0];
|
|
206
|
+
if (!file) {
|
|
207
|
+
editorConsole.error(`No downloadable files for ${online.name}.`, 'asset');
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
url = file.url;
|
|
211
|
+
format = file.format;
|
|
212
|
+
includes = file.includes;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const params: Parameters<typeof downloadOnlineAssetWithHistory>[0] = {
|
|
216
|
+
source: online.source,
|
|
217
|
+
id: online.id,
|
|
218
|
+
name: online.name,
|
|
219
|
+
url,
|
|
220
|
+
format,
|
|
221
|
+
};
|
|
222
|
+
if (includes) params.includes = includes;
|
|
223
|
+
const result = await downloadOnlineAssetWithHistory(params);
|
|
224
|
+
if (result.ok && result.path) return `/${result.path}`;
|
|
225
|
+
editorConsole.error(`Could not download ${online.name}.`, 'asset');
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function cameraObjectOf(object: THREE.Object3D): THREE.Camera | null {
|
|
230
|
+
if ((object as THREE.Camera).isCamera) return object as THREE.Camera;
|
|
231
|
+
const owned = getUserData(object, '_camera');
|
|
232
|
+
return owned?.isCamera ? owned : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function cameraLensLabel(camera: THREE.Camera): string {
|
|
236
|
+
const perspective = camera as THREE.PerspectiveCamera;
|
|
237
|
+
if (perspective.isPerspectiveCamera) return `${perspective.fov.toFixed(0)}° perspective`;
|
|
238
|
+
const orthographic = camera as THREE.OrthographicCamera;
|
|
239
|
+
if (orthographic.isOrthographicCamera) return `Orthographic · ${orthographic.zoom.toFixed(2)}×`;
|
|
240
|
+
return camera.type;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function cameraPoseCapability(
|
|
244
|
+
adapter: AuthoringAdapter,
|
|
245
|
+
id: string,
|
|
246
|
+
): { canAuthorPose: boolean; poseRefusal?: string } {
|
|
247
|
+
const transforms = adapter.transforms;
|
|
248
|
+
if (!transforms) {
|
|
249
|
+
return { canAuthorPose: false, poseRefusal: 'This camera has no transform authoring path.' };
|
|
250
|
+
}
|
|
251
|
+
const position = transforms.editability?.(id, 'position') ?? { writable: true };
|
|
252
|
+
const rotation = transforms.editability?.(id, 'rotation') ?? { writable: true };
|
|
253
|
+
if (position.writable && rotation.writable) return { canAuthorPose: true };
|
|
254
|
+
return {
|
|
255
|
+
canAuthorPose: false,
|
|
256
|
+
poseRefusal: position.reason ?? rotation.reason ?? 'This camera pose is read-only.',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function cameraSubjectFor(
|
|
261
|
+
adapter: AuthoringAdapter,
|
|
262
|
+
objectMap: Map<string, THREE.Object3D>,
|
|
263
|
+
id: string,
|
|
264
|
+
): CameraAuthoringSubject | null {
|
|
265
|
+
// Camera authoring exists only in the adopted Edit scene, whose objectMap is
|
|
266
|
+
// the authoritative live index. Do not ask a temporarily suspended
|
|
267
|
+
// composite root to resolve stale Edit selection during play-stop handoff;
|
|
268
|
+
// that boundary correctly refuses ids it does not own and would turn an
|
|
269
|
+
// ordinary transition into an editor error.
|
|
270
|
+
const object = objectMap.get(id) ?? null;
|
|
271
|
+
if (!object) return null;
|
|
272
|
+
const camera = cameraObjectOf(object);
|
|
273
|
+
if (!camera) return null;
|
|
274
|
+
return {
|
|
275
|
+
id,
|
|
276
|
+
name: object.name || camera.name || 'Camera',
|
|
277
|
+
camera,
|
|
278
|
+
lens: cameraLensLabel(camera),
|
|
279
|
+
...cameraPoseCapability(adapter, id),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* `id -> tags` for the tag-referencing post effects (outline). Tags are read
|
|
285
|
+
* from the format-neutral `userData['tags']` key — `@volter/threejs-runtime/ecs/scene-query`'s
|
|
286
|
+
* canonical location, so every adapter's nodes carry them the same way.
|
|
287
|
+
*/
|
|
288
|
+
function buildEntityTagMap(store: EditorShellStore): Map<string, string[]> {
|
|
289
|
+
const tagMap = new Map<string, string[]>();
|
|
290
|
+
for (const [id, obj] of store.objectMap) {
|
|
291
|
+
const tags = getUserData(obj, 'tags');
|
|
292
|
+
if (Array.isArray(tags) && tags.length > 0) tagMap.set(id, tags);
|
|
293
|
+
}
|
|
294
|
+
return tagMap;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function installWorldRootStage(options: WorldRootStageOptions): WorldRootStage {
|
|
298
|
+
const { store, documentId, container, canvas, renderer, stats } = options;
|
|
299
|
+
|
|
300
|
+
// --- Post-processing ---
|
|
301
|
+
const composer = new EffectComposer(renderer);
|
|
302
|
+
// Renderer-coupled scene depth belongs to the renderer that presents the
|
|
303
|
+
// Scene document. The R3F design mount only constructs and settles the
|
|
304
|
+
// live Object3D graph; its detached no-draw renderer cannot produce depth,
|
|
305
|
+
// and its game camera is not the camera the editor is looking through.
|
|
306
|
+
//
|
|
307
|
+
// This pass is scene-generic: while no armed soft-particle renderer lives
|
|
308
|
+
// under the displayed scene it returns before traversing or drawing. When
|
|
309
|
+
// one does, it uses this viewport's real WebGL context and current camera.
|
|
310
|
+
const softParticleDepth = createSoftParticleDepthPass();
|
|
311
|
+
const viewportShading = new ViewportShadingRenderer();
|
|
312
|
+
const profiler = createPerformanceProfiler();
|
|
313
|
+
// Real GPU time for the viewport's own draw (EXT_disjoint_timer_query_
|
|
314
|
+
// webgl2 — same instrument the play path wires in setup-three-root-
|
|
315
|
+
// adapter). This loop used to hardcode `gpuMs: null`, which left the
|
|
316
|
+
// Profiler's GPU tile permanently "n/a" in edit mode and made a
|
|
317
|
+
// GPU-bound editor unexplainable from the panel. Results resolve a few
|
|
318
|
+
// frames late; `poll()` reports the most recently RESOLVED query, never
|
|
319
|
+
// an interpolation. Unsupported contexts stay honestly null.
|
|
320
|
+
const gpuTimer = createWebGLGpuTimer(renderer.getContext());
|
|
321
|
+
// Timing is gated on the profiler so a closed Profiler pays zero query
|
|
322
|
+
// overhead; end() self-pairs with begin(), so the gate lives here alone.
|
|
323
|
+
const drawTimed = (draw: () => void): void => {
|
|
324
|
+
if (profiler.enabled) gpuTimer.begin();
|
|
325
|
+
try {
|
|
326
|
+
draw();
|
|
327
|
+
} finally {
|
|
328
|
+
gpuTimer.end();
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const pollGpuMs = (): number | null => (profiler.enabled ? gpuTimer.poll() : null);
|
|
332
|
+
const unregisterPerformanceSource = registerPerformanceSource({
|
|
333
|
+
id: documentId,
|
|
334
|
+
label: 'Scene viewport',
|
|
335
|
+
kind: 'scene',
|
|
336
|
+
profiler,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// --- Scene ---
|
|
340
|
+
const scene = new THREE.Scene();
|
|
341
|
+
// The palette's viewport background when it carries one; the editor's own
|
|
342
|
+
// grey otherwise. Theme changes follow through the viewport's look
|
|
343
|
+
// subscription (`EditorViewport`).
|
|
344
|
+
scene.background = new THREE.Color(nativeViewportLook(canvas).background ?? 0xaaaaaa);
|
|
345
|
+
|
|
346
|
+
// --- Bind store to scene ---
|
|
347
|
+
store.bindScene(scene, renderer);
|
|
348
|
+
|
|
349
|
+
// --- Viewport ---
|
|
350
|
+
// Bind navigation to the whole authoring surface. A DOM selection layer
|
|
351
|
+
// above the WebGL canvas must not disable right-drag orbit/fly controls.
|
|
352
|
+
const viewport = new EditorViewport(canvas, scene, store, container, {
|
|
353
|
+
renderer,
|
|
354
|
+
onlineAssetResolver: resolveDroppedOnlineAsset,
|
|
355
|
+
onProjectionChange: setThreeViewportProjection,
|
|
356
|
+
});
|
|
357
|
+
// Bind the REAL viewport camera + orbit target so store.cameraPose (the
|
|
358
|
+
// `editor.viewport.camera.get` facet, see command-listener.ts's
|
|
359
|
+
// collectState) and the thumbnail-capture path read live data instead of
|
|
360
|
+
// null — previously bindScene's `camera` param was never passed here, so
|
|
361
|
+
// `store.cameraPose` (and _captureThumbnail's camera read) stayed dark.
|
|
362
|
+
store.bindScene(scene, renderer, viewport.batchedRenderer, viewport.camera);
|
|
363
|
+
store.setOrbitTarget(viewport.orbitControls.target);
|
|
364
|
+
const cameraPosition = new THREE.Vector3();
|
|
365
|
+
const cameraTarget = new THREE.Vector3();
|
|
366
|
+
const cameraForward = new THREE.Vector3();
|
|
367
|
+
const readVisibleCameraPose = () => {
|
|
368
|
+
const camera = viewport.renderCamera;
|
|
369
|
+
camera.getWorldPosition(cameraPosition);
|
|
370
|
+
if (viewport.cameraViewMode === 'pilot') {
|
|
371
|
+
cameraTarget.copy(viewport.orbitControls.target);
|
|
372
|
+
} else if (!viewport.cameraViewMode) {
|
|
373
|
+
cameraTarget.copy(viewport.orbitControls.target);
|
|
374
|
+
} else {
|
|
375
|
+
camera.getWorldDirection(cameraForward);
|
|
376
|
+
cameraTarget.copy(cameraPosition).add(cameraForward.multiplyScalar(10));
|
|
377
|
+
}
|
|
378
|
+
const fov = (camera as THREE.PerspectiveCamera).fov;
|
|
379
|
+
return {
|
|
380
|
+
position: cameraPosition,
|
|
381
|
+
target: cameraTarget,
|
|
382
|
+
fov: typeof fov === 'number' ? fov : 0,
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
// THE PRESENCE this stage publishes, and the markers it paints for the other
|
|
386
|
+
// participants, are one module now (`stage-presence-markers.ts`) keyed by
|
|
387
|
+
// THIS stage's document id — the world root's is the Scene document's.
|
|
388
|
+
const presence = bindStagePresenceMarkers({
|
|
389
|
+
scene,
|
|
390
|
+
store,
|
|
391
|
+
documentId,
|
|
392
|
+
container,
|
|
393
|
+
canvas,
|
|
394
|
+
viewport,
|
|
395
|
+
readVisibleCameraPose,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
let lastPresenceCamera = '';
|
|
399
|
+
// DEV/e2e hook: lets the per-game feature matrix assert the viewport camera
|
|
400
|
+
// reframes (view presets / focus / orbit) on an ingested scene.
|
|
401
|
+
if (import.meta.env.DEV) {
|
|
402
|
+
(window as unknown as Record<string, unknown>)['__vgaiViewport'] = viewport;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// --- Composer rebuild ---
|
|
406
|
+
// The un-apply half of the adopted image config below. `applySceneRender-
|
|
407
|
+
// Pipeline` re-derives toneMapping/exposure on every rebuild, but the
|
|
408
|
+
// seam's other fields (outputColorSpace, shadowMap.type, clearColor) have
|
|
409
|
+
// no re-derive path of their own — so each rebuild first runs the RESTORE
|
|
410
|
+
// the previous apply returned, putting back exactly what that apply found.
|
|
411
|
+
// Without this, an adoption ending (or a remount that drops the
|
|
412
|
+
// declaration) would leave the previous world's colour space on the
|
|
413
|
+
// editor's renderer until page reload.
|
|
414
|
+
let restoreAdoptedImage: (() => void) | null = null;
|
|
415
|
+
let selectionOutline: ReturnType<typeof createThreeSelectionOutline> | null = null;
|
|
416
|
+
let selectionOutlinePass: EffectPass | null = null;
|
|
417
|
+
let selectionOutlineUnderlayPass: Pass | null = null;
|
|
418
|
+
const syncSelectionTheme = (): void => {
|
|
419
|
+
if (selectionOutline)
|
|
420
|
+
setThreeSelectionOutlineColors(selectionOutline, nativeSelectionColors(renderer.domElement));
|
|
421
|
+
};
|
|
422
|
+
const unsubscribeSelectionTheme = subscribeNativeSelectionTheme(
|
|
423
|
+
renderer.domElement,
|
|
424
|
+
syncSelectionTheme,
|
|
425
|
+
);
|
|
426
|
+
const syncSelectionOutline = (): void => {
|
|
427
|
+
if (!selectionOutline) return;
|
|
428
|
+
const adapter = getActiveAuthoring(store);
|
|
429
|
+
const roots = [...store.selectedEntityIds]
|
|
430
|
+
// Edit↔Play swaps the active child before its native graph is rebuilt.
|
|
431
|
+
// Do not route a stale outgoing id through the composite during that
|
|
432
|
+
// handoff: it is temporarily absent, not an ownership error.
|
|
433
|
+
.filter((id) => adapter.hierarchy.node(id) !== null)
|
|
434
|
+
.map((id) => entityObject3D(adapter, store.objectMap, id))
|
|
435
|
+
.filter((object): object is THREE.Object3D => object !== null);
|
|
436
|
+
syncThreeSelectionOutline(selectionOutline, roots);
|
|
437
|
+
const active = selectionOutline.selection.size > 0;
|
|
438
|
+
if (selectionOutlinePass) {
|
|
439
|
+
selectionOutlinePass.enabled = active;
|
|
440
|
+
selectionOutlinePass.renderToScreen = active;
|
|
441
|
+
}
|
|
442
|
+
// EffectComposer assigns screen ownership only when passes are added;
|
|
443
|
+
// toggling its final pass later does not promote the preceding pass.
|
|
444
|
+
// Hand the screen back explicitly so deselection paints a fresh frame
|
|
445
|
+
// instead of leaving the last outlined frame in the canvas.
|
|
446
|
+
if (selectionOutlineUnderlayPass) selectionOutlineUnderlayPass.renderToScreen = !active;
|
|
447
|
+
};
|
|
448
|
+
function rebuildComposer(): void {
|
|
449
|
+
// The chain always builds BARE here: nothing authors a render
|
|
450
|
+
// environment for the editor's own composer, and an adapter-backed
|
|
451
|
+
// world composes its own post chain (R3F's fiber-native
|
|
452
|
+
// postprocessing). `editor-viewport.ts` bumps `composerVersion` (via
|
|
453
|
+
// `store.reapplyEnvironment()`) on the world hide/show edges, which is
|
|
454
|
+
// what re-runs this rebuild.
|
|
455
|
+
const hiddenRootId = resolveThreeViewportRootId(store);
|
|
456
|
+
void hiddenRootId;
|
|
457
|
+
// Unwind the previous adoption's config BEFORE re-deriving, so the
|
|
458
|
+
// re-derive below and the (possibly absent) re-apply at the end both
|
|
459
|
+
// start from the editor's own values — see `restoreAdoptedImage`'s decl.
|
|
460
|
+
restoreAdoptedImage?.();
|
|
461
|
+
restoreAdoptedImage = null;
|
|
462
|
+
// `applySceneRenderPipeline` disposes the previous pass but an
|
|
463
|
+
// OutlineEffect's Selection owns temporary render-layer bits on the
|
|
464
|
+
// selected objects. Clear those explicitly before dropping the effect.
|
|
465
|
+
selectionOutline?.selection.clear();
|
|
466
|
+
selectionOutline = null;
|
|
467
|
+
selectionOutlinePass = null;
|
|
468
|
+
selectionOutlineUnderlayPass = null;
|
|
469
|
+
applySceneRenderPipeline(
|
|
470
|
+
composer,
|
|
471
|
+
renderer,
|
|
472
|
+
store.scene ?? scene,
|
|
473
|
+
viewport.renderCamera,
|
|
474
|
+
undefined,
|
|
475
|
+
{
|
|
476
|
+
// Same cap as the renderer-construction call site above.
|
|
477
|
+
basePixelRatio: Math.min(window.devicePixelRatio || 1, 2),
|
|
478
|
+
objectMap: store.objectMap,
|
|
479
|
+
entityTags: buildEntityTagMap(store),
|
|
480
|
+
},
|
|
481
|
+
);
|
|
482
|
+
// Editor selection is appended AFTER the world's image pipeline. It is
|
|
483
|
+
// viewport chrome, never an authored post effect, and therefore remains
|
|
484
|
+
// legible under every shading mode without entering Game rendering.
|
|
485
|
+
// This stage already loaded `postprocessing` for its own composer, so it
|
|
486
|
+
// hands the factory its own bindings — no second module load, and the
|
|
487
|
+
// outline is attached synchronously exactly as before.
|
|
488
|
+
selectionOutline = createThreeSelectionOutline(
|
|
489
|
+
{ BlendFunction, KernelSize, OutlineEffect },
|
|
490
|
+
store.scene ?? scene,
|
|
491
|
+
viewport.renderCamera,
|
|
492
|
+
nativeSelectionColors(renderer.domElement),
|
|
493
|
+
);
|
|
494
|
+
syncSelectionTheme();
|
|
495
|
+
selectionOutlineUnderlayPass = composer.passes.at(-1) ?? null;
|
|
496
|
+
selectionOutlinePass = new EffectPass(viewport.renderCamera, selectionOutline);
|
|
497
|
+
selectionOutlinePass.enabled = false;
|
|
498
|
+
composer.addPass(selectionOutlinePass);
|
|
499
|
+
syncSelectionOutline();
|
|
500
|
+
// LAST, and after the engine defaults above have been re-derived: the
|
|
501
|
+
// ADOPTED content's own declaration about the renderer that draws it
|
|
502
|
+
// (`EditorShellStore.adoptedImageConfig`). Everything before this line
|
|
503
|
+
// configures the renderer from the EDITOR's defaults, so a world that was
|
|
504
|
+
// authored for a different colour pipeline would otherwise be drawn in a
|
|
505
|
+
// pipeline it never asked for — see that getter's doc comment.
|
|
506
|
+
//
|
|
507
|
+
// Renderer PROPERTIES only. A world's own post chain stays its own: it
|
|
508
|
+
// renders from the world's camera, not the viewport's, and putting the
|
|
509
|
+
// game's grade on this frame would put it on the grid, the gizmo and
|
|
510
|
+
// every helper too. That line is what keeps editor chrome legible under
|
|
511
|
+
// any content.
|
|
512
|
+
const adoptedImage = store.adoptedImageConfig;
|
|
513
|
+
if (adoptedImage) {
|
|
514
|
+
restoreAdoptedImage = applyWorldRendererConfig(THREE, renderer, adoptedImage);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const applyViewportPresentation = (): void => {
|
|
518
|
+
viewport.setProjection(threeViewportPresentation().projection);
|
|
519
|
+
store.setViewportCamera(viewport.freeCamera);
|
|
520
|
+
rebuildComposer();
|
|
521
|
+
};
|
|
522
|
+
const unsubscribeViewportPresentation =
|
|
523
|
+
subscribeThreeViewportPresentation(applyViewportPresentation);
|
|
524
|
+
applyViewportPresentation();
|
|
525
|
+
|
|
526
|
+
const resolveCameraSubject = (id: string): CameraAuthoringSubject | null =>
|
|
527
|
+
cameraSubjectFor(getActiveAuthoring(store), store.objectMap, id);
|
|
528
|
+
|
|
529
|
+
let pilotGestureId: string | null = null;
|
|
530
|
+
const finishPilotGesture = (): void => {
|
|
531
|
+
if (!pilotGestureId) return;
|
|
532
|
+
endAuthoringTransformEdit(getActiveAuthoring(store), pilotGestureId);
|
|
533
|
+
pilotGestureId = null;
|
|
534
|
+
};
|
|
535
|
+
const beginPilotGesture = (): void => {
|
|
536
|
+
const view = cameraAuthoringPresentation().view;
|
|
537
|
+
if (view?.mode !== 'pilot' || !view.subject.canAuthorPose) return;
|
|
538
|
+
pilotGestureId = view.subject.id;
|
|
539
|
+
beginAuthoringTransformEdit(getActiveAuthoring(store), pilotGestureId);
|
|
540
|
+
};
|
|
541
|
+
const applyPilotGesture = (): void => {
|
|
542
|
+
if (!pilotGestureId || viewport.cameraViewMode !== 'pilot') return;
|
|
543
|
+
const subject = resolveCameraSubject(pilotGestureId);
|
|
544
|
+
const adapter = getActiveAuthoring(store);
|
|
545
|
+
const transforms = adapter.transforms;
|
|
546
|
+
if (!subject || !transforms) return;
|
|
547
|
+
// `transforms.get` REFUSES BY THROWING for a node with no transform truth
|
|
548
|
+
// (composite-authoring-adapter's unowned/untransformed refusals) rather
|
|
549
|
+
// than answering an identity pose. This runs on every OrbitControls
|
|
550
|
+
// 'change', so an escaping throw would kill the control loop AND repeat
|
|
551
|
+
// the message per frame: report it once and END the gesture, which is
|
|
552
|
+
// what a subject with no pose to write actually means.
|
|
553
|
+
let previous: Transform;
|
|
554
|
+
try {
|
|
555
|
+
previous = transforms.get(pilotGestureId);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
editorConsole.error(
|
|
558
|
+
error instanceof Error ? error.message : String(error),
|
|
559
|
+
'camera-authoring',
|
|
560
|
+
);
|
|
561
|
+
finishPilotGesture();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
applyAuthoringTransform(adapter, pilotGestureId, {
|
|
565
|
+
position: subject.camera.position.toArray() as [number, number, number],
|
|
566
|
+
rotation: subject.camera.quaternion.toArray() as [number, number, number, number],
|
|
567
|
+
scale: previous.scale,
|
|
568
|
+
});
|
|
569
|
+
store.notifyIngestEdit();
|
|
570
|
+
};
|
|
571
|
+
viewport.orbitControls.addEventListener('start', beginPilotGesture);
|
|
572
|
+
viewport.orbitControls.addEventListener('change', applyPilotGesture);
|
|
573
|
+
viewport.orbitControls.addEventListener('end', finishPilotGesture);
|
|
574
|
+
|
|
575
|
+
const disposeCameraAuthoring = installCameraAuthoringHost({
|
|
576
|
+
active: () => store.playState === 'stopped' && store.activeViewportTab === 'edit',
|
|
577
|
+
selected: () => {
|
|
578
|
+
const id = store.selectedEntityId;
|
|
579
|
+
return id ? resolveCameraSubject(id) : null;
|
|
580
|
+
},
|
|
581
|
+
resolve: resolveCameraSubject,
|
|
582
|
+
showView: (subject: CameraAuthoringSubject, mode: CameraViewMode) => {
|
|
583
|
+
finishPilotGesture();
|
|
584
|
+
viewport.setCameraView(subject.camera, mode);
|
|
585
|
+
rebuildComposer();
|
|
586
|
+
presence.reportCamera();
|
|
587
|
+
},
|
|
588
|
+
leaveView: () => {
|
|
589
|
+
finishPilotGesture();
|
|
590
|
+
viewport.clearCameraView();
|
|
591
|
+
rebuildComposer();
|
|
592
|
+
presence.reportCamera();
|
|
593
|
+
},
|
|
594
|
+
alignToViewport: (subject: CameraAuthoringSubject) => {
|
|
595
|
+
const adapter = getActiveAuthoring(store);
|
|
596
|
+
const transforms = adapter.transforms;
|
|
597
|
+
if (!transforms || !subject.canAuthorPose) return;
|
|
598
|
+
const targetObject = store.objectMap.get(subject.id) ?? null;
|
|
599
|
+
if (!targetObject) return;
|
|
600
|
+
const worldPosition = viewport.camera.getWorldPosition(new THREE.Vector3());
|
|
601
|
+
const worldQuaternion = viewport.camera.getWorldQuaternion(new THREE.Quaternion());
|
|
602
|
+
const localPosition = worldPosition.clone();
|
|
603
|
+
const localQuaternion = worldQuaternion.clone();
|
|
604
|
+
if (targetObject.parent) {
|
|
605
|
+
targetObject.parent.updateWorldMatrix(true, false);
|
|
606
|
+
targetObject.parent.worldToLocal(localPosition);
|
|
607
|
+
const parentWorldQuaternion = targetObject.parent.getWorldQuaternion(
|
|
608
|
+
new THREE.Quaternion(),
|
|
609
|
+
);
|
|
610
|
+
localQuaternion.premultiply(parentWorldQuaternion.invert());
|
|
611
|
+
}
|
|
612
|
+
// Same refusal contract as applyPilotGesture above — surface it and
|
|
613
|
+
// abort, before any beginEdit opens a gesture that cannot be written.
|
|
614
|
+
let previous: Transform;
|
|
615
|
+
try {
|
|
616
|
+
previous = transforms.get(subject.id);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
editorConsole.error(
|
|
619
|
+
error instanceof Error ? error.message : String(error),
|
|
620
|
+
'camera-authoring',
|
|
621
|
+
);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
beginAuthoringTransformEdit(adapter, subject.id);
|
|
625
|
+
applyAuthoringTransform(adapter, subject.id, {
|
|
626
|
+
position: localPosition.toArray() as [number, number, number],
|
|
627
|
+
rotation: localQuaternion.toArray() as [number, number, number, number],
|
|
628
|
+
scale: previous.scale,
|
|
629
|
+
});
|
|
630
|
+
store.notifyIngestEdit();
|
|
631
|
+
endAuthoringTransformEdit(adapter, subject.id);
|
|
632
|
+
},
|
|
633
|
+
subscribe: store.subscribe,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// --- Store subscription → sync viewport ---
|
|
637
|
+
// Rebuild the post-processing chain only when its inputs actually change
|
|
638
|
+
// (environment / scene structure / play-stop swap), tracked by composerVersion.
|
|
639
|
+
// Previously this ran on EVERY notify, so a gizmo drag — which notifies once
|
|
640
|
+
// per pointer-move — reconstructed every EffectPass and recompiled shaders
|
|
641
|
+
// 60×/s, causing severe stutter on any scene with post effects enabled.
|
|
642
|
+
// Spec 28 — figma mode's backdrop is the STANDARD transparency
|
|
643
|
+
// checkerboard, not a flat color: with no three surface showing, the area
|
|
644
|
+
// behind the DOM roots isn't "a gray scene", it's NOTHING, and the
|
|
645
|
+
// checkerboard is the universal signal for that. Achieved by fading the
|
|
646
|
+
// WebGL canvas to opacity 0 (keeps layout AND pointer flow identical — the
|
|
647
|
+
// layered-pick path still runs) so the container's own checkerboard shows
|
|
648
|
+
// through behind the DOM world layers appended above it. Driven off the
|
|
649
|
+
// store here rather than from React, because the canvas is the stage's and
|
|
650
|
+
// React never owns it.
|
|
651
|
+
let lastThreejsSurface: boolean | null = null;
|
|
652
|
+
const syncSurfaceVisibility = (): void => {
|
|
653
|
+
const visible = isThreejsSurfaceVisible(store);
|
|
654
|
+
if (visible === lastThreejsSurface) return;
|
|
655
|
+
lastThreejsSurface = visible;
|
|
656
|
+
canvas.style.opacity = visible ? '1' : '0';
|
|
657
|
+
// Dark-UI transparency checkerboard (VS Code image-preview style — the
|
|
658
|
+
// light Figma checker glares in a dark editor and washes out
|
|
659
|
+
// light-on-transparent HUD content). V-20 — the PATTERN itself is a
|
|
660
|
+
// deliberate, documented design choice (left alone); `themeVars.surface.raised`
|
|
661
|
+
// is an exact-value token match for the base tone, so swapped in. The
|
|
662
|
+
// gradient's `#444649` has no exact token in the current set, so it stays
|
|
663
|
+
// a literal per the audit's own "only swap exact matches" guidance.
|
|
664
|
+
container.style.backgroundColor = visible ? '' : themeVars.surface.raised;
|
|
665
|
+
container.style.backgroundImage = visible
|
|
666
|
+
? ''
|
|
667
|
+
: 'linear-gradient(45deg, #444649 25%, transparent 25%, transparent 75%, #444649 75%), linear-gradient(45deg, #444649 25%, transparent 25%, transparent 75%, #444649 75%)';
|
|
668
|
+
container.style.backgroundPosition = visible ? '' : '0 0, 8px 8px';
|
|
669
|
+
container.style.backgroundSize = visible ? '' : '16px 16px';
|
|
670
|
+
};
|
|
671
|
+
syncSurfaceVisibility();
|
|
672
|
+
|
|
673
|
+
let lastComposerVersion = store.composerVersion;
|
|
674
|
+
const unsubStore = store.subscribe(() => {
|
|
675
|
+
syncSurfaceVisibility();
|
|
676
|
+
// Swap viewport scene if the store's scene changed (play mode enter/exit)
|
|
677
|
+
const storeScene = store.scene;
|
|
678
|
+
if (storeScene && storeScene !== viewport.currentScene) {
|
|
679
|
+
viewport.setScene(storeScene);
|
|
680
|
+
}
|
|
681
|
+
viewport.objectMap = store.objectMap;
|
|
682
|
+
viewport.syncFromStore();
|
|
683
|
+
if (store.composerVersion !== lastComposerVersion) {
|
|
684
|
+
lastComposerVersion = store.composerVersion;
|
|
685
|
+
rebuildComposer();
|
|
686
|
+
} else {
|
|
687
|
+
syncSelectionOutline();
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// --- Pending scene auto-frame (see the 'focus-scene' action below) ---
|
|
692
|
+
/** Live lookup for the adopted game's own camera — never a snapshot; see
|
|
693
|
+
* the store. Null for a world that has none to wait for. */
|
|
694
|
+
let gameCameraLookup: (() => THREE.Camera | null) | null = null;
|
|
695
|
+
/** Everything about WHEN the camera may still move on its own, and the
|
|
696
|
+
* reader's gesture ending that, lives in `auto-frame-window.ts`. */
|
|
697
|
+
const autoFrame = new AutoFrameWindow({
|
|
698
|
+
seedFromGameCamera: () => {
|
|
699
|
+
const gameCamera = gameCameraLookup?.() ?? null;
|
|
700
|
+
return !!gameCamera && viewport.seedFromGameCamera(gameCamera);
|
|
701
|
+
},
|
|
702
|
+
frameContent: () => viewport.focusOnScene(),
|
|
703
|
+
});
|
|
704
|
+
const cancelAutoFrame = (): void => {
|
|
705
|
+
autoFrame.cancel();
|
|
706
|
+
gameCameraLookup = null;
|
|
707
|
+
};
|
|
708
|
+
viewport.orbitControls.addEventListener('start', cancelAutoFrame);
|
|
709
|
+
// A finished gesture is the pose worth remembering (viewport-pose-memory).
|
|
710
|
+
const rememberPose = (): void => saveViewportPose(viewport.camera, viewport.orbitControls.target);
|
|
711
|
+
viewport.orbitControls.addEventListener('end', rememberPose);
|
|
712
|
+
|
|
713
|
+
// --- Viewport action subscription ---
|
|
714
|
+
const unsubActions = store.onViewportAction((action) => {
|
|
715
|
+
switch (action.type) {
|
|
716
|
+
// Resolved through the ACTIVE ADAPTER first, same as selection and the
|
|
717
|
+
// gizmo — an adopted play scene's nodes live in the adapter's walk and
|
|
718
|
+
// never reach `objectMap`, so a store-only lookup silently framed
|
|
719
|
+
// nothing for every entity in a running game (`entity-object.ts`).
|
|
720
|
+
case 'focus-entity': {
|
|
721
|
+
const obj = entityObject3D(getActiveAuthoring(store), store.objectMap, action.id);
|
|
722
|
+
if (obj) viewport.focusOn(obj);
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
case 'focus-scene': {
|
|
726
|
+
// A game builds itself asynchronously — a level-streaming game
|
|
727
|
+
// captures on its first frame and keeps parsing its world in for
|
|
728
|
+
// seconds afterwards, and its own camera can appear later still — so
|
|
729
|
+
// "open on the world" cannot be a single attempt at mount. The
|
|
730
|
+
// window (auto-frame-window.ts) tries now and keeps trying on a slow
|
|
731
|
+
// cadence; the frame hook below rides it.
|
|
732
|
+
// A remembered pose is a reader gesture that already happened, and
|
|
733
|
+
// the gesture outranks the whole window (auto-frame-window.ts) —
|
|
734
|
+
// restore it and never open the window. First ever open of a
|
|
735
|
+
// project still gets the designed first-look framing.
|
|
736
|
+
{
|
|
737
|
+
const remembered = savedViewportPose();
|
|
738
|
+
if (remembered) {
|
|
739
|
+
viewport.camera.position.set(...remembered.position);
|
|
740
|
+
viewport.orbitControls.target.set(...remembered.target);
|
|
741
|
+
viewport.orbitControls.update();
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
gameCameraLookup = action.gameCamera ?? null;
|
|
746
|
+
autoFrame.begin(performance.now(), gameCameraLookup !== null);
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
case 'focus-selection': {
|
|
750
|
+
const adapter = getActiveAuthoring(store);
|
|
751
|
+
const objects = [...store.selectedEntityIds]
|
|
752
|
+
.map((id) => entityObject3D(adapter, store.objectMap, id))
|
|
753
|
+
.filter((o): o is THREE.Object3D => o !== null);
|
|
754
|
+
viewport.focusOnMultiple(objects);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case 'snap-selection-to-floor':
|
|
758
|
+
viewport.snapSelectionToFloor();
|
|
759
|
+
break;
|
|
760
|
+
case 'set-view-preset':
|
|
761
|
+
viewport.setViewPreset(action.preset);
|
|
762
|
+
break;
|
|
763
|
+
case 'set-camera-pose':
|
|
764
|
+
viewport.setPose(action.position, action.target, action.fov);
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// --- Error capture → editor console ---
|
|
770
|
+
// Owned by `installEditorConsoleCapture()` (main.tsx) since it became
|
|
771
|
+
// session-lifetime: this stage's mount/unmount is the wrong lifetime for a
|
|
772
|
+
// page-error listener (an error thrown before the viewport mounts, or after
|
|
773
|
+
// it unmounts, is still the session's news), and two owners would have
|
|
774
|
+
// double-logged every uncaught error.
|
|
775
|
+
|
|
776
|
+
// --- Server logs ---
|
|
777
|
+
connectServerLogs();
|
|
778
|
+
|
|
779
|
+
// --- Resize ---
|
|
780
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
781
|
+
// Docking, HMR, and tab switches can briefly collapse the panel to 0x0.
|
|
782
|
+
// Three's EffectComposer allocates incomplete render targets at that
|
|
783
|
+
// size, then floods the console when the shared RAF renders the frame.
|
|
784
|
+
const w = Math.max(1, container.clientWidth);
|
|
785
|
+
const h = Math.max(1, container.clientHeight);
|
|
786
|
+
renderer.setSize(w, h);
|
|
787
|
+
composer.setSize(w, h);
|
|
788
|
+
viewport.resize(w, h);
|
|
789
|
+
});
|
|
790
|
+
resizeObserver.observe(container);
|
|
791
|
+
|
|
792
|
+
// --- Frame loop ---
|
|
793
|
+
let lastTime = performance.now();
|
|
794
|
+
let sceneNeedsFirstFrame = true;
|
|
795
|
+
// A queued callback can survive long enough to meet an HMR/unmount cleanup.
|
|
796
|
+
// Never let it touch the renderer after that renderer has been disposed.
|
|
797
|
+
let disposed = false;
|
|
798
|
+
// The renderer exists before the project's authoring surface does. A draw
|
|
799
|
+
// of the empty editor scene is not a loaded Scene viewport: wait until the
|
|
800
|
+
// complete edit-mode mount / ingest adoption chain has settled, then stamp
|
|
801
|
+
// the first draw that can actually contain the project's authored world.
|
|
802
|
+
let sceneContentReady = false;
|
|
803
|
+
|
|
804
|
+
function drawViewport(dt: number): boolean {
|
|
805
|
+
return drawSceneUnlessRefused(viewport.currentScene, () => {
|
|
806
|
+
// P26 attribution (SwiftShader, CitadelArena probe): a cold first
|
|
807
|
+
// draw took 2.275s while steady-state draws were ~1.3ms with or
|
|
808
|
+
// without the arena selected. Selection bounds were <=0.2ms for the
|
|
809
|
+
// live 80-node subtree (and ~4.1ms for a synthetic 20k-node root).
|
|
810
|
+
// Keep cold shader/driver work distinct from selected-subtree cost:
|
|
811
|
+
// the latter is invalidated in SelectionBrackets.updateIfNeeded().
|
|
812
|
+
// Take game-scene depth BEFORE `renderWithInfrastructure` attaches
|
|
813
|
+
// the grid, gizmos, selection helpers, and other editor-only
|
|
814
|
+
// objects. Those are presentation chrome and must never make an
|
|
815
|
+
// authored particle fade. The pass restores the previous render
|
|
816
|
+
// target before the ordinary composer draw begins.
|
|
817
|
+
softParticleDepth.render(renderer, viewport.currentScene, viewport.renderCamera);
|
|
818
|
+
viewport.renderWithInfrastructure(() => {
|
|
819
|
+
// The Scene camera is not the game's camera, so the game's
|
|
820
|
+
// distance-calibrated fog is not this view's look — see
|
|
821
|
+
// `scene-view-fog.ts`. Presentation-only and restored in a
|
|
822
|
+
// `finally`, exactly like the shading modes it wraps.
|
|
823
|
+
withSceneFogNeutralized(viewport.currentScene, () =>
|
|
824
|
+
viewportShading.render(
|
|
825
|
+
viewport.currentScene,
|
|
826
|
+
store.shadingMode,
|
|
827
|
+
() => composer.render(dt),
|
|
828
|
+
isEditorViewportShadingTarget,
|
|
829
|
+
),
|
|
830
|
+
);
|
|
831
|
+
viewport.renderViewCube(renderer);
|
|
832
|
+
viewport.renderCameraPreview(
|
|
833
|
+
renderer,
|
|
834
|
+
cameraAuthoringPresentation().preview?.camera ?? null,
|
|
835
|
+
options.cameraPreview(),
|
|
836
|
+
);
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const unregisterCanvasFrame = registerPresentedCanvasFrame(canvas, async () => {
|
|
842
|
+
if (
|
|
843
|
+
disposed ||
|
|
844
|
+
!isEditorPresentationActive() ||
|
|
845
|
+
!container.checkVisibility({ visibilityProperty: true, checkVisibilityCSS: true })
|
|
846
|
+
)
|
|
847
|
+
return null;
|
|
848
|
+
if (!drawViewport(0)) return null;
|
|
849
|
+
const frame = document.createElement('canvas');
|
|
850
|
+
frame.width = canvas.width;
|
|
851
|
+
frame.height = canvas.height;
|
|
852
|
+
frame.getContext('2d')!.drawImage(canvas, 0, 0);
|
|
853
|
+
return frame;
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
/** Whether this stage is the one the shared panels are following, and so the
|
|
857
|
+
* one that fills the shell's frame/camera readout — see its use below. */
|
|
858
|
+
const ownsShellReadout = (): boolean => focusedStageStore(store) === store;
|
|
859
|
+
|
|
860
|
+
function frame(now: number, resumed: boolean): void {
|
|
861
|
+
if (disposed) return;
|
|
862
|
+
// The Scene document stays MOUNTED during Play with its overlay ancestor
|
|
863
|
+
// hidden by inherited `visibility: hidden`. The host keeps
|
|
864
|
+
// this stage's loop alive so Edit resumes naturally AND so the play-entry
|
|
865
|
+
// camera flight keeps its clock through the cross-fade — but do
|
|
866
|
+
// absolutely no editor presentation/update/profiling work while that
|
|
867
|
+
// document is hidden.
|
|
868
|
+
if (resumed) lastTime = now;
|
|
869
|
+
if (!container.checkVisibility({ visibilityProperty: true, checkVisibilityCSS: true })) {
|
|
870
|
+
lastTime = now;
|
|
871
|
+
sceneNeedsFirstFrame = true;
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
profiler.beginFrame();
|
|
876
|
+
const frameTime = now - lastTime;
|
|
877
|
+
const dt = frameTime / 1000;
|
|
878
|
+
// THE SHELL'S READOUT IS THE FOCUSED STAGE'S (ARCHITECTURE-CORE §One stage
|
|
879
|
+
// unit 4). `EditorStats` is one mutable struct the shell owns and
|
|
880
|
+
// `CameraInfo`/`StatsOverlay` read; every stage can fill it, and the one
|
|
881
|
+
// that does is whichever stage the panels are following — so a model
|
|
882
|
+
// document's readout reports the camera its reader is looking through,
|
|
883
|
+
// and this stage takes it back the moment focus returns (including while
|
|
884
|
+
// the Game tab holds focus during Play, which owns no stage).
|
|
885
|
+
if (ownsShellReadout()) {
|
|
886
|
+
stats.frameTime = frameTime;
|
|
887
|
+
stats.fps = 1000 / frameTime;
|
|
888
|
+
}
|
|
889
|
+
lastTime = now;
|
|
890
|
+
presence.syncMarkers(dt);
|
|
891
|
+
|
|
892
|
+
autoFrame.tick(now);
|
|
893
|
+
|
|
894
|
+
profiler.beginPhase();
|
|
895
|
+
viewport.update(dt);
|
|
896
|
+
// The door's frame hook (the play-entry camera flight rides it): after
|
|
897
|
+
// viewport.update so a flight has final say over the camera pose
|
|
898
|
+
// (orbit damping never fights it).
|
|
899
|
+
options.runFrame(dt);
|
|
900
|
+
// NOTHING ADVANCES CONTENT HERE. Edit is static on every surface, and the
|
|
901
|
+
// Scene view is no exception: the design world's frameloop is never run
|
|
902
|
+
// (`authoring/r3f-design-session.ts`), its one advance is the bounded
|
|
903
|
+
// settle at mount, and this loop drives only editor-owned presentation —
|
|
904
|
+
// orbit damping, the play-entry flight, the view cube, and the editor's
|
|
905
|
+
// own batched particle renderer, which lives on the editor layer.
|
|
906
|
+
viewport.batchedRenderer.update(dt);
|
|
907
|
+
profiler.endPhase('editor');
|
|
908
|
+
profiler.beginPhase();
|
|
909
|
+
renderer.info.reset();
|
|
910
|
+
// A world built by a three the editor did not make can be beyond this
|
|
911
|
+
// renderer's reach — the throw is what says so, once, and then this
|
|
912
|
+
// scene stops being drawn here. See `scene-view-drawability.ts`.
|
|
913
|
+
drawTimed(() => {
|
|
914
|
+
drawViewport(dt);
|
|
915
|
+
});
|
|
916
|
+
profiler.reportRender({
|
|
917
|
+
gpuMs: pollGpuMs(),
|
|
918
|
+
drawCalls: renderer.info.render.calls,
|
|
919
|
+
triangles: renderer.info.render.triangles,
|
|
920
|
+
geometries: renderer.info.memory.geometries,
|
|
921
|
+
textures: renderer.info.memory.textures,
|
|
922
|
+
});
|
|
923
|
+
profiler.endPhase('render');
|
|
924
|
+
if (ownsShellReadout()) {
|
|
925
|
+
stats.drawCalls = renderer.info.render.calls;
|
|
926
|
+
stats.triangles = renderer.info.render.triangles;
|
|
927
|
+
}
|
|
928
|
+
if (sceneNeedsFirstFrame && sceneContentReady) {
|
|
929
|
+
sceneNeedsFirstFrame = false;
|
|
930
|
+
recordViewportFirstFrame(documentId);
|
|
931
|
+
}
|
|
932
|
+
profiler.endFrame();
|
|
933
|
+
|
|
934
|
+
// Update camera info for CameraInfo panel
|
|
935
|
+
const visiblePose = readVisibleCameraPose();
|
|
936
|
+
const cam = visiblePose.position;
|
|
937
|
+
const tgt = visiblePose.target;
|
|
938
|
+
if (ownsShellReadout()) {
|
|
939
|
+
stats.cameraPosition.x = cam.x;
|
|
940
|
+
stats.cameraPosition.y = cam.y;
|
|
941
|
+
stats.cameraPosition.z = cam.z;
|
|
942
|
+
stats.cameraTarget.x = tgt.x;
|
|
943
|
+
stats.cameraTarget.y = tgt.y;
|
|
944
|
+
stats.cameraTarget.z = tgt.z;
|
|
945
|
+
}
|
|
946
|
+
const presenceCamera = `${cam.x},${cam.y},${cam.z},${tgt.x},${tgt.y},${tgt.z},${visiblePose.fov}`;
|
|
947
|
+
if (presenceCamera !== lastPresenceCamera) {
|
|
948
|
+
lastPresenceCamera = presenceCamera;
|
|
949
|
+
presence.reportCamera();
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// --- Start ---
|
|
954
|
+
viewport.objectMap = store.objectMap;
|
|
955
|
+
viewport.syncFromStore();
|
|
956
|
+
rebuildComposer();
|
|
957
|
+
|
|
958
|
+
let editModeComposite: CompositeAuthoringAdapter | undefined;
|
|
959
|
+
let disposeR3FSession: (() => void) | undefined;
|
|
960
|
+
|
|
961
|
+
async function installStage(composite: CompositeAuthoringAdapter): Promise<void> {
|
|
962
|
+
if (disposed) return;
|
|
963
|
+
options.onMountStatus('mounting');
|
|
964
|
+
disposeR3FSession?.();
|
|
965
|
+
disposeR3FSession = undefined;
|
|
966
|
+
editModeComposite = composite;
|
|
967
|
+
options.onRootIds(
|
|
968
|
+
composite
|
|
969
|
+
.childAdapters()
|
|
970
|
+
.filter((child) => child.role === 'world' && child.kind === 'three')
|
|
971
|
+
.map((child) => child.worldId),
|
|
972
|
+
);
|
|
973
|
+
// Design-mount the three medium's design session on THIS stage, so EDIT
|
|
974
|
+
// mode authors the live scene with source write-back. The stage names no
|
|
975
|
+
// medium: `design-time-mount-registry.ts` answers by kind and the
|
|
976
|
+
// integration that owns the surface registered it — the same seam a `dom`
|
|
977
|
+
// or `canvas` layer mounts through, on the surface those media do not
|
|
978
|
+
// have. Nothing registered is a real answer: a product that composes no
|
|
979
|
+
// three integration leaves the world root's own Boundary node standing.
|
|
980
|
+
const stageMount = designTimeMountFor('three');
|
|
981
|
+
if (disposed || editModeComposite !== composite) return;
|
|
982
|
+
const disposeSession = stageMount
|
|
983
|
+
? await stageMount.mountWorldRootSession({ store, composite, renderer })
|
|
984
|
+
: () => {};
|
|
985
|
+
if (disposed || editModeComposite !== composite) {
|
|
986
|
+
disposeSession();
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
disposeR3FSession = disposeSession;
|
|
990
|
+
if (!disposed) options.onMountStatus('ready');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const authoringStage = attachProjectAuthoringStage(store, installStage);
|
|
994
|
+
authoringStage.ready
|
|
995
|
+
.then(() => reportEditorState(collectState(store)))
|
|
996
|
+
.catch((err) => {
|
|
997
|
+
editorConsole.error(
|
|
998
|
+
`Editor boot failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
999
|
+
'scene',
|
|
1000
|
+
);
|
|
1001
|
+
options.onMountStatus('ready');
|
|
1002
|
+
})
|
|
1003
|
+
.finally(() => {
|
|
1004
|
+
// Success and a surfaced terminal failure are both completed viewport
|
|
1005
|
+
// states. In either case the next visible draw is the first honest
|
|
1006
|
+
// frame a startup/activation measurement may certify.
|
|
1007
|
+
sceneContentReady = true;
|
|
1008
|
+
sceneNeedsFirstFrame = true;
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
return {
|
|
1012
|
+
scene,
|
|
1013
|
+
viewport,
|
|
1014
|
+
frame,
|
|
1015
|
+
// PLAY ADOPTION IS THIS BINDING'S — the condition is "the stage is showing
|
|
1016
|
+
// the world root" (ARCHITECTURE-CORE §One stage). A `build` binding shows
|
|
1017
|
+
// one constructed Object3D and presents nothing, so its presenter declines
|
|
1018
|
+
// every root; this one hands the live scene to the store's adoption stack.
|
|
1019
|
+
present: (roots) => presentThreeRoots(store, roots),
|
|
1020
|
+
setHelper: (kind, object) => viewport.setHelper(kind, object),
|
|
1021
|
+
dispose: () => {
|
|
1022
|
+
disposed = true;
|
|
1023
|
+
unregisterPerformanceSource();
|
|
1024
|
+
unregisterCanvasFrame();
|
|
1025
|
+
resizeObserver.disconnect();
|
|
1026
|
+
unsubStore();
|
|
1027
|
+
unsubActions();
|
|
1028
|
+
disposeCameraAuthoring();
|
|
1029
|
+
viewport.orbitControls.removeEventListener('start', beginPilotGesture);
|
|
1030
|
+
viewport.orbitControls.removeEventListener('change', applyPilotGesture);
|
|
1031
|
+
viewport.orbitControls.removeEventListener('end', finishPilotGesture);
|
|
1032
|
+
viewport.orbitControls.removeEventListener('start', cancelAutoFrame);
|
|
1033
|
+
viewport.orbitControls.removeEventListener('end', rememberPose);
|
|
1034
|
+
authoringStage.dispose();
|
|
1035
|
+
disposeR3FSession?.();
|
|
1036
|
+
presence.dispose();
|
|
1037
|
+
viewportShading.dispose();
|
|
1038
|
+
gpuTimer.dispose();
|
|
1039
|
+
unsubscribeViewportPresentation();
|
|
1040
|
+
unsubscribeSelectionTheme();
|
|
1041
|
+
selectionOutline?.selection.clear();
|
|
1042
|
+
softParticleDepth.dispose();
|
|
1043
|
+
composer.dispose();
|
|
1044
|
+
},
|
|
1045
|
+
};
|
|
1046
|
+
}
|