@volter/editor-game 0.5.67 → 0.5.69
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/NOTICE +7 -7
- package/contributions/autoplay.service.ts +1 -1
- package/contributions/build-player.document.tsx +95 -0
- package/contributions/build-profiles.document.tsx +1 -1
- package/contributions/build-progress.status.tsx +2 -2
- package/contributions/build.header.tsx +1 -1
- package/contributions/connection.status.tsx +1 -1
- package/contributions/coverage.service.ts +2 -2
- package/contributions/game-eval.command.ts +6 -6
- package/contributions/gameplay.command.ts +2 -2
- package/contributions/generations.status.tsx +1 -1
- package/contributions/ingest.service.ts +2 -2
- package/contributions/play.command.ts +6 -6
- package/contributions/react/react-inspector.service.ts +1 -1
- package/package.json +9 -8
- package/serving/index.ts +1 -1
- package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
- package/src/asset-budget/asset-budget-model.ts +14 -14
- package/src/asset-budget/optimize-apply.ts +8 -8
- package/src/bridge/call.ts +3 -3
- package/src/bridge/dispatch.ts +12 -12
- package/src/bridge/screenshot.ts +6 -6
- package/src/build/BuildProfilesPanel.tsx +19 -8
- package/src/build/build-session.ts +35 -0
- package/src/canvas/canvas-board/CanvasBoardDocument.tsx +78 -5
- package/src/canvas/canvas-board/canvas-component-board.ts +1 -1
- package/src/canvas/canvas-design-mount.ts +1 -1
- package/src/coverage/live-project-verbs.ts +3 -3
- package/src/coverage/native-system-coverage.ts +3 -3
- package/src/coverage/root-coverage.ts +2 -2
- package/src/coverage/session-coverage.ts +2 -2
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +2 -2
- package/src/game-document/DevicePresetPicker.tsx +6 -6
- package/src/game-document/GameCaptureFrameButton.tsx +1 -1
- package/src/game-document/GameDocument.tsx +2 -2
- package/src/game-document/GamePanel.tsx +9 -9
- package/src/game-document/InstanceInspectorPicker.tsx +1 -1
- package/src/game-document/device-preview.ts +4 -4
- package/src/game-document/physics-debug.ts +2 -2
- package/src/generation/GenerationActivity.tsx +22 -22
- package/src/generation/GenerationGallery.css +79 -79
- package/src/generation/generation-documents.tsx +25 -25
- package/src/generation/generation-presentation.ts +1 -1
- package/src/host/adapter-reach.ts +1 -1
- package/src/host/adapter-runtime-bindings.ts +5 -5
- package/src/host/after-paint.ts +3 -3
- package/src/host/api/configurations.ts +1 -1
- package/src/host/authoring/canvas-runtime-recognition.ts +2 -2
- package/src/host/authoring/contract-hierarchy-authoring.ts +6 -6
- package/src/host/authoring/contract-scenes-stories.ts +5 -5
- package/src/host/authoring/ingest-data-writer.ts +1 -1
- package/src/host/authoring/ingest-source-persistence.ts +2 -2
- package/src/host/authoring/mount-isolated-pixi-screen.ts +1 -1
- package/src/host/authoring/pixi-authoring-adapter.ts +206 -28
- package/src/host/authoring/pixi-creatable-kinds.ts +80 -0
- package/src/host/authoring/pixi-live-write-target.ts +6 -5
- package/src/host/authoring/pixi-source-write-target.ts +118 -20
- package/src/host/authoring/pixi-structure-history.ts +3 -3
- package/src/host/binding-resolver.ts +6 -6
- package/src/host/browser-transpile.ts +10 -10
- package/src/host/canvas-preview-frames.ts +1 -1
- package/src/host/components/CameraAuthoringOverlay.tsx +14 -14
- package/src/host/components/HeaderTelemetry.tsx +11 -11
- package/src/host/components/PixiIsolationSceneContent.tsx +3 -3
- package/src/host/components/ResolutionPicker.tsx +4 -4
- package/src/host/components/scene-document.tsx +3 -3
- package/src/host/components/world-root-stage-binding.tsx +1 -1
- package/src/host/components/world-root-stage.ts +1 -1
- package/src/host/coverage/authoring-read-probe.ts +1 -1
- package/src/host/coverage/capability-coverage.ts +22 -22
- package/src/host/coverage/game-contract-seam-evidence.ts +8 -8
- package/src/host/coverage/project-verb-coverage.ts +1 -1
- package/src/host/coverage/system-adapter-coverage.ts +19 -6
- package/src/host/game-location-guard.ts +3 -3
- package/src/host/game-module-access.ts +6 -6
- package/src/host/game-realm-page.ts +3 -3
- package/src/host/game-realm-reclaim.ts +1 -1
- package/src/host/gameplay-recording.ts +5 -5
- package/src/host/gated-globals.ts +13 -13
- package/src/host/ingest/registry.ts +14 -14
- package/src/host/play-control-hook.ts +1 -1
- package/src/host/playwright-shim.ts +2 -2
- package/src/host/projection/pixi.ts +2 -2
- package/src/host/react-mount-runtime.ts +1 -1
- package/src/host/roots/canvas-root.tsx +13 -1
- package/src/host/roots/r3f-root.tsx +1 -1
- package/src/host/scene-view-drawability.ts +1 -1
- package/src/host/sdk/tools.ts +1 -1
- package/src/host/served-bundle-runtime-modules.ts +2 -0
- package/src/host/staged-projects.ts +1 -1
- package/src/host/stories/story-pixi-preview.ts +1 -1
- package/src/host/stories/story-three-preview.ts +1 -1
- package/src/host/three-ingest-runtime.ts +1 -1
- package/src/ingest/active-ingest.ts +1 -1
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
- package/src/ingest/authoring/ingest-root-adapter.ts +11 -11
- package/src/ingest/capture-wait-report.ts +4 -4
- package/src/ingest/discovery-public-ingest.ts +4 -4
- package/src/ingest/dom-stub-mark.ts +1 -1
- package/src/ingest/entry-load.ts +1 -1
- package/src/ingest/game-contract-realm.ts +3 -3
- package/src/ingest/game-pointer-lock.ts +1 -1
- package/src/ingest/ingest-canvas-scene-document.tsx +3 -3
- package/src/ingest/ingest-evidence-hook.ts +6 -6
- package/src/ingest/ingest-frame-snapshot.ts +2 -2
- package/src/ingest/ingest-play-commands.ts +1 -1
- package/src/ingest/ingest-play-control.ts +11 -11
- package/src/ingest/ingest-render-debug.ts +5 -5
- package/src/ingest/ingest-siblings.ts +3 -3
- package/src/ingest/module-mode.ts +2 -2
- package/src/ingest/mount-canvas-ingest-root.ts +10 -10
- package/src/ingest/mount-coverage.ts +3 -3
- package/src/ingest/mount-dom-ingest-root.ts +7 -7
- package/src/ingest/mount-ingest-root.ts +10 -10
- package/src/ingest/mount-three-ingest-root.ts +5 -5
- package/src/ingest/resolve-three.ts +2 -2
- package/src/ingest/served-bundle.ts +6 -6
- package/src/ingest/served-html-boot.ts +8 -8
- package/src/ingest/surface-dom.ts +9 -9
- package/src/ingest/surface-three.ts +1 -1
- package/src/ingest/types.ts +1 -1
- package/src/ingest/unmount-ingest-root.ts +3 -3
- package/src/network/NetworkInspectorPanel.tsx +521 -54
- package/src/network/network-inspector-model.ts +6 -0
- package/src/play/play-boot-stall.ts +1 -1
- package/src/play/play-mode.ts +31 -30
- package/src/play/play-recording.ts +12 -12
- package/src/play-bar/PlayBar.tsx +8 -8
- package/src/play-bar/PlayerCountPicker.tsx +3 -3
- package/src/profiler/FrameDebuggerPanel.tsx +2 -2
- package/src/profiler/PerformancePanel.tsx +4 -4
- package/src/react/design-time-react-mount.ts +5 -5
- package/src/react/dom-authoring-adapter.ts +3 -3
- package/src/react/react-inspector-section.tsx +2 -2
- package/src/react/react-world-authoring-adapter.ts +20 -20
- package/src/react/story-documents/story-documents.tsx +4 -4
- package/src/react/ui-component-board.ts +1 -1
- package/src/runtime/adapter/audio-meter.ts +1 -1
- package/src/runtime/adapter/ingest/contract-debug-adapter.ts +4 -4
- package/src/runtime/adapter/ingest/contract-system-adapters.ts +9 -9
- package/src/runtime/adapter/ingest/merge-debug-adapters.ts +2 -2
- package/src/runtime/adapter/ingest/observation-debug-adapter.ts +1 -1
- package/src/runtime/adapter/native-debug-module.ts +5 -5
- package/src/runtime/audio/pose-guard.ts +1 -1
- package/src/runtime/core/game-scoped-slot.ts +1 -1
- package/src/runtime/create-runtime.ts +15 -15
- package/src/runtime/debug-bridge.ts +25 -25
- package/src/runtime/debug-registry.ts +17 -17
- package/src/runtime/dev/instruments.ts +1 -1
- package/src/runtime/dev/performance-profiler.ts +2 -2
- package/src/runtime/dev/register-render-vitals.ts +2 -2
- package/src/runtime/dev/static-batch-advisor.ts +3 -3
- package/src/runtime/dev-build.ts +3 -3
- package/src/runtime/game.ts +5 -5
- package/src/runtime/input-router.ts +24 -11
- package/src/runtime/mount-manifest.ts +9 -9
- package/src/runtime/pixi/authoring.ts +1 -1
- package/src/runtime/pixi/ingest.ts +1 -1
- package/src/runtime/presentation.ts +1 -1
- package/src/runtime/render-control.ts +21 -21
- package/src/runtime/render-seed.ts +3 -3
- package/src/runtime/run-ticks-settled.ts +1 -1
- package/src/services/game-audio.ts +3 -3
- package/src/services/game-network.ts +125 -15
- package/src/three/authoring/model-asset-inspector-section.css +15 -15
- package/src/three/authoring/model-asset-inspector-section.tsx +7 -7
- package/src/three/authoring/r3f-design-session.ts +10 -10
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5 -5
- package/src/three/authoring/three-authoring-adapter.ts +5 -4
- package/src/three/authoring/typed-three-inspector.ts +1 -1
- package/src/three/story-documents/three-story-documents.tsx +3 -3
- package/src/three/three-board/board-scene.ts +9 -9
- package/src/three/three-board/three-component-board.ts +1 -1
package/src/bridge/dispatch.ts
CHANGED
|
@@ -109,19 +109,19 @@ function recordContractSystemUse(member: string, value: unknown, name?: string):
|
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
111
|
* #140 — the generic session-wire dispatch table: the SAME method-name
|
|
112
|
-
* surface `window.
|
|
113
|
-
* `
|
|
112
|
+
* surface `window.__volter` exposes (`@volter/editor-game/runtime/debug-bridge`'s
|
|
113
|
+
* `VolterDebugHandle` — `state`/`stateAll`/`providers`/`commands`/`events`/
|
|
114
114
|
* `snapshot`/`invoke`/`runTicks`/`input.*`), built from the SAME
|
|
115
115
|
* registry-backed accessors the sibling
|
|
116
116
|
* `gameplay.command.ts` verbs (`list-gameplay-state`/`inject-input`/`run-ticks`)
|
|
117
117
|
* already use. This is what makes the
|
|
118
118
|
* `bridge-call` relay op (below) a session-generic primitive rather than a
|
|
119
119
|
* test-specific one — any client of the live session (the `@volter/editor-live`
|
|
120
|
-
* `RelayTransport`, a future one-shot `
|
|
120
|
+
* `RelayTransport`, a future one-shot `volter game state/call/hold` REPL) can
|
|
121
121
|
* issue `{method, callArgs}` against it with no dependency on a proof run's
|
|
122
122
|
* own lifecycle/env, and get byte-identical dispatch to the page-transport's
|
|
123
123
|
* `page.evaluate(bridgeCallInPage)` (D17) — without requiring the editor tab
|
|
124
|
-
* to be loaded with `?
|
|
124
|
+
* to be loaded with `?volter-debug=1` (that gate exists for the bridge's own
|
|
125
125
|
* production exposure; this relay is already local-loopback-only — see
|
|
126
126
|
* `editor-server.ts`'s CSRF/origin guard).
|
|
127
127
|
*/
|
|
@@ -228,7 +228,7 @@ export async function dispatchBridgeMethod(
|
|
|
228
228
|
// funnels window error/unhandledrejection into the editor console as
|
|
229
229
|
// level 'error' / source 'runtime' (`installEditorConsoleCapture`'s
|
|
230
230
|
// boot-time listeners) — the SAME events and message format
|
|
231
|
-
// `window.
|
|
231
|
+
// `window.__volter`'s own handle buffers. Fenced to the current play run
|
|
232
232
|
// via `inPlayRun` so a probe never reads a previous run's stale
|
|
233
233
|
// failures, capped at 100 for parity with the bridge's ring buffer.
|
|
234
234
|
pageErrors: collectPlayRunPageErrors(),
|
|
@@ -308,10 +308,10 @@ export async function dispatchBridgeMethod(
|
|
|
308
308
|
case 'input.clearVirtualActions':
|
|
309
309
|
// bridge↔wire coverage-parity gate (`bridge-wire-parity.test.ts`)
|
|
310
310
|
// closed this trio's gap: `startRecording`/`stopRecording`/`isRecording`
|
|
311
|
-
// exist on the page bridge's `
|
|
311
|
+
// exist on the page bridge's `VolterDebugInputHandle` (`runtime/
|
|
312
312
|
// debug-bridge.ts`) but had no relay case at all — an agent driving the
|
|
313
313
|
// input-trace recorder over the session wire (RelayTransport) instead of
|
|
314
|
-
// `?
|
|
314
|
+
// `?volter-debug=1` simply had no door to it. Same single optional
|
|
315
315
|
// trailing `worldId` shape as `clearVirtualActions`.
|
|
316
316
|
case 'input.startRecording':
|
|
317
317
|
case 'input.stopRecording':
|
|
@@ -320,10 +320,10 @@ export async function dispatchBridgeMethod(
|
|
|
320
320
|
// D15/T-D15.5 (rebase composition with #140): this wire is a THIRD
|
|
321
321
|
// door to the virtual-input target — it must resolve through the SAME
|
|
322
322
|
// `DebugRegistry.getVirtualInputTarget(worldId?)` the page bridge
|
|
323
|
-
// (`window.
|
|
323
|
+
// (`window.__volter.input.*`) and the `inject-input` relay case use, so
|
|
324
324
|
// no two doors can ever disagree about which world an unqualified
|
|
325
325
|
// actuation targets. Each method takes the page bridge's own optional
|
|
326
|
-
// trailing `worldId` (byte-parity with `
|
|
326
|
+
// trailing `worldId` (byte-parity with `VolterDebugInputHandle`); an
|
|
327
327
|
// explicit, unregistered id throws the registry's structured
|
|
328
328
|
// `DEBUG_INPUT_WORLD_NOT_FOUND`, shipped back over the wire as-is.
|
|
329
329
|
const worldId = (
|
|
@@ -365,7 +365,7 @@ export async function dispatchBridgeMethod(
|
|
|
365
365
|
return inputTarget.isInputRecording();
|
|
366
366
|
}
|
|
367
367
|
case 'input.scheduleActionAtTick': {
|
|
368
|
-
// bridge↔wire coverage-parity gate: `window.
|
|
368
|
+
// bridge↔wire coverage-parity gate: `window.__volter.input.
|
|
369
369
|
// scheduleActionAtTick` (`runtime/debug-bridge.ts`, D15/T-D15.5) had NO
|
|
370
370
|
// bridge-call case — it was reachable over the relay ONLY via the
|
|
371
371
|
// separate `inject-input` command's `atTick` field (see that case,
|
|
@@ -390,7 +390,7 @@ export async function dispatchBridgeMethod(
|
|
|
390
390
|
}
|
|
391
391
|
case 'input.injectPointerDelta':
|
|
392
392
|
case 'input.injectPointerPosition': {
|
|
393
|
-
// pointer-dispatch op: `window.
|
|
393
|
+
// pointer-dispatch op: `window.__volter.input.injectPointerDelta`/
|
|
394
394
|
// `injectPointerPosition` (`runtime/debug-bridge.ts`) had no relay case
|
|
395
395
|
// — this is the HONEST gameplay-seam reading of "pointer dispatch": the
|
|
396
396
|
// engine's existing virtual pointer-injection, never a synthesized DOM
|
|
@@ -417,7 +417,7 @@ export async function dispatchBridgeMethod(
|
|
|
417
417
|
return undefined;
|
|
418
418
|
}
|
|
419
419
|
case 'holdFor': {
|
|
420
|
-
// #140/D17: byte-identical semantics to `window.
|
|
420
|
+
// #140/D17: byte-identical semantics to `window.__volter.holdFor` (door
|
|
421
421
|
// a, `runtime/debug-bridge.ts`) — same `getInputTarget(worldId?)`
|
|
422
422
|
// resolution as the `input.*` cases above, same `waitForHoldBudget`
|
|
423
423
|
// poll/stall loop (imported, not hand-copied, so the two doors can
|
package/src/bridge/screenshot.ts
CHANGED
|
@@ -51,7 +51,7 @@ import { captureLiveCanvasFrame } from './live-frames';
|
|
|
51
51
|
*
|
|
52
52
|
* BOTH legs read a canvas, and a canvas is only readable after its frame when
|
|
53
53
|
* its WebGL context was created with `preserveDrawingBuffer: true`. Every
|
|
54
|
-
* canvas the
|
|
54
|
+
* canvas the volter RUNTIME mounts sets it (`@volter/editor-game/runtime/create-runtime`,
|
|
55
55
|
* D5 §1/§4); a canvas an INGESTED game created is the game's own and usually
|
|
56
56
|
* does not, which read as a silently BLACK screenshot over a perfectly
|
|
57
57
|
* healthy running game. {@link liveCanvasFrame} is the answer for that
|
|
@@ -102,8 +102,8 @@ function screenshotResult(
|
|
|
102
102
|
* and the play log whose event timestamps convert an event into an offset.
|
|
103
103
|
*
|
|
104
104
|
* THIS USED TO BE A REFUSAL, AND THE REFUSAL WAS THE DEFECT (2026-08-29,
|
|
105
|
-
* cold fox #3). Because `
|
|
106
|
-
* `
|
|
105
|
+
* cold fox #3). Because `volter-game-editor play` always records, refusing the still made
|
|
106
|
+
* `volter-game-editor screenshot` dead for the rest of the session — and the look lane's
|
|
107
107
|
* questions ("is the ear proportion right", "is the fox sunk into the
|
|
108
108
|
* ground") are exactly the ones a still DOES answer. The refusal was
|
|
109
109
|
* discouraging a wrong question by making a right one impossible, and the
|
|
@@ -160,7 +160,7 @@ export async function handleBridgeScreenshot(
|
|
|
160
160
|
// Same reachability rule `handleBridgeCall` uses, and for the same reason:
|
|
161
161
|
// an ingested game's mount IS the running game, so it never creates a
|
|
162
162
|
// first-party play session and `isPlayModeActive()` alone made
|
|
163
|
-
// `
|
|
163
|
+
// `volter-game-editor screenshot` permanently unusable against ingest. Loosening the gate
|
|
164
164
|
// costs nothing in honesty — the ingest mount renders into the very
|
|
165
165
|
// container `getInstanceContainer()` returns (`play-mode.ts`'s
|
|
166
166
|
// `getGameContainer` is shared with the ingest mount paths), and when there is
|
|
@@ -171,10 +171,10 @@ export async function handleBridgeScreenshot(
|
|
|
171
171
|
// TRUE. `hasLiveDebugPlane()` covers an ingest that publishes a debug plane
|
|
172
172
|
// projected from a declared contract — which a canvas ingest has no way to
|
|
173
173
|
// do (`mount-canvas-ingest-root.ts` publishes `setActiveSystems({})`), so
|
|
174
|
-
// `
|
|
174
|
+
// `volter-game-editor screenshot` answered "not in play mode" over a mounted, running,
|
|
175
175
|
// self-ticking game whose play state the mount had itself set to `playing`.
|
|
176
176
|
// A lying refusal is worse than a missing feature: it sends the reader to
|
|
177
|
-
// `
|
|
177
|
+
// `volter-game-editor play`, which is not the thing that was wrong. The question this gate
|
|
178
178
|
// asks is "is a game surface live here", and a live ingest is one.
|
|
179
179
|
if (!isPlayModeActive() && !hasLiveDebugPlane() && !isIngestActive()) return notPlayingResult();
|
|
180
180
|
// Capture the ADDRESSED seat, not always the primary. `game.instance(id)`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* THE BUILD PROFILES PANEL — the configuration surface the `build-profiles`
|
|
3
3
|
* document draws: the project's build-role configurations as a target list,
|
|
4
|
-
* the name/resolution/app-id fields it writes back into `
|
|
4
|
+
* the name/resolution/app-id fields it writes back into `volter.project.json`
|
|
5
5
|
* through the project's file history, the Build trigger, and the finished
|
|
6
6
|
* build's report. The LOG is not here: it streams into the Build Output tab
|
|
7
7
|
* (the native Output panel’s Build channel), which is the whole point of the split — a
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
33
33
|
import {
|
|
34
34
|
type BuildTarget,
|
|
35
|
+
buildAndRun,
|
|
35
36
|
buildSessionVersion,
|
|
36
37
|
cancelBuild,
|
|
37
38
|
getBuildSession,
|
|
@@ -48,7 +49,7 @@ interface ProjectConfig {
|
|
|
48
49
|
appId: string;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const MANIFEST_PATH = '
|
|
52
|
+
const MANIFEST_PATH = 'volter.project.json';
|
|
52
53
|
|
|
53
54
|
const DEFAULT_CONFIG: ProjectConfig = {
|
|
54
55
|
name: 'My Game',
|
|
@@ -215,7 +216,7 @@ export function BuildProfilesPanel() {
|
|
|
215
216
|
<div style={fieldRowStyle}>
|
|
216
217
|
<label style={labelStyle}>Name</label>
|
|
217
218
|
<TextInput
|
|
218
|
-
className="
|
|
219
|
+
className="volter-input"
|
|
219
220
|
style={{ flex: 1 }}
|
|
220
221
|
value={config.name}
|
|
221
222
|
onChange={(e) => updateConfig({ name: e.target.value })}
|
|
@@ -227,7 +228,7 @@ export function BuildProfilesPanel() {
|
|
|
227
228
|
<div style={{ display: 'flex', alignItems: 'center', gap: spaceVar[2], flex: 1 }}>
|
|
228
229
|
<TextInput
|
|
229
230
|
type="number"
|
|
230
|
-
className="
|
|
231
|
+
className="volter-input"
|
|
231
232
|
style={{ width: 0, flex: 1 }}
|
|
232
233
|
value={config.resolution.width}
|
|
233
234
|
onChange={(e) => {
|
|
@@ -238,7 +239,7 @@ export function BuildProfilesPanel() {
|
|
|
238
239
|
<span style={{ fontSize: fontSizeVar.sm, color: text[3] }}>×</span>
|
|
239
240
|
<TextInput
|
|
240
241
|
type="number"
|
|
241
|
-
className="
|
|
242
|
+
className="volter-input"
|
|
242
243
|
style={{ width: 0, flex: 1 }}
|
|
243
244
|
value={config.resolution.height}
|
|
244
245
|
onChange={(e) => {
|
|
@@ -252,7 +253,7 @@ export function BuildProfilesPanel() {
|
|
|
252
253
|
<div style={fieldRowStyle}>
|
|
253
254
|
<label style={labelStyle}>App ID</label>
|
|
254
255
|
<TextInput
|
|
255
|
-
className="
|
|
256
|
+
className="volter-input"
|
|
256
257
|
style={{ flex: 1 }}
|
|
257
258
|
value={config.appId}
|
|
258
259
|
onChange={(e) => updateConfig({ appId: e.target.value })}
|
|
@@ -308,7 +309,7 @@ export function BuildProfilesPanel() {
|
|
|
308
309
|
<a
|
|
309
310
|
href={getDownloadUrl(artifact)}
|
|
310
311
|
download={artifact}
|
|
311
|
-
className="
|
|
312
|
+
className="volter-btn"
|
|
312
313
|
data-variant="secondary"
|
|
313
314
|
data-size="default"
|
|
314
315
|
style={{
|
|
@@ -331,6 +332,16 @@ export function BuildProfilesPanel() {
|
|
|
331
332
|
>
|
|
332
333
|
{phase === 'done' ? 'Build Again' : 'Build'}
|
|
333
334
|
</Button>
|
|
335
|
+
<Button
|
|
336
|
+
data-testid="build-profiles-build-and-run"
|
|
337
|
+
onClick={() => {
|
|
338
|
+
if (!selected) return;
|
|
339
|
+
if (phase === 'done') resetBuildSession();
|
|
340
|
+
void buildAndRun(selected);
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
Build And Run
|
|
344
|
+
</Button>
|
|
334
345
|
</>
|
|
335
346
|
)}
|
|
336
347
|
</div>
|
|
@@ -383,7 +394,7 @@ function BuildReportSummary({ report }: { report: BuildReport }) {
|
|
|
383
394
|
position: 'absolute',
|
|
384
395
|
inset: 0,
|
|
385
396
|
width: `${Math.max(2, (file.bytes / largest) * 100)}%`,
|
|
386
|
-
background: 'color-mix(in srgb, var(--
|
|
397
|
+
background: 'color-mix(in srgb, var(--volter-accent) 16%, transparent)',
|
|
387
398
|
}}
|
|
388
399
|
/>
|
|
389
400
|
<div
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from '../host/api/configurations';
|
|
18
18
|
import type { EditorHostOutputDiagnostic } from '@volter/editor-sdk/host';
|
|
19
19
|
import { editorHost } from '@volter/editor-sdk/host';
|
|
20
|
+
import { BASE } from '@volter/editor-sdk/kit/api-base';
|
|
20
21
|
import type { BuildReport } from '@volter/editor-sdk/session/build-report';
|
|
21
22
|
import { useEffect, useState } from 'react';
|
|
22
23
|
import { formatBytes } from './format-bytes';
|
|
@@ -34,6 +35,40 @@ export function openBuildProfilesDocument(): boolean {
|
|
|
34
35
|
return editorHost().workspace.openContributedDocument(BUILD_PROFILES_DOCUMENT_ID);
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/** The Build Player document's contribution id (`build-player.document.tsx`). */
|
|
39
|
+
export const BUILD_PLAYER_DOCUMENT_ID = 'build-player.document';
|
|
40
|
+
|
|
41
|
+
/** Where the editor server serves the project's built `dist/` (`routes/configurations.ts`). */
|
|
42
|
+
export const BUILD_PLAYER_URL = `${BASE}/build/web/index.html`;
|
|
43
|
+
|
|
44
|
+
let _runs = 0;
|
|
45
|
+
const _runListeners = new Set<() => void>();
|
|
46
|
+
|
|
47
|
+
/** How many times the build has been run: the player reloads on each. */
|
|
48
|
+
export function buildRunCount(): number {
|
|
49
|
+
return _runs;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function subscribeBuildRuns(fn: () => void): () => void {
|
|
53
|
+
_runListeners.add(fn);
|
|
54
|
+
return () => {
|
|
55
|
+
_runListeners.delete(fn);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Run the last build in the Build Player document, fresh. */
|
|
60
|
+
export function runBuild(): boolean {
|
|
61
|
+
_runs++;
|
|
62
|
+
for (const fn of _runListeners) fn();
|
|
63
|
+
return editorHost().workspace.openContributedDocument(BUILD_PLAYER_DOCUMENT_ID);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Unity's Build And Run: build, and run the result when the build succeeds. */
|
|
67
|
+
export async function buildAndRun(target: BuildTarget): Promise<void> {
|
|
68
|
+
await startBuild(target);
|
|
69
|
+
if (_session.phase === 'done' && _session.result?.ok) runBuild();
|
|
70
|
+
}
|
|
71
|
+
|
|
37
72
|
/** A build-role configuration's id (ARCHITECTURE-CORE §The project model). */
|
|
38
73
|
export type BuildTarget = string;
|
|
39
74
|
export type BuildPhase = 'idle' | 'building' | 'done';
|
|
@@ -74,7 +74,8 @@ import {
|
|
|
74
74
|
} from '@volter/editor-sdk/kit/workspace-document-registry';
|
|
75
75
|
import { storyBoardPresentation } from '@volter/editor-sdk/kit/stories/story-presentation';
|
|
76
76
|
import { getProjectStoryModules, subscribeProjectStoryModules } from '@volter/editor-sdk/kit/stories/story-registry';
|
|
77
|
-
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
77
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
78
|
+
import type { AuthoringAdapter, EditorNode } from '@volter/editor-project/adapter';
|
|
78
79
|
import {
|
|
79
80
|
buildCanvasBoard,
|
|
80
81
|
type CanvasBoard,
|
|
@@ -113,6 +114,8 @@ interface CanvasBoardPresentation {
|
|
|
113
114
|
zoomAt(clientX: number, clientY: number): boolean;
|
|
114
115
|
/** The exhibit the board is currently showing as active, for the Inspector. */
|
|
115
116
|
activeStoryId(): string | null;
|
|
117
|
+
/** Select one frame by its id, as a click on it does (the layers list's row). */
|
|
118
|
+
selectStory(storyId: string): boolean;
|
|
116
119
|
dispose(): void;
|
|
117
120
|
}
|
|
118
121
|
|
|
@@ -131,6 +134,53 @@ interface CanvasBoardPresentation {
|
|
|
131
134
|
* An ungrouped exhibit (no district) carries no title and falls back to the
|
|
132
135
|
* substrate's own module-path derivation, exactly like an untitled dom story.
|
|
133
136
|
*/
|
|
137
|
+
/**
|
|
138
|
+
* THE BOARD'S LAYERS — Figma's layers panel lists a page's frames; this adapter hands the
|
|
139
|
+
* Hierarchy the board's frames the same way, each with its district beside its name. It owns no
|
|
140
|
+
* transform, inspector or structure: a frame is chosen here, and choosing one is the same act as
|
|
141
|
+
* clicking it on the board.
|
|
142
|
+
*/
|
|
143
|
+
function canvasBoardAdapter(
|
|
144
|
+
board: CanvasBoard,
|
|
145
|
+
selected: () => string | null,
|
|
146
|
+
select: (storyId: string) => void,
|
|
147
|
+
): AuthoringAdapter & { notify(): void } {
|
|
148
|
+
const listeners = new Set<() => void>();
|
|
149
|
+
const nodes: EditorNode[] = boardRows(board).map((row) => ({
|
|
150
|
+
id: row.id,
|
|
151
|
+
label: row.label,
|
|
152
|
+
...(row.title ? { secondaryLabel: row.title } : {}),
|
|
153
|
+
kind: 'frame',
|
|
154
|
+
typeLabel: 'Frame',
|
|
155
|
+
parentId: null,
|
|
156
|
+
childIds: [],
|
|
157
|
+
}));
|
|
158
|
+
return {
|
|
159
|
+
capabilities: { transform: false, inspectorFields: false, persist: false },
|
|
160
|
+
hierarchy: {
|
|
161
|
+
roots: () => nodes,
|
|
162
|
+
node: (id) => nodes.find((node) => node.id === id) ?? null,
|
|
163
|
+
},
|
|
164
|
+
selection: {
|
|
165
|
+
get: () => {
|
|
166
|
+
const id = selected();
|
|
167
|
+
return id ? [id] : [];
|
|
168
|
+
},
|
|
169
|
+
set: (ids) => {
|
|
170
|
+
const id = ids.at(-1);
|
|
171
|
+
if (id) select(id);
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
subscribe(listener) {
|
|
175
|
+
listeners.add(listener);
|
|
176
|
+
return () => listeners.delete(listener);
|
|
177
|
+
},
|
|
178
|
+
notify() {
|
|
179
|
+
for (const listener of listeners) listener();
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
134
184
|
function boardRows(board: CanvasBoard): ReactStoryBoardStory[] {
|
|
135
185
|
return [
|
|
136
186
|
...board.exhibits.map((exhibit) => ({
|
|
@@ -305,6 +355,13 @@ function presentCanvasBoard(
|
|
|
305
355
|
activeStoryId(): string | null {
|
|
306
356
|
return selectedStoryId;
|
|
307
357
|
},
|
|
358
|
+
selectStory(storyId: string): boolean {
|
|
359
|
+
if (!rows.some((row) => row.id === storyId)) return false;
|
|
360
|
+
selectedStoryId = storyId;
|
|
361
|
+
storyBoard?.activate(storyId, false);
|
|
362
|
+
onSelectionChanged?.();
|
|
363
|
+
return true;
|
|
364
|
+
},
|
|
308
365
|
zoomAt(clientX: number, clientY: number): boolean {
|
|
309
366
|
return storyBoard?.zoomAtClientPoint(clientX, clientY) ?? false;
|
|
310
367
|
},
|
|
@@ -446,13 +503,29 @@ function CanvasBoardContent({ active }: WorkspaceDocumentContentProps) {
|
|
|
446
503
|
// never reach the no-selection seam at all
|
|
447
504
|
// (`workspace-document-registry.ts`'s `WorkspaceDocumentSelection`, and
|
|
448
505
|
// `components/AssetEditorShell.tsx`, which registers the same pair).
|
|
506
|
+
// The layers list: the board's frames through their own adapter. The node stays null so the
|
|
507
|
+
// Inspector keeps describing the chosen frame through the subject published below.
|
|
508
|
+
const boardAdapter = useMemo(
|
|
509
|
+
() =>
|
|
510
|
+
board
|
|
511
|
+
? canvasBoardAdapter(
|
|
512
|
+
board,
|
|
513
|
+
() => presentationRef.current?.activeStoryId() ?? null,
|
|
514
|
+
(storyId) => presentationRef.current?.selectStory(storyId),
|
|
515
|
+
)
|
|
516
|
+
: null,
|
|
517
|
+
[board],
|
|
518
|
+
);
|
|
519
|
+
useEffect(() => {
|
|
520
|
+
boardAdapter?.notify();
|
|
521
|
+
}, [boardAdapter, selectedStoryId]);
|
|
449
522
|
useEffect(() => {
|
|
450
523
|
if (!active) return;
|
|
451
524
|
return registerWorkspaceDocumentSelection(CANVAS_COMPONENTS_DOCUMENT_ID, () => ({
|
|
452
|
-
adapter:
|
|
525
|
+
adapter: boardAdapter,
|
|
453
526
|
nodeId: null,
|
|
454
527
|
}));
|
|
455
|
-
}, [active]);
|
|
528
|
+
}, [active, boardAdapter]);
|
|
456
529
|
useEffect(() => {
|
|
457
530
|
if (!active || !selectedExhibit) return;
|
|
458
531
|
return publishDocumentInspectionSubject(CANVAS_COMPONENTS_DOCUMENT_ID, () => ({
|
|
@@ -479,8 +552,8 @@ function CanvasBoardContent({ active }: WorkspaceDocumentContentProps) {
|
|
|
479
552
|
return (
|
|
480
553
|
<div
|
|
481
554
|
data-testid="canvas-board"
|
|
482
|
-
data-
|
|
483
|
-
data-
|
|
555
|
+
data-volter-backdrop-color={active ? REACT_DESIGN_CANVAS_COLOR : undefined}
|
|
556
|
+
data-volter-backdrop-policy={active ? 'dark-frost' : undefined}
|
|
484
557
|
style={{
|
|
485
558
|
position: 'absolute',
|
|
486
559
|
inset: 0,
|
|
@@ -51,6 +51,6 @@ export const canvasComponentBoard: ComponentBoard = {
|
|
|
51
51
|
`This project has no ${CANVAS_COMPONENTS_TITLE} board (${CANVAS_COMPONENTS_DOCUMENT_ID}): ` +
|
|
52
52
|
"board presence keys on STORIES, not on roots, and none of this project's stories declares " +
|
|
53
53
|
'the "canvas" medium. Declare it — a manifest root entry naming the story\'s module, or a ' +
|
|
54
|
-
'`
|
|
54
|
+
'`volter.adapter.ts` regionInclude that covers it — and the board appears. `volter-game-editor console` ' +
|
|
55
55
|
'names every story it could not place.',
|
|
56
56
|
};
|
|
@@ -120,7 +120,7 @@ function createDesignGame(): GameInternal {
|
|
|
120
120
|
* which part of the unbounded authored plane is visible. */
|
|
121
121
|
function createDesignCanvas(): HTMLCanvasElement {
|
|
122
122
|
const canvas = document.createElement('canvas');
|
|
123
|
-
canvas.dataset['
|
|
123
|
+
canvas.dataset['volterCanvasSceneSurface'] = 'true';
|
|
124
124
|
canvas.style.position = 'absolute';
|
|
125
125
|
canvas.style.inset = '0';
|
|
126
126
|
canvas.style.width = '100%';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* It reads the session's own singletons plus two project files the browser
|
|
6
6
|
* cannot read for itself, and hands the facts to the pure derivation. The
|
|
7
|
-
* report goes out the doors coverage already uses — `
|
|
7
|
+
* report goes out the doors coverage already uses — `volter-game-editor status`'s
|
|
8
8
|
* `projectCoverage` facet, and the editor console via
|
|
9
9
|
* `coverage/session-coverage.ts`, which unions this family with the other two
|
|
10
10
|
* so one headline counts them all. No new transport, no new panel: the console
|
|
@@ -59,7 +59,7 @@ let _filesFor: string | null = null;
|
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Drop the cached file facts so the next read re-asks the dev server. Called
|
|
62
|
-
* on `
|
|
62
|
+
* on `volter-game-editor restart` (a restart is precisely "my project's files changed —
|
|
63
63
|
* re-read them"; the probe-measured failure was the dependency detector
|
|
64
64
|
* grading rows against a package.json edit two restarts old) and by tests.
|
|
65
65
|
*/
|
|
@@ -148,7 +148,7 @@ export function projectVerbFacts(): ProjectVerbFacts | null {
|
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* The `project.*` report for the open project, re-derived on every read — the
|
|
151
|
-
* scripts a project declares change while it is open (`
|
|
151
|
+
* scripts a project declares change while it is open (`volter-game-editor add` writes them),
|
|
152
152
|
* so a cached answer would start lying.
|
|
153
153
|
*/
|
|
154
154
|
export function projectVerbCoverage(): CapabilityCoverageReport | null {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* It reads the session's own singletons and hands the facts to the pure
|
|
6
6
|
* derivation. The report goes out the two doors coverage already uses —
|
|
7
|
-
* `
|
|
7
|
+
* `volter-game-editor status`'s `systemCoverage` facet, and the editor console via
|
|
8
8
|
* `coverage/session-coverage.ts`, which unions this family with the others so
|
|
9
9
|
* one headline counts them all — so an unbound slot becomes a STANDING
|
|
10
10
|
* condition rather than a one-off line. No new transport, no new panel, and no
|
|
@@ -129,8 +129,8 @@ export function nativeSystemCoverage(): CapabilityCoverageReport | null {
|
|
|
129
129
|
// NetworkingAdapter for a single-player game, telling its author to
|
|
130
130
|
// register one because "this project declares a `server` block" when it
|
|
131
131
|
// declares nothing of the sort. A matchmaker is a listener; no port, no
|
|
132
|
-
// server. (`
|
|
133
|
-
// `packages/
|
|
132
|
+
// server. (`volter-game-editor edit`'s auto-boot had the identical bug — see
|
|
133
|
+
// `packages/volter-cli/src/colyseus-autoboot.ts`'s `readDeclaredProcess`.)
|
|
134
134
|
declaresServer: (project.config.configurations ?? []).some(
|
|
135
135
|
(c) => c.kind === 'process' && typeof (c as { port?: unknown }).port === 'number',
|
|
136
136
|
),
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
* Two doors, one derivation — the same shape `ingest/mount-coverage.ts` already
|
|
24
24
|
* uses:
|
|
25
25
|
*
|
|
26
|
-
* - `
|
|
26
|
+
* - `volter-game-editor status`, through {@link rootCoverageFacet}, so an agent reads the
|
|
27
27
|
* same answer without opening the editor;
|
|
28
28
|
* - the editor console, through `coverage/session-coverage.ts`, which unions
|
|
29
29
|
* this family with the game- and project-scoped ones so the warning's
|
|
30
30
|
* headline counts every seam rather than this family's 21. The console
|
|
31
31
|
* ledger turns that into a standing unresolved condition, loud on every
|
|
32
|
-
* `
|
|
32
|
+
* `volter` command until it is closed.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import { measureAdapter } from '../host/adapter-reach';
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*
|
|
25
25
|
* It owns assembly and the console door. Every family still derives itself, in
|
|
26
26
|
* its own file, from its own facts; nothing here decides a verdict. The
|
|
27
|
-
* per-family facets on `
|
|
27
|
+
* per-family facets on `volter-game-editor status` (`command-listener.ts`) stay separate for
|
|
28
28
|
* the same reason they always were — a machine reader wants the rows keyed by
|
|
29
29
|
* subject, and it is the HUMAN-facing sentence that has to be a union.
|
|
30
30
|
*/
|
|
@@ -90,7 +90,7 @@ export function sessionCoverageReport(
|
|
|
90
90
|
*
|
|
91
91
|
* Keyed on the rendered TEXT rather than a mount or project token, because the
|
|
92
92
|
* union's inputs come and go inside one session (a root mounts, a design
|
|
93
|
-
* session attaches, `
|
|
93
|
+
* session attaches, `volter-game-editor add` lands a script) and the honest rule is "say it
|
|
94
94
|
* when the answer changes". A recurrence after a real change is a new condition
|
|
95
95
|
* and gets said again; the server ledger sums repeats of an unchanged one.
|
|
96
96
|
*/
|
|
@@ -73,10 +73,10 @@ function RailColumn({ children }: { readonly children: ReactNode }) {
|
|
|
73
73
|
width: RAIL_WIDTH,
|
|
74
74
|
height: 600,
|
|
75
75
|
display: 'flex',
|
|
76
|
-
border: '1px solid var(--
|
|
76
|
+
border: '1px solid var(--volter-border-2)',
|
|
77
77
|
borderRadius: 8,
|
|
78
78
|
overflow: 'hidden',
|
|
79
|
-
background: 'var(--
|
|
79
|
+
background: 'var(--volter-bg-1)',
|
|
80
80
|
}}
|
|
81
81
|
>
|
|
82
82
|
{children}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* W2c (F13) — Game-tab device preset dropdown. Chrome-device-mode-style:
|
|
3
3
|
* picking a preset constrains the game mount to that device's CSS resolution
|
|
4
4
|
* + DPR (letterboxed/scaled by `GamePanel`), phone/tablet presets add the
|
|
5
|
-
* safe-area overlay + `--
|
|
5
|
+
* safe-area overlay + `--volter-safe-area-inset-*` CSS vars and auto-enable
|
|
6
6
|
* pointer-as-touch. State lives in `../device-preview.ts`; this component is
|
|
7
7
|
* pure UI over it.
|
|
8
8
|
*/
|
|
@@ -30,18 +30,18 @@ export function DevicePresetPicker({
|
|
|
30
30
|
const isFit = active.kind === 'fit';
|
|
31
31
|
|
|
32
32
|
return (
|
|
33
|
-
<div className="
|
|
33
|
+
<div className="volter-playbar-popover-anchor">
|
|
34
34
|
<Button
|
|
35
35
|
ref={ref}
|
|
36
36
|
variant="ghost"
|
|
37
37
|
size="comfortable"
|
|
38
38
|
aria-haspopup="menu"
|
|
39
39
|
aria-expanded={open}
|
|
40
|
-
aria-controls={open ? '
|
|
40
|
+
aria-controls={open ? 'volter-device-preset-menu' : undefined}
|
|
41
41
|
data-testid="device-preset-picker"
|
|
42
42
|
onClick={() => setOpen((value) => !value)}
|
|
43
43
|
title={
|
|
44
|
-
'Device preview — constrain the game to a device resolution + DPR. Phone/tablet presets show the safe area, set --
|
|
44
|
+
'Device preview — constrain the game to a device resolution + DPR. Phone/tablet presets show the safe area, set --volter-safe-area-inset-top/right/bottom/left CSS vars on the game mount, and send mouse input as touch.'
|
|
45
45
|
}
|
|
46
46
|
>
|
|
47
47
|
<EditorIcon icon={faMobileScreenButton} size="md" />
|
|
@@ -49,9 +49,9 @@ export function DevicePresetPicker({
|
|
|
49
49
|
</Button>
|
|
50
50
|
{open && (
|
|
51
51
|
<AnchoredMenu
|
|
52
|
-
id="
|
|
52
|
+
id="volter-device-preset-menu"
|
|
53
53
|
anchorRef={ref}
|
|
54
|
-
className="
|
|
54
|
+
className="volter-resolution-menu"
|
|
55
55
|
gap={4}
|
|
56
56
|
onDismiss={() => setOpen(false)}
|
|
57
57
|
>
|
|
@@ -60,7 +60,7 @@ function GameDebugMenu() {
|
|
|
60
60
|
data-testid="game-debug-menu"
|
|
61
61
|
aria-haspopup="menu"
|
|
62
62
|
aria-expanded={open}
|
|
63
|
-
aria-controls={open ? '
|
|
63
|
+
aria-controls={open ? 'volter-game-debug-menu' : undefined}
|
|
64
64
|
onClick={() => setOpen((value) => !value)}
|
|
65
65
|
>
|
|
66
66
|
<span>Debug</span>
|
|
@@ -68,7 +68,7 @@ function GameDebugMenu() {
|
|
|
68
68
|
</Button>
|
|
69
69
|
{open && (
|
|
70
70
|
<AnchoredMenu
|
|
71
|
-
id="
|
|
71
|
+
id="volter-game-debug-menu"
|
|
72
72
|
anchorRef={ref}
|
|
73
73
|
gap={3}
|
|
74
74
|
onDismiss={() => setOpen(false)}
|
|
@@ -74,7 +74,7 @@ import {
|
|
|
74
74
|
* surfaces (stacked canvases + React DOM layers). */
|
|
75
75
|
export function collectGameSurfaces(container: HTMLElement): Element[] {
|
|
76
76
|
return Array.from(container.children).filter(
|
|
77
|
-
(node) => (node as HTMLElement).dataset?.['
|
|
77
|
+
(node) => (node as HTMLElement).dataset?.['volterRootSurface'] === 'true',
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -250,7 +250,7 @@ function InstanceViewport({ index }: { index: number }) {
|
|
|
250
250
|
focused={mountedId != null && focused === mountedId}
|
|
251
251
|
onFocus={mountedId ? () => setFocusedInstance(mountedId) : undefined}
|
|
252
252
|
>
|
|
253
|
-
<div ref={ref} data-
|
|
253
|
+
<div ref={ref} data-volter-instance-viewport="" style={{ position: 'absolute', inset: 0 }} />
|
|
254
254
|
</InstanceSlot>
|
|
255
255
|
);
|
|
256
256
|
}
|
|
@@ -355,7 +355,7 @@ export function GamePanel({ style }: { style?: React.CSSProperties }) {
|
|
|
355
355
|
// the same call — and switching resolution DURING play would silently drop
|
|
356
356
|
// the running game's canvases with the removed element. Only nodes the game
|
|
357
357
|
// HOST mounted are
|
|
358
|
-
// stashed (world surfaces carry `data-
|
|
358
|
+
// stashed (world surfaces carry `data-volter-root-surface`): the fill/device branches are keyed below so
|
|
359
359
|
// React really remounts, but if it ever aliases the container DOM node
|
|
360
360
|
// again, a blind childNodes copy would capture React-owned children too —
|
|
361
361
|
// that exact aliasing put the incoming inner div in the stash and made
|
|
@@ -412,7 +412,7 @@ export function GamePanel({ style }: { style?: React.CSSProperties }) {
|
|
|
412
412
|
// which consults `deviceEmulatedPixelRatio()` once the session exists.)
|
|
413
413
|
// What size this game is PRESENTED at. Two things can name one and the precedence lives in
|
|
414
414
|
// `resolvePresentedSize`: a device preset the user picked wins, and otherwise the project's own
|
|
415
|
-
// declared `resolution` (`
|
|
415
|
+
// declared `resolution` (`volter.project.json`) puts the game on screen at its authored logical
|
|
416
416
|
// size, uniformly scaled and letterboxed by the branch below — presentation scaling, never a
|
|
417
417
|
// stretch mode. A project that declares neither fills the panel exactly as before.
|
|
418
418
|
const declaredResolution = project?.config.resolution;
|
|
@@ -474,16 +474,16 @@ export function GamePanel({ style }: { style?: React.CSSProperties }) {
|
|
|
474
474
|
|
|
475
475
|
// Safe-area contract: the game MOUNT container always carries the
|
|
476
476
|
// inset CSS vars (0px unless the active preset declares insets), so game
|
|
477
|
-
// UI can style with `var(--
|
|
477
|
+
// UI can style with `var(--volter-safe-area-inset-top, 0px)` etc. Real
|
|
478
478
|
// `env(safe-area-inset-*)` cannot be injected from a same-document host
|
|
479
479
|
// (env() is UA-supplied for the top-level viewport only) — the vars + the
|
|
480
480
|
// visual overlay below ARE the emulation.
|
|
481
481
|
const safeArea = preset.safeArea;
|
|
482
482
|
const safeAreaVars = {
|
|
483
|
-
'--
|
|
484
|
-
'--
|
|
485
|
-
'--
|
|
486
|
-
'--
|
|
483
|
+
'--volter-safe-area-inset-top': `${safeArea?.top ?? 0}px`,
|
|
484
|
+
'--volter-safe-area-inset-right': `${safeArea?.right ?? 0}px`,
|
|
485
|
+
'--volter-safe-area-inset-bottom': `${safeArea?.bottom ?? 0}px`,
|
|
486
|
+
'--volter-safe-area-inset-left': `${safeArea?.left ?? 0}px`,
|
|
487
487
|
} as React.CSSProperties;
|
|
488
488
|
|
|
489
489
|
return (
|