@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,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getInspectedNetworking,
|
|
3
|
+
inspectedInstanceId,
|
|
4
|
+
inspectedInstanceVersion,
|
|
5
|
+
setInspectedInstance,
|
|
6
|
+
subscribeInspectedInstance,
|
|
7
|
+
} from '@volter/editor-core/authoring/active-systems';
|
|
8
|
+
import { useEditorStore } from '@volter/editor-core/editor-runtime';
|
|
9
|
+
import { gameRealmDiagnostics } from '../host/gated-globals';
|
|
10
|
+
import { faEye } from '@fortawesome/free-solid-svg-icons';
|
|
11
|
+
import {
|
|
12
|
+
AnchoredMenu,
|
|
13
|
+
Button,
|
|
14
|
+
DisclosureIcon,
|
|
15
|
+
EditorIcon,
|
|
16
|
+
MenuItem,
|
|
17
|
+
MenuSeparator,
|
|
18
|
+
Text,
|
|
19
|
+
themeVars,
|
|
20
|
+
} from '@volter/editor-sdk/widgets';
|
|
21
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
22
|
+
import {
|
|
23
|
+
focusedInstanceId,
|
|
24
|
+
instanceEntries,
|
|
25
|
+
subscribeFocusedInstance,
|
|
26
|
+
subscribeGameSession,
|
|
27
|
+
} from '../play/play-mode';
|
|
28
|
+
|
|
29
|
+
function instancesSnapshot(): string {
|
|
30
|
+
return instanceEntries()
|
|
31
|
+
.map(({ id, name }) => `${id}\u0000${name}`)
|
|
32
|
+
.join('\u0001');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Diagnostics({ instanceId }: { instanceId: string }) {
|
|
36
|
+
const store = useEditorStore();
|
|
37
|
+
useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
38
|
+
const focused = useSyncExternalStore(
|
|
39
|
+
subscribeFocusedInstance,
|
|
40
|
+
focusedInstanceId,
|
|
41
|
+
focusedInstanceId,
|
|
42
|
+
);
|
|
43
|
+
const [, refresh] = useState(0);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const timer = window.setInterval(() => refresh((value) => value + 1), 250);
|
|
46
|
+
return () => window.clearInterval(timer);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const realm = gameRealmDiagnostics(instanceId);
|
|
50
|
+
const networking = getInspectedNetworking();
|
|
51
|
+
const timers = realm
|
|
52
|
+
? (realm.loop.activeTimeouts ?? 0) +
|
|
53
|
+
(realm.loop.activeIntervals ?? 0) +
|
|
54
|
+
(realm.loop.activeAnimationFrames ?? 0) +
|
|
55
|
+
realm.loop.pending
|
|
56
|
+
: 0;
|
|
57
|
+
const listeners = realm ? realm.listeners.window + realm.listeners.document : 0;
|
|
58
|
+
const rows: [string, string][] = [
|
|
59
|
+
['Mount', instanceId],
|
|
60
|
+
['Simulation', store.playState === 'paused' ? 'Paused' : 'Running'],
|
|
61
|
+
['Input', focused === instanceId ? 'Keyboard focus' : 'Inactive'],
|
|
62
|
+
[
|
|
63
|
+
'Input events',
|
|
64
|
+
realm
|
|
65
|
+
? `${realm.loop.input ?? 0} observed · ${realm.loop.inputBlocked ?? 0} blocked`
|
|
66
|
+
: 'Not measured',
|
|
67
|
+
],
|
|
68
|
+
// Two different nothings: no adapter at all, and an adapter that cannot
|
|
69
|
+
// read connection state. Neither is "disconnected".
|
|
70
|
+
[
|
|
71
|
+
'Network',
|
|
72
|
+
!networking
|
|
73
|
+
? 'Not registered'
|
|
74
|
+
: (networking.getConnectionState?.() ?? 'Not reported by this adapter'),
|
|
75
|
+
],
|
|
76
|
+
['Owned callbacks', String(timers)],
|
|
77
|
+
['Owned listeners', String(listeners)],
|
|
78
|
+
['Realm globals', String(realm?.globals ?? 0)],
|
|
79
|
+
// Same-origin browser storage is intentionally disclosed rather than
|
|
80
|
+
// presented as isolated. A durable virtual profile would require a stable
|
|
81
|
+
// project-owned identity beyond the transient mount id.
|
|
82
|
+
[
|
|
83
|
+
'Browser storage',
|
|
84
|
+
realm
|
|
85
|
+
? `Shared origin · ${realm.storage.local.reads + realm.storage.session.reads} reads · ${realm.storage.local.writes + realm.storage.session.writes} writes`
|
|
86
|
+
: 'Shared origin',
|
|
87
|
+
],
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
role="none"
|
|
93
|
+
style={{
|
|
94
|
+
display: 'grid',
|
|
95
|
+
gridTemplateColumns: '112px minmax(120px, 1fr)',
|
|
96
|
+
gap: '4px 12px',
|
|
97
|
+
padding: '8px 10px',
|
|
98
|
+
fontSize: 10,
|
|
99
|
+
lineHeight: '15px',
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{rows.map(([label, value]) => (
|
|
103
|
+
<div key={label} style={{ display: 'contents' }}>
|
|
104
|
+
<span style={{ color: themeVars.content.dim }}>{label}</span>
|
|
105
|
+
<span
|
|
106
|
+
style={{
|
|
107
|
+
color: themeVars.content.primary,
|
|
108
|
+
fontFamily: label === 'Mount' ? themeVars.typography.mono : undefined,
|
|
109
|
+
overflow: 'hidden',
|
|
110
|
+
textOverflow: 'ellipsis',
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{value}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Runtime context selector for a split Game document. This changes only
|
|
122
|
+
* editor instruments; it never retargets scene authoring or agent commands. */
|
|
123
|
+
export function InstanceInspectorPicker() {
|
|
124
|
+
useSyncExternalStore(subscribeGameSession, instancesSnapshot, instancesSnapshot);
|
|
125
|
+
useSyncExternalStore(
|
|
126
|
+
subscribeInspectedInstance,
|
|
127
|
+
inspectedInstanceVersion,
|
|
128
|
+
inspectedInstanceVersion,
|
|
129
|
+
);
|
|
130
|
+
const entries = instanceEntries();
|
|
131
|
+
const inspectedId = inspectedInstanceId();
|
|
132
|
+
const selected = entries.find((entry) => entry.id === inspectedId) ?? entries[0];
|
|
133
|
+
const [open, setOpen] = useState(false);
|
|
134
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
135
|
+
|
|
136
|
+
if (entries.length < 2 || !selected) return null;
|
|
137
|
+
const diagnostics = gameRealmDiagnostics(selected.id);
|
|
138
|
+
const warning = (diagnostics?.loop.firedWhileHeld ?? 0) > 0;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="vgai-playbar-popover-anchor">
|
|
142
|
+
<Button
|
|
143
|
+
ref={ref}
|
|
144
|
+
variant="ghost"
|
|
145
|
+
size="compact"
|
|
146
|
+
aria-haspopup="menu"
|
|
147
|
+
aria-expanded={open}
|
|
148
|
+
onClick={() => setOpen((value) => !value)}
|
|
149
|
+
title={`Runtime instruments inspect ${selected.name}; agent-addressed commands are unchanged`}
|
|
150
|
+
>
|
|
151
|
+
<EditorIcon
|
|
152
|
+
icon={faEye}
|
|
153
|
+
size="sm"
|
|
154
|
+
style={{ color: warning ? themeVars.semantic.warning : undefined }}
|
|
155
|
+
/>
|
|
156
|
+
<Text variant="code">Inspect {selected.name}</Text>
|
|
157
|
+
<DisclosureIcon direction="down" />
|
|
158
|
+
</Button>
|
|
159
|
+
{open && (
|
|
160
|
+
<AnchoredMenu
|
|
161
|
+
anchorRef={ref}
|
|
162
|
+
gap={3}
|
|
163
|
+
onDismiss={() => setOpen(false)}
|
|
164
|
+
style={{ minWidth: 270 }}
|
|
165
|
+
>
|
|
166
|
+
{entries.map((entry) => (
|
|
167
|
+
<MenuItem
|
|
168
|
+
key={entry.id}
|
|
169
|
+
role="menuitemradio"
|
|
170
|
+
aria-checked={entry.id === selected.id}
|
|
171
|
+
onSelect={() => setInspectedInstance(entry.id)}
|
|
172
|
+
>
|
|
173
|
+
{entry.id === selected.id ? '✓ ' : ' '}
|
|
174
|
+
{entry.name}
|
|
175
|
+
</MenuItem>
|
|
176
|
+
))}
|
|
177
|
+
<MenuSeparator />
|
|
178
|
+
<Diagnostics instanceId={selected.id} />
|
|
179
|
+
</AnchoredMenu>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Play-mode crowd debug draw (W1a) — agent-clearance cylinders + velocity
|
|
3
|
+
* arrows for every live crowd agent, drawn INTO the running game's scene so
|
|
4
|
+
* the Game tab shows them over the actual gameplay.
|
|
5
|
+
*
|
|
6
|
+
* Reads only the `NavigationAdapter.crowdAgents()` capability off the active
|
|
7
|
+
* game's `SystemAdapters` (installed by play-mode's `setActiveSystems`) — the
|
|
8
|
+
* editor never touches recast's `Crowd` directly. A game whose navigation
|
|
9
|
+
* adapter omits `crowdAgents` simply draws nothing (capability-absence
|
|
10
|
+
* degradation, same doctrine as the rest of the seam).
|
|
11
|
+
*
|
|
12
|
+
* Toggled from the Game tab's Debug menu (`CenterDocuments.tsx`), mirroring
|
|
13
|
+
* the active-document debug-control pattern there.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getActiveSystems } from '@volter/editor-core/authoring/active-systems';
|
|
17
|
+
import type { NavCrowdAgentState } from '@volter/editor-project/adapter';
|
|
18
|
+
import { setUserData } from '@volter/threejs-runtime/ecs/user-data';
|
|
19
|
+
import * as THREE from 'three';
|
|
20
|
+
import { getGameScene } from '../play/play-mode';
|
|
21
|
+
|
|
22
|
+
let _enabled = false;
|
|
23
|
+
let _raf = 0;
|
|
24
|
+
let _group: THREE.Group | null = null;
|
|
25
|
+
let _attachedScene: THREE.Scene | null = null;
|
|
26
|
+
|
|
27
|
+
/** Unit cylinder scaled per-agent to (radius, height, radius). */
|
|
28
|
+
const _cylGeo = new THREE.CylinderGeometry(1, 1, 1, 16, 1, true);
|
|
29
|
+
const _cylMat = new THREE.MeshBasicMaterial({
|
|
30
|
+
color: 0x22ccee,
|
|
31
|
+
transparent: true,
|
|
32
|
+
opacity: 0.45,
|
|
33
|
+
side: THREE.DoubleSide,
|
|
34
|
+
depthWrite: false,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
interface AgentVisual {
|
|
38
|
+
root: THREE.Group;
|
|
39
|
+
cylinder: THREE.Mesh;
|
|
40
|
+
/** Per-visual material clone, so a FAILED agent can flush red without
|
|
41
|
+
* repainting the whole crowd. */
|
|
42
|
+
cylinderMaterial: THREE.MeshBasicMaterial;
|
|
43
|
+
arrow: THREE.ArrowHelper;
|
|
44
|
+
/** The agent's REQUESTED PATH — its local corridor corners, drawn in scene
|
|
45
|
+
* space (not under `root`, which follows the agent). */
|
|
46
|
+
path: THREE.Line;
|
|
47
|
+
/** The move target, a small marker at the corridor's end. */
|
|
48
|
+
target: THREE.Mesh;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const _pathMat = new THREE.LineBasicMaterial({ color: 0xffaa00, transparent: true, opacity: 0.9 });
|
|
52
|
+
const _targetGeo = new THREE.SphereGeometry(0.12, 12, 8);
|
|
53
|
+
const _targetMat = new THREE.MeshBasicMaterial({ color: 0xffaa00, depthWrite: false });
|
|
54
|
+
const FAILED_COLOR = 0xff3344;
|
|
55
|
+
const OK_COLOR = 0x22ccee;
|
|
56
|
+
|
|
57
|
+
let _visuals: AgentVisual[] = [];
|
|
58
|
+
|
|
59
|
+
export function crowdDebugEnabled(): boolean {
|
|
60
|
+
return _enabled;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function setCrowdDebugEnabled(enabled: boolean): void {
|
|
64
|
+
if (enabled === _enabled) return;
|
|
65
|
+
_enabled = enabled;
|
|
66
|
+
if (enabled) {
|
|
67
|
+
_raf = requestAnimationFrame(_tick);
|
|
68
|
+
} else {
|
|
69
|
+
cancelAnimationFrame(_raf);
|
|
70
|
+
_teardown();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _tick(): void {
|
|
75
|
+
if (!_enabled) return;
|
|
76
|
+
_raf = requestAnimationFrame(_tick);
|
|
77
|
+
const scene = getGameScene();
|
|
78
|
+
const agents = scene ? (getActiveSystems().navigation?.crowdAgents?.() ?? []) : [];
|
|
79
|
+
|
|
80
|
+
// (Re)attach the group to whatever game scene is currently live — play
|
|
81
|
+
// restarts swap scenes out from under a long-lived toggle.
|
|
82
|
+
if (scene !== _attachedScene) {
|
|
83
|
+
if (_attachedScene && _group) _attachedScene.remove(_group);
|
|
84
|
+
_attachedScene = scene;
|
|
85
|
+
if (scene) {
|
|
86
|
+
if (!_group) {
|
|
87
|
+
_group = new THREE.Group();
|
|
88
|
+
_group.name = 'crowd-debug-draw';
|
|
89
|
+
setUserData(_group, 'editorHelper', true);
|
|
90
|
+
}
|
|
91
|
+
scene.add(_group);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!_group || !scene) return;
|
|
95
|
+
|
|
96
|
+
_syncVisualCount(_group, agents.length);
|
|
97
|
+
for (let i = 0; i < agents.length; i++) {
|
|
98
|
+
_updateAgentVisual(_visuals[i]!, agents[i]!);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Grow/shrink the pooled per-agent visuals to match the live agent count. */
|
|
103
|
+
function _syncVisualCount(group: THREE.Group, count: number): void {
|
|
104
|
+
while (_visuals.length < count) {
|
|
105
|
+
const root = new THREE.Group();
|
|
106
|
+
const cylinderMaterial = _cylMat.clone();
|
|
107
|
+
const cylinder = new THREE.Mesh(_cylGeo, cylinderMaterial);
|
|
108
|
+
const arrow = new THREE.ArrowHelper(
|
|
109
|
+
new THREE.Vector3(1, 0, 0),
|
|
110
|
+
new THREE.Vector3(),
|
|
111
|
+
1,
|
|
112
|
+
0xffaa00,
|
|
113
|
+
);
|
|
114
|
+
const path = new THREE.Line(new THREE.BufferGeometry(), _pathMat);
|
|
115
|
+
const target = new THREE.Mesh(_targetGeo, _targetMat);
|
|
116
|
+
root.add(cylinder);
|
|
117
|
+
root.add(arrow);
|
|
118
|
+
group.add(root);
|
|
119
|
+
group.add(path);
|
|
120
|
+
group.add(target);
|
|
121
|
+
_visuals.push({ root, cylinder, cylinderMaterial, arrow, path, target });
|
|
122
|
+
}
|
|
123
|
+
while (_visuals.length > count) {
|
|
124
|
+
const v = _visuals.pop()!;
|
|
125
|
+
group.remove(v.root);
|
|
126
|
+
group.remove(v.path);
|
|
127
|
+
group.remove(v.target);
|
|
128
|
+
v.arrow.dispose();
|
|
129
|
+
v.path.geometry.dispose();
|
|
130
|
+
v.cylinderMaterial.dispose();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _updateAgentVisual(v: AgentVisual, a: NavCrowdAgentState): void {
|
|
135
|
+
v.root.position.set(a.position.x, a.position.y, a.position.z);
|
|
136
|
+
v.cylinder.scale.set(a.radius, a.height, a.radius);
|
|
137
|
+
v.cylinder.position.y = a.height / 2;
|
|
138
|
+
// A FAILED agent — off the navmesh, going nowhere — flushes red; everything
|
|
139
|
+
// else keeps the crowd colour. An adapter that predates `state` reports
|
|
140
|
+
// undefined and draws as walking.
|
|
141
|
+
v.cylinderMaterial.color.setHex(a.state === 'invalid' ? FAILED_COLOR : OK_COLOR);
|
|
142
|
+
// The REQUESTED PATH: the agent's local corridor, drawn from its own feet
|
|
143
|
+
// through the corners, with the move target marked at its own position.
|
|
144
|
+
const corners = a.corners ?? [];
|
|
145
|
+
if (corners.length > 0) {
|
|
146
|
+
v.path.visible = true;
|
|
147
|
+
v.path.geometry.setFromPoints([
|
|
148
|
+
new THREE.Vector3(a.position.x, a.position.y + 0.05, a.position.z),
|
|
149
|
+
...corners.map((c) => new THREE.Vector3(c.x, c.y + 0.05, c.z)),
|
|
150
|
+
]);
|
|
151
|
+
} else {
|
|
152
|
+
v.path.visible = false;
|
|
153
|
+
}
|
|
154
|
+
if (a.target && corners.length > 0) {
|
|
155
|
+
v.target.visible = true;
|
|
156
|
+
v.target.position.set(a.target.x, a.target.y + 0.05, a.target.z);
|
|
157
|
+
} else {
|
|
158
|
+
v.target.visible = false;
|
|
159
|
+
}
|
|
160
|
+
const speed = Math.hypot(a.velocity.x, a.velocity.y, a.velocity.z);
|
|
161
|
+
if (speed > 0.01) {
|
|
162
|
+
v.arrow.visible = true;
|
|
163
|
+
v.arrow.position.y = a.height / 2;
|
|
164
|
+
v.arrow.setDirection(
|
|
165
|
+
new THREE.Vector3(a.velocity.x / speed, a.velocity.y / speed, a.velocity.z / speed),
|
|
166
|
+
);
|
|
167
|
+
// Arrow length = half a second of travel, min 0.2 so it stays legible.
|
|
168
|
+
v.arrow.setLength(Math.max(speed * 0.5, 0.2));
|
|
169
|
+
} else {
|
|
170
|
+
v.arrow.visible = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _teardown(): void {
|
|
175
|
+
if (_group) {
|
|
176
|
+
_attachedScene?.remove(_group);
|
|
177
|
+
for (const v of _visuals) v.arrow.dispose();
|
|
178
|
+
_group.clear();
|
|
179
|
+
}
|
|
180
|
+
_visuals = [];
|
|
181
|
+
_group = null;
|
|
182
|
+
_attachedScene = null;
|
|
183
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device preview (W2c, F13 — critique O4). Chrome-device-mode-style presets
|
|
3
|
+
* for the Game tab: a preset constrains the game mount to a device's CSS
|
|
4
|
+
* resolution (letterboxed/centered in the panel, scaled down to fit — the
|
|
5
|
+
* scale/letterbox math is `GamePanel`'s existing device-resolution path in
|
|
6
|
+
* `CenterDocuments.tsx`), applies the device's DPR to the running renderers
|
|
7
|
+
* (`resizeGame(w, h, dpr)` → `GameSession.resize`'s optional third arg), and
|
|
8
|
+
* for phone/tablet presets:
|
|
9
|
+
*
|
|
10
|
+
* - SAFE AREA — the preset carries notch/home-indicator insets. `GamePanel`
|
|
11
|
+
* draws a visual overlay (`data-testid="device-safe-area-overlay"`) and
|
|
12
|
+
* sets CSS custom properties on the game mount container
|
|
13
|
+
* (`data-testid="game-runtime-container"`), the element game canvases and
|
|
14
|
+
* react-world DOM layers mount into:
|
|
15
|
+
* --vgai-safe-area-inset-top / -right / -bottom / -left (CSS px)
|
|
16
|
+
* Game UI reads them with `var(--vgai-safe-area-inset-top, 0px)`.
|
|
17
|
+
* Recorded choice: real `env(safe-area-inset-*)` injection is NOT
|
|
18
|
+
* possible from a same-document host (env() values come from the UA for
|
|
19
|
+
* the top-level viewport only), so the overlay + CSS vars ARE the
|
|
20
|
+
* contract here.
|
|
21
|
+
*
|
|
22
|
+
* - POINTER-AS-TOUCH — mouse input reaches the game as pointer events with
|
|
23
|
+
* `pointerType: 'touch'`, emulated at the game-mount DOM boundary
|
|
24
|
+
* (`attachDeviceTouchTarget` below): a capture-phase listener on the
|
|
25
|
+
* mount container swallows mouse-typed pointer events and re-dispatches a
|
|
26
|
+
* clone with `pointerType: 'touch'` on the same target — so anything the
|
|
27
|
+
* game registered (raw canvas listeners, gated `window`/`document`
|
|
28
|
+
* listeners via `gated-globals.ts` — the clone bubbles to the real window
|
|
29
|
+
* exactly like the original would, so the T6.3 input gate still applies)
|
|
30
|
+
* sees touch. The engine `InputManager` is untouched: it listens for
|
|
31
|
+
* compatibility `mouse*` events on `window`, which the browser still
|
|
32
|
+
* fires (we never `preventDefault()` the pointer event). Fidelity limits,
|
|
33
|
+
* recorded: real mobile fires compat mousedown/up AFTER a tap completes
|
|
34
|
+
* and never streams hover mousemove — here the InputManager sees
|
|
35
|
+
* desktop-timed mouse events alongside the touch clones, so games mixing
|
|
36
|
+
* pointer handlers with InputManager mouse state get simultaneous rather
|
|
37
|
+
* than sequenced input, and hover-dependent logic that breaks on device
|
|
38
|
+
* still works in emulation. No `TouchEvent` (`touchstart`/`touchmove`)
|
|
39
|
+
* synthesis (Chrome device mode does synthesize these) — raw-touch-event
|
|
40
|
+
* games see nothing. A game capture-phase window/document pointer
|
|
41
|
+
* listener registers ahead of this seam and sees the original mouse
|
|
42
|
+
* event before the clone.
|
|
43
|
+
*
|
|
44
|
+
* Persistence: preset + touch override are the PROJECT settings layer's
|
|
45
|
+
* `devicePreview` (`settings-store.ts` → `.vgai/settings.json`, committed):
|
|
46
|
+
* the frame a game is designed for is a fact about the project, shared by
|
|
47
|
+
* everyone who opens it.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import { projectSettings, updateProjectSettings } from '@volter/editor-core/settings-store';
|
|
51
|
+
|
|
52
|
+
export type DevicePresetKind = 'fit' | 'phone' | 'tablet' | 'desktop';
|
|
53
|
+
|
|
54
|
+
export interface SafeAreaInsets {
|
|
55
|
+
readonly top: number;
|
|
56
|
+
readonly right: number;
|
|
57
|
+
readonly bottom: number;
|
|
58
|
+
readonly left: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface DevicePreset {
|
|
62
|
+
readonly id: string;
|
|
63
|
+
readonly label: string;
|
|
64
|
+
readonly kind: DevicePresetKind;
|
|
65
|
+
/** CSS resolution; null for the default fit-panel preset. */
|
|
66
|
+
readonly width: number | null;
|
|
67
|
+
readonly height: number | null;
|
|
68
|
+
/** Device pixel ratio applied to the game renderers; null → host default. */
|
|
69
|
+
readonly dpr: number | null;
|
|
70
|
+
/** Notch/home-indicator insets in CSS px; null → no safe-area treatment. */
|
|
71
|
+
readonly safeArea: SafeAreaInsets | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const FIT_PRESET_ID = 'fit';
|
|
75
|
+
|
|
76
|
+
/** Chrome-device-mode-class presets. Insets are the real devices' values
|
|
77
|
+
* (iPhone 14: 47pt notch + 34pt home indicator; landscape rotates the notch
|
|
78
|
+
* to the sides; Android: 24dp status bar + 24dp gesture bar; iPad: 24pt
|
|
79
|
+
* status bar + 20pt home indicator). */
|
|
80
|
+
export const DEVICE_PRESETS: readonly DevicePreset[] = [
|
|
81
|
+
{
|
|
82
|
+
id: FIT_PRESET_ID,
|
|
83
|
+
label: 'Fit panel',
|
|
84
|
+
kind: 'fit',
|
|
85
|
+
width: null,
|
|
86
|
+
height: null,
|
|
87
|
+
dpr: null,
|
|
88
|
+
safeArea: null,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'iphone-portrait',
|
|
92
|
+
label: 'iPhone 14 — 390×844 @3x',
|
|
93
|
+
kind: 'phone',
|
|
94
|
+
width: 390,
|
|
95
|
+
height: 844,
|
|
96
|
+
dpr: 3,
|
|
97
|
+
safeArea: { top: 47, right: 0, bottom: 34, left: 0 },
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'iphone-landscape',
|
|
101
|
+
label: 'iPhone 14 landscape — 844×390 @3x',
|
|
102
|
+
kind: 'phone',
|
|
103
|
+
width: 844,
|
|
104
|
+
height: 390,
|
|
105
|
+
dpr: 3,
|
|
106
|
+
safeArea: { top: 0, right: 47, bottom: 21, left: 47 },
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'android-portrait',
|
|
110
|
+
label: 'Android — 412×915 @2.6x',
|
|
111
|
+
kind: 'phone',
|
|
112
|
+
width: 412,
|
|
113
|
+
height: 915,
|
|
114
|
+
dpr: 2.6,
|
|
115
|
+
safeArea: { top: 24, right: 0, bottom: 24, left: 0 },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'ipad-portrait',
|
|
119
|
+
label: 'iPad — 820×1180 @2x',
|
|
120
|
+
kind: 'tablet',
|
|
121
|
+
width: 820,
|
|
122
|
+
height: 1180,
|
|
123
|
+
dpr: 2,
|
|
124
|
+
safeArea: { top: 24, right: 0, bottom: 20, left: 0 },
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'desktop-1080p',
|
|
128
|
+
label: 'Desktop — 1920×1080 @1x',
|
|
129
|
+
kind: 'desktop',
|
|
130
|
+
width: 1920,
|
|
131
|
+
height: 1080,
|
|
132
|
+
dpr: 1,
|
|
133
|
+
safeArea: null,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const FIT_PRESET = DEVICE_PRESETS[0] as DevicePreset;
|
|
138
|
+
|
|
139
|
+
function presetById(id: string): DevicePreset {
|
|
140
|
+
return DEVICE_PRESETS.find((p) => p.id === id) ?? FIT_PRESET;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- store (house module-scope useSyncExternalStore shape) ------------------
|
|
144
|
+
|
|
145
|
+
let _preset: DevicePreset = FIT_PRESET;
|
|
146
|
+
/** null → auto (on for phone/tablet presets); boolean → explicit override. */
|
|
147
|
+
let _touchOverride: boolean | null = null;
|
|
148
|
+
let _version = 0;
|
|
149
|
+
const _listeners = new Set<() => void>();
|
|
150
|
+
|
|
151
|
+
function notify(): void {
|
|
152
|
+
_version++;
|
|
153
|
+
for (const fn of _listeners) fn();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function subscribeDevicePreview(fn: () => void): () => void {
|
|
157
|
+
_listeners.add(fn);
|
|
158
|
+
return () => {
|
|
159
|
+
_listeners.delete(fn);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const devicePreviewVersion = (): number => _version;
|
|
164
|
+
|
|
165
|
+
export function devicePreset(): DevicePreset {
|
|
166
|
+
return _preset;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Effective pointer-as-touch state: explicit override, else auto-on for
|
|
170
|
+
* phone/tablet presets. */
|
|
171
|
+
export function touchEmulationActive(): boolean {
|
|
172
|
+
return _touchOverride ?? (_preset.kind === 'phone' || _preset.kind === 'tablet');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function touchEmulationOverride(): boolean | null {
|
|
176
|
+
return _touchOverride;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function setDevicePreset(id: string): void {
|
|
180
|
+
const next = presetById(id);
|
|
181
|
+
if (next === _preset) return;
|
|
182
|
+
_preset = next;
|
|
183
|
+
// A new device's auto touch policy takes over; the override was a choice
|
|
184
|
+
// about the PREVIOUS preset.
|
|
185
|
+
_touchOverride = null;
|
|
186
|
+
persist();
|
|
187
|
+
notify();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function setTouchEmulationOverride(enabled: boolean | null): void {
|
|
191
|
+
if (enabled === _touchOverride) return;
|
|
192
|
+
_touchOverride = enabled;
|
|
193
|
+
persist();
|
|
194
|
+
notify();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The renderer pixel ratio the active preset emulates, or `null` for the
|
|
198
|
+
* host default (`min(devicePixelRatio, 2)` — `create-runtime.ts`'s mount
|
|
199
|
+
* policy). `play-mode.ts` consults this on session boot so a preset chosen
|
|
200
|
+
* BEFORE Play still lands its DPR. */
|
|
201
|
+
export function deviceEmulatedPixelRatio(): number | null {
|
|
202
|
+
return _preset.dpr;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The host-default pixel ratio `GamePanel` passes to restore after a preset
|
|
206
|
+
* is cleared — the same formula the runtime mounts with. */
|
|
207
|
+
export function hostDefaultPixelRatio(): number {
|
|
208
|
+
return Math.min(typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1, 2);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- project persistence (the PROJECT settings layer) ----------------------
|
|
212
|
+
|
|
213
|
+
function persist(): void {
|
|
214
|
+
updateProjectSettings({ devicePreview: { preset: _preset.id, touch: _touchOverride } });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Load the active project's declared preset into the store (no persist
|
|
218
|
+
* write-back) and return it — `GamePanel` calls this on mount and applies
|
|
219
|
+
* the returned preset's resolution to the shared game-resolution store. */
|
|
220
|
+
export function restoreDevicePresetForProject(): DevicePreset {
|
|
221
|
+
const stored = projectSettings().devicePreview;
|
|
222
|
+
if (stored?.preset) {
|
|
223
|
+
_preset = presetById(stored.preset);
|
|
224
|
+
_touchOverride = typeof stored.touch === 'boolean' ? stored.touch : null;
|
|
225
|
+
} else {
|
|
226
|
+
// Nothing declared for THIS project: reset module state, or a preset
|
|
227
|
+
// carried over from a previously-open project would silently apply here
|
|
228
|
+
// (and a later toggle would persist it under this project).
|
|
229
|
+
_preset = FIT_PRESET;
|
|
230
|
+
_touchOverride = null;
|
|
231
|
+
}
|
|
232
|
+
notify();
|
|
233
|
+
return _preset;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// --- pointer-as-touch emulation at the game-mount DOM boundary --------------
|
|
237
|
+
|
|
238
|
+
const EMULATED_TYPES = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'] as const;
|
|
239
|
+
|
|
240
|
+
/** Clones this module dispatched — never re-intercept our own events. */
|
|
241
|
+
const _synthetic = new WeakSet<Event>();
|
|
242
|
+
|
|
243
|
+
let _touchTarget: HTMLElement | null = null;
|
|
244
|
+
let _detach: (() => void) | null = null;
|
|
245
|
+
|
|
246
|
+
function interceptPointer(e: Event): void {
|
|
247
|
+
if (!touchEmulationActive()) return;
|
|
248
|
+
if (_synthetic.has(e)) return;
|
|
249
|
+
if (!(e instanceof PointerEvent) || e.pointerType !== 'mouse') return;
|
|
250
|
+
const target = e.target;
|
|
251
|
+
if (!target) return;
|
|
252
|
+
// Swallow the mouse-typed pointer event before it reaches the game (canvas
|
|
253
|
+
// listeners, gated window/document listeners) …
|
|
254
|
+
e.stopImmediatePropagation();
|
|
255
|
+
// … and deliver the identical gesture as touch. Compat mouse events are
|
|
256
|
+
// deliberately NOT suppressed (no preventDefault): the engine InputManager
|
|
257
|
+
// listens for window `mouse*` and keeps working, exactly as on a real
|
|
258
|
+
// mobile browser where compat mouse events accompany touches.
|
|
259
|
+
const clone = new PointerEvent(e.type, {
|
|
260
|
+
bubbles: true,
|
|
261
|
+
cancelable: e.cancelable,
|
|
262
|
+
composed: true,
|
|
263
|
+
view: e.view,
|
|
264
|
+
detail: e.detail,
|
|
265
|
+
clientX: e.clientX,
|
|
266
|
+
clientY: e.clientY,
|
|
267
|
+
screenX: e.screenX,
|
|
268
|
+
screenY: e.screenY,
|
|
269
|
+
button: e.button,
|
|
270
|
+
buttons: e.buttons,
|
|
271
|
+
ctrlKey: e.ctrlKey,
|
|
272
|
+
shiftKey: e.shiftKey,
|
|
273
|
+
altKey: e.altKey,
|
|
274
|
+
metaKey: e.metaKey,
|
|
275
|
+
pointerId: e.pointerId,
|
|
276
|
+
pointerType: 'touch',
|
|
277
|
+
isPrimary: true,
|
|
278
|
+
pressure: e.type === 'pointerup' || e.type === 'pointercancel' ? 0 : Math.max(e.pressure, 0.5),
|
|
279
|
+
width: 20,
|
|
280
|
+
height: 20,
|
|
281
|
+
});
|
|
282
|
+
_synthetic.add(clone);
|
|
283
|
+
target.dispatchEvent(clone);
|
|
284
|
+
if (clone.defaultPrevented) e.preventDefault();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* (Re)bind the emulation seam to the game mount container. `GamePanel`'s
|
|
289
|
+
* container callback ref calls this alongside `setGameContainer`; `null`
|
|
290
|
+
* detaches. Listeners are capture-phase so they run before the canvas (the
|
|
291
|
+
* event target) and before any bubble listener the game installed; they
|
|
292
|
+
* no-op unless {@link touchEmulationActive}.
|
|
293
|
+
*/
|
|
294
|
+
export function attachDeviceTouchTarget(el: HTMLElement | null): void {
|
|
295
|
+
if (el === _touchTarget) return;
|
|
296
|
+
_detach?.();
|
|
297
|
+
_detach = null;
|
|
298
|
+
_touchTarget = el;
|
|
299
|
+
if (!el) return;
|
|
300
|
+
for (const type of EMULATED_TYPES) {
|
|
301
|
+
el.addEventListener(type, interceptPointer, { capture: true });
|
|
302
|
+
}
|
|
303
|
+
_detach = () => {
|
|
304
|
+
for (const type of EMULATED_TYPES) {
|
|
305
|
+
el.removeEventListener(type, interceptPointer, { capture: true });
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The detach half, keyed by the element letting go — the same ownership rule
|
|
312
|
+
* as `play-mode.ts`'s `releaseGameContainer`, for the same overlap: two
|
|
313
|
+
* GamePanels coexist across a close/reopen gesture, and a STALE panel's
|
|
314
|
+
* cleanup calling `attachDeviceTouchTarget(null)` after the new panel
|
|
315
|
+
* attached would strip the CURRENT element's listeners.
|
|
316
|
+
*/
|
|
317
|
+
export function detachDeviceTouchTarget(el: HTMLElement): void {
|
|
318
|
+
if (_touchTarget !== el) return;
|
|
319
|
+
attachDeviceTouchTarget(null);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// --- e2e state hook (house rule: assert state, never screenshot-diff) -------
|
|
323
|
+
|
|
324
|
+
function _stateForE2E() {
|
|
325
|
+
return {
|
|
326
|
+
presetId: _preset.id,
|
|
327
|
+
presetLabel: _preset.label,
|
|
328
|
+
dpr: _preset.dpr,
|
|
329
|
+
safeArea: _preset.safeArea,
|
|
330
|
+
touchActive: touchEmulationActive(),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (typeof window !== 'undefined') {
|
|
335
|
+
(window as unknown as Record<string, unknown>)['__vgaiDevicePreview'] = _stateForE2E;
|
|
336
|
+
}
|