@vgai/editor-sdk 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/package.json +2 -2
- package/src/client.ts +79 -29
- package/src/contributions.ts +30 -2
- package/src/editor-view.ts +3 -10
- package/src/index.ts +6 -0
- package/src/types.ts +194 -19
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@vgai/editor-sdk",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.14",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@types/three": "^0.180.0",
|
|
26
|
-
"@vgai/sdk": "0.5.
|
|
26
|
+
"@vgai/sdk": "0.5.14"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"@vgai/engine": "*",
|
package/src/client.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { GenerationJobsDocument } from '@vgai/sdk/generations';
|
|
|
2
2
|
import type { DocumentProbeResult, DocumentProbeStep } from './document-probe.js';
|
|
3
3
|
import type {
|
|
4
4
|
ActiveDocumentCapture,
|
|
5
|
+
AnimationCaptureAction,
|
|
5
6
|
AssetCompareCapture,
|
|
6
7
|
AssetCompareOptions,
|
|
7
8
|
AssetKind,
|
|
@@ -12,7 +13,11 @@ import type {
|
|
|
12
13
|
EditorState,
|
|
13
14
|
EditorView,
|
|
14
15
|
GameCapture,
|
|
16
|
+
GameplayRecordingCapture,
|
|
17
|
+
GameplayRecordingOptions,
|
|
18
|
+
GameplayRecordingStarted,
|
|
15
19
|
HelperVisibility,
|
|
20
|
+
InspectedFieldWrite,
|
|
16
21
|
InspectedHierarchy,
|
|
17
22
|
InspectedInspection,
|
|
18
23
|
LabeledShotSetCapture,
|
|
@@ -46,7 +51,7 @@ const DEFAULT_URL = 'http://127.0.0.1:20173';
|
|
|
46
51
|
* `/__editor/command` has always answered `{ ok: false, error, code }` and
|
|
47
52
|
* this client has always dropped the `code` on the floor, so every caller that
|
|
48
53
|
* wanted to react to a specific refusal had to substring-match an English
|
|
49
|
-
* sentence. `vgai screenshot`'s
|
|
54
|
+
* sentence. `vgai screenshot`'s loop-recovery fallback is the first caller that
|
|
50
55
|
* genuinely must branch (`BRIDGE_SCREENSHOT_STALE` has a working recovery;
|
|
51
56
|
* "not in play mode" does not), and a fallback keyed on prose would fire on
|
|
52
57
|
* the wrong failure the first time someone rewords the message.
|
|
@@ -148,19 +153,36 @@ export class EditorClient {
|
|
|
148
153
|
*/
|
|
149
154
|
readonly game: GameDebugDoor;
|
|
150
155
|
|
|
151
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Called with the raw body of EVERY response this client receives — command
|
|
158
|
+
* envelopes and `/__editor/state` alike, on success AND on refusal.
|
|
159
|
+
*
|
|
160
|
+
* It exists for exactly one contract: the server stamps `unresolvedConsole`
|
|
161
|
+
* onto every envelope (`server-utils.ts`'s `commandResponseFor`), and the CLI
|
|
162
|
+
* has to see those counts to be loud about them. Routing that through a
|
|
163
|
+
* single observer here — rather than teaching each of the CLI's output sites
|
|
164
|
+
* to unpack a response — is what keeps the loudness contract ONE mechanism.
|
|
165
|
+
* The observer must not throw; anything it raises is swallowed, because a
|
|
166
|
+
* reporting hook may never break the command it is reporting on.
|
|
167
|
+
*/
|
|
168
|
+
private readonly onEnvelope: ((body: unknown) => void) | null;
|
|
169
|
+
|
|
170
|
+
constructor(opts?: { url?: string; onEnvelope?: (body: unknown) => void }) {
|
|
152
171
|
if (opts !== undefined && (typeof opts !== 'object' || opts === null || Array.isArray(opts))) {
|
|
153
172
|
throw new TypeError(
|
|
154
173
|
'EditorClient options must be an object. Use new EditorClient({ url: "http://127.0.0.1:20173" }), not new EditorClient("...").',
|
|
155
174
|
);
|
|
156
175
|
}
|
|
157
|
-
const unknownOptions = Object.keys(opts ?? {}).filter(
|
|
176
|
+
const unknownOptions = Object.keys(opts ?? {}).filter(
|
|
177
|
+
(key) => key !== 'url' && key !== 'onEnvelope',
|
|
178
|
+
);
|
|
158
179
|
if (unknownOptions.length > 0) {
|
|
159
180
|
throw new Error(
|
|
160
181
|
`EditorClient: unknown option${unknownOptions.length === 1 ? '' : 's'} ${unknownOptions.map((key) => `"${key}"`).join(', ')}. Use { url: "http://127.0.0.1:<port>" } to target an editor.`,
|
|
161
182
|
);
|
|
162
183
|
}
|
|
163
184
|
this.baseUrl = (opts?.url ?? DEFAULT_URL).replace(/\/$/, '');
|
|
185
|
+
this.onEnvelope = opts?.onEnvelope ?? null;
|
|
164
186
|
this.game = {
|
|
165
187
|
state: async (name: string): Promise<unknown> => {
|
|
166
188
|
// `keys` narrows the relay to the one provider asked for, so a panel
|
|
@@ -202,6 +224,7 @@ export class EditorClient {
|
|
|
202
224
|
);
|
|
203
225
|
}
|
|
204
226
|
const data = (await res.json()) as { ok: boolean; error?: string; code?: string } & T;
|
|
227
|
+
this.observe(data);
|
|
205
228
|
if (!data.ok) {
|
|
206
229
|
throw new EditorCommandError(
|
|
207
230
|
data.error ?? `Editor command failed: ${res.status}`,
|
|
@@ -329,13 +352,13 @@ export class EditorClient {
|
|
|
329
352
|
* debug seam") or no game canvas is mounted yet. Never returns a blank or
|
|
330
353
|
* editor-only frame as a stand-in.
|
|
331
354
|
*
|
|
332
|
-
* `opts.
|
|
333
|
-
*
|
|
355
|
+
* `opts.refreshStarvedFrame` is the loop-starvation leg: without recent rAF
|
|
356
|
+
* progress the canvas holds a provably stale frame and the relay
|
|
334
357
|
* refuses it with `BRIDGE_SCREENSHOT_STALE` rather than pass it off as
|
|
335
358
|
* current. Setting this asks the relay to render exactly ONE deterministic
|
|
336
359
|
* tick (`runTicks(1, {render:'last'})`) first — the same escape
|
|
337
360
|
* `@vgai/live`'s `RelayTransport.screenshot` has always used, which is why
|
|
338
|
-
* `vgai eval` could
|
|
361
|
+
* `vgai eval` could recover these frames while `vgai screenshot` could not.
|
|
339
362
|
* Off by default: a caller who does not ask must never be handed a frame
|
|
340
363
|
* that only exists because the capture drove the game.
|
|
341
364
|
*
|
|
@@ -346,12 +369,12 @@ export class EditorClient {
|
|
|
346
369
|
* worthless — so the contamination only happens when someone asks for it.
|
|
347
370
|
*/
|
|
348
371
|
async captureGame(opts?: {
|
|
349
|
-
|
|
372
|
+
refreshStarvedFrame?: boolean;
|
|
350
373
|
devLayers?: boolean;
|
|
351
374
|
}): Promise<GameCapture> {
|
|
352
375
|
const data = await this.command<GameCapture>({
|
|
353
376
|
type: 'bridge-screenshot',
|
|
354
|
-
...(opts?.
|
|
377
|
+
...(opts?.refreshStarvedFrame === true ? { refreshStarvedFrame: true } : {}),
|
|
355
378
|
...(opts?.devLayers === true ? { devLayers: true } : {}),
|
|
356
379
|
});
|
|
357
380
|
const layers = data.layers;
|
|
@@ -367,10 +390,28 @@ export class EditorClient {
|
|
|
367
390
|
// warning sentence is written where the pixels are, so nothing here
|
|
368
391
|
// re-derives (or softens) it.
|
|
369
392
|
...(flatness && typeof flatness.dominantFraction === 'number' ? { flatness } : {}),
|
|
370
|
-
...(data.
|
|
393
|
+
...(data.loopRecoveryFrame === true ? { loopRecoveryFrame: true } : {}),
|
|
371
394
|
};
|
|
372
395
|
}
|
|
373
396
|
|
|
397
|
+
/** Start recording the same clean running-game composite `captureGame`
|
|
398
|
+
* photographs. Recording state lives in the editor page, so another process
|
|
399
|
+
* may stop it later through the same project session. */
|
|
400
|
+
async startGameplayRecording(
|
|
401
|
+
options: GameplayRecordingOptions = {},
|
|
402
|
+
): Promise<GameplayRecordingStarted> {
|
|
403
|
+
return this.command<GameplayRecordingStarted>({
|
|
404
|
+
type: 'bridge-recording-start',
|
|
405
|
+
...(options.fps !== undefined ? { fps: options.fps } : {}),
|
|
406
|
+
...(options.devLayers === true ? { devLayers: true } : {}),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Stop the page-owned recorder and return its WebM path and metadata. */
|
|
411
|
+
async stopGameplayRecording(): Promise<GameplayRecordingCapture> {
|
|
412
|
+
return this.command<GameplayRecordingCapture>({ type: 'bridge-recording-stop' });
|
|
413
|
+
}
|
|
414
|
+
|
|
374
415
|
/**
|
|
375
416
|
* Capture an isolated, deterministic four-view preview through the editor's
|
|
376
417
|
* native Asset Lab. The SDK delegates rendering to the editor; it never
|
|
@@ -572,14 +613,21 @@ export class EditorClient {
|
|
|
572
613
|
return data.hierarchy;
|
|
573
614
|
}
|
|
574
615
|
|
|
575
|
-
/**
|
|
576
|
-
|
|
577
|
-
|
|
616
|
+
/**
|
|
617
|
+
* Write one editable path through the active Inspector's own IO.
|
|
618
|
+
*
|
|
619
|
+
* The answer carries `write` as well as the subject, because an ack alone
|
|
620
|
+
* cannot be believed: a write with no persistence route open succeeds and
|
|
621
|
+
* changes no byte, and `write.persisted` is how the caller tells the two
|
|
622
|
+
* apart without diffing the tree (`InspectedWriteDestination` in `types.ts`).
|
|
623
|
+
*/
|
|
624
|
+
async setInspectionField(path: string, value: unknown): Promise<InspectedFieldWrite> {
|
|
625
|
+
const data = await this.command<InspectedFieldWrite>({
|
|
578
626
|
type: 'set-inspection-field',
|
|
579
627
|
path,
|
|
580
628
|
value,
|
|
581
629
|
});
|
|
582
|
-
return data.subject;
|
|
630
|
+
return { subject: data.subject, write: data.write };
|
|
583
631
|
}
|
|
584
632
|
|
|
585
633
|
/**
|
|
@@ -622,21 +670,6 @@ export class EditorClient {
|
|
|
622
670
|
|
|
623
671
|
// --- Display (set semantics) ---
|
|
624
672
|
|
|
625
|
-
/**
|
|
626
|
-
* The Game document's "Persist to game source" consent, over the relay —
|
|
627
|
-
* the same session-scoped switch the checkbox flips, refused for the same
|
|
628
|
-
* reasons (it answers with the server's own words when the base cannot be
|
|
629
|
-
* written). `recorder` names who accounts for the diff a write produces.
|
|
630
|
-
*/
|
|
631
|
-
async setSourcePersistConsent(
|
|
632
|
-
enabled: boolean,
|
|
633
|
-
): Promise<{ enabled: boolean; recorder: string | null }> {
|
|
634
|
-
return this.command<{ enabled: boolean; recorder: string | null }>({
|
|
635
|
-
type: 'set-source-persist-consent',
|
|
636
|
-
enabled,
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
|
|
640
673
|
async setGrid(enabled: boolean): Promise<void> {
|
|
641
674
|
await this.command({ type: 'set-grid', enabled });
|
|
642
675
|
}
|
|
@@ -653,6 +686,11 @@ export class EditorClient {
|
|
|
653
686
|
await this.command({ type: 'set-shading-mode', mode });
|
|
654
687
|
}
|
|
655
688
|
|
|
689
|
+
/** Drive the recorder owned by the active native animation document. */
|
|
690
|
+
async recordAnimation(action: AnimationCaptureAction): Promise<void> {
|
|
691
|
+
await this.command({ type: 'animation-capture', action });
|
|
692
|
+
}
|
|
693
|
+
|
|
656
694
|
async setHelperType(helperType: keyof HelperVisibility, enabled: boolean): Promise<void> {
|
|
657
695
|
await this.command({ type: 'set-helper-type', helperType, enabled });
|
|
658
696
|
}
|
|
@@ -800,7 +838,19 @@ export class EditorClient {
|
|
|
800
838
|
async getState(): Promise<EditorState> {
|
|
801
839
|
const res = await fetch(`${this.baseUrl}/__editor/state`);
|
|
802
840
|
if (!res.ok) throw new Error(`Failed to get editor state: ${res.status} ${res.statusText}`);
|
|
803
|
-
|
|
841
|
+
const state = (await res.json()) as EditorState;
|
|
842
|
+
this.observe(state);
|
|
843
|
+
return state;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/** Hand one response body to {@link onEnvelope}, never letting it throw. */
|
|
847
|
+
private observe(body: unknown): void {
|
|
848
|
+
if (this.onEnvelope === null) return;
|
|
849
|
+
try {
|
|
850
|
+
this.onEnvelope(body);
|
|
851
|
+
} catch {
|
|
852
|
+
// A reporting hook may never break the command it is reporting on.
|
|
853
|
+
}
|
|
804
854
|
}
|
|
805
855
|
|
|
806
856
|
/**
|
package/src/contributions.ts
CHANGED
|
@@ -41,6 +41,15 @@ export interface ToolContributionNode {
|
|
|
41
41
|
export interface ToolObject3DPreviewSource {
|
|
42
42
|
readonly root: Object3D;
|
|
43
43
|
readonly animations?: readonly AnimationClip[];
|
|
44
|
+
/**
|
|
45
|
+
* The source's OWN tick, and the host drives it EXACTLY ONCE per mount — for
|
|
46
|
+
* the bounded design-time settle that gives a physics-owned body the pose it
|
|
47
|
+
* actually rests in (`authoring/design-time-settle.ts`). It is never a frame
|
|
48
|
+
* loop: content time does not advance on a design-time surface, so a source
|
|
49
|
+
* that animates from here shows its settled pose and then holds it. Play is
|
|
50
|
+
* where a world runs.
|
|
51
|
+
*/
|
|
52
|
+
update?(deltaSeconds: number): void;
|
|
44
53
|
dispose(): void;
|
|
45
54
|
}
|
|
46
55
|
|
|
@@ -73,7 +82,6 @@ export interface ToolObject3DPreviewProps {
|
|
|
73
82
|
readonly height?: number | string;
|
|
74
83
|
readonly background?: ColorRepresentation;
|
|
75
84
|
readonly showSkeleton?: boolean;
|
|
76
|
-
readonly autoplay?: boolean;
|
|
77
85
|
readonly cameraDirection?: readonly [number, number, number];
|
|
78
86
|
/** Host diagnostic grid. Defaults off with setupPreview, on otherwise. */
|
|
79
87
|
readonly showGrid?: boolean;
|
|
@@ -124,7 +132,6 @@ export interface ToolObject3DAuthoringProps {
|
|
|
124
132
|
* viewport dressing (environment, gradient backdrop, key light). */
|
|
125
133
|
readonly background?: ColorRepresentation;
|
|
126
134
|
readonly cameraDirection?: readonly [number, number, number];
|
|
127
|
-
readonly autoplay?: boolean;
|
|
128
135
|
/**
|
|
129
136
|
* Optional binding from live native clips back to ordinary project source.
|
|
130
137
|
* Project code only serializes its own TypeScript shape; the editor owns the
|
|
@@ -280,8 +287,29 @@ export interface ToolAnimationSourceBinding {
|
|
|
280
287
|
readonly serialize: (animations: readonly AnimationClip[]) => string;
|
|
281
288
|
}
|
|
282
289
|
|
|
290
|
+
/** Format-neutral identity for a project-owned Asset Lab document.
|
|
291
|
+
*
|
|
292
|
+
* This surface deliberately accepts no React children. Project tools may run
|
|
293
|
+
* a different React major than the editor, so their native UI stays in their
|
|
294
|
+
* own runtime while this editor-owned subject publishes the standard Asset
|
|
295
|
+
* Lab context, selection floor, and Inspector identity row beside it.
|
|
296
|
+
*/
|
|
297
|
+
export interface ToolAssetDocumentProps {
|
|
298
|
+
/** Stable id of the open workspace document. */
|
|
299
|
+
readonly documentId: string;
|
|
300
|
+
/** Subject name shown by the Inspector. */
|
|
301
|
+
readonly title: string;
|
|
302
|
+
/** Ecosystem-neutral asset kind shown by the Inspector. */
|
|
303
|
+
readonly type: string;
|
|
304
|
+
/** Optional provenance or concise state shown with the identity row. */
|
|
305
|
+
readonly status?: string;
|
|
306
|
+
/** Whether this is the active center document. */
|
|
307
|
+
readonly active?: boolean;
|
|
308
|
+
}
|
|
309
|
+
|
|
283
310
|
/** Editor-owned native surfaces available to ordinary project React tools. */
|
|
284
311
|
export interface ToolContributionSurfaces {
|
|
312
|
+
readonly AssetDocument: import('react').ComponentType<ToolAssetDocumentProps>;
|
|
285
313
|
readonly Object3DPreview: import('react').ComponentType<ToolObject3DPreviewProps>;
|
|
286
314
|
readonly Object3DAuthoring: import('react').ComponentType<ToolObject3DAuthoringProps>;
|
|
287
315
|
}
|
package/src/editor-view.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { AssetKind, EditorCameraState, EditorView, ViewPreset } from './types.js';
|
|
2
|
+
import { EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS } from './types.js';
|
|
2
3
|
|
|
3
4
|
const PREFIX = 'view.';
|
|
4
5
|
const MAX_QUERY_LENGTH = 4096;
|
|
6
|
+
const WORKSPACE_DOCUMENT_IDS = new Set<string>(EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS);
|
|
5
7
|
const ASSET_KINDS = new Set<AssetKind>([
|
|
6
8
|
'model',
|
|
7
9
|
'image',
|
|
@@ -147,16 +149,7 @@ function parseDocument(params: URLSearchParams): EditorView['document'] {
|
|
|
147
149
|
if (kind === 'project-tool') return { kind, name: value };
|
|
148
150
|
if (kind === 'generation') return { kind, id: value };
|
|
149
151
|
if (kind === 'story') return parseStoryDocument(params, value);
|
|
150
|
-
if (
|
|
151
|
-
kind === 'workspace' &&
|
|
152
|
-
[
|
|
153
|
-
'workspace:scene',
|
|
154
|
-
'workspace:game',
|
|
155
|
-
'workspace:build-profiles',
|
|
156
|
-
'account',
|
|
157
|
-
'project-tools',
|
|
158
|
-
].includes(value)
|
|
159
|
-
) {
|
|
152
|
+
if (kind === 'workspace' && WORKSPACE_DOCUMENT_IDS.has(value)) {
|
|
160
153
|
return {
|
|
161
154
|
kind,
|
|
162
155
|
id: value as Extract<NonNullable<EditorView['document']>, { kind: 'workspace' }>['id'],
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ export { editorViewFromUrl, editorViewUrl } from './editor-view.js';
|
|
|
29
29
|
export type { ExtensionContributionState } from './extension.js';
|
|
30
30
|
export type {
|
|
31
31
|
ActiveDocumentCapture,
|
|
32
|
+
AnimationCaptureAction,
|
|
32
33
|
AssetCompareCapture,
|
|
33
34
|
AssetCompareOptions,
|
|
34
35
|
AssetCompareView,
|
|
@@ -47,9 +48,13 @@ export type {
|
|
|
47
48
|
EditorView,
|
|
48
49
|
EditorViewDocument,
|
|
49
50
|
GameCapture,
|
|
51
|
+
GameplayRecordingCapture,
|
|
52
|
+
GameplayRecordingOptions,
|
|
53
|
+
GameplayRecordingStarted,
|
|
50
54
|
HelperVisibility,
|
|
51
55
|
InspectedAction,
|
|
52
56
|
InspectedField,
|
|
57
|
+
InspectedFieldWrite,
|
|
53
58
|
InspectedHierarchy,
|
|
54
59
|
InspectedHierarchyRow,
|
|
55
60
|
InspectedInspection,
|
|
@@ -57,6 +62,7 @@ export type {
|
|
|
57
62
|
InspectedSection,
|
|
58
63
|
InspectedSectionBody,
|
|
59
64
|
InspectedSubject,
|
|
65
|
+
InspectedWriteDestination,
|
|
60
66
|
InspectionPresentationKind,
|
|
61
67
|
InspectionSurface,
|
|
62
68
|
LabeledShotSetCapture,
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface HelperVisibility {
|
|
|
25
25
|
cameras: boolean;
|
|
26
26
|
colliders: boolean;
|
|
27
27
|
joints: boolean;
|
|
28
|
+
particles: boolean;
|
|
28
29
|
lod: boolean;
|
|
29
30
|
audio: boolean;
|
|
30
31
|
splines: boolean;
|
|
@@ -72,10 +73,10 @@ export interface ViewportCapture {
|
|
|
72
73
|
* never a silently HUD-less image passed off as the whole game. `layers` is
|
|
73
74
|
* only present on the composite leg.
|
|
74
75
|
*
|
|
75
|
-
* `flatness` and `
|
|
76
|
-
* PIXELS: how much of the frame is one flat surface (a near-blank capture
|
|
76
|
+
* `flatness` and `loopRecoveryFrame` are the same honesty contract applied to
|
|
77
|
+
* the PIXELS: how much of the frame is one flat surface (a near-blank capture
|
|
77
78
|
* carries its own "weak evidence" warning), and whether the frame exists only
|
|
78
|
-
* because
|
|
79
|
+
* because capture recovered a starved host loop with one deterministic tick.
|
|
79
80
|
*/
|
|
80
81
|
export interface GameCapture {
|
|
81
82
|
base64: string;
|
|
@@ -92,9 +93,38 @@ export interface GameCapture {
|
|
|
92
93
|
/** Present iff `degenerate`; the sentence to show the caller verbatim. */
|
|
93
94
|
warning?: string;
|
|
94
95
|
};
|
|
95
|
-
/** True when the
|
|
96
|
+
/** True when the host loop was starved and the runtime rendered one
|
|
96
97
|
* deterministic tick to produce this frame. */
|
|
97
|
-
|
|
98
|
+
loopRecoveryFrame?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Options for recording the clean running-game composite. */
|
|
102
|
+
export interface GameplayRecordingOptions {
|
|
103
|
+
/** Requested real-time capture cadence. Defaults to 30; range 1–60. */
|
|
104
|
+
fps?: number;
|
|
105
|
+
/** Include `dev: true` game layers. Off by default so ordinary evidence is
|
|
106
|
+
* the player-visible game, not its debugging overlay. */
|
|
107
|
+
devLayers?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Facts fixed when a gameplay recording starts. */
|
|
111
|
+
export interface GameplayRecordingStarted {
|
|
112
|
+
startedAt: string;
|
|
113
|
+
mimeType: string;
|
|
114
|
+
width: number;
|
|
115
|
+
height: number;
|
|
116
|
+
fps: number;
|
|
117
|
+
audio: boolean;
|
|
118
|
+
layers: { canvases: number; domOverlays: number };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Final browser recording. Media chunks stream directly to the project while
|
|
122
|
+
* recording, so the command result stays small even for a long playthrough. */
|
|
123
|
+
export interface GameplayRecordingCapture extends GameplayRecordingStarted {
|
|
124
|
+
path: string;
|
|
125
|
+
durationMs: number;
|
|
126
|
+
droppedFrames: number;
|
|
127
|
+
frameErrors: number;
|
|
98
128
|
}
|
|
99
129
|
|
|
100
130
|
export type AssetPreviewView = 'front' | 'right' | 'top' | 'perspective';
|
|
@@ -307,13 +337,14 @@ export interface EditorState {
|
|
|
307
337
|
* Issue #175 — the REAL engine `GameLoop.liveness` behind the current play
|
|
308
338
|
* session, distinct from `playState` above (editor UI state — a store
|
|
309
339
|
* flag that never reflected whether the loop was actually ticking).
|
|
310
|
-
* `'
|
|
311
|
-
* the
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
340
|
+
* `'loop-starved'` means the host loop has observed no recent rAF progress:
|
|
341
|
+
* either the current `document.hidden` gate deliberately parked it, or an
|
|
342
|
+
* armed visible-page callback has not arrived within the starvation
|
|
343
|
+
* interval. It is explicitly NOT a conclusion that the tab is hidden.
|
|
344
|
+
* `null` while not in play mode (no loop to report on) or against an older
|
|
345
|
+
* server that predates this field.
|
|
315
346
|
*/
|
|
316
|
-
loopLiveness?: 'running' | '
|
|
347
|
+
loopLiveness?: 'running' | 'loop-starved' | 'stopped' | null;
|
|
317
348
|
/**
|
|
318
349
|
* The connected editor tab's OWN reported visibility/focus
|
|
319
350
|
* (`command-listener.ts`'s `collectPresence`, straight off
|
|
@@ -323,8 +354,20 @@ export interface EditorState {
|
|
|
323
354
|
* BACKGROUNDED tab satisfies perfectly while the engine hidden-pauses its
|
|
324
355
|
* loop underneath. `null` when the page has no `document` at all; absent
|
|
325
356
|
* against an older server that predates the field.
|
|
357
|
+
*
|
|
358
|
+
* P21 — `reportedAt` is `Date.now()` in the TAB at the moment those two
|
|
359
|
+
* values were read. It is what makes this snapshot readable as a
|
|
360
|
+
* measurement rather than a fact: the tab re-POSTs state on
|
|
361
|
+
* visibilitychange/focus/blur, after commands and on store changes, so
|
|
362
|
+
* between those moments this ages, and a reader with no age has no way to
|
|
363
|
+
* tell a 40ms-old reading from a 40s-old one. Absent against an editor
|
|
364
|
+
* page that predates the field — never fabricated from the read time.
|
|
326
365
|
*/
|
|
327
|
-
presence?: {
|
|
366
|
+
presence?: {
|
|
367
|
+
visibility: 'visible' | 'hidden' | 'prerender';
|
|
368
|
+
focused: boolean;
|
|
369
|
+
reportedAt?: number;
|
|
370
|
+
} | null;
|
|
328
371
|
/**
|
|
329
372
|
* The pending-restart reason when source changed while the game was
|
|
330
373
|
* RUNNING and the running session is now stale (e.g. an R3F entry-file
|
|
@@ -394,6 +437,13 @@ export interface EditorState {
|
|
|
394
437
|
* attach/detach report, and the command receipts). Absent against an older
|
|
395
438
|
* server, and absent for a tab the server cannot measure.
|
|
396
439
|
*
|
|
440
|
+
* `pageErrors` is the OTHER half of that verdict — WHY. Uncaught errors and
|
|
441
|
+
* unhandled rejections captured by the page shell's inline bootstrap, which
|
|
442
|
+
* runs before the module graph, so the boot failure that leaves no listener
|
|
443
|
+
* is exactly the one they explain. They come back on a plain GET and need no
|
|
444
|
+
* cooperation from the page beyond the handler itself. `[]` means the page
|
|
445
|
+
* reported none; absent means the server cannot measure this tab.
|
|
446
|
+
*
|
|
397
447
|
* `census` is the tab's RESOURCE PROFILE, sampled by the page every five
|
|
398
448
|
* seconds and carried on the heartbeat: what a browser-level renderer death
|
|
399
449
|
* would otherwise leave unexplained. `heapUsedMB`/`heapLimitMB` are null off
|
|
@@ -408,12 +458,17 @@ export interface EditorState {
|
|
|
408
458
|
presentFor: number;
|
|
409
459
|
lastBeatAgo: number | null;
|
|
410
460
|
epochCount: number;
|
|
461
|
+
/** Age of the DOCUMENT running in this tab (this page-load), as opposed
|
|
462
|
+
* to `presentFor`, which is the age of the tab and survives its
|
|
463
|
+
* reloads. Absent against an older server. */
|
|
464
|
+
epochAgeMs?: number;
|
|
411
465
|
visibility: 'visible' | 'hidden';
|
|
412
466
|
route: 'project' | 'no-project' | 'unknown';
|
|
413
467
|
blessed: boolean;
|
|
414
468
|
channel: 'open' | 'down';
|
|
415
469
|
unresponsive: boolean;
|
|
416
470
|
commandListener?: 'ready' | 'not attached' | (string & {});
|
|
471
|
+
pageErrors?: string[];
|
|
417
472
|
census?: {
|
|
418
473
|
heapUsedMB: number | null;
|
|
419
474
|
heapLimitMB: number | null;
|
|
@@ -432,6 +487,33 @@ export interface EditorState {
|
|
|
432
487
|
* stopped trying — the browser, not the editor, is what to check.
|
|
433
488
|
*/
|
|
434
489
|
tabAutoOpen?: { attempts: number; stopped: boolean };
|
|
490
|
+
/**
|
|
491
|
+
* Epoch ms of the last genuine HTML page load this server served — i.e.
|
|
492
|
+
* when the editor tab last did a FULL document load (first open, reload,
|
|
493
|
+
* self-heal). Server-observed (`editor-server.ts`'s response-finish
|
|
494
|
+
* middleware), never browser-reported. `null` until the first page load.
|
|
495
|
+
*
|
|
496
|
+
* P20 reads this against {@link publicAssets} to answer "did bytes under
|
|
497
|
+
* `public/` change since the running document loaded", which is when
|
|
498
|
+
* module-scope loaders and the page-lifetime asset caches (Pixi `Assets`,
|
|
499
|
+
* three's loader caches) can still be serving the OLD bytes.
|
|
500
|
+
*/
|
|
501
|
+
lastIndexRequestAt?: number | null;
|
|
502
|
+
/**
|
|
503
|
+
* P20 — what the server has OBSERVED land under this project's `public/`
|
|
504
|
+
* during this server lifetime, from the same chokidar watcher that
|
|
505
|
+
* broadcasts `assets-changed`. `lastChangedAt` is `null` when nothing has
|
|
506
|
+
* changed since the server started. Absent against an older server.
|
|
507
|
+
*
|
|
508
|
+
* This is a divergence signal, not a cache verdict: nothing here knows
|
|
509
|
+
* whether the running page actually holds a stale copy of those bytes,
|
|
510
|
+
* only that they changed after it loaded.
|
|
511
|
+
*/
|
|
512
|
+
publicAssets?: {
|
|
513
|
+
lastChangedAt: number | null;
|
|
514
|
+
lastPath: string | null;
|
|
515
|
+
changedCount: number;
|
|
516
|
+
};
|
|
435
517
|
/**
|
|
436
518
|
* Epoch ms when the server last received a state POST from a browser tab —
|
|
437
519
|
* i.e. the age of the cached snapshot above. Omitted if no tab has ever
|
|
@@ -515,13 +597,76 @@ export interface EditorState {
|
|
|
515
597
|
* project has no readable manifest name.
|
|
516
598
|
*/
|
|
517
599
|
projectName?: string | null;
|
|
600
|
+
/**
|
|
601
|
+
* The open project's ADAPTER, resolved (ARCHITECTURE-CORE §The editor
|
|
602
|
+
* protocol). `source: 'module'` means the project's own `vgai.adapter.ts`
|
|
603
|
+
* supplied the binding table; `'native'` means it declared none and got
|
|
604
|
+
* `nativeAdapter()` — the declared native default, not a silent fallback.
|
|
605
|
+
*
|
|
606
|
+
* `null`/absent means NOBODY HAS LOOKED YET (no project open, or the load
|
|
607
|
+
* has not finished), which is deliberately distinct from a loaded adapter
|
|
608
|
+
* whose `scenes.entries` is empty — that is a real, gradable answer. A
|
|
609
|
+
* non-null `error` means the project's own module did NOT load and the
|
|
610
|
+
* table below is the native default standing in, with the failure named.
|
|
611
|
+
*
|
|
612
|
+
* Structurally declared here rather than imported from
|
|
613
|
+
* `@vgai/engine/adapter/adapter-module` because this interface is the WIRE
|
|
614
|
+
* contract: everything in it has already been through JSON.
|
|
615
|
+
*/
|
|
616
|
+
adapter?: {
|
|
617
|
+
source: 'module' | 'native';
|
|
618
|
+
modulePath: string | null;
|
|
619
|
+
regions: {
|
|
620
|
+
id: string;
|
|
621
|
+
surface: string;
|
|
622
|
+
/** true = this region grades the game's own dev layer, not shipped content. */
|
|
623
|
+
dev: boolean;
|
|
624
|
+
projector: string;
|
|
625
|
+
dialect: string | null;
|
|
626
|
+
anchors: string[];
|
|
627
|
+
}[];
|
|
628
|
+
scenes: {
|
|
629
|
+
default: string | null;
|
|
630
|
+
entries: {
|
|
631
|
+
id: string;
|
|
632
|
+
label: string;
|
|
633
|
+
kind: 'scene' | 'prefab';
|
|
634
|
+
region: string | null;
|
|
635
|
+
authorable: boolean;
|
|
636
|
+
reach: { kind: string; [field: string]: unknown };
|
|
637
|
+
source?: { path: string; export?: string };
|
|
638
|
+
finder?: string;
|
|
639
|
+
}[];
|
|
640
|
+
};
|
|
641
|
+
notes: string[];
|
|
642
|
+
error: string | null;
|
|
643
|
+
} | null;
|
|
518
644
|
}
|
|
519
645
|
|
|
520
646
|
export type ViewPreset = 'top' | 'front' | 'right' | 'perspective';
|
|
521
647
|
export type ShadingMode = 'solid' | 'clay' | 'unlit' | 'wireframe' | 'normals' | 'overdraw';
|
|
648
|
+
|
|
649
|
+
/** One operation on the active native animation document's recorder. */
|
|
650
|
+
export type AnimationCaptureAction = 'start' | 'stop' | 'review' | 'commit' | 'discard';
|
|
522
651
|
export type TransformMode = 'translate' | 'rotate' | 'scale';
|
|
523
652
|
export type TransformSpace = 'world' | 'local';
|
|
524
653
|
|
|
654
|
+
/** Stable editor-owned workspace documents that may appear in a shareable
|
|
655
|
+
* view. One runtime list owns both URL parsing and the public id type. */
|
|
656
|
+
export const EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS = [
|
|
657
|
+
'workspace:scene',
|
|
658
|
+
'workspace:game',
|
|
659
|
+
'workspace:3d-components',
|
|
660
|
+
'workspace:2d-components',
|
|
661
|
+
'workspace:ui-components',
|
|
662
|
+
'workspace:dev',
|
|
663
|
+
'workspace:build-profiles',
|
|
664
|
+
'account',
|
|
665
|
+
'project-tools',
|
|
666
|
+
] as const;
|
|
667
|
+
|
|
668
|
+
export type EditorViewWorkspaceDocumentId = (typeof EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS)[number];
|
|
669
|
+
|
|
525
670
|
/**
|
|
526
671
|
* A durable, intentionally small projection of what an editor is presenting.
|
|
527
672
|
* This is not workspace persistence: panel sizes, transient tool state, and
|
|
@@ -538,12 +683,7 @@ export type EditorViewDocument =
|
|
|
538
683
|
| { kind: 'generation'; id: string }
|
|
539
684
|
| {
|
|
540
685
|
kind: 'workspace';
|
|
541
|
-
id:
|
|
542
|
-
| 'workspace:scene'
|
|
543
|
-
| 'workspace:game'
|
|
544
|
-
| 'workspace:build-profiles'
|
|
545
|
-
| 'account'
|
|
546
|
-
| 'project-tools';
|
|
686
|
+
id: EditorViewWorkspaceDocumentId;
|
|
547
687
|
};
|
|
548
688
|
|
|
549
689
|
export interface EditorView {
|
|
@@ -556,7 +696,7 @@ export interface EditorView {
|
|
|
556
696
|
frame?: 'document' | 'selection';
|
|
557
697
|
grid?: boolean;
|
|
558
698
|
};
|
|
559
|
-
utility?: 'profiler' | 'console' | 'animation' | 'light-explorer';
|
|
699
|
+
utility?: 'profiler' | 'console' | 'animation' | 'behavior' | 'light-explorer';
|
|
560
700
|
}
|
|
561
701
|
|
|
562
702
|
export interface PresentedEditorView {
|
|
@@ -727,6 +867,41 @@ export interface InspectedNothing {
|
|
|
727
867
|
* Narrow with `'none' in result`. */
|
|
728
868
|
export type InspectedInspection = InspectedSubject | InspectedNothing;
|
|
729
869
|
|
|
870
|
+
/**
|
|
871
|
+
* WHERE THIS WRITE WENT — carried by every `editor.setField()` ack.
|
|
872
|
+
*
|
|
873
|
+
* A write with no persistence route still succeeds: it lands on the live
|
|
874
|
+
* object and journals live-only, exactly as designed. Without this the ack was
|
|
875
|
+
* indistinguishable from one that reached a file, so a caller could only find
|
|
876
|
+
* out by diffing the tree — and a healthy consent-off session read as a silent
|
|
877
|
+
* no-op. `persisted: false` with `destination: "live-only (not saved)"` is the
|
|
878
|
+
* honest floor: never silence, and never a fabricated file name.
|
|
879
|
+
*
|
|
880
|
+
* IT IS PER-EDIT, produced by the component that performed the write and
|
|
881
|
+
* returned through the editor's persistence pipe — never a property of the
|
|
882
|
+
* session, the surface or the adapter. A composite holding a live-only three
|
|
883
|
+
* root beside a source-backed DOM root has no single true answer, and the
|
|
884
|
+
* adapter-wide one it used to give was the DOM root's (measured on the
|
|
885
|
+
* vendored racing game: a three-root edit acked `persisted: true` against a
|
|
886
|
+
* file it never touched). The ack is also AWAITED: it resolves after the bytes
|
|
887
|
+
* have landed, so a caller holding it can diff the tree immediately.
|
|
888
|
+
*/
|
|
889
|
+
export interface InspectedWriteDestination {
|
|
890
|
+
/** Where THIS edit's bytes landed, in the writer's own words — a source
|
|
891
|
+
* file, the game's own JSX, or a named non-target like
|
|
892
|
+
* `"live-only (not saved)"`. */
|
|
893
|
+
destination: string;
|
|
894
|
+
/** Whether a byte actually moved for THIS edit. */
|
|
895
|
+
persisted: boolean;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/** What `editor.setField()` answers: the subject after the write, plus where
|
|
899
|
+
* the write went. */
|
|
900
|
+
export interface InspectedFieldWrite {
|
|
901
|
+
subject: InspectedInspection;
|
|
902
|
+
write: InspectedWriteDestination;
|
|
903
|
+
}
|
|
904
|
+
|
|
730
905
|
// ---------------------------------------------------------------- hierarchy
|
|
731
906
|
//
|
|
732
907
|
// The wire mirror of the editor's own `SerializedHierarchyPanel`
|