@vgai/editor-sdk 0.5.15 → 0.5.17
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 +212 -37
- package/src/contributions.ts +81 -1
- package/src/editor-view.ts +0 -1
- package/src/index.ts +7 -1
- package/src/types.ts +34 -22
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.17",
|
|
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.17"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"@vgai/engine": "*",
|
package/src/client.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
ProjectTemplate,
|
|
27
27
|
ProjectToolCatalog,
|
|
28
28
|
ProjectToolOutcome,
|
|
29
|
+
RagdollGenerationResult,
|
|
29
30
|
RecentProject,
|
|
30
31
|
ShadingMode,
|
|
31
32
|
StoryCaptureOptions,
|
|
@@ -103,6 +104,23 @@ export class EditorCommandError extends Error {
|
|
|
103
104
|
*/
|
|
104
105
|
const COMMAND_DEADLINE_MS = 150_000;
|
|
105
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Ceiling on {@link EditorClient.getUnresolvedConsole}. The CLI drains this
|
|
109
|
+
* on every verb, including ones that never wait for a command envelope, so
|
|
110
|
+
* a silent hang here would become a silent hang on `vgai examples`. The
|
|
111
|
+
* server route is a plain in-process GET; 1.5s is already longer than it
|
|
112
|
+
* should ever take.
|
|
113
|
+
*/
|
|
114
|
+
const CONSOLE_DRAIN_TIMEOUT_MS = 1_500;
|
|
115
|
+
|
|
116
|
+
/** Context accompanying one {@link EditorClient} response observation. */
|
|
117
|
+
export interface EditorEnvelopeObservation {
|
|
118
|
+
/** True only when this body came from the console-ledger endpoint and its
|
|
119
|
+
* `entries` field is therefore the complete named console set. Command
|
|
120
|
+
* payloads may also own an unrelated `entries` field. */
|
|
121
|
+
readonly unresolvedConsoleComplete: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
106
124
|
/**
|
|
107
125
|
* The GAME DEBUG PLANE, as a contribution's client sees it.
|
|
108
126
|
*
|
|
@@ -130,6 +148,27 @@ export interface HistoryStep {
|
|
|
130
148
|
readonly redoLabel: string | null;
|
|
131
149
|
}
|
|
132
150
|
|
|
151
|
+
/** What `open()` acknowledges: the workspace document the scene-table entry
|
|
152
|
+
* resolved to, and the title its tab now carries — the game's own word for
|
|
153
|
+
* that composition, not a filename. */
|
|
154
|
+
export interface OpenedDocument {
|
|
155
|
+
readonly documentId: string;
|
|
156
|
+
readonly title: string;
|
|
157
|
+
/**
|
|
158
|
+
* The GAME's own answer, present only when opening navigated a running game
|
|
159
|
+
* (a scene the adapter declares reachable through the game's scenes
|
|
160
|
+
* contract, or a native swap-slot remount): what was asked for, and which
|
|
161
|
+
* scene the game reports it is in once its own navigation settled. `current`
|
|
162
|
+
* can differ from `requested` — that is the game's reading, not a host claim.
|
|
163
|
+
*/
|
|
164
|
+
readonly scene?: { readonly requested: string; readonly current: string | null };
|
|
165
|
+
/**
|
|
166
|
+
* Present when opening restarted play at a native swap-slot key rather than
|
|
167
|
+
* navigating a live contract — the slot is a module-level const.
|
|
168
|
+
*/
|
|
169
|
+
readonly restart?: true;
|
|
170
|
+
}
|
|
171
|
+
|
|
133
172
|
export interface GameDebugDoor {
|
|
134
173
|
/**
|
|
135
174
|
* Read ONE registered state provider by name (`'bot.tester'`). `undefined`
|
|
@@ -165,9 +204,14 @@ export class EditorClient {
|
|
|
165
204
|
* The observer must not throw; anything it raises is swallowed, because a
|
|
166
205
|
* reporting hook may never break the command it is reporting on.
|
|
167
206
|
*/
|
|
168
|
-
private readonly onEnvelope:
|
|
169
|
-
|
|
170
|
-
|
|
207
|
+
private readonly onEnvelope:
|
|
208
|
+
| ((body: unknown, observation: EditorEnvelopeObservation) => void)
|
|
209
|
+
| null;
|
|
210
|
+
|
|
211
|
+
constructor(opts?: {
|
|
212
|
+
url?: string;
|
|
213
|
+
onEnvelope?: (body: unknown, observation: EditorEnvelopeObservation) => void;
|
|
214
|
+
}) {
|
|
171
215
|
if (opts !== undefined && (typeof opts !== 'object' || opts === null || Array.isArray(opts))) {
|
|
172
216
|
throw new TypeError(
|
|
173
217
|
'EditorClient options must be an object. Use new EditorClient({ url: "http://127.0.0.1:20173" }), not new EditorClient("...").',
|
|
@@ -223,8 +267,7 @@ export class EditorClient {
|
|
|
223
267
|
true,
|
|
224
268
|
);
|
|
225
269
|
}
|
|
226
|
-
const data = (await
|
|
227
|
-
this.observe(data);
|
|
270
|
+
const data = (await this.readJson(res)) as { ok: boolean; error?: string; code?: string } & T;
|
|
228
271
|
if (!data.ok) {
|
|
229
272
|
throw new EditorCommandError(
|
|
230
273
|
data.error ?? `Editor command failed: ${res.status}`,
|
|
@@ -361,21 +404,11 @@ export class EditorClient {
|
|
|
361
404
|
* `vgai eval` could recover these frames while `vgai screenshot` could not.
|
|
362
405
|
* Off by default: a caller who does not ask must never be handed a frame
|
|
363
406
|
* that only exists because the capture drove the game.
|
|
364
|
-
*
|
|
365
|
-
* `opts.devLayers` puts the game's DEV LAYERS (`dev: true` roots — its own
|
|
366
|
-
* in-game dev GUI) INTO the frame. Also off by default, for the mirror-image
|
|
367
|
-
* reason: a panel frame at the moment of a failure is strong evidence, and a
|
|
368
|
-
* "does the game look right" frame with the panel painted over it is
|
|
369
|
-
* worthless — so the contamination only happens when someone asks for it.
|
|
370
407
|
*/
|
|
371
|
-
async captureGame(opts?: {
|
|
372
|
-
refreshStarvedFrame?: boolean;
|
|
373
|
-
devLayers?: boolean;
|
|
374
|
-
}): Promise<GameCapture> {
|
|
408
|
+
async captureGame(opts?: { refreshStarvedFrame?: boolean }): Promise<GameCapture> {
|
|
375
409
|
const data = await this.command<GameCapture>({
|
|
376
410
|
type: 'bridge-screenshot',
|
|
377
411
|
...(opts?.refreshStarvedFrame === true ? { refreshStarvedFrame: true } : {}),
|
|
378
|
-
...(opts?.devLayers === true ? { devLayers: true } : {}),
|
|
379
412
|
});
|
|
380
413
|
const layers = data.layers;
|
|
381
414
|
const flatness = data.flatness;
|
|
@@ -403,7 +436,6 @@ export class EditorClient {
|
|
|
403
436
|
return this.command<GameplayRecordingStarted>({
|
|
404
437
|
type: 'bridge-recording-start',
|
|
405
438
|
...(options.fps !== undefined ? { fps: options.fps } : {}),
|
|
406
|
-
...(options.devLayers === true ? { devLayers: true } : {}),
|
|
407
439
|
});
|
|
408
440
|
}
|
|
409
441
|
|
|
@@ -543,6 +575,11 @@ export class EditorClient {
|
|
|
543
575
|
await this.command({ type: 'open-asset-tab', path, kind });
|
|
544
576
|
}
|
|
545
577
|
|
|
578
|
+
/** Generate a project-owned native Rapier ragdoll from a rigged model asset. */
|
|
579
|
+
async generateRagdoll(assetPath: string): Promise<RagdollGenerationResult> {
|
|
580
|
+
return this.command<RagdollGenerationResult>({ type: 'generate-ragdoll', assetPath });
|
|
581
|
+
}
|
|
582
|
+
|
|
546
583
|
async closeAsset(key: string): Promise<void> {
|
|
547
584
|
await this.command({ type: 'close-asset-tab', key });
|
|
548
585
|
}
|
|
@@ -586,6 +623,15 @@ export class EditorClient {
|
|
|
586
623
|
return data.subject;
|
|
587
624
|
}
|
|
588
625
|
|
|
626
|
+
/** Run one verb exposed by the active Inspector subject, by its id. */
|
|
627
|
+
async runInspectionAction(actionId: string): Promise<InspectedInspection> {
|
|
628
|
+
const data = await this.command<{ subject: InspectedInspection }>({
|
|
629
|
+
type: 'run-inspection-action',
|
|
630
|
+
actionId,
|
|
631
|
+
});
|
|
632
|
+
return data.subject;
|
|
633
|
+
}
|
|
634
|
+
|
|
589
635
|
/**
|
|
590
636
|
* The HIERARCHY PANEL's actual rendered row tree, as data.
|
|
591
637
|
*
|
|
@@ -630,6 +676,57 @@ export class EditorClient {
|
|
|
630
676
|
return { subject: data.subject, write: data.write };
|
|
631
677
|
}
|
|
632
678
|
|
|
679
|
+
/**
|
|
680
|
+
* REMOVE one editable path's authored override — the other half of the write
|
|
681
|
+
* door, and the only one that can express byte-ABSENCE.
|
|
682
|
+
*
|
|
683
|
+
* {@link setInspectionField} writes a VALUE, so reverting a property an
|
|
684
|
+
* authoring gesture ADDED puts the default back EXPLICITLY and leaves the
|
|
685
|
+
* source one attribute heavier than it started. This drops the property, so
|
|
686
|
+
* whatever governs it in its absence takes over — the same `io.remove` the
|
|
687
|
+
* Inspector's revert arrow calls, the same persistence pipe, the same awaited
|
|
688
|
+
* `{ destination, persisted }` ack.
|
|
689
|
+
*
|
|
690
|
+
* Rejects with `code: 'REMOVAL_UNAVAILABLE'` when the field does not declare
|
|
691
|
+
* itself removable or the lane implements no removal door. That refusal is a
|
|
692
|
+
* MISSING SEAM, not a failed removal, and it is coded rather than phrased
|
|
693
|
+
* precisely so a caller can grade the two differently.
|
|
694
|
+
*/
|
|
695
|
+
async removeInspectionField(path: string): Promise<InspectedFieldWrite> {
|
|
696
|
+
const data = await this.command<InspectedFieldWrite>({
|
|
697
|
+
type: 'remove-inspection-field',
|
|
698
|
+
path,
|
|
699
|
+
});
|
|
700
|
+
return { subject: data.subject, write: data.write };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* OPEN one piece of the adapter's SCENE TABLE by id — a scene, a prefab, or
|
|
705
|
+
* a story state, because the table makes them siblings (they differ only in
|
|
706
|
+
* instance site). The ids are exactly what `getState().adapter.scenes.entries`
|
|
707
|
+
* reports, so the table is both the menu and the address space.
|
|
708
|
+
*
|
|
709
|
+
* With a game LIVE in the session, opening a scene the adapter declares
|
|
710
|
+
* reachable through that game's own scenes contract NAVIGATES it — the same
|
|
711
|
+
* switch the editor's own scene picker makes — and the answer carries the
|
|
712
|
+
* game's own reading (`scene`).
|
|
713
|
+
*
|
|
714
|
+
* Rejects with a coded reason rather than prose: `SCENE_NOT_FOUND` (and it
|
|
715
|
+
* names the ids that DO exist), `SCENE_NOT_OPENABLE` carrying the adapter's
|
|
716
|
+
* own declared reason for a scene it says nothing can reach,
|
|
717
|
+
* `SCENE_NAVIGATION_NOT_RUNNING` for a live-only scene with no game running,
|
|
718
|
+
* `SCENE_CONTRACT_UNAVAILABLE` / `SCENE_NOT_IN_CONTRACT` (naming the ids the
|
|
719
|
+
* game itself publishes) / `SCENE_SWITCH_FAILED` when the running game's own
|
|
720
|
+
* navigation cannot take it, `SCENE_NOT_OPENABLE_LIVE` when this session has
|
|
721
|
+
* no remount for a native swap-slot scene,
|
|
722
|
+
* `SCENE_TABLE_UNAVAILABLE` before the adapter has loaded, and
|
|
723
|
+
* `SCENE_DOCUMENT_NOT_MOUNTED` when the host has no document for a piece the
|
|
724
|
+
* table says is openable — a host gap, not a table statement.
|
|
725
|
+
*/
|
|
726
|
+
async open(id: string): Promise<OpenedDocument> {
|
|
727
|
+
return this.command<OpenedDocument>({ type: 'open', id });
|
|
728
|
+
}
|
|
729
|
+
|
|
633
730
|
/**
|
|
634
731
|
* Undo / redo one project transaction — the same queue the keyboard shortcut
|
|
635
732
|
* drives. `moved` is false when there was nothing left in that direction.
|
|
@@ -728,12 +825,16 @@ export class EditorClient {
|
|
|
728
825
|
headers: { 'Content-Type': 'application/json' },
|
|
729
826
|
body: JSON.stringify({ name, location, template, ...(exampleId ? { exampleId } : {}) }),
|
|
730
827
|
});
|
|
828
|
+
const data = (await this.readJson(res)) as {
|
|
829
|
+
ok?: boolean;
|
|
830
|
+
error?: string;
|
|
831
|
+
path?: string;
|
|
832
|
+
config?: ProjectInfo['config'];
|
|
833
|
+
};
|
|
731
834
|
if (!res.ok) {
|
|
732
|
-
|
|
733
|
-
throw new Error(body.error ?? `Create project failed: ${res.status}`);
|
|
835
|
+
throw new Error(data.error ?? `Create project failed: ${res.status}`);
|
|
734
836
|
}
|
|
735
|
-
|
|
736
|
-
return { path: data.path, config: data.config };
|
|
837
|
+
return { path: data.path as string, config: data.config as ProjectInfo['config'] };
|
|
737
838
|
}
|
|
738
839
|
|
|
739
840
|
async openProject(path: string): Promise<void> {
|
|
@@ -742,23 +843,23 @@ export class EditorClient {
|
|
|
742
843
|
headers: { 'Content-Type': 'application/json' },
|
|
743
844
|
body: JSON.stringify({ path }),
|
|
744
845
|
});
|
|
846
|
+
const body = (await this.readJson(res)) as { error?: string };
|
|
745
847
|
if (!res.ok) {
|
|
746
|
-
const body = (await res.json()) as { error?: string };
|
|
747
848
|
throw new Error(body.error ?? `Open project failed: ${res.status}`);
|
|
748
849
|
}
|
|
749
850
|
}
|
|
750
851
|
|
|
751
852
|
async getProject(): Promise<ProjectInfo | null> {
|
|
752
853
|
const res = await fetch(`${this.baseUrl}/__editor/project`);
|
|
753
|
-
|
|
754
|
-
|
|
854
|
+
const data = (await this.readJson(res)) as { project: ProjectInfo | null; error?: string };
|
|
855
|
+
if (!res.ok) throw new Error(data.error ?? `Failed to get project: ${res.status}`);
|
|
755
856
|
return data.project;
|
|
756
857
|
}
|
|
757
858
|
|
|
758
859
|
async listRecentProjects(): Promise<RecentProject[]> {
|
|
759
860
|
const res = await fetch(`${this.baseUrl}/__editor/recent-projects`);
|
|
760
|
-
|
|
761
|
-
|
|
861
|
+
const data = (await this.readJson(res)) as { projects: RecentProject[]; error?: string };
|
|
862
|
+
if (!res.ok) throw new Error(data.error ?? `Failed to list projects: ${res.status}`);
|
|
762
863
|
return data.projects;
|
|
763
864
|
}
|
|
764
865
|
|
|
@@ -769,8 +870,9 @@ export class EditorClient {
|
|
|
769
870
|
* editor browser merely because they were listed. */
|
|
770
871
|
async listProjectTools(): Promise<ProjectToolCatalog> {
|
|
771
872
|
const res = await fetch(`${this.baseUrl}/__editor/project-tools`);
|
|
873
|
+
const body = await this.readJson(res);
|
|
772
874
|
if (!res.ok) throw new Error(`Failed to list project tools: ${res.status}`);
|
|
773
|
-
return
|
|
875
|
+
return body as ProjectToolCatalog;
|
|
774
876
|
}
|
|
775
877
|
|
|
776
878
|
/** Execute one Node-hosted project tool through the shared validated
|
|
@@ -793,7 +895,7 @@ export class EditorClient {
|
|
|
793
895
|
...(options.instance !== undefined ? { instance: options.instance } : {}),
|
|
794
896
|
}),
|
|
795
897
|
});
|
|
796
|
-
const body = (await
|
|
898
|
+
const body = (await this.readJson(res)) as ProjectToolOutcome;
|
|
797
899
|
if (!body || typeof body !== 'object' || typeof body.ok !== 'boolean') {
|
|
798
900
|
throw new Error(`Project tool returned an invalid response (${res.status}).`);
|
|
799
901
|
}
|
|
@@ -806,8 +908,9 @@ export class EditorClient {
|
|
|
806
908
|
* request/result shapes remain on their registered operations. */
|
|
807
909
|
async listGenerationJobs(): Promise<GenerationJobsDocument> {
|
|
808
910
|
const res = await fetch(`${this.baseUrl}/__editor/generations`);
|
|
911
|
+
const body = await this.readJson(res);
|
|
809
912
|
if (!res.ok) throw new Error(`Failed to list generation jobs: ${res.status}`);
|
|
810
|
-
return
|
|
913
|
+
return body as GenerationJobsDocument;
|
|
811
914
|
}
|
|
812
915
|
|
|
813
916
|
/** Forget operational job state. Accepted provenance and project assets
|
|
@@ -816,8 +919,9 @@ export class EditorClient {
|
|
|
816
919
|
const res = await fetch(`${this.baseUrl}/__editor/generations/${encodeURIComponent(id)}`, {
|
|
817
920
|
method: 'DELETE',
|
|
818
921
|
});
|
|
819
|
-
|
|
820
|
-
|
|
922
|
+
const body = (await this.readJson(res)) as { removed?: boolean; error?: string };
|
|
923
|
+
if (!res.ok) throw new Error(body.error ?? `Failed to forget generation job: ${res.status}`);
|
|
924
|
+
return body.removed === true;
|
|
821
925
|
}
|
|
822
926
|
|
|
823
927
|
// --- Logs ---
|
|
@@ -826,10 +930,10 @@ export class EditorClient {
|
|
|
826
930
|
Array<{ t: number; level: string; msg: string; source?: string }>
|
|
827
931
|
> {
|
|
828
932
|
const res = await fetch(`${this.baseUrl}/__editor/log-entries`);
|
|
829
|
-
|
|
830
|
-
const data = (await res.json()) as {
|
|
933
|
+
const data = (await this.readJson(res)) as {
|
|
831
934
|
entries: Array<{ t: number; level: string; msg: string; source?: string }>;
|
|
832
935
|
};
|
|
936
|
+
if (!res.ok) return [];
|
|
833
937
|
return data.entries;
|
|
834
938
|
}
|
|
835
939
|
|
|
@@ -837,17 +941,88 @@ export class EditorClient {
|
|
|
837
941
|
|
|
838
942
|
async getState(): Promise<EditorState> {
|
|
839
943
|
const res = await fetch(`${this.baseUrl}/__editor/state`);
|
|
944
|
+
const state = (await this.readJson(res)) as EditorState;
|
|
840
945
|
if (!res.ok) throw new Error(`Failed to get editor state: ${res.status} ${res.statusText}`);
|
|
841
|
-
const state = (await res.json()) as EditorState;
|
|
842
|
-
this.observe(state);
|
|
843
946
|
return state;
|
|
844
947
|
}
|
|
845
948
|
|
|
949
|
+
/**
|
|
950
|
+
* The complete unresolved console set the session is holding right now.
|
|
951
|
+
*
|
|
952
|
+
* Command envelopes only carry COUNTS (`unresolvedConsole` on
|
|
953
|
+
* `commandResponseFor`). The named conditions live on GET `/__editor/console`.
|
|
954
|
+
* This is the method that turns "a command that exits before an envelope
|
|
955
|
+
* arrives" into a real reading: the CLI calls it at start and at exit
|
|
956
|
+
* through the same {@link onEnvelope} observer every other response uses.
|
|
957
|
+
* A session that does not answer within {@link CONSOLE_DRAIN_TIMEOUT_MS} is
|
|
958
|
+
* a thrown error the caller treats as "nothing learned", never a hang.
|
|
959
|
+
*/
|
|
960
|
+
async getUnresolvedConsole(opts?: { all?: boolean }): Promise<unknown> {
|
|
961
|
+
const res = await fetch(
|
|
962
|
+
`${this.baseUrl}/__editor/console${opts?.all === true ? '?all=1' : ''}`,
|
|
963
|
+
{ signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS) },
|
|
964
|
+
);
|
|
965
|
+
const body = await this.readJson(res, true);
|
|
966
|
+
if (!res.ok) {
|
|
967
|
+
throw new Error(`Failed to read unresolved console: ${res.status}`);
|
|
968
|
+
}
|
|
969
|
+
return body;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/** Acknowledge one named console condition. The response is observed and
|
|
973
|
+
* hydrated through the same path as every other client response. */
|
|
974
|
+
async acknowledgeConsole(input: {
|
|
975
|
+
readonly id: string;
|
|
976
|
+
readonly reason: string;
|
|
977
|
+
readonly by: string;
|
|
978
|
+
}): Promise<unknown> {
|
|
979
|
+
const res = await fetch(`${this.baseUrl}/__editor/console/ack`, {
|
|
980
|
+
method: 'POST',
|
|
981
|
+
headers: { 'Content-Type': 'application/json' },
|
|
982
|
+
body: JSON.stringify(input),
|
|
983
|
+
signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS * 4),
|
|
984
|
+
});
|
|
985
|
+
const body = await this.readJson(res);
|
|
986
|
+
return body;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** Parse one JSON body and hand it to {@link onEnvelope}.
|
|
990
|
+
*
|
|
991
|
+
* A command/state envelope carries current counts but not the named set. If
|
|
992
|
+
* an observer is installed, do the bounded console GET before resolving the
|
|
993
|
+
* original request. That makes a subsequent `process.exit()` safe: the
|
|
994
|
+
* observer has already received every condition and occurrence count. */
|
|
995
|
+
private async readJson(res: Response, consoleComplete = false): Promise<unknown> {
|
|
996
|
+
const body: unknown = await res.json();
|
|
997
|
+
this.observe(body, { unresolvedConsoleComplete: consoleComplete });
|
|
998
|
+
if (
|
|
999
|
+
this.onEnvelope !== null &&
|
|
1000
|
+
!consoleComplete &&
|
|
1001
|
+
body !== null &&
|
|
1002
|
+
typeof body === 'object' &&
|
|
1003
|
+
'unresolvedConsole' in body
|
|
1004
|
+
) {
|
|
1005
|
+
try {
|
|
1006
|
+
const consoleRes = await fetch(`${this.baseUrl}/__editor/console`, {
|
|
1007
|
+
signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS),
|
|
1008
|
+
});
|
|
1009
|
+
if (consoleRes.ok) {
|
|
1010
|
+
const consoleBody: unknown = await consoleRes.json();
|
|
1011
|
+
this.observe(consoleBody, { unresolvedConsoleComplete: true });
|
|
1012
|
+
}
|
|
1013
|
+
} catch {
|
|
1014
|
+
// The original response remains authoritative. A reporting follow-up
|
|
1015
|
+
// may degrade to its count-only envelope, never break the command.
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return body;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
846
1021
|
/** Hand one response body to {@link onEnvelope}, never letting it throw. */
|
|
847
|
-
private observe(body: unknown): void {
|
|
1022
|
+
private observe(body: unknown, observation: EditorEnvelopeObservation): void {
|
|
848
1023
|
if (this.onEnvelope === null) return;
|
|
849
1024
|
try {
|
|
850
|
-
this.onEnvelope(body);
|
|
1025
|
+
this.onEnvelope(body, observation);
|
|
851
1026
|
} catch {
|
|
852
1027
|
// A reporting hook may never break the command it is reporting on.
|
|
853
1028
|
}
|
package/src/contributions.ts
CHANGED
|
@@ -40,6 +40,13 @@ export interface ToolContributionNode {
|
|
|
40
40
|
*/
|
|
41
41
|
export interface ToolObject3DPreviewSource {
|
|
42
42
|
readonly root: Object3D;
|
|
43
|
+
/**
|
|
44
|
+
* Optional authored roots for the document hierarchy when `root` is a
|
|
45
|
+
* presentation/lifetime container rather than authored content itself.
|
|
46
|
+
* Framing, ticking and disposal still own `root`; this list only scopes the
|
|
47
|
+
* shared Hierarchy. Omitted means `root`, preserving the native tree.
|
|
48
|
+
*/
|
|
49
|
+
readonly hierarchyRoots?: readonly Object3D[];
|
|
43
50
|
readonly animations?: readonly AnimationClip[];
|
|
44
51
|
/**
|
|
45
52
|
* The source's OWN tick, and the host drives it EXACTLY ONCE per mount — for
|
|
@@ -314,6 +321,28 @@ export interface ToolContributionSurfaces {
|
|
|
314
321
|
readonly Object3DAuthoring: import('react').ComponentType<ToolObject3DAuthoringProps>;
|
|
315
322
|
}
|
|
316
323
|
|
|
324
|
+
/**
|
|
325
|
+
* The live game a contribution is looking at, or `null` when nothing is
|
|
326
|
+
* playing.
|
|
327
|
+
*
|
|
328
|
+
* A dev-GUI contribution's whole subject is the RUNNING game — its stats,
|
|
329
|
+
* cheats, tuning handles and event stream all hang off the live `Game`, and
|
|
330
|
+
* there is no other door to it from a contribution (the editor's own state is
|
|
331
|
+
* not the game's). It is `unknown` deliberately: `@vgai/engine`'s `Game` is
|
|
332
|
+
* the project's dependency, not this package's, so a contribution narrows it
|
|
333
|
+
* with its own import rather than making every consumer of this SDK carry the
|
|
334
|
+
* engine's types.
|
|
335
|
+
*
|
|
336
|
+
* `instanceId` is the mount id the editor's runtime instruments are pointed
|
|
337
|
+
* at (the Inspect selector). With several seats live that is the ONLY thing
|
|
338
|
+
* distinguishing two mounts of the same project, so a contribution that
|
|
339
|
+
* caches per-game state keys it on this and re-reads when it changes.
|
|
340
|
+
*/
|
|
341
|
+
export interface ToolContributionPlay {
|
|
342
|
+
readonly game: unknown;
|
|
343
|
+
readonly instanceId: string;
|
|
344
|
+
}
|
|
345
|
+
|
|
317
346
|
export interface ToolContributionProps {
|
|
318
347
|
/** The exact registered callable this contribution presents. */
|
|
319
348
|
readonly tool: ProjectToolCatalogEntry;
|
|
@@ -325,13 +354,42 @@ export interface ToolContributionProps {
|
|
|
325
354
|
readonly surfaces: ToolContributionSurfaces;
|
|
326
355
|
/** Sanitized product-account projection. Never contains an access token. */
|
|
327
356
|
readonly account: GenerationAccountProjection;
|
|
357
|
+
/** The inspected play instance, or `null` while nothing is playing. */
|
|
358
|
+
readonly play: ToolContributionPlay | null;
|
|
328
359
|
/** Present when mounted as a workspace document contribution. */
|
|
329
360
|
readonly documentId?: string;
|
|
330
361
|
/** Whether that workspace document is the active center subject. */
|
|
331
362
|
readonly active?: boolean;
|
|
332
363
|
}
|
|
333
364
|
|
|
334
|
-
|
|
365
|
+
/**
|
|
366
|
+
* A `workspace.utility`'s props — the shared shape, except that `tool` may be
|
|
367
|
+
* absent.
|
|
368
|
+
*
|
|
369
|
+
* It is its own type rather than a relaxation of {@link ToolContributionProps}
|
|
370
|
+
* because only the two PRESENTING points earn the relaxation (this one and
|
|
371
|
+
* {@link ToolInspectorContributionProps}): a record-reading drawer panel (a
|
|
372
|
+
* log, a run timeline) presents what HAPPENED rather than one callable's
|
|
373
|
+
* output, so there is no honest name to put there and the loader stopped
|
|
374
|
+
* demanding a false one. Widening the shared props instead would hand every
|
|
375
|
+
* RUNNING contribution a `tool` it must now null-check while its own point
|
|
376
|
+
* still guarantees one.
|
|
377
|
+
*/
|
|
378
|
+
export interface ToolUtilityContributionProps extends Omit<ToolContributionProps, 'tool'> {
|
|
379
|
+
/** The callable this utility declared, when it declared one at all. */
|
|
380
|
+
readonly tool?: ProjectToolCatalogEntry;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* A `selection.inspector`'s props. `tool` is OPTIONAL here for the same reason
|
|
385
|
+
* it is on a utility: a section may present state the editor already has —
|
|
386
|
+
* readings, verbs a game registered as debug commands — and drive no single
|
|
387
|
+
* registered callable at all. A section that DOES commit through one still
|
|
388
|
+
* declares it and still gets the resolved entry.
|
|
389
|
+
*/
|
|
390
|
+
export interface ToolInspectorContributionProps extends Omit<ToolContributionProps, 'tool'> {
|
|
391
|
+
/** The callable this section declared, when it declared one at all. */
|
|
392
|
+
readonly tool?: ProjectToolCatalogEntry;
|
|
335
393
|
readonly node: ToolContributionNode | null;
|
|
336
394
|
readonly nodeId: string | null;
|
|
337
395
|
}
|
|
@@ -373,9 +431,31 @@ export type ToolGenerationResultContributionMatch = (
|
|
|
373
431
|
result: unknown,
|
|
374
432
|
) => boolean;
|
|
375
433
|
|
|
434
|
+
/**
|
|
435
|
+
* What the inspector is showing AROUND the node a `selection.inspector`
|
|
436
|
+
* contribution is being matched against.
|
|
437
|
+
*
|
|
438
|
+
* It exists for one question a `node`/`adapter` pair cannot answer: with
|
|
439
|
+
* NOTHING selected, `node` is `null` on every surface alike — an open Asset
|
|
440
|
+
* Lab document's empty state and the play surface's Game subject are the same
|
|
441
|
+
* two arguments. A contribution that matches `node === null` therefore matched
|
|
442
|
+
* BOTH, and the empty-state subject of whatever document happened to be open
|
|
443
|
+
* grew sections belonging to another surface entirely.
|
|
444
|
+
*
|
|
445
|
+
* `nullSubjectId` is the id of the empty-state subject actually being composed
|
|
446
|
+
* (`inspection/null-subject.ts`), so a contribution scopes itself POSITIVELY —
|
|
447
|
+
* `ctx.nullSubjectId === 'game'` — rather than by guessing from the adapter.
|
|
448
|
+
* It is `null` whenever a node IS selected, which is the honest answer: there
|
|
449
|
+
* is no empty-state subject in that composition.
|
|
450
|
+
*/
|
|
451
|
+
export interface ToolInspectorContributionMatchContext {
|
|
452
|
+
readonly nullSubjectId: string | null;
|
|
453
|
+
}
|
|
454
|
+
|
|
376
455
|
/** Optional named export required by `selection.inspector` contributions. */
|
|
377
456
|
export type ToolInspectorContributionMatch = (
|
|
378
457
|
node: ToolContributionNode | null,
|
|
379
458
|
/** Adapter-native API. Import its concrete type when a contribution needs it. */
|
|
380
459
|
adapter: unknown,
|
|
460
|
+
context: ToolInspectorContributionMatchContext,
|
|
381
461
|
) => boolean;
|
package/src/editor-view.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,7 +4,12 @@ export type {
|
|
|
4
4
|
GenerationJobStatus,
|
|
5
5
|
GenerationJobsDocument,
|
|
6
6
|
} from '@vgai/sdk/generations';
|
|
7
|
-
export type {
|
|
7
|
+
export type {
|
|
8
|
+
EditorEnvelopeObservation,
|
|
9
|
+
GameDebugDoor,
|
|
10
|
+
HistoryStep,
|
|
11
|
+
OpenedDocument,
|
|
12
|
+
} from './client.js';
|
|
8
13
|
export { EditorClient, EditorCommandError } from './client.js';
|
|
9
14
|
export type {
|
|
10
15
|
ToolAssetInspectorContributionMatch,
|
|
@@ -73,6 +78,7 @@ export type {
|
|
|
73
78
|
ProjectToolCatalogEntry,
|
|
74
79
|
ProjectToolContribution,
|
|
75
80
|
ProjectToolOutcome,
|
|
81
|
+
RagdollGenerationResult,
|
|
76
82
|
RecentProject,
|
|
77
83
|
ShadingMode,
|
|
78
84
|
ShotSetPoseRotation,
|
package/src/types.ts
CHANGED
|
@@ -9,15 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export type ViewportTab = 'edit' | 'play';
|
|
11
11
|
|
|
12
|
-
export type AssetKind =
|
|
13
|
-
| 'model'
|
|
14
|
-
| 'image'
|
|
15
|
-
| 'audio'
|
|
16
|
-
| 'animation'
|
|
17
|
-
| 'json'
|
|
18
|
-
| 'prefab'
|
|
19
|
-
| 'material'
|
|
20
|
-
| 'source';
|
|
12
|
+
export type AssetKind = 'model' | 'image' | 'audio' | 'animation' | 'json' | 'prefab' | 'source';
|
|
21
13
|
|
|
22
14
|
export interface HelperVisibility {
|
|
23
15
|
bounds: boolean;
|
|
@@ -58,6 +50,16 @@ export interface ViewportCapture {
|
|
|
58
50
|
mimeType: 'image/png';
|
|
59
51
|
}
|
|
60
52
|
|
|
53
|
+
/** Result of the editor's native Asset Lab ragdoll-generation operation. */
|
|
54
|
+
export interface RagdollGenerationResult {
|
|
55
|
+
capability: 'added' | 'present';
|
|
56
|
+
componentPath: string;
|
|
57
|
+
storyPath: string;
|
|
58
|
+
documentId: string;
|
|
59
|
+
bodies: number;
|
|
60
|
+
joints: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
61
63
|
/**
|
|
62
64
|
* Unit 4 (live-front-door wave) — the RUNNING GAME's pixels, as captured by
|
|
63
65
|
* the `bridge-screenshot` relay op (`command-listener.ts`'s
|
|
@@ -102,9 +104,6 @@ export interface GameCapture {
|
|
|
102
104
|
export interface GameplayRecordingOptions {
|
|
103
105
|
/** Requested real-time capture cadence. Defaults to 30; range 1–60. */
|
|
104
106
|
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
107
|
}
|
|
109
108
|
|
|
110
109
|
/** Facts fixed when a gameplay recording starts. */
|
|
@@ -383,6 +382,8 @@ export interface EditorState {
|
|
|
383
382
|
activeViewportTab: ViewportTab;
|
|
384
383
|
/** The actual active center document, including tool/source documents. */
|
|
385
384
|
activeDocumentId?: string | null;
|
|
385
|
+
/** Every center document the editor currently has open, by stable registry id. */
|
|
386
|
+
openDocumentIds?: string[];
|
|
386
387
|
activeTabKey: string;
|
|
387
388
|
showGrid: boolean;
|
|
388
389
|
showHelpers: boolean;
|
|
@@ -599,9 +600,13 @@ export interface EditorState {
|
|
|
599
600
|
projectName?: string | null;
|
|
600
601
|
/**
|
|
601
602
|
* The open project's ADAPTER, resolved (ARCHITECTURE-CORE §The editor
|
|
602
|
-
* protocol). `source: '
|
|
603
|
-
* supplied the binding table
|
|
604
|
-
*
|
|
603
|
+
* protocol). `source` names WHOSE declaration is running: `'project'` = the
|
|
604
|
+
* project's own `vgai.adapter.ts` supplied the binding table (and it always
|
|
605
|
+
* outranks the registry); `'registry'` = the HOST's in-tree ingest registry
|
|
606
|
+
* supplied it, matched on this project's ingest root id, with `modulePath`
|
|
607
|
+
* naming the repo file — a binding the project did not ship, stated rather
|
|
608
|
+
* than inferred; `'native'` = it declared none and got `nativeAdapter()` —
|
|
609
|
+
* the declared native default, not a silent fallback.
|
|
605
610
|
*
|
|
606
611
|
* `null`/absent means NOBODY HAS LOOKED YET (no project open, or the load
|
|
607
612
|
* has not finished), which is deliberately distinct from a loaded adapter
|
|
@@ -614,13 +619,11 @@ export interface EditorState {
|
|
|
614
619
|
* contract: everything in it has already been through JSON.
|
|
615
620
|
*/
|
|
616
621
|
adapter?: {
|
|
617
|
-
source: '
|
|
622
|
+
source: 'project' | 'registry' | 'native';
|
|
618
623
|
modulePath: string | null;
|
|
619
624
|
regions: {
|
|
620
625
|
id: string;
|
|
621
626
|
surface: string;
|
|
622
|
-
/** true = this region grades the game's own dev layer, not shipped content. */
|
|
623
|
-
dev: boolean;
|
|
624
627
|
projector: string;
|
|
625
628
|
dialect: string | null;
|
|
626
629
|
anchors: string[];
|
|
@@ -655,11 +658,11 @@ export type TransformSpace = 'world' | 'local';
|
|
|
655
658
|
* view. One runtime list owns both URL parsing and the public id type. */
|
|
656
659
|
export const EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS = [
|
|
657
660
|
'workspace:scene',
|
|
661
|
+
'workspace:canvas-scene',
|
|
658
662
|
'workspace:game',
|
|
659
663
|
'workspace:3d-components',
|
|
660
664
|
'workspace:2d-components',
|
|
661
665
|
'workspace:ui-components',
|
|
662
|
-
'workspace:dev',
|
|
663
666
|
'workspace:build-profiles',
|
|
664
667
|
'account',
|
|
665
668
|
'project-tools',
|
|
@@ -790,14 +793,17 @@ export interface InspectedField {
|
|
|
790
793
|
/** The value shown is the declared default — the document does not carry it. */
|
|
791
794
|
defaulted?: boolean;
|
|
792
795
|
readonly?: boolean;
|
|
796
|
+
/** The same reason shown by the Inspector and returned by a refused write. */
|
|
797
|
+
readonlyReason?: string;
|
|
793
798
|
resettable?: boolean;
|
|
794
799
|
revertsTo?: string;
|
|
795
800
|
group?: string;
|
|
796
801
|
options?: readonly unknown[];
|
|
797
802
|
}
|
|
798
803
|
|
|
799
|
-
/** A section's content.
|
|
800
|
-
*
|
|
804
|
+
/** A section's content. Custom RENDERING remains opaque — the wire never
|
|
805
|
+
* introspects React — while any ordinary descriptor channel that chrome owns
|
|
806
|
+
* remains visible as `fields`, including its write-refusal reasons. The two
|
|
801
807
|
* opaque kinds are distinguished because "this subject has a live preview"
|
|
802
808
|
* is a real fact about it: `custom` is a contributed block, `preview` is the
|
|
803
809
|
* subject's own square view of itself.
|
|
@@ -809,7 +815,13 @@ export interface InspectedField {
|
|
|
809
815
|
* quaternion behind them is not on this wire). */
|
|
810
816
|
export type InspectedSectionBody =
|
|
811
817
|
| { kind: 'fields'; fields: readonly InspectedField[] }
|
|
812
|
-
| {
|
|
818
|
+
| {
|
|
819
|
+
kind: 'custom';
|
|
820
|
+
id: string;
|
|
821
|
+
title: string;
|
|
822
|
+
data?: Record<string, unknown>;
|
|
823
|
+
fields?: readonly InspectedField[];
|
|
824
|
+
}
|
|
813
825
|
| { kind: 'preview'; id: string; title: string };
|
|
814
826
|
|
|
815
827
|
export interface InspectedSection {
|