@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,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the canvas entry adapter from the same module graph as project
|
|
3
|
+
* worlds — the `@pixi/react` sibling of `r3f-entry-runtime.ts`.
|
|
4
|
+
*
|
|
5
|
+
* The packaged editor's UI is prebuilt with its own React, `@pixi/react` and
|
|
6
|
+
* `pixi.js`, while project modules are transformed by a separate,
|
|
7
|
+
* project-rooted Vite server. Building a project component's adapter with the
|
|
8
|
+
* UI bundle's factory crosses those graphs: the reconciler's hooks and the
|
|
9
|
+
* component's hooks come from two Reacts, the tree never commits, and
|
|
10
|
+
* the canvas mount fails its own first-commit ceiling ten seconds
|
|
11
|
+
* later. See `../vite-plugin-module-doorways.ts` for the measured failure.
|
|
12
|
+
*
|
|
13
|
+
* `@volter/game-runtime/canvas-react` is dynamic-imported on BOTH branches, deliberately: a
|
|
14
|
+
* static import would put `pixi.js` and the Pixi reconciler in every editor
|
|
15
|
+
* bundle, including one opening a three-only project — the same reason the
|
|
16
|
+
* call sites this replaces were already dynamic.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { resolveCanvasEntryAdapter } from '@volter/game-runtime/canvas-react';
|
|
20
|
+
import type { RootAdapter } from '@volter/editor-project/adapter';
|
|
21
|
+
import type * as PIXI from 'pixi.js';
|
|
22
|
+
import { CANVAS_RUNTIME_PATH } from '@volter/editor-sdk/host';
|
|
23
|
+
import { isPackagedRuntime } from '@volter/editor-core/packaged-runtime';
|
|
24
|
+
|
|
25
|
+
type CanvasEntryResolver = typeof resolveCanvasEntryAdapter;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The `pixi.js` module namespace a canvas surface constructs and class-checks
|
|
29
|
+
* with. Every editor-side collaborator of one mounted canvas world takes THIS,
|
|
30
|
+
* never a static import — see `../vite-plugin-module-doorways.ts` for the four
|
|
31
|
+
* measured failures a second namespace causes.
|
|
32
|
+
*/
|
|
33
|
+
export type CanvasPixiNamespace = typeof PIXI;
|
|
34
|
+
|
|
35
|
+
interface PackagedCanvasRuntime {
|
|
36
|
+
readonly resolve: CanvasEntryResolver;
|
|
37
|
+
readonly projectPixi: CanvasPixiNamespace;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let cachedPackagedRuntime: Promise<PackagedCanvasRuntime> | null = null;
|
|
41
|
+
|
|
42
|
+
async function packagedRuntime(): Promise<PackagedCanvasRuntime> {
|
|
43
|
+
const mod = (await import(/* @vite-ignore */ CANVAS_RUNTIME_PATH)) as {
|
|
44
|
+
resolveCanvasEntryAdapter?: unknown;
|
|
45
|
+
projectPixi?: unknown;
|
|
46
|
+
};
|
|
47
|
+
if (typeof mod.resolveCanvasEntryAdapter !== 'function') {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"The packaged runtime's synthetic canvas module did not export " +
|
|
50
|
+
'`resolveCanvasEntryAdapter` — see vite-plugin-module-doorways.ts.',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (typeof mod.projectPixi !== 'object' || mod.projectPixi === null) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"The packaged runtime's synthetic canvas module did not export " +
|
|
56
|
+
'`projectPixi` — see vite-plugin-module-doorways.ts.',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
resolve: mod.resolveCanvasEntryAdapter as CanvasEntryResolver,
|
|
61
|
+
projectPixi: mod.projectPixi as CanvasPixiNamespace,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* THE `pixi.js` OF THE GRAPH A CANVAS WORLD MOUNTS IN — the project's under the
|
|
67
|
+
* packaged runtime, this bundle's own copy otherwise.
|
|
68
|
+
*
|
|
69
|
+
* Dynamic on BOTH branches for the reason this module's header gives: a static
|
|
70
|
+
* `pixi.js` import here would put the whole renderer in every editor bundle,
|
|
71
|
+
* including one opening a three-only project.
|
|
72
|
+
*
|
|
73
|
+
* A mount site resolves this ONCE and hands it to everything it builds for that
|
|
74
|
+
* world. Nothing further down calls it per node — a namespace that could differ
|
|
75
|
+
* between two collaborators of the same surface is the bug, not the seam.
|
|
76
|
+
*/
|
|
77
|
+
export async function resolveCanvasPixiForEditor(): Promise<CanvasPixiNamespace> {
|
|
78
|
+
if (!(await isPackagedRuntime())) return import('pixi.js');
|
|
79
|
+
if (!cachedPackagedRuntime) cachedPackagedRuntime = packagedRuntime();
|
|
80
|
+
return (await cachedPackagedRuntime).projectPixi;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The adjudicator `resolveCanvasEntryAdapter` — from the PROJECT's graph
|
|
84
|
+
* under the packaged runtime, from this bundle's own engine copy otherwise. */
|
|
85
|
+
export async function resolveCanvasEntryAdapterForEditor(
|
|
86
|
+
entryModule: unknown,
|
|
87
|
+
rootId: string,
|
|
88
|
+
): Promise<RootAdapter<'canvas'> | null> {
|
|
89
|
+
if (!(await isPackagedRuntime())) {
|
|
90
|
+
const { resolveCanvasEntryAdapter } = await import('@volter/game-runtime/canvas-react');
|
|
91
|
+
return resolveCanvasEntryAdapter(entryModule, rootId);
|
|
92
|
+
}
|
|
93
|
+
if (!cachedPackagedRuntime) cachedPackagedRuntime = packagedRuntime();
|
|
94
|
+
return (await cachedPackagedRuntime).resolve(entryModule, rootId);
|
|
95
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
faCamera,
|
|
3
|
+
faEllipsis,
|
|
4
|
+
faGripLines,
|
|
5
|
+
faThumbtack,
|
|
6
|
+
faXmark,
|
|
7
|
+
} from '@fortawesome/free-solid-svg-icons';
|
|
8
|
+
import {
|
|
9
|
+
Button,
|
|
10
|
+
EditorBadge,
|
|
11
|
+
EditorIcon,
|
|
12
|
+
EditorPopover,
|
|
13
|
+
EditorSurface,
|
|
14
|
+
IconButton,
|
|
15
|
+
Inline,
|
|
16
|
+
Text,
|
|
17
|
+
Tooltip,
|
|
18
|
+
} from '@volter/editor-sdk/widgets';
|
|
19
|
+
import { type RefObject, useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
20
|
+
import type * as THREE from 'three';
|
|
21
|
+
import {
|
|
22
|
+
alignCameraToViewport,
|
|
23
|
+
cameraAuthoringPresentation,
|
|
24
|
+
cameraAuthoringVersion,
|
|
25
|
+
leaveCameraView,
|
|
26
|
+
pilotCamera,
|
|
27
|
+
subscribeCameraAuthoring,
|
|
28
|
+
toggleCameraCompositionGuides,
|
|
29
|
+
toggleCameraPreviewPin,
|
|
30
|
+
viewThroughCamera,
|
|
31
|
+
} from '@volter/editor-core/camera-authoring';
|
|
32
|
+
|
|
33
|
+
export interface CameraAuthoringOverlayProps {
|
|
34
|
+
readonly previewRef: RefObject<HTMLDivElement | null>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function CameraPreview({ previewRef }: CameraAuthoringOverlayProps) {
|
|
38
|
+
const { preview, previewPinned } = cameraAuthoringPresentation();
|
|
39
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
40
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!menuOpen) return;
|
|
44
|
+
const dismiss = (event: PointerEvent) => {
|
|
45
|
+
if (!menuRef.current?.contains(event.target as Node)) setMenuOpen(false);
|
|
46
|
+
};
|
|
47
|
+
document.addEventListener('pointerdown', dismiss);
|
|
48
|
+
return () => document.removeEventListener('pointerdown', dismiss);
|
|
49
|
+
}, [menuOpen]);
|
|
50
|
+
|
|
51
|
+
if (!preview) return null;
|
|
52
|
+
const aspect =
|
|
53
|
+
(preview.camera as THREE.PerspectiveCamera).isPerspectiveCamera &&
|
|
54
|
+
Number.isFinite((preview.camera as THREE.PerspectiveCamera).aspect)
|
|
55
|
+
? (preview.camera as THREE.PerspectiveCamera).aspect
|
|
56
|
+
: 16 / 9;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="vgai-camera-preview" data-testid="camera-preview">
|
|
60
|
+
<Inline className="vgai-camera-preview-header" gap={4} align="center">
|
|
61
|
+
<EditorIcon icon={faCamera} size="sm" />
|
|
62
|
+
<Text
|
|
63
|
+
variant="label"
|
|
64
|
+
truncate
|
|
65
|
+
className="vgai-camera-preview-title"
|
|
66
|
+
title={`${preview.name} · ${preview.lens}`}
|
|
67
|
+
>
|
|
68
|
+
{preview.name}
|
|
69
|
+
</Text>
|
|
70
|
+
<Tooltip text={previewPinned ? 'Unpin camera preview' : 'Pin camera preview'}>
|
|
71
|
+
<IconButton
|
|
72
|
+
aria-label={previewPinned ? 'Unpin camera preview' : 'Pin camera preview'}
|
|
73
|
+
aria-pressed={previewPinned}
|
|
74
|
+
size="compact"
|
|
75
|
+
onClick={toggleCameraPreviewPin}
|
|
76
|
+
>
|
|
77
|
+
<EditorIcon icon={faThumbtack} size="sm" />
|
|
78
|
+
</IconButton>
|
|
79
|
+
</Tooltip>
|
|
80
|
+
<div ref={menuRef} className="vgai-camera-preview-menu-anchor">
|
|
81
|
+
<Tooltip text="Camera actions">
|
|
82
|
+
<IconButton
|
|
83
|
+
aria-label="Camera actions"
|
|
84
|
+
aria-expanded={menuOpen}
|
|
85
|
+
size="compact"
|
|
86
|
+
onClick={() => setMenuOpen((open) => !open)}
|
|
87
|
+
>
|
|
88
|
+
<EditorIcon icon={faEllipsis} size="sm" />
|
|
89
|
+
</IconButton>
|
|
90
|
+
</Tooltip>
|
|
91
|
+
{menuOpen ? (
|
|
92
|
+
<EditorPopover className="vgai-camera-preview-menu">
|
|
93
|
+
<Button
|
|
94
|
+
variant="ghost"
|
|
95
|
+
size="compact"
|
|
96
|
+
disabled={!preview.canAuthorPose}
|
|
97
|
+
title={preview.poseRefusal}
|
|
98
|
+
onClick={() => {
|
|
99
|
+
setMenuOpen(false);
|
|
100
|
+
pilotCamera(preview);
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
Pilot Camera
|
|
104
|
+
</Button>
|
|
105
|
+
<Button
|
|
106
|
+
variant="ghost"
|
|
107
|
+
size="compact"
|
|
108
|
+
onClick={() => {
|
|
109
|
+
setMenuOpen(false);
|
|
110
|
+
viewThroughCamera(preview);
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
View Through Camera
|
|
114
|
+
</Button>
|
|
115
|
+
<Button
|
|
116
|
+
variant="ghost"
|
|
117
|
+
size="compact"
|
|
118
|
+
disabled={!preview.canAuthorPose}
|
|
119
|
+
title={preview.poseRefusal}
|
|
120
|
+
onClick={() => {
|
|
121
|
+
setMenuOpen(false);
|
|
122
|
+
alignCameraToViewport(preview);
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
Align Camera to Current View
|
|
126
|
+
</Button>
|
|
127
|
+
</EditorPopover>
|
|
128
|
+
) : null}
|
|
129
|
+
</div>
|
|
130
|
+
</Inline>
|
|
131
|
+
<div
|
|
132
|
+
ref={previewRef}
|
|
133
|
+
className="vgai-camera-preview-image"
|
|
134
|
+
style={{ aspectRatio: String(Math.max(0.5, Math.min(3, aspect))) }}
|
|
135
|
+
role="img"
|
|
136
|
+
aria-label={`Live view through ${preview.name}`}
|
|
137
|
+
onDoubleClick={() => viewThroughCamera(preview)}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function CompositionGuides() {
|
|
144
|
+
return (
|
|
145
|
+
<div className="vgai-camera-guides" aria-hidden="true">
|
|
146
|
+
<div className="vgai-camera-guide vgai-camera-guide-v1" />
|
|
147
|
+
<div className="vgai-camera-guide vgai-camera-guide-v2" />
|
|
148
|
+
<div className="vgai-camera-guide vgai-camera-guide-h1" />
|
|
149
|
+
<div className="vgai-camera-guide vgai-camera-guide-h2" />
|
|
150
|
+
<div className="vgai-camera-safe-frame" />
|
|
151
|
+
<div className="vgai-camera-center-mark" />
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function CameraViewBanner() {
|
|
157
|
+
const { view, guidesVisible } = cameraAuthoringPresentation();
|
|
158
|
+
if (!view) return null;
|
|
159
|
+
const piloting = view.mode === 'pilot';
|
|
160
|
+
return (
|
|
161
|
+
<>
|
|
162
|
+
{guidesVisible ? <CompositionGuides /> : null}
|
|
163
|
+
<EditorSurface
|
|
164
|
+
variant="overlay"
|
|
165
|
+
border
|
|
166
|
+
className="vgai-camera-view-banner vgai-chrome-island vgai-glass-island"
|
|
167
|
+
data-testid="camera-view-banner"
|
|
168
|
+
>
|
|
169
|
+
<Inline gap={6} align="center">
|
|
170
|
+
<EditorBadge>{piloting ? 'PILOTING' : 'CAMERA VIEW'}</EditorBadge>
|
|
171
|
+
<Text variant="label">{view.subject.name}</Text>
|
|
172
|
+
<Text variant="caption" tone="muted">
|
|
173
|
+
Exact camera view
|
|
174
|
+
</Text>
|
|
175
|
+
<Tooltip text={guidesVisible ? 'Hide composition guides' : 'Show composition guides'}>
|
|
176
|
+
<IconButton
|
|
177
|
+
aria-label={guidesVisible ? 'Hide composition guides' : 'Show composition guides'}
|
|
178
|
+
aria-pressed={guidesVisible}
|
|
179
|
+
size="compact"
|
|
180
|
+
onClick={toggleCameraCompositionGuides}
|
|
181
|
+
>
|
|
182
|
+
<EditorIcon icon={faGripLines} size="sm" />
|
|
183
|
+
</IconButton>
|
|
184
|
+
</Tooltip>
|
|
185
|
+
{!piloting ? (
|
|
186
|
+
<Button
|
|
187
|
+
size="compact"
|
|
188
|
+
variant="primary"
|
|
189
|
+
disabled={!view.subject.canAuthorPose}
|
|
190
|
+
title={view.subject.poseRefusal}
|
|
191
|
+
onClick={() => pilotCamera(view.subject)}
|
|
192
|
+
>
|
|
193
|
+
Pilot
|
|
194
|
+
</Button>
|
|
195
|
+
) : null}
|
|
196
|
+
<Tooltip text="Exit camera view">
|
|
197
|
+
<IconButton aria-label="Exit camera view" size="compact" onClick={leaveCameraView}>
|
|
198
|
+
<EditorIcon icon={faXmark} size="sm" />
|
|
199
|
+
</IconButton>
|
|
200
|
+
</Tooltip>
|
|
201
|
+
</Inline>
|
|
202
|
+
</EditorSurface>
|
|
203
|
+
</>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Selected-camera preview and view/pilot presentation for the Scene viewport. */
|
|
208
|
+
export function CameraAuthoringOverlay({ previewRef }: CameraAuthoringOverlayProps) {
|
|
209
|
+
useSyncExternalStore(subscribeCameraAuthoring, cameraAuthoringVersion, cameraAuthoringVersion);
|
|
210
|
+
return (
|
|
211
|
+
<>
|
|
212
|
+
<CameraPreview previewRef={previewRef} />
|
|
213
|
+
<CameraViewBanner />
|
|
214
|
+
</>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { faVolumeHigh, faVolumeXmark } from '@fortawesome/free-solid-svg-icons';
|
|
2
|
+
import { AnchoredMenu, Button, EditorIcon, Inline, MenuItem } from '@volter/editor-sdk/widgets';
|
|
3
|
+
import type { PerformanceSnapshot } from '@volter/game-runtime/dev/performance-profiler';
|
|
4
|
+
import type { AudioAdapter, AudioMeterFrame } from '@volter/editor-project/adapter';
|
|
5
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
activeAudioVersion,
|
|
8
|
+
getAllActiveAudio,
|
|
9
|
+
getInspectedAudio,
|
|
10
|
+
subscribeActiveAudio,
|
|
11
|
+
} from '@volter/editor-core/authoring/active-systems';
|
|
12
|
+
import { useEditorStore } from '@volter/editor-core/editor-runtime';
|
|
13
|
+
import { useActivePerformanceSource } from '../use-active-performance-source';
|
|
14
|
+
import {
|
|
15
|
+
activeChromeRegions,
|
|
16
|
+
chromeRegionsKey,
|
|
17
|
+
subscribeChromeRegions,
|
|
18
|
+
} from '@volter/editor-core/workspace-regions';
|
|
19
|
+
import {
|
|
20
|
+
AUDIO_METER_SEGMENTS,
|
|
21
|
+
audioMeterSegmentCount,
|
|
22
|
+
FRAME_BUDGET_60_FPS_MS,
|
|
23
|
+
frameSparklinePoints,
|
|
24
|
+
PERFORMANCE_SPARKLINE_SAMPLES,
|
|
25
|
+
performanceTelemetryTone,
|
|
26
|
+
sampleFrameInterval,
|
|
27
|
+
} from './header-telemetry-model';
|
|
28
|
+
import { profilerView } from './utility-view-state';
|
|
29
|
+
|
|
30
|
+
const MUTE_KEY = 'editor:muted';
|
|
31
|
+
const EMPTY_PERFORMANCE: PerformanceSnapshot = {
|
|
32
|
+
enabled: false,
|
|
33
|
+
recording: false,
|
|
34
|
+
fps: 0,
|
|
35
|
+
cpuMs: 0,
|
|
36
|
+
p95Ms: 0,
|
|
37
|
+
p99Ms: 0,
|
|
38
|
+
frames: [],
|
|
39
|
+
phases: [],
|
|
40
|
+
systems: [],
|
|
41
|
+
components: [],
|
|
42
|
+
render: {
|
|
43
|
+
gpuMs: null,
|
|
44
|
+
drawCalls: 0,
|
|
45
|
+
triangles: 0,
|
|
46
|
+
geometries: 0,
|
|
47
|
+
textures: 0,
|
|
48
|
+
renderPasses: 0,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function storedMutePreference(): boolean {
|
|
53
|
+
try {
|
|
54
|
+
return localStorage.getItem(MUTE_KEY) === '1';
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function persistMutePreference(muted: boolean): void {
|
|
61
|
+
try {
|
|
62
|
+
localStorage.setItem(MUTE_KEY, muted ? '1' : '0');
|
|
63
|
+
} catch {
|
|
64
|
+
// Storage can be unavailable in an embedded/locked-down host. Muting the
|
|
65
|
+
// live adapters still succeeds; only the next-session preference is lost.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function audioDisplayLevel(adapter: AudioAdapter, frames: readonly AudioMeterFrame[]): number {
|
|
70
|
+
const master = frames.find((frame) => frame.id === 'master')?.level;
|
|
71
|
+
if (!adapter.isMuted() && master !== undefined) return master;
|
|
72
|
+
// The first-party master tap is post-gain, so it truthfully flatlines while
|
|
73
|
+
// muted. The bus taps are pre-master: their maximum is an ACTIVITY reading,
|
|
74
|
+
// not a fabricated mixed output level, and lets grey bars say "signal is
|
|
75
|
+
// present but silenced". External adapters that expose only master degrade
|
|
76
|
+
// to that honest (possibly zero) value.
|
|
77
|
+
const upstream = frames.filter((frame) => frame.id !== 'master').map((frame) => frame.level);
|
|
78
|
+
return upstream.length > 0 ? Math.max(...upstream) : (master ?? 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* AUTHORING IS SILENT: the host's own policy, not each game's manners.
|
|
83
|
+
*
|
|
84
|
+
* In edit mode a mounted world's system adapters are live (the design session
|
|
85
|
+
* publishes `game.systemAdapters`), so a game whose audio starts with its
|
|
86
|
+
* scene sounds at the person EDITING it. The engine only mutes for pause
|
|
87
|
+
* (`setRootGates`), which is a runtime concern; nothing muted a world for
|
|
88
|
+
* being authored rather than played. So the effective mute for every active
|
|
89
|
+
* adapter is `userMuted || !playing`, applied here, with the user's own
|
|
90
|
+
* preference left untouched: the toggle keeps meaning "the user wants mute",
|
|
91
|
+
* and while stopped it decides what happens once play starts.
|
|
92
|
+
*
|
|
93
|
+
* Only `'playing'` counts as audible so the two gates agree instead of
|
|
94
|
+
* fighting: `'paused'` is already muted by the engine, which restores the
|
|
95
|
+
* pre-pause value on resume — which is exactly this effective value, and the
|
|
96
|
+
* play-state subscription re-applies it on that transition anyway.
|
|
97
|
+
*/
|
|
98
|
+
function AudioTelemetry() {
|
|
99
|
+
const store = useEditorStore();
|
|
100
|
+
useSyncExternalStore(store.subscribe, store.getShellSnapshot ?? store.getSnapshot);
|
|
101
|
+
const playing = store.playState === 'playing';
|
|
102
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
103
|
+
const [level, setLevel] = useState(0);
|
|
104
|
+
const [muted, setMuted] = useState(false);
|
|
105
|
+
const [meterAvailable, setMeterAvailable] = useState(false);
|
|
106
|
+
// The user's own wish, which the authoring gate above never clobbers — the
|
|
107
|
+
// menu names THIS (what the next click changes), while the icon and the
|
|
108
|
+
// state label report the effective, audible-or-not truth.
|
|
109
|
+
const [userMuted, setUserMuted] = useState(storedMutePreference);
|
|
110
|
+
const desiredMuteRef = useRef(storedMutePreference());
|
|
111
|
+
const appliedRef = useRef(new WeakMap<object, boolean>());
|
|
112
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
113
|
+
const audioVersion = useSyncExternalStore(
|
|
114
|
+
subscribeActiveAudio,
|
|
115
|
+
activeAudioVersion,
|
|
116
|
+
activeAudioVersion,
|
|
117
|
+
);
|
|
118
|
+
const adapter = getInspectedAudio();
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
void audioVersion;
|
|
122
|
+
const desired = desiredMuteRef.current || !playing;
|
|
123
|
+
for (const active of getAllActiveAudio()) {
|
|
124
|
+
if (appliedRef.current.get(active) === desired) continue;
|
|
125
|
+
active.setMuted(desired);
|
|
126
|
+
appliedRef.current.set(active, desired);
|
|
127
|
+
}
|
|
128
|
+
}, [audioVersion, playing]);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!adapter) {
|
|
132
|
+
setLevel(0);
|
|
133
|
+
setMuted(false);
|
|
134
|
+
setMeterAvailable(false);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const handle = adapter.acquireMeters?.() ?? null;
|
|
138
|
+
setMeterAvailable(handle !== null);
|
|
139
|
+
const sample = () => {
|
|
140
|
+
setMuted(adapter.isMuted());
|
|
141
|
+
setLevel(handle ? audioDisplayLevel(adapter, handle.read()) : 0);
|
|
142
|
+
};
|
|
143
|
+
sample();
|
|
144
|
+
const id = setInterval(sample, 100);
|
|
145
|
+
return () => {
|
|
146
|
+
clearInterval(id);
|
|
147
|
+
handle?.dispose();
|
|
148
|
+
};
|
|
149
|
+
}, [adapter]);
|
|
150
|
+
|
|
151
|
+
// The button toggles the USER's preference, never the effective state:
|
|
152
|
+
// reading it back off the adapters would make "unmute" while stopped (where
|
|
153
|
+
// everything is muted by the authoring gate) unmute the world at the person
|
|
154
|
+
// editing it, and would lose the preference the next time play starts.
|
|
155
|
+
const toggleMute = () => {
|
|
156
|
+
const nextPreference = !desiredMuteRef.current;
|
|
157
|
+
const effective = nextPreference || !playing;
|
|
158
|
+
for (const item of getAllActiveAudio()) {
|
|
159
|
+
item.resume?.();
|
|
160
|
+
item.setMuted(effective);
|
|
161
|
+
appliedRef.current.set(item, effective);
|
|
162
|
+
}
|
|
163
|
+
desiredMuteRef.current = nextPreference;
|
|
164
|
+
setUserMuted(nextPreference);
|
|
165
|
+
persistMutePreference(nextPreference);
|
|
166
|
+
setMuted(effective);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const filled = audioMeterSegmentCount(level);
|
|
170
|
+
// Muted-because-not-playing is the one state a person cannot explain from
|
|
171
|
+
// the icon alone; it rides in the existing tooltip rather than new chrome.
|
|
172
|
+
const muteReason = adapter && muted && !userMuted && !playing ? ' (not playing)' : '';
|
|
173
|
+
const stateLabel = !adapter
|
|
174
|
+
? 'Audio unavailable'
|
|
175
|
+
: muted
|
|
176
|
+
? 'Audio muted'
|
|
177
|
+
: level > 0
|
|
178
|
+
? 'Audio playing'
|
|
179
|
+
: 'Audio quiet';
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div className="vgai-header-audio" data-muted={muted || undefined} data-available={!!adapter}>
|
|
183
|
+
<Button
|
|
184
|
+
ref={buttonRef}
|
|
185
|
+
type="button"
|
|
186
|
+
variant="ghost"
|
|
187
|
+
size="compact"
|
|
188
|
+
className="vgai-header-telemetry-button"
|
|
189
|
+
data-testid="header-audio-telemetry"
|
|
190
|
+
aria-label={`${stateLabel}; open audio controls`}
|
|
191
|
+
aria-haspopup="menu"
|
|
192
|
+
aria-expanded={menuOpen}
|
|
193
|
+
title={`${stateLabel}${muteReason}${meterAvailable ? '' : ' · level metering unavailable'}`}
|
|
194
|
+
onClick={() => setMenuOpen((open) => !open)}
|
|
195
|
+
>
|
|
196
|
+
<EditorIcon icon={muted ? faVolumeXmark : faVolumeHigh} size="sm" />
|
|
197
|
+
<span className="vgai-audio-vu" aria-hidden="true">
|
|
198
|
+
{Array.from({ length: AUDIO_METER_SEGMENTS }, (_, index) => (
|
|
199
|
+
<i key={index} data-filled={index < filled || undefined} />
|
|
200
|
+
))}
|
|
201
|
+
</span>
|
|
202
|
+
</Button>
|
|
203
|
+
{menuOpen && (
|
|
204
|
+
<AnchoredMenu anchorRef={buttonRef} onDismiss={() => setMenuOpen(false)} gap={3}>
|
|
205
|
+
<MenuItem
|
|
206
|
+
onClick={() => {
|
|
207
|
+
toggleMute();
|
|
208
|
+
setMenuOpen(false);
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{userMuted ? 'Unmute audio' : 'Mute audio'}
|
|
212
|
+
</MenuItem>
|
|
213
|
+
</AnchoredMenu>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function PerformanceTelemetry() {
|
|
220
|
+
const source = useActivePerformanceSource();
|
|
221
|
+
const profiler = source?.profiler ?? null;
|
|
222
|
+
const [snapshot, setSnapshot] = useState<PerformanceSnapshot>(EMPTY_PERFORMANCE);
|
|
223
|
+
const [frameHistory, setFrameHistory] = useState<number[]>([]);
|
|
224
|
+
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (!profiler) {
|
|
227
|
+
setSnapshot(EMPTY_PERFORMANCE);
|
|
228
|
+
setFrameHistory([]);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const telemetryWasEnabled = profiler.telemetryEnabled;
|
|
232
|
+
profiler.telemetryEnabled = true;
|
|
233
|
+
const initial = profiler.getSnapshot();
|
|
234
|
+
const initialSample = sampleFrameInterval(initial.frames, 0);
|
|
235
|
+
let sampledThroughFrameId = initialSample.frameId;
|
|
236
|
+
setSnapshot(initial);
|
|
237
|
+
setFrameHistory(initialSample.intervalMs === null ? [] : [initialSample.intervalMs]);
|
|
238
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
239
|
+
const publish = () => {
|
|
240
|
+
timer = null;
|
|
241
|
+
const next = profiler.getSnapshot();
|
|
242
|
+
const sample = sampleFrameInterval(next.frames, sampledThroughFrameId);
|
|
243
|
+
sampledThroughFrameId = sample.frameId;
|
|
244
|
+
setSnapshot(next);
|
|
245
|
+
const interval = sample.intervalMs;
|
|
246
|
+
if (interval !== null) {
|
|
247
|
+
setFrameHistory((history) => [...history, interval].slice(-PERFORMANCE_SPARKLINE_SAMPLES));
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const unsubscribe = profiler.subscribe(() => {
|
|
251
|
+
if (timer === null) timer = setTimeout(publish, 500);
|
|
252
|
+
});
|
|
253
|
+
return () => {
|
|
254
|
+
unsubscribe();
|
|
255
|
+
if (timer !== null) clearTimeout(timer);
|
|
256
|
+
if (!telemetryWasEnabled) profiler.telemetryEnabled = false;
|
|
257
|
+
};
|
|
258
|
+
}, [profiler]);
|
|
259
|
+
|
|
260
|
+
const latestFrameMs = frameHistory.at(-1) ?? null;
|
|
261
|
+
const points = frameSparklinePoints(frameHistory, 72, 18);
|
|
262
|
+
const fps = latestFrameMs
|
|
263
|
+
? Math.round(1000 / latestFrameMs)
|
|
264
|
+
: snapshot.fps > 0
|
|
265
|
+
? Math.round(snapshot.fps)
|
|
266
|
+
: null;
|
|
267
|
+
const frameMs = latestFrameMs ?? (snapshot.fps > 0 ? 1000 / snapshot.fps : null);
|
|
268
|
+
const tone = performanceTelemetryTone(frameHistory);
|
|
269
|
+
const title = source
|
|
270
|
+
? `${source.label}: ${fps ?? '—'} FPS · ${frameMs?.toFixed(1) ?? '—'} ms frame time · open Profiler`
|
|
271
|
+
: 'The active document has no live render loop to profile';
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<Button
|
|
275
|
+
type="button"
|
|
276
|
+
variant="ghost"
|
|
277
|
+
size="compact"
|
|
278
|
+
className="vgai-header-telemetry-button vgai-header-performance"
|
|
279
|
+
data-testid="header-performance-telemetry"
|
|
280
|
+
data-performance-tone={tone}
|
|
281
|
+
disabled={!source}
|
|
282
|
+
aria-label={title}
|
|
283
|
+
title={title}
|
|
284
|
+
onClick={() => profilerView.open('profiler')}
|
|
285
|
+
>
|
|
286
|
+
<svg
|
|
287
|
+
className="vgai-performance-graph"
|
|
288
|
+
aria-hidden="true"
|
|
289
|
+
viewBox="0 0 72 18"
|
|
290
|
+
width="72"
|
|
291
|
+
height="18"
|
|
292
|
+
>
|
|
293
|
+
<line
|
|
294
|
+
className="vgai-performance-budget"
|
|
295
|
+
x1="0"
|
|
296
|
+
x2="72"
|
|
297
|
+
y1={18 - (FRAME_BUDGET_60_FPS_MS / 50) * 18}
|
|
298
|
+
y2={18 - (FRAME_BUDGET_60_FPS_MS / 50) * 18}
|
|
299
|
+
/>
|
|
300
|
+
{points && <polyline className="vgai-performance-sparkline" points={points} />}
|
|
301
|
+
</svg>
|
|
302
|
+
<span className="vgai-performance-readout">
|
|
303
|
+
<strong className="vgai-header-fps">{fps ?? '—'} FPS</strong>
|
|
304
|
+
<span className="vgai-header-frame-ms">{frameMs?.toFixed(1) ?? '—'} ms</span>
|
|
305
|
+
</span>
|
|
306
|
+
</Button>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Persistent, glanceable runtime telemetry. The audio VU is an instantaneous
|
|
312
|
+
* segmented magnitude; performance is a historical line, so the two readings
|
|
313
|
+
* remain distinguishable without color.
|
|
314
|
+
*
|
|
315
|
+
* IT IS A CHROME REGION (`telemetry`, `workspace-regions.ts`), the way the
|
|
316
|
+
* workspace tab strip is: measured against Blender's Modeling frame, this
|
|
317
|
+
* cluster is the brightest, highest-chroma ink in a window whose brightest
|
|
318
|
+
* chrome ink is 216 — a yellow readout, an orange sparkline and a lit meter
|
|
319
|
+
* where Blender's corner carries two quiet wells (Scene, ViewLayer) — and it
|
|
320
|
+
* is the first thing the eye lands on. A style that wants that corner quiet
|
|
321
|
+
* declares `telemetry: 'hidden'`; absent means shown, so every existing skin
|
|
322
|
+
* is unchanged. Only these three readings are behind the region: Invite and
|
|
323
|
+
* Account are product identity, not look, and they are already quiet. The
|
|
324
|
+
* Profiler utility remains the door to the same numbers, which is what the
|
|
325
|
+
* readout's own tooltip says.
|
|
326
|
+
*/
|
|
327
|
+
export function HeaderTelemetry() {
|
|
328
|
+
useSyncExternalStore(subscribeChromeRegions, chromeRegionsKey, chromeRegionsKey);
|
|
329
|
+
if (activeChromeRegions().telemetry === 'hidden') return null;
|
|
330
|
+
return (
|
|
331
|
+
<Inline
|
|
332
|
+
className="vgai-header-telemetry vgai-chrome-island vgai-glass-island"
|
|
333
|
+
data-island-scale="compact"
|
|
334
|
+
align="center"
|
|
335
|
+
gap={1}
|
|
336
|
+
>
|
|
337
|
+
<AudioTelemetry />
|
|
338
|
+
<PerformanceTelemetry />
|
|
339
|
+
</Inline>
|
|
340
|
+
);
|
|
341
|
+
}
|