@vgai/engine 0.5.13 → 0.5.14
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/dist/adapter/adapter-module.d.ts +447 -0
- package/dist/adapter/adapter-module.d.ts.map +1 -0
- package/dist/adapter/adapter-module.js +308 -0
- package/dist/adapter/authoring.d.ts +357 -10
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/authoring.js +87 -1
- package/dist/adapter/constraint.d.ts +9 -3
- package/dist/adapter/constraint.d.ts.map +1 -1
- package/dist/adapter/finders/finder-result.d.ts +28 -0
- package/dist/adapter/finders/finder-result.d.ts.map +1 -0
- package/dist/adapter/finders/finder-result.js +10 -0
- package/dist/adapter/finders/index.d.ts +38 -0
- package/dist/adapter/finders/index.d.ts.map +1 -0
- package/dist/adapter/finders/index.js +41 -0
- package/dist/adapter/finders/prefabs-from-stories.d.ts +64 -0
- package/dist/adapter/finders/prefabs-from-stories.d.ts.map +1 -0
- package/dist/adapter/finders/prefabs-from-stories.js +92 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +67 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +226 -0
- package/dist/adapter/first-party-systems.d.ts.map +1 -1
- package/dist/adapter/first-party-systems.js +29 -0
- package/dist/adapter/index.d.ts +7 -3
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +5 -2
- package/dist/adapter/ingest/contract-system-adapters.d.ts +24 -1
- package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
- package/dist/adapter/ingest/contract-system-adapters.js +100 -34
- package/dist/adapter/ingest/game-contract.d.ts +188 -2
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +107 -0
- package/dist/adapter/ingest/mount-readiness.d.ts +84 -0
- package/dist/adapter/ingest/mount-readiness.d.ts.map +1 -0
- package/dist/adapter/ingest/mount-readiness.js +81 -0
- package/dist/adapter/ingest/scene-capture.d.ts +91 -2
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +110 -18
- package/dist/adapter/ingest/structural-ids.d.ts +11 -5
- package/dist/adapter/ingest/structural-ids.d.ts.map +1 -1
- package/dist/adapter/ingest/structural-ids.js +18 -9
- package/dist/adapter/ingest/upstream-pin.d.ts +4 -2
- package/dist/adapter/ingest/upstream-pin.d.ts.map +1 -1
- package/dist/adapter/ingest/upstream-pin.js +4 -2
- package/dist/adapter/ingest/visible-capture-window.d.ts +116 -0
- package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -0
- package/dist/adapter/ingest/visible-capture-window.js +141 -0
- package/dist/adapter/loop-gate-report.d.ts +3 -4
- package/dist/adapter/loop-gate-report.d.ts.map +1 -1
- package/dist/adapter/loop-gate-report.js +3 -4
- package/dist/adapter/root-adapter.d.ts +22 -2
- package/dist/adapter/root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.js +15 -0
- package/dist/adapter/system-adapter.d.ts +161 -2
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/adapter/system-adapter.js +18 -1
- package/dist/animation/camera-ownership.d.ts +4 -2
- package/dist/animation/camera-ownership.d.ts.map +1 -1
- package/dist/animation/camera-ownership.js +58 -1
- package/dist/animation/runtime-inspection.d.ts +70 -0
- package/dist/animation/runtime-inspection.d.ts.map +1 -0
- package/dist/animation/runtime-inspection.js +25 -0
- package/dist/animation/xstate-animation-binding.d.ts +9 -1
- package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
- package/dist/animation/xstate-animation-binding.js +48 -1
- package/dist/asset-formats/material.d.ts +1 -1
- package/dist/asset-formats/mesh.d.ts +1 -1
- package/dist/asset-formats/particles.d.ts +13 -0
- package/dist/asset-formats/particles.d.ts.map +1 -1
- package/dist/asset-formats/particles.js +28 -0
- package/dist/behavior/xstate-inspection.d.ts +24 -0
- package/dist/behavior/xstate-inspection.d.ts.map +1 -0
- package/dist/behavior/xstate-inspection.js +25 -0
- package/dist/core/game-loop.d.ts +7 -5
- package/dist/core/game-loop.d.ts.map +1 -1
- package/dist/core/game-loop.js +39 -18
- package/dist/core/types.d.ts +11 -12
- package/dist/core/types.d.ts.map +1 -1
- package/dist/data/data-asset.js +1 -1
- package/dist/dev/instruments.d.ts.map +1 -1
- package/dist/dev/instruments.js +5 -1
- package/dist/dev/render-debug-adapter.d.ts +19 -2
- package/dist/dev/render-debug-adapter.d.ts.map +1 -1
- package/dist/dev/render-debug-adapter.js +10 -0
- package/dist/ecs/user-data.d.ts +10 -2
- package/dist/ecs/user-data.d.ts.map +1 -1
- package/dist/ecs/user-data.js +6 -2
- package/dist/input/binding-identity.d.ts +10 -0
- package/dist/input/binding-identity.d.ts.map +1 -0
- package/dist/input/binding-identity.js +36 -0
- package/dist/input/input-manager.d.ts +18 -185
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +78 -68
- package/dist/manifest/load.d.ts +14 -0
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/manifest/load.js +6 -1
- package/dist/manifest/schema.d.ts +18 -0
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +64 -7
- package/dist/pixi/authoring.d.ts +27 -0
- package/dist/pixi/authoring.d.ts.map +1 -1
- package/dist/pixi/authoring.js +10 -3
- package/dist/pixi/index.d.ts +1 -1
- package/dist/pixi/index.d.ts.map +1 -1
- package/dist/pixi/index.js +1 -1
- package/dist/pixi/ingest.d.ts +8 -0
- package/dist/pixi/ingest.d.ts.map +1 -1
- package/dist/pixi/ingest.js +10 -1
- package/dist/pixi/render-pass-bracket.d.ts +56 -0
- package/dist/pixi/render-pass-bracket.d.ts.map +1 -0
- package/dist/pixi/render-pass-bracket.js +98 -0
- package/dist/pixi/scene-capture.d.ts +32 -1
- package/dist/pixi/scene-capture.d.ts.map +1 -1
- package/dist/pixi/scene-capture.js +21 -9
- package/dist/pixi/system-adapters.d.ts +29 -0
- package/dist/pixi/system-adapters.d.ts.map +1 -1
- package/dist/pixi/system-adapters.js +27 -0
- package/dist/render/particles-factory.d.ts +16 -2
- package/dist/render/particles-factory.d.ts.map +1 -1
- package/dist/render/particles-factory.js +88 -5
- package/dist/render/soft-particle-depth.d.ts +149 -0
- package/dist/render/soft-particle-depth.d.ts.map +1 -0
- package/dist/render/soft-particle-depth.js +305 -0
- package/dist/render/voxel-surface.d.ts +117 -0
- package/dist/render/voxel-surface.d.ts.map +1 -0
- package/dist/render/voxel-surface.js +263 -0
- package/dist/runtime/create-runtime.js +7 -0
- package/dist/runtime/debug-bridge.d.ts +15 -15
- package/dist/runtime/debug-bridge.d.ts.map +1 -1
- package/dist/runtime/debug-bridge.js +27 -26
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +1 -2
- package/dist/setup/setup-particles.d.ts.map +1 -1
- package/dist/setup/setup-particles.js +9 -0
- package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
- package/dist/world3d-react/r3f-adapter.js +42 -3
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
- package/dist/world3d-react/rapier-physics-bridge.js +201 -11
- package/dist-config/config.js.map +1 -1
- package/package.json +4 -2
- package/schemas/engine-capabilities.json +12 -12
- package/schemas/vgai-project.schema.json +41 -2
- package/src/adapter/adapter-module.ts +607 -0
- package/src/adapter/authoring.ts +440 -10
- package/src/adapter/constraint.ts +10 -3
- package/src/adapter/finders/finder-result.ts +29 -0
- package/src/adapter/finders/index.ts +62 -0
- package/src/adapter/finders/prefabs-from-stories.ts +142 -0
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +304 -0
- package/src/adapter/first-party-systems.ts +30 -0
- package/src/adapter/index.ts +32 -1
- package/src/adapter/ingest/contract-system-adapters.ts +116 -32
- package/src/adapter/ingest/game-contract.ts +265 -2
- package/src/adapter/ingest/mount-readiness.ts +140 -0
- package/src/adapter/ingest/scene-capture.ts +199 -22
- package/src/adapter/ingest/structural-ids.ts +21 -9
- package/src/adapter/ingest/upstream-pin.ts +4 -2
- package/src/adapter/ingest/visible-capture-window.ts +213 -0
- package/src/adapter/loop-gate-report.ts +3 -4
- package/src/adapter/root-adapter.ts +22 -2
- package/src/adapter/setup-three-root-adapter.ts +16 -0
- package/src/adapter/system-adapter.ts +188 -2
- package/src/animation/camera-ownership.ts +68 -3
- package/src/animation/runtime-inspection.ts +104 -0
- package/src/animation/xstate-animation-binding.ts +53 -1
- package/src/asset-formats/particles.ts +31 -0
- package/src/behavior/xstate-inspection.ts +51 -0
- package/src/core/game-loop.ts +39 -17
- package/src/core/types.ts +11 -12
- package/src/data/data-asset.ts +1 -1
- package/src/dev/instruments.ts +5 -1
- package/src/dev/render-debug-adapter.ts +22 -2
- package/src/ecs/user-data.ts +10 -2
- package/src/input/binding-identity.ts +37 -0
- package/src/input/input-manager.ts +93 -68
- package/src/manifest/load.ts +23 -2
- package/src/manifest/schema.ts +76 -7
- package/src/pixi/authoring.ts +37 -5
- package/src/pixi/index.ts +2 -0
- package/src/pixi/ingest.ts +15 -2
- package/src/pixi/render-pass-bracket.ts +117 -0
- package/src/pixi/scene-capture.ts +63 -13
- package/src/pixi/system-adapters.ts +41 -0
- package/src/render/particles-factory.ts +89 -6
- package/src/render/soft-particle-depth.ts +336 -0
- package/src/render/voxel-surface.ts +358 -0
- package/src/runtime/create-runtime.ts +7 -0
- package/src/runtime/debug-bridge.ts +32 -31
- package/src/runtime/game.ts +1 -2
- package/src/setup/setup-particles.ts +9 -0
- package/src/world3d-react/r3f-adapter.tsx +48 -2
- package/src/world3d-react/rapier-physics-bridge.tsx +249 -11
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
import type * as THREE from 'three';
|
|
53
|
+
import {
|
|
54
|
+
startVisibleCaptureWindow,
|
|
55
|
+
type VisibilityClock,
|
|
56
|
+
type VisibleCaptureWindow,
|
|
57
|
+
} from './visible-capture-window';
|
|
53
58
|
|
|
54
59
|
/** A live runtime captured from an external game on its first rendered frame. */
|
|
55
60
|
export interface CapturedRuntime {
|
|
@@ -82,6 +87,78 @@ export interface SceneCaptureOptions {
|
|
|
82
87
|
* so (`packages/editor/src/host-renderers.ts`).
|
|
83
88
|
*/
|
|
84
89
|
isHostRenderer?: (renderer: unknown) => boolean;
|
|
90
|
+
/**
|
|
91
|
+
* The game's OWN declared world, read from its contract
|
|
92
|
+
* (`window.vgaiGame` — the host passes a reader, never a cached value, because
|
|
93
|
+
* the contract is declared by the game's modules and may not exist yet when
|
|
94
|
+
* the trap installs).
|
|
95
|
+
*
|
|
96
|
+
* When it answers non-null, the DECLARATION decides: only a render of that
|
|
97
|
+
* scene is adopted, and first-render-wins never runs. When it answers null —
|
|
98
|
+
* the case for every game that declares nothing — behaviour is unchanged and
|
|
99
|
+
* the adoption is reported as `measured`.
|
|
100
|
+
*/
|
|
101
|
+
declaredScene?: () => unknown;
|
|
102
|
+
/**
|
|
103
|
+
* How the world was adopted, and every DISTINCT world seen afterwards.
|
|
104
|
+
*
|
|
105
|
+
* First-non-host-render-wins is a good measured default and a permanent,
|
|
106
|
+
* SILENT commitment: a splash scene, a shadow pre-pass, or a
|
|
107
|
+
* render-to-texture warm-up that happens to draw first is adopted as the game
|
|
108
|
+
* forever, and the real world that renders one frame later reaches no reader
|
|
109
|
+
* at all. This is that reader. It never changes which world is adopted — it
|
|
110
|
+
* makes the ambiguity a recorded fact (`packages/editor/src/world-adoption.ts`
|
|
111
|
+
* publishes it to `vgai status`).
|
|
112
|
+
*
|
|
113
|
+
* Post-processing games legitimately render several (scene, camera) pairs per
|
|
114
|
+
* frame, so alternates are INFORMATION, never an error. Host renders are
|
|
115
|
+
* excluded by the same `isHostRenderer` declaration the adoption itself uses.
|
|
116
|
+
*/
|
|
117
|
+
onWorldAdoption?: (event: WorldAdoptionEvent) => void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* One world-adoption fact. `adopted` fires exactly once, when the trap commits
|
|
122
|
+
* to a (scene, camera, renderer) triple; `alternate` fires for each DISTINCT
|
|
123
|
+
* triple seen afterwards, up to {@link MAX_RECORDED_ALTERNATES}.
|
|
124
|
+
*/
|
|
125
|
+
export type WorldAdoptionEvent =
|
|
126
|
+
| {
|
|
127
|
+
readonly phase: 'adopted';
|
|
128
|
+
/** `declared` = the contract named this scene; `measured` = first render won. */
|
|
129
|
+
readonly source: 'declared' | 'measured';
|
|
130
|
+
readonly sceneId: string;
|
|
131
|
+
readonly cameraId: string;
|
|
132
|
+
}
|
|
133
|
+
| {
|
|
134
|
+
readonly phase: 'alternate';
|
|
135
|
+
readonly sceneId: string;
|
|
136
|
+
readonly cameraId: string;
|
|
137
|
+
/** `false` ⇒ a SECOND renderer is drawing, which is the stronger signal. */
|
|
138
|
+
readonly sameRenderer: boolean;
|
|
139
|
+
/** Draws observed when this alternate first appeared — how far past the
|
|
140
|
+
* adoption it is, without a wall clock. */
|
|
141
|
+
readonly drawCount: number;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* How many distinct alternates are recorded before the trap stops looking.
|
|
146
|
+
*
|
|
147
|
+
* The bound is the point: this runs inside the game's own render call, and a
|
|
148
|
+
* post-processing chain can present a new (scene, camera) pair every frame. A
|
|
149
|
+
* handful names the ambiguity; an unbounded set would turn a diagnostic into a
|
|
150
|
+
* leak on the hottest path in the process.
|
|
151
|
+
*/
|
|
152
|
+
export const MAX_RECORDED_ALTERNATES = 8;
|
|
153
|
+
|
|
154
|
+
/** A three object's identity, as a string a status facet can carry. `type` is
|
|
155
|
+
* what a reader recognizes ("Scene", "PerspectiveCamera"); `uuid` is what
|
|
156
|
+
* makes two of the same type tellable apart. */
|
|
157
|
+
function objectId(value: unknown): string {
|
|
158
|
+
const obj = value as { type?: unknown; uuid?: unknown } | null | undefined;
|
|
159
|
+
const type = typeof obj?.type === 'string' ? obj.type : 'unknown';
|
|
160
|
+
const uuid = typeof obj?.uuid === 'string' ? obj.uuid : '(no uuid)';
|
|
161
|
+
return `${type}:${uuid}`;
|
|
85
162
|
}
|
|
86
163
|
|
|
87
164
|
/**
|
|
@@ -104,6 +181,28 @@ export interface RenderPassHooks {
|
|
|
104
181
|
after(): void;
|
|
105
182
|
}
|
|
106
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Options for {@link SceneCaptureHandle.waitForCapture}. Passing a bare number
|
|
186
|
+
* is the same as `{ timeoutMs }` — the shape every existing caller uses.
|
|
187
|
+
*/
|
|
188
|
+
export interface CaptureWaitOptions {
|
|
189
|
+
/** The capture window, in VISIBLE milliseconds (default 10s). */
|
|
190
|
+
timeoutMs?: number | undefined;
|
|
191
|
+
/** Injected in tests; defaults to the document's own visibility. */
|
|
192
|
+
visibility?: VisibilityClock | undefined;
|
|
193
|
+
/**
|
|
194
|
+
* Called with a LIVE view of the wait when it begins, and with `null` the
|
|
195
|
+
* moment it ends (captured, expired, or the window was cancelled).
|
|
196
|
+
*
|
|
197
|
+
* A wait parked on a hidden tab is otherwise indistinguishable from a hung
|
|
198
|
+
* mount: nothing renders, nothing fails, and every door reports silence.
|
|
199
|
+
* This is the seam the editor publishes to `vgai status` so the answer is
|
|
200
|
+
* "waiting for the first visible frame — the tab is hidden", not a
|
|
201
|
+
* countdown that is not running.
|
|
202
|
+
*/
|
|
203
|
+
onWait?: ((wait: VisibleCaptureWindow | null) => void) | undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
107
206
|
/** Handle returned by {@link installSceneCapture}. */
|
|
108
207
|
export interface SceneCaptureHandle {
|
|
109
208
|
/** The captured runtime, or null until the game renders its first frame. */
|
|
@@ -116,8 +215,17 @@ export interface SceneCaptureHandle {
|
|
|
116
215
|
* renders come through the same trap and are not the game's frame.
|
|
117
216
|
*/
|
|
118
217
|
setRenderPassHooks(hooks: RenderPassHooks | null): void;
|
|
119
|
-
/**
|
|
120
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Resolve once a scene+camera is captured.
|
|
220
|
+
*
|
|
221
|
+
* The timeout is a budget of **visible** time, not wall-clock time: a hidden
|
|
222
|
+
* document cannot render (the browser parks rAF), so counting hidden time
|
|
223
|
+
* against the game is counting time it was not allowed to use. The wait
|
|
224
|
+
* therefore PARKS while `document.hidden` and resumes on `visibilitychange`
|
|
225
|
+
* — see `visible-capture-window.ts` for the whole argument. Rejects only
|
|
226
|
+
* when the window is spent with the document VISIBLE.
|
|
227
|
+
*/
|
|
228
|
+
waitForCapture(options?: number | CaptureWaitOptions): Promise<CapturedRuntime>;
|
|
121
229
|
/** Total `render()` calls observed through the trap (a liveness signal). */
|
|
122
230
|
getDrawCount(): number;
|
|
123
231
|
/** The game's last (non-null) `setAnimationLoop` callback for a renderer, so the
|
|
@@ -184,6 +292,14 @@ export function installSceneCapture(
|
|
|
184
292
|
|
|
185
293
|
let captured: CapturedRuntime | null = null;
|
|
186
294
|
let drawCount = 0;
|
|
295
|
+
/** Memoized answer of `opts.declaredScene` — asked each render until it
|
|
296
|
+
* answers, because a game declares its contract from its own modules and may
|
|
297
|
+
* not have run yet when the trap installs. Once it answers, it is fixed:
|
|
298
|
+
* a declaration that changes mid-boot is not a thing the host chases. */
|
|
299
|
+
let declaredScene: unknown = null;
|
|
300
|
+
/** Distinct alternates already reported, keyed by the same triple identity the
|
|
301
|
+
* event carries — see {@link MAX_RECORDED_ALTERNATES} for why it is bounded. */
|
|
302
|
+
const seenAlternates = new Set<string>();
|
|
187
303
|
/** Set post-capture by the host; see {@link SceneCaptureHandle.setRenderPassHooks}. */
|
|
188
304
|
let renderPassHooks: RenderPassHooks | null = null;
|
|
189
305
|
const waiters: Array<(rt: CapturedRuntime) => void> = [];
|
|
@@ -240,6 +356,55 @@ export function installSceneCapture(
|
|
|
240
356
|
}
|
|
241
357
|
}
|
|
242
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Commit to a world, DECLARATION FIRST.
|
|
361
|
+
*
|
|
362
|
+
* First-non-host-render-wins is the measured default and a permanent one, so
|
|
363
|
+
* a game that states which scene is its world is not made to race its own
|
|
364
|
+
* splash screen. `declaredScene` is asked until it answers (the game's own
|
|
365
|
+
* modules declare the contract, and may not have run when the trap installed);
|
|
366
|
+
* once it does, only that scene is adopted.
|
|
367
|
+
*/
|
|
368
|
+
function adoptWorld(self: unknown, scene: unknown, camera: unknown): void {
|
|
369
|
+
if (declaredScene === null) declaredScene = opts?.declaredScene?.() ?? null;
|
|
370
|
+
if (declaredScene !== null && declaredScene !== scene) return;
|
|
371
|
+
captured = {
|
|
372
|
+
scene: scene as THREE.Scene,
|
|
373
|
+
camera: camera as THREE.Camera,
|
|
374
|
+
renderer: self as THREE.WebGLRenderer,
|
|
375
|
+
};
|
|
376
|
+
opts?.onWorldAdoption?.({
|
|
377
|
+
phase: 'adopted',
|
|
378
|
+
source: declaredScene === null ? 'measured' : 'declared',
|
|
379
|
+
sceneId: objectId(scene),
|
|
380
|
+
cameraId: objectId(camera),
|
|
381
|
+
});
|
|
382
|
+
for (const resolve of waiters.splice(0)) resolve(captured);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* A DISTINCT world drew after the adopted one. Recorded, never acted on:
|
|
387
|
+
* which world is adopted does not change (that would break every handle the
|
|
388
|
+
* host already built from it) — but the reader stops being the only party who
|
|
389
|
+
* could have noticed. Deduped and bounded because this is the game's own
|
|
390
|
+
* render call.
|
|
391
|
+
*/
|
|
392
|
+
function recordAlternateWorld(self: unknown, scene: unknown, camera: unknown): void {
|
|
393
|
+
if (!captured || seenAlternates.size >= MAX_RECORDED_ALTERNATES) return;
|
|
394
|
+
if (scene === captured.scene && camera === captured.camera) return;
|
|
395
|
+
const sameRenderer = self === captured.renderer;
|
|
396
|
+
const key = `${objectId(scene)}|${objectId(camera)}|${sameRenderer}`;
|
|
397
|
+
if (seenAlternates.has(key)) return;
|
|
398
|
+
seenAlternates.add(key);
|
|
399
|
+
opts?.onWorldAdoption?.({
|
|
400
|
+
phase: 'alternate',
|
|
401
|
+
sceneId: objectId(scene),
|
|
402
|
+
cameraId: objectId(camera),
|
|
403
|
+
sameRenderer,
|
|
404
|
+
drawCount,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
243
408
|
Object.defineProperty(proto, 'render', {
|
|
244
409
|
configurable: true,
|
|
245
410
|
set(this: Record<symbol, unknown>, fn: unknown) {
|
|
@@ -269,14 +434,9 @@ export function installSceneCapture(
|
|
|
269
434
|
// Whether that happened at all came down to whether the editor
|
|
270
435
|
// rendered a frame inside the game's capture window, so the same
|
|
271
436
|
// session could pass or fail on timing alone.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
scene: scene as THREE.Scene,
|
|
276
|
-
camera: camera as THREE.Camera,
|
|
277
|
-
renderer: self as unknown as THREE.WebGLRenderer,
|
|
278
|
-
};
|
|
279
|
-
for (const resolve of waiters.splice(0)) resolve(captured);
|
|
437
|
+
if (opts?.isHostRenderer?.(self) !== true && (scene as { isScene?: boolean })?.isScene) {
|
|
438
|
+
if (captured) recordAlternateWorld(self, scene, camera);
|
|
439
|
+
else adoptWorld(self, scene, camera);
|
|
280
440
|
}
|
|
281
441
|
return forwardRender(self, args);
|
|
282
442
|
};
|
|
@@ -340,24 +500,41 @@ export function installSceneCapture(
|
|
|
340
500
|
composer.setPixelRatio(pixelRatio);
|
|
341
501
|
}
|
|
342
502
|
},
|
|
343
|
-
waitForCapture(
|
|
503
|
+
waitForCapture(options) {
|
|
504
|
+
const opts: CaptureWaitOptions =
|
|
505
|
+
typeof options === 'number' ? { timeoutMs: options } : (options ?? {});
|
|
506
|
+
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
344
507
|
if (captured) return Promise.resolve(captured);
|
|
345
508
|
return new Promise<CapturedRuntime>((resolve, reject) => {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
)
|
|
355
|
-
|
|
509
|
+
// A budget of VISIBLE time. The window disarms itself while the
|
|
510
|
+
// document is hidden and resumes when it comes back, so a tab that
|
|
511
|
+
// boots in the background waits instead of dying — and the trap it is
|
|
512
|
+
// waiting on stays installed the whole time, which is what makes the
|
|
513
|
+
// first frame after foregrounding a capture rather than a retry.
|
|
514
|
+
const captureWindow = startVisibleCaptureWindow({
|
|
515
|
+
budgetMs: timeoutMs,
|
|
516
|
+
...(opts.visibility !== undefined ? { clock: opts.visibility } : {}),
|
|
517
|
+
onExpire: () => {
|
|
518
|
+
const i = waiters.indexOf(wrapped);
|
|
519
|
+
if (i >= 0) waiters.splice(i, 1);
|
|
520
|
+
opts.onWait?.(null);
|
|
521
|
+
reject(
|
|
522
|
+
new Error(
|
|
523
|
+
`Scene capture timed out after ${timeoutMs}ms of VISIBLE time ` +
|
|
524
|
+
`(${Math.round(captureWindow.elapsedHiddenMs())}ms hidden, which is not counted — ` +
|
|
525
|
+
'a hidden tab cannot render) — the game never rendered, ' +
|
|
526
|
+
'or it bundles its own (un-shared) copy of three.',
|
|
527
|
+
),
|
|
528
|
+
);
|
|
529
|
+
},
|
|
530
|
+
});
|
|
356
531
|
const wrapped = (rt: CapturedRuntime) => {
|
|
357
|
-
|
|
532
|
+
captureWindow.cancel();
|
|
533
|
+
opts.onWait?.(null);
|
|
358
534
|
resolve(rt);
|
|
359
535
|
};
|
|
360
536
|
waiters.push(wrapped);
|
|
537
|
+
opts.onWait?.(captureWindow);
|
|
361
538
|
});
|
|
362
539
|
},
|
|
363
540
|
uninstall() {
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* reflection helper that rides along with it) the editor's live-three authoring
|
|
4
4
|
* adapter uses to address the objects of a world whose source carries no
|
|
5
5
|
* serve-time identity stamps
|
|
6
|
-
* (`packages/editor/src/
|
|
6
|
+
* (`packages/editor/src/projection/three.ts`, `structuralIdentity`).
|
|
7
7
|
*
|
|
8
8
|
* Identity: each object gets a **structural-path id** — deterministic from the
|
|
9
9
|
* scene's shape (position in the tree + three.js type + name), so the SAME id
|
|
10
10
|
* re-binds to the SAME object after the game rebuilds its scene within a
|
|
11
|
-
* session.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* session. The pure walk returns that identity beside the native objects; the
|
|
12
|
+
* editor keeps the reverse lookup in its authoring adapter rather than writing
|
|
13
|
+
* editor currency into a foreign graph.
|
|
14
14
|
*
|
|
15
15
|
* This module used to be the shared core of a per-game JSON sidecar
|
|
16
16
|
* persistence system, which was deleted outright (2026-08-02) — ingest edits
|
|
@@ -36,7 +36,7 @@ const EDITOR_ONLY_LAYER_MASK = 1 << 31;
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Editor furniture, not game content. Excluded from the walk entirely — see
|
|
39
|
-
* {@link
|
|
39
|
+
* {@link collectStructuralIds} for why that exclusion is what makes the id
|
|
40
40
|
* scheme's central promise true.
|
|
41
41
|
*/
|
|
42
42
|
function isEditorOnly(o: THREE.Object3D): boolean {
|
|
@@ -67,7 +67,7 @@ export interface StructuralIdWalk {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* (Re)
|
|
70
|
+
* (Re)collect structural-path ids for every object under `scene` (and, if
|
|
71
71
|
* given, the separately-captured render `camera`, under the fixed
|
|
72
72
|
* {@link CAMERA_ID}). Idempotent and deterministic from scene structure
|
|
73
73
|
* (position + type + name) — so a re-walk after the game rebuilds part of its
|
|
@@ -89,7 +89,7 @@ export interface StructuralIdWalk {
|
|
|
89
89
|
* hierarchy/inspector projections built on this walk, where they were
|
|
90
90
|
* selectable and colorable as if they were game content.
|
|
91
91
|
*/
|
|
92
|
-
export function
|
|
92
|
+
export function collectStructuralIds(
|
|
93
93
|
scene: THREE.Object3D,
|
|
94
94
|
camera?: THREE.Object3D | undefined,
|
|
95
95
|
): StructuralIdWalk {
|
|
@@ -102,7 +102,6 @@ export function assignStructuralIds(
|
|
|
102
102
|
path: string,
|
|
103
103
|
): void => {
|
|
104
104
|
const id = `ingest:${path}:${o.type}:${o.name || ''}`;
|
|
105
|
-
setUserData(o, 'entityId', id);
|
|
106
105
|
byId.set(id, o);
|
|
107
106
|
count++;
|
|
108
107
|
if (o.isLight) lights++;
|
|
@@ -119,9 +118,22 @@ export function assignStructuralIds(
|
|
|
119
118
|
};
|
|
120
119
|
visitChildren(scene, '');
|
|
121
120
|
if (camera) {
|
|
122
|
-
setUserData(camera, 'entityId', CAMERA_ID);
|
|
123
121
|
byId.set(CAMERA_ID, camera);
|
|
124
122
|
count++;
|
|
125
123
|
}
|
|
126
124
|
return { byId, count, meshes, lights };
|
|
127
125
|
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Compatibility entry point for first-party callers that explicitly want
|
|
129
|
+
* structural ids stamped into their own graph. Foreign ingest authoring uses
|
|
130
|
+
* {@link collectStructuralIds} and never calls this mutating form.
|
|
131
|
+
*/
|
|
132
|
+
export function assignStructuralIds(
|
|
133
|
+
scene: THREE.Object3D,
|
|
134
|
+
camera?: THREE.Object3D | undefined,
|
|
135
|
+
): StructuralIdWalk {
|
|
136
|
+
const walk = collectStructuralIds(scene, camera);
|
|
137
|
+
for (const [id, object] of walk.byId) setUserData(object, 'entityId', id);
|
|
138
|
+
return walk;
|
|
139
|
+
}
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
* - Commit: **`6339493130b4b1d4d28f0f52d17b2fba738c7d47`**
|
|
28
28
|
*
|
|
29
29
|
* i.e. a Markdown bullet whose label is `Commit:` and whose value is a bold,
|
|
30
|
-
* backtick-code full (or abbreviated) git SHA. Every vendored game in
|
|
31
|
-
* repo (`tanks`, `
|
|
30
|
+
* backtick-code full (or abbreviated) git SHA. Every BUNDLE-vendored game in
|
|
31
|
+
* this repo (`tanks`, `simcity`) follows this exact shape. Source-tree
|
|
32
|
+
* vendored games carry their pin in `vendor/games/<id>.UPSTREAM.lock`'s
|
|
33
|
+
* machine-readable `commit` instead and never reach this parser.
|
|
32
34
|
*/
|
|
33
35
|
|
|
34
36
|
/** Matches `- Commit: **\`<sha>\`**` (any amount of internal whitespace, case-insensitive hex). */
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capture window is a budget of VISIBLE time, not of wall-clock time.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS MODULE EXISTS. An ingest mount waits for the game to render its
|
|
5
|
+
* first frame and fails by name when that never happens
|
|
6
|
+
* (`scene-capture.ts`'s `waitForCapture`). That wait used to be a plain
|
|
7
|
+
* `setTimeout`, i.e. a wall-clock deadline started AT BOOT — and a game cannot
|
|
8
|
+
* render while its tab is hidden, because the browser parks `requestAnimation
|
|
9
|
+
* Frame` for a backgrounded document. The two facts together make one
|
|
10
|
+
* deterministic failure: a tab that opens in the BACKGROUND (the normal human
|
|
11
|
+
* path — `vgai edit` auto-opens a tab that routinely lands behind the current
|
|
12
|
+
* window) burns its whole capture window unable to draw, the deadline fires,
|
|
13
|
+
* the mount dies terminally, and foregrounding the tab later changes nothing.
|
|
14
|
+
* Agents, whose tabs happen to be visible, never saw it.
|
|
15
|
+
*
|
|
16
|
+
* So the clock only runs while the document is VISIBLE. Hidden time is not
|
|
17
|
+
* spent, and while a tab is hidden the wait PARKS rather than expiring: the
|
|
18
|
+
* capture trap stays installed, the game's modules stay live, and the first
|
|
19
|
+
* frame the tab draws after the human brings it forward is trapped exactly as
|
|
20
|
+
* it would have been at boot. That is the retry — no second mount, no
|
|
21
|
+
* re-running `load()` (which would re-construct module-level state: the
|
|
22
|
+
* ARCHITECTURE-CORE "LOADING CONSTRUCTS, once" contract), and no forcing of a
|
|
23
|
+
* frame: the game's own loop resumes on its own when the browser un-parks it.
|
|
24
|
+
*
|
|
25
|
+
* There is deliberately no wall-clock cap on the parked state. An unspent
|
|
26
|
+
* budget is not a failure — a human returning to the tab is what spends it —
|
|
27
|
+
* and a cap would be exactly the boot-time clock this module exists to
|
|
28
|
+
* remove. The parked wait is reported instead of being silent: see
|
|
29
|
+
* `CaptureWaitObserver` in `scene-capture.ts` and `vgai status`'s
|
|
30
|
+
* `ingestCaptureWait`.
|
|
31
|
+
*
|
|
32
|
+
* The state machine is a pure function of (banked segments, now, hidden) so
|
|
33
|
+
* the whole accrual rule is testable with no browser and no timers
|
|
34
|
+
* (`packages/engine/test/visible-capture-window.test.ts`); the runtime half
|
|
35
|
+
* below is only the timer arm/disarm around it.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Everything the budget reads about the outside world. Injected so the pure
|
|
40
|
+
* core stays pure and the runtime half is drivable by a fake in tests — and,
|
|
41
|
+
* as a side effect, so this module never hard-depends on `document` existing
|
|
42
|
+
* (unit runners, SSR).
|
|
43
|
+
*/
|
|
44
|
+
export interface VisibilityClock {
|
|
45
|
+
/** Milliseconds, monotonic-ish; `performance.now()`/`Date.now()` both fit. */
|
|
46
|
+
now(): number;
|
|
47
|
+
/** True while the document is hidden (no rAF, so no frame can be captured). */
|
|
48
|
+
hidden(): boolean;
|
|
49
|
+
/** Subscribe to visibility transitions; returns the unsubscribe. */
|
|
50
|
+
subscribe(onChange: () => void): () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The real one: `document.visibilityState` + `visibilitychange`. Falls back
|
|
54
|
+
* to permanently-visible where there is no `document` at all, which is the
|
|
55
|
+
* honest answer for a headless caller — it has no tab to background. */
|
|
56
|
+
export function documentVisibilityClock(): VisibilityClock {
|
|
57
|
+
const doc = typeof document === 'undefined' ? null : document;
|
|
58
|
+
return {
|
|
59
|
+
now: () => (typeof performance === 'undefined' ? Date.now() : performance.now()),
|
|
60
|
+
hidden: () => doc?.hidden === true,
|
|
61
|
+
subscribe(onChange) {
|
|
62
|
+
if (!doc) return () => {};
|
|
63
|
+
doc.addEventListener('visibilitychange', onChange);
|
|
64
|
+
return () => doc.removeEventListener('visibilitychange', onChange);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The banked halves of the wait plus the segment currently running. Immutable:
|
|
71
|
+
* every transition returns a new value, so a caller can hold one and compare.
|
|
72
|
+
*/
|
|
73
|
+
export interface VisibleBudgetState {
|
|
74
|
+
/** The window, in VISIBLE milliseconds. */
|
|
75
|
+
readonly budgetMs: number;
|
|
76
|
+
/** Visible time banked from completed segments. */
|
|
77
|
+
readonly visibleMs: number;
|
|
78
|
+
/** Hidden time banked from completed segments (reported, never spent). */
|
|
79
|
+
readonly hiddenMs: number;
|
|
80
|
+
/** When the current segment started. */
|
|
81
|
+
readonly since: number;
|
|
82
|
+
/** Whether the current segment is a hidden one. */
|
|
83
|
+
readonly hidden: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Open the window at `now`, in whichever visibility the document is in. */
|
|
87
|
+
export function beginVisibleBudget(
|
|
88
|
+
budgetMs: number,
|
|
89
|
+
now: number,
|
|
90
|
+
hidden: boolean,
|
|
91
|
+
): VisibleBudgetState {
|
|
92
|
+
return { budgetMs, visibleMs: 0, hiddenMs: 0, since: now, hidden };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Apply the document's current visibility at `now`: bank the segment that just
|
|
97
|
+
* ended into its own bucket and start the next one. A call that does not change
|
|
98
|
+
* visibility is a no-op *by value* (same accrual, same segment start), so a
|
|
99
|
+
* duplicate `visibilitychange` can never bank a zero-length segment twice or
|
|
100
|
+
* restart the clock.
|
|
101
|
+
*/
|
|
102
|
+
export function applyVisibility(
|
|
103
|
+
state: VisibleBudgetState,
|
|
104
|
+
now: number,
|
|
105
|
+
hidden: boolean,
|
|
106
|
+
): VisibleBudgetState {
|
|
107
|
+
if (hidden === state.hidden) return state;
|
|
108
|
+
const elapsed = Math.max(0, now - state.since);
|
|
109
|
+
return {
|
|
110
|
+
budgetMs: state.budgetMs,
|
|
111
|
+
visibleMs: state.hidden ? state.visibleMs : state.visibleMs + elapsed,
|
|
112
|
+
hiddenMs: state.hidden ? state.hiddenMs + elapsed : state.hiddenMs,
|
|
113
|
+
since: now,
|
|
114
|
+
hidden,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Visible time spent so far, including the segment in flight. */
|
|
119
|
+
export function visibleElapsedMs(state: VisibleBudgetState, now: number): number {
|
|
120
|
+
return state.visibleMs + (state.hidden ? 0 : Math.max(0, now - state.since));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Hidden time so far, including the segment in flight. Never spent — it is
|
|
124
|
+
* reported so a failure message can say what the window did NOT count. */
|
|
125
|
+
export function hiddenElapsedMs(state: VisibleBudgetState, now: number): number {
|
|
126
|
+
return state.hiddenMs + (state.hidden ? Math.max(0, now - state.since) : 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Visible time left in the window; `0` once it is spent. */
|
|
130
|
+
export function visibleRemainingMs(state: VisibleBudgetState, now: number): number {
|
|
131
|
+
return Math.max(0, state.budgetMs - visibleElapsedMs(state, now));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A live view of one running window, for the status wire and the failure message. */
|
|
135
|
+
export interface VisibleCaptureWindow {
|
|
136
|
+
/** The window's size, in visible milliseconds. */
|
|
137
|
+
readonly budgetMs: number;
|
|
138
|
+
/** Visible milliseconds spent so far. */
|
|
139
|
+
elapsedVisibleMs(): number;
|
|
140
|
+
/** Milliseconds this window has spent parked while hidden (not counted). */
|
|
141
|
+
elapsedHiddenMs(): number;
|
|
142
|
+
/** Whether the wait is parked right now because the document is hidden. */
|
|
143
|
+
isHidden(): boolean;
|
|
144
|
+
/** Stop the timer and drop the visibility listener. Idempotent. */
|
|
145
|
+
cancel(): void;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Start a window that calls `onExpire` after `budgetMs` of VISIBLE time.
|
|
150
|
+
* While the document is hidden the timer is disarmed entirely (so a throttled
|
|
151
|
+
* background timer cannot fire it late either) and re-armed with the remaining
|
|
152
|
+
* budget when the document comes back.
|
|
153
|
+
*/
|
|
154
|
+
export function startVisibleCaptureWindow(opts: {
|
|
155
|
+
budgetMs: number;
|
|
156
|
+
onExpire: () => void;
|
|
157
|
+
/** Defaults to the document's own visibility. */
|
|
158
|
+
clock?: VisibilityClock;
|
|
159
|
+
/** Defaults to `setTimeout`/`clearTimeout`. */
|
|
160
|
+
setTimer?: (fn: () => void, ms: number) => unknown;
|
|
161
|
+
clearTimer?: (handle: unknown) => void;
|
|
162
|
+
}): VisibleCaptureWindow {
|
|
163
|
+
const clock = opts.clock ?? documentVisibilityClock();
|
|
164
|
+
const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
165
|
+
const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>));
|
|
166
|
+
|
|
167
|
+
let state = beginVisibleBudget(opts.budgetMs, clock.now(), clock.hidden());
|
|
168
|
+
let timer: unknown = null;
|
|
169
|
+
let done = false;
|
|
170
|
+
|
|
171
|
+
function disarm(): void {
|
|
172
|
+
if (timer !== null) {
|
|
173
|
+
clearTimer(timer);
|
|
174
|
+
timer = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function arm(): void {
|
|
179
|
+
disarm();
|
|
180
|
+
if (done || state.hidden) return;
|
|
181
|
+
timer = setTimer(
|
|
182
|
+
() => {
|
|
183
|
+
timer = null;
|
|
184
|
+
if (done) return;
|
|
185
|
+
done = true;
|
|
186
|
+
unsubscribe();
|
|
187
|
+
opts.onExpire();
|
|
188
|
+
},
|
|
189
|
+
visibleRemainingMs(state, clock.now()),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const unsubscribe = clock.subscribe(() => {
|
|
194
|
+
if (done) return;
|
|
195
|
+
state = applyVisibility(state, clock.now(), clock.hidden());
|
|
196
|
+
arm();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
arm();
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
budgetMs: opts.budgetMs,
|
|
203
|
+
elapsedVisibleMs: () => visibleElapsedMs(state, clock.now()),
|
|
204
|
+
elapsedHiddenMs: () => hiddenElapsedMs(state, clock.now()),
|
|
205
|
+
isHidden: () => state.hidden,
|
|
206
|
+
cancel() {
|
|
207
|
+
if (done) return;
|
|
208
|
+
done = true;
|
|
209
|
+
disarm();
|
|
210
|
+
unsubscribe();
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Capability-gate shortfall reporting (D10, T7.6) — the honesty half of the
|
|
3
|
-
* play-control contract. §(d) draws the `loop` axis (`gated | self-driven`)
|
|
4
|
-
*
|
|
3
|
+
* play-control contract. §(d) draws the `loop` axis (`gated | self-driven`) on
|
|
4
|
+
* its own: a `self-driven` world (an ingested game
|
|
5
5
|
* driving its own rAF/ticker) pauses for real ONLY if its adapter implements an
|
|
6
6
|
* explicit loop-gate capability (`MountedRootBase.setPaused`/`step`) —
|
|
7
7
|
* otherwise it "honestly reports so" rather than a silent no-op (the
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
* has the identical shape: absent or unable to silence a world's audio on pause
|
|
11
11
|
* must report, not pretend.
|
|
12
12
|
*
|
|
13
|
-
* Pure compute + message-formatting
|
|
14
|
-
* (`packages/editor/src/achieved-tier.ts`) — callers own the actual
|
|
13
|
+
* Pure compute + message-formatting — callers own the actual
|
|
15
14
|
* `console.warn`/`editorConsole.warn` call; this module never logs.
|
|
16
15
|
*/
|
|
17
16
|
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import type { Container } from 'pixi.js';
|
|
27
27
|
import type * as THREE from 'three';
|
|
28
|
+
import type { WorldRendererConfig } from '../world3d-react/renderer-config';
|
|
28
29
|
import type { AdapterSurface } from './adapter-surface';
|
|
29
30
|
import type { AuthoringAdapter } from './authoring';
|
|
30
31
|
import type { HostContextFor } from './host-context';
|
|
@@ -78,8 +79,8 @@ export interface MountedRootBase {
|
|
|
78
79
|
* not a promise: implement `setPaused`/`step` only where the game exposes
|
|
79
80
|
* a sanctioned pause/step mechanism. Gating a raw-rAF loop from outside
|
|
80
81
|
* was demonstrated and REJECTED (D5 — it halts the loop rather than
|
|
81
|
-
* pausing it)
|
|
82
|
-
*
|
|
82
|
+
* pausing it). Where `setPaused` is absent the host reports the
|
|
83
|
+
* shortfall loudly, naming the mechanism that would close it (T7.6).
|
|
83
84
|
*/
|
|
84
85
|
readonly drivesOwnLoop: boolean;
|
|
85
86
|
|
|
@@ -112,6 +113,25 @@ export interface MountedThreeRoot extends MountedRootBase {
|
|
|
112
113
|
/** The live scene + camera the editor inspects/renders for authoring. */
|
|
113
114
|
readonly scene: THREE.Scene;
|
|
114
115
|
readonly camera: THREE.Camera;
|
|
116
|
+
/**
|
|
117
|
+
* The colour pipeline this world was authored for, REPORTED rather than
|
|
118
|
+
* applied — the adapter has already applied it to the renderer its own host
|
|
119
|
+
* handed it (`world3d-react/renderer-config.ts`).
|
|
120
|
+
*
|
|
121
|
+
* It is here because the host that MOUNTS a world is not always the host
|
|
122
|
+
* that DRAWS it. The editor's design session mounts against a
|
|
123
|
+
* non-rasterizing renderer on purpose and then draws the mounted scene with
|
|
124
|
+
* the editor viewport's own `WebGLRenderer`, so the world's declaration
|
|
125
|
+
* lands on a surface with no pixels while the surface with pixels never
|
|
126
|
+
* hears it. Reporting the declaration is what lets that host apply the same
|
|
127
|
+
* config, through the same `applyWorldRendererConfig`, to the renderer that
|
|
128
|
+
* actually produces the frame.
|
|
129
|
+
*
|
|
130
|
+
* Absent means the world declared nothing and the drawing host should leave
|
|
131
|
+
* its own configuration alone — the same "absent = don't touch" rule the
|
|
132
|
+
* config's own fields follow.
|
|
133
|
+
*/
|
|
134
|
+
readonly rendererConfig?: WorldRendererConfig | undefined;
|
|
115
135
|
}
|
|
116
136
|
|
|
117
137
|
/** A live, mounted canvas world (T7.3) — the pixi analog of
|