@vgai/editor-sdk 0.5.2 → 0.5.3
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/README.md +6 -35
- package/package.json +2 -2
- package/src/client.ts +211 -18
- package/src/contributions.ts +20 -1
- package/src/extension.ts +2 -72
- package/src/index.ts +18 -7
- package/src/types.ts +290 -20
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@ to the caller.
|
|
|
11
11
|
```ts
|
|
12
12
|
import { EditorClient } from '@vgai/editor-sdk';
|
|
13
13
|
|
|
14
|
-
const editor = new EditorClient(); // default http://
|
|
15
|
-
const projectEditor = new EditorClient({ url: 'http://
|
|
14
|
+
const editor = new EditorClient(); // default http://127.0.0.1:20173
|
|
15
|
+
const projectEditor = new EditorClient({ url: 'http://127.0.0.1:25786' });
|
|
16
16
|
await editor.play();
|
|
17
17
|
|
|
18
18
|
// Present the same editor subject/view to the connected human and get a
|
|
@@ -69,7 +69,7 @@ import type { ToolContributionProps } from '@vgai/editor-sdk/contributions';
|
|
|
69
69
|
export default function MapBuilder({ tool, client, account }: ToolContributionProps) {
|
|
70
70
|
return (
|
|
71
71
|
<Button
|
|
72
|
-
variant="
|
|
72
|
+
variant="primary"
|
|
73
73
|
onClick={() => void client.runProjectTool(tool.name, { seed: 42 }, { confirm: true })}
|
|
74
74
|
title={`Default execution: ${account.preferredRoute}`}
|
|
75
75
|
>
|
|
@@ -99,7 +99,7 @@ utilities. Badges are for short statuses and counts, not headings.
|
|
|
99
99
|
### Extension contract (`@vgai/editor-sdk/extension`)
|
|
100
100
|
|
|
101
101
|
The typed contract for everything a game project contributes TO the editor —
|
|
102
|
-
|
|
102
|
+
three surfaces, one degradation ladder (`ExtensionContributionTier`:
|
|
103
103
|
`active` / `absent` / `failed`). An absent contribution hides its surface
|
|
104
104
|
(the editor never fabricates placeholder data); a failing one is contained
|
|
105
105
|
per-contribution and reported loudly on the editor console — never a crashed
|
|
@@ -110,36 +110,7 @@ editor, never silent fake output.
|
|
|
110
110
|
there is no parallel rail.
|
|
111
111
|
2. **Inspector sections** — `selection.inspector` / `asset.inspector` tool
|
|
112
112
|
contributions with an exported `match`.
|
|
113
|
-
3. **
|
|
114
|
-
`static editorGizmo: ComponentGizmoHook` (the Unity `OnDrawGizmos`
|
|
115
|
-
analog). The editor renders its objects for every scene entity carrying
|
|
116
|
-
that component — editor-only layer, Helpers ▸ Components toggle,
|
|
117
|
-
raycast-excluded, draw-only. Props arrive schema-parsed through the
|
|
118
|
-
component's own Zod `static schema`; build objects with `ctx.three` (the
|
|
119
|
-
editor's three module):
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
import type { ComponentGizmoHook } from '@vgai/editor-sdk/extension';
|
|
123
|
-
|
|
124
|
-
export class TriggerZone extends GameComponent {
|
|
125
|
-
static schema = z.object({ radius: z.number().default(2) });
|
|
126
|
-
static editorGizmo: ComponentGizmoHook = ({ props, three }) => {
|
|
127
|
-
const geo = new three.RingGeometry(0.98 * (props.radius as number), props.radius as number);
|
|
128
|
-
return new three.Mesh(geo, new three.MeshBasicMaterial({ wireframe: true }));
|
|
129
|
-
};
|
|
130
|
-
update() {}
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
The scaffold template's `SceneCamera` (`src/scripts/components/
|
|
135
|
-
scene-camera.ts`) is the worked reference.
|
|
136
|
-
|
|
137
|
-
Editing a component file in edit mode re-captures the registry and
|
|
138
|
-
redraws contributed gizmos (script-HMR); registry restructures may need a
|
|
139
|
-
project reopen. Recorded deferral: in the HOSTED (browser) editor,
|
|
140
|
-
contributed component gizmos resolve `absent` — example registries are
|
|
141
|
-
evaluated for play but not captured at design time.
|
|
142
|
-
4. **System adapters** — runtime capabilities registered from game code via
|
|
113
|
+
3. **System adapters** — runtime capabilities registered from game code via
|
|
143
114
|
`ctx.registerSystemAdapter?.(kind, impl)` (`SystemAdapters` in
|
|
144
115
|
`@vgai/engine`). Deliberately not re-exported here: the engine already
|
|
145
116
|
publishes that seam and every consumer of it also imports the engine.
|
|
@@ -168,7 +139,7 @@ buffers and images must resolve on the same project origin. The thin CLI
|
|
|
168
139
|
equivalent writes the four views and contact sheet to disk:
|
|
169
140
|
|
|
170
141
|
```bash
|
|
171
|
-
npm run vgai --
|
|
142
|
+
npm run vgai -- screenshot /models/storefront.glb --out artifacts/storefront
|
|
172
143
|
```
|
|
173
144
|
|
|
174
145
|
## Limitations
|
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.3",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@types/three": "^0.180.0",
|
|
25
|
-
"@vgai/sdk": "0.5.
|
|
25
|
+
"@vgai/sdk": "0.5.3"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": "^19.0.0",
|
package/src/client.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
EditorView,
|
|
13
13
|
GameCapture,
|
|
14
14
|
HelperVisibility,
|
|
15
|
+
InspectedInspection,
|
|
15
16
|
LabeledShotSetCapture,
|
|
16
17
|
PresentedEditorView,
|
|
17
18
|
ProjectInfo,
|
|
@@ -20,31 +21,111 @@ import type {
|
|
|
20
21
|
ProjectToolOutcome,
|
|
21
22
|
RecentProject,
|
|
22
23
|
ShadingMode,
|
|
24
|
+
StoryCaptureOptions,
|
|
25
|
+
StoryVariantCapture,
|
|
23
26
|
TransformMode,
|
|
24
27
|
TransformSpace,
|
|
25
28
|
Vec3Value,
|
|
26
29
|
ViewPreset,
|
|
27
30
|
ViewportCapture,
|
|
31
|
+
ViewportTab,
|
|
28
32
|
} from './types.js';
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
// The editor's default origin, spelled out because this package deliberately
|
|
35
|
+
// does not depend on `@vgai/engine`. `DEFAULT_EDITOR_PORT` in
|
|
36
|
+
// `packages/engine/src/manifest/editor-port.ts` is the owner of the number and
|
|
37
|
+
// of the never-`localhost` rule; keep this in step with it.
|
|
38
|
+
const DEFAULT_URL = 'http://127.0.0.1:20173';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A refused editor command, carrying the relay's own STRUCTURED failure code
|
|
42
|
+
* alongside the prose.
|
|
43
|
+
*
|
|
44
|
+
* `/__editor/command` has always answered `{ ok: false, error, code }` and
|
|
45
|
+
* this client has always dropped the `code` on the floor, so every caller that
|
|
46
|
+
* wanted to react to a specific refusal had to substring-match an English
|
|
47
|
+
* sentence. `vgai screenshot`'s hidden-tab fallback is the first caller that
|
|
48
|
+
* genuinely must branch (`BRIDGE_SCREENSHOT_STALE` has a working recovery;
|
|
49
|
+
* "not in play mode" does not), and a fallback keyed on prose would fire on
|
|
50
|
+
* the wrong failure the first time someone rewords the message.
|
|
51
|
+
*/
|
|
52
|
+
export class EditorCommandError extends Error {
|
|
53
|
+
readonly code: string | undefined;
|
|
54
|
+
constructor(message: string, code?: string | undefined) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = 'EditorCommandError';
|
|
57
|
+
this.code = code;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The GAME DEBUG PLANE, as a contribution's client sees it.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately the same two words `@vgai/live`'s session binding uses
|
|
65
|
+
* (`game.state(name)` / `game.command(name, ...args)`), because it is the same
|
|
66
|
+
* plane: whatever the running game registered through `ctx.debug` — a provider
|
|
67
|
+
* read by name, a command invoked by name. A tool contribution that wants the
|
|
68
|
+
* game's own vitals in its panel has this door and no other; there is
|
|
69
|
+
* deliberately no per-capability method, because a game names its own
|
|
70
|
+
* providers and commands.
|
|
71
|
+
*
|
|
72
|
+
* Both legs reject LOUDLY (`EditorCommandError`) rather than answer with a
|
|
73
|
+
* placeholder: play not running is `'not in play mode — start play before
|
|
74
|
+
* using the debug seam'`, and an unregistered command carries
|
|
75
|
+
* `code: 'DEBUG_COMMAND_NOT_REGISTERED'`. A panel decides what to show for
|
|
76
|
+
* those; the client never invents one.
|
|
77
|
+
*/
|
|
78
|
+
export interface GameDebugDoor {
|
|
79
|
+
/**
|
|
80
|
+
* Read ONE registered state provider by name (`'bot.tester'`). `undefined`
|
|
81
|
+
* when the running game registered no such provider — or no debug adapter at
|
|
82
|
+
* all, which is an honest answer rather than a refusal.
|
|
83
|
+
*/
|
|
84
|
+
state(name: string): Promise<unknown>;
|
|
85
|
+
/** Invoke ONE registered debug command by name, with its own arguments. */
|
|
86
|
+
command(name: string, ...args: unknown[]): Promise<unknown>;
|
|
87
|
+
}
|
|
31
88
|
|
|
32
89
|
export class EditorClient {
|
|
33
90
|
private readonly baseUrl: string;
|
|
34
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The running game's debug plane — see {@link GameDebugDoor}. It rides the
|
|
94
|
+
* SAME `/__editor/command` relay every other method here uses (relay cases
|
|
95
|
+
* `inspect-gameplay-state` / `invoke-debug-command`, `command-listener.ts`),
|
|
96
|
+
* so a tool contribution reaches the game through the client it already has
|
|
97
|
+
* rather than a second channel of its own.
|
|
98
|
+
*/
|
|
99
|
+
readonly game: GameDebugDoor;
|
|
100
|
+
|
|
35
101
|
constructor(opts?: { url?: string }) {
|
|
36
102
|
if (opts !== undefined && (typeof opts !== 'object' || opts === null || Array.isArray(opts))) {
|
|
37
103
|
throw new TypeError(
|
|
38
|
-
'EditorClient options must be an object. Use new EditorClient({ url: "http://
|
|
104
|
+
'EditorClient options must be an object. Use new EditorClient({ url: "http://127.0.0.1:20173" }), not new EditorClient("...").',
|
|
39
105
|
);
|
|
40
106
|
}
|
|
41
107
|
const unknownOptions = Object.keys(opts ?? {}).filter((key) => key !== 'url');
|
|
42
108
|
if (unknownOptions.length > 0) {
|
|
43
109
|
throw new Error(
|
|
44
|
-
`EditorClient: unknown option${unknownOptions.length === 1 ? '' : 's'} ${unknownOptions.map((key) => `"${key}"`).join(', ')}. Use { url: "http://
|
|
110
|
+
`EditorClient: unknown option${unknownOptions.length === 1 ? '' : 's'} ${unknownOptions.map((key) => `"${key}"`).join(', ')}. Use { url: "http://127.0.0.1:<port>" } to target an editor.`,
|
|
45
111
|
);
|
|
46
112
|
}
|
|
47
113
|
this.baseUrl = (opts?.url ?? DEFAULT_URL).replace(/\/$/, '');
|
|
114
|
+
this.game = {
|
|
115
|
+
state: async (name: string): Promise<unknown> => {
|
|
116
|
+
// `keys` narrows the relay to the one provider asked for, so a panel
|
|
117
|
+
// polling one vital never drags the whole plane's `stateAll()` across
|
|
118
|
+
// the wire. A game with no debug adapter answers `{ state: null }`.
|
|
119
|
+
const data = await this.command<{ state: Record<string, unknown> | null }>({
|
|
120
|
+
type: 'inspect-gameplay-state',
|
|
121
|
+
keys: [name],
|
|
122
|
+
});
|
|
123
|
+
return data.state?.[name];
|
|
124
|
+
},
|
|
125
|
+
command: async (name: string, ...args: unknown[]): Promise<unknown> =>
|
|
126
|
+
(await this.command<{ result: unknown }>({ type: 'invoke-debug-command', name, args }))
|
|
127
|
+
.result,
|
|
128
|
+
};
|
|
48
129
|
}
|
|
49
130
|
|
|
50
131
|
private async command<T extends object = Record<string, never>>(
|
|
@@ -55,9 +136,9 @@ export class EditorClient {
|
|
|
55
136
|
headers: { 'Content-Type': 'application/json' },
|
|
56
137
|
body: JSON.stringify(body),
|
|
57
138
|
});
|
|
58
|
-
const data = (await res.json()) as { ok: boolean; error?: string } & T;
|
|
139
|
+
const data = (await res.json()) as { ok: boolean; error?: string; code?: string } & T;
|
|
59
140
|
if (!data.ok) {
|
|
60
|
-
throw new
|
|
141
|
+
throw new EditorCommandError(data.error ?? `Editor command failed: ${res.status}`, data.code);
|
|
61
142
|
}
|
|
62
143
|
return data;
|
|
63
144
|
}
|
|
@@ -118,6 +199,17 @@ export class EditorClient {
|
|
|
118
199
|
await this.command({ type: 'focus-selection' });
|
|
119
200
|
}
|
|
120
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Frame the EDIT viewport camera on one entity — the strict sibling of
|
|
204
|
+
* {@link focusEntity}. Same framing; an id the scene does not know is a
|
|
205
|
+
* refusal naming the id (`EditorCommandError`, code `ENTITY_NOT_FOUND`)
|
|
206
|
+
* rather than `focusEntity`'s silent no-op, so a caller that frames an
|
|
207
|
+
* entity before capturing it cannot photograph the wrong thing.
|
|
208
|
+
*/
|
|
209
|
+
async frameEntity(id: string): Promise<void> {
|
|
210
|
+
await this.command({ type: 'frame-entity', id });
|
|
211
|
+
}
|
|
212
|
+
|
|
121
213
|
async viewPreset(preset: ViewPreset): Promise<void> {
|
|
122
214
|
await this.command({ type: 'view-preset', preset });
|
|
123
215
|
}
|
|
@@ -142,7 +234,7 @@ export class EditorClient {
|
|
|
142
234
|
/**
|
|
143
235
|
* Unit 4 (live-front-door wave) — capture the RUNNING GAME (`vgai
|
|
144
236
|
* screenshot`'s wire leg). Sends the SAME `bridge-screenshot` relay op
|
|
145
|
-
* `@vgai/
|
|
237
|
+
* `@vgai/live`'s `RelayTransport.screenshot` (and therefore
|
|
146
238
|
* `game.screenshot()` on the relay path) already sends, so all three
|
|
147
239
|
* surfaces composite the identical full game stack — canvas(es) plus the
|
|
148
240
|
* HUD/react DOM layers — rather than any of them inventing a second,
|
|
@@ -154,10 +246,34 @@ export class EditorClient {
|
|
|
154
246
|
* mode isn't running ("not in play mode — start play before using the
|
|
155
247
|
* debug seam") or no game canvas is mounted yet. Never returns a blank or
|
|
156
248
|
* editor-only frame as a stand-in.
|
|
249
|
+
*
|
|
250
|
+
* `opts.refreshHiddenFrame` is the hidden-tab leg: a backgrounded tab
|
|
251
|
+
* pauses the loop, so the canvas holds a provably stale frame and the relay
|
|
252
|
+
* refuses it with `BRIDGE_SCREENSHOT_STALE` rather than pass it off as
|
|
253
|
+
* current. Setting this asks the relay to render exactly ONE deterministic
|
|
254
|
+
* tick (`runTicks(1, {render:'last'})`) first — the same escape
|
|
255
|
+
* `@vgai/live`'s `RelayTransport.screenshot` has always used, which is why
|
|
256
|
+
* `vgai eval` could capture hidden frames while `vgai screenshot` could not.
|
|
257
|
+
* Off by default: a caller who does not ask must never be handed a frame
|
|
258
|
+
* that only exists because the capture drove the game.
|
|
259
|
+
*
|
|
260
|
+
* `opts.devLayers` puts the game's DEV LAYERS (`dev: true` roots — its own
|
|
261
|
+
* in-game dev GUI) INTO the frame. Also off by default, for the mirror-image
|
|
262
|
+
* reason: a panel frame at the moment of a failure is strong evidence, and a
|
|
263
|
+
* "does the game look right" frame with the panel painted over it is
|
|
264
|
+
* worthless — so the contamination only happens when someone asks for it.
|
|
157
265
|
*/
|
|
158
|
-
async captureGame(
|
|
159
|
-
|
|
266
|
+
async captureGame(opts?: {
|
|
267
|
+
refreshHiddenFrame?: boolean;
|
|
268
|
+
devLayers?: boolean;
|
|
269
|
+
}): Promise<GameCapture> {
|
|
270
|
+
const data = await this.command<GameCapture>({
|
|
271
|
+
type: 'bridge-screenshot',
|
|
272
|
+
...(opts?.refreshHiddenFrame === true ? { refreshHiddenFrame: true } : {}),
|
|
273
|
+
...(opts?.devLayers === true ? { devLayers: true } : {}),
|
|
274
|
+
});
|
|
160
275
|
const layers = data.layers;
|
|
276
|
+
const flatness = data.flatness;
|
|
161
277
|
return {
|
|
162
278
|
base64: data.base64,
|
|
163
279
|
mimeType: data.mimeType,
|
|
@@ -165,6 +281,11 @@ export class EditorClient {
|
|
|
165
281
|
...(layers && Number.isInteger(layers.canvases) && Number.isInteger(layers.domOverlays)
|
|
166
282
|
? { layers }
|
|
167
283
|
: {}),
|
|
284
|
+
// Pass the pixel-honesty fields through as the page reported them: the
|
|
285
|
+
// warning sentence is written where the pixels are, so nothing here
|
|
286
|
+
// re-derives (or softens) it.
|
|
287
|
+
...(flatness && typeof flatness.dominantFraction === 'number' ? { flatness } : {}),
|
|
288
|
+
...(data.hiddenFrame === true ? { hiddenFrame: true } : {}),
|
|
168
289
|
};
|
|
169
290
|
}
|
|
170
291
|
|
|
@@ -191,7 +312,7 @@ export class EditorClient {
|
|
|
191
312
|
}
|
|
192
313
|
|
|
193
314
|
/**
|
|
194
|
-
* A project-defined labeled shot set (`vgai
|
|
315
|
+
* A project-defined labeled shot set (`vgai screenshot <target> --shots <set>`):
|
|
195
316
|
* the DEFINITION travels with the command (project data — see
|
|
196
317
|
* `AssetPreviewShotSetDefinition`; the CLI resolves it from the registered
|
|
197
318
|
* `project.<set>.previewShots` tool), and the editor's generic
|
|
@@ -215,13 +336,15 @@ export class EditorClient {
|
|
|
215
336
|
width: data.width,
|
|
216
337
|
height: data.height,
|
|
217
338
|
shots: data.shots,
|
|
339
|
+
// An older editor predates the empty-frame guard and sends none.
|
|
340
|
+
warnings: data.warnings ?? [],
|
|
218
341
|
contactSheet: data.contactSheet,
|
|
219
342
|
};
|
|
220
343
|
}
|
|
221
344
|
|
|
222
345
|
/**
|
|
223
|
-
* B8.4 — score the asset against a reference GLB (`vgai
|
|
224
|
-
* --compare <ref.glb>`): matched orthographic front + side silhouettes
|
|
346
|
+
* B8.4 — score the asset against a reference GLB (`vgai screenshot
|
|
347
|
+
* <model.glb> --compare <ref.glb>`): matched orthographic front + side silhouettes
|
|
225
348
|
* (equal-height bounding-box framing, both yaw-normalized to face the
|
|
226
349
|
* camera), per-view IoU numbers, and overlay evidence images. The
|
|
227
350
|
* reference GLB's raw bytes travel base64 in the command; the editor
|
|
@@ -242,9 +365,38 @@ export class EditorClient {
|
|
|
242
365
|
return { width: data.width, height: data.height, views: data.views };
|
|
243
366
|
}
|
|
244
367
|
|
|
368
|
+
/**
|
|
369
|
+
* The STORY lane (`vgai screenshot <file>.stories.tsx`): every CSF export of
|
|
370
|
+
* one project story file rendered in the live session's DOM and captured
|
|
371
|
+
* through the same composite leg {@link captureGame} uses, returned as
|
|
372
|
+
* per-export images plus one variant sheet. `options.story` narrows to a
|
|
373
|
+
* single export.
|
|
374
|
+
*
|
|
375
|
+
* Rendering happens in the EDITOR — the SDK never imports, composes or
|
|
376
|
+
* mounts a CSF module itself; the session already owns that machinery for
|
|
377
|
+
* its Stories panel and this drives it.
|
|
378
|
+
*/
|
|
379
|
+
async captureStoryVariants(
|
|
380
|
+
modulePath: string,
|
|
381
|
+
options: StoryCaptureOptions = {},
|
|
382
|
+
): Promise<StoryVariantCapture> {
|
|
383
|
+
const data = await this.command<StoryVariantCapture>({
|
|
384
|
+
type: 'capture-story-variants',
|
|
385
|
+
modulePath,
|
|
386
|
+
...options,
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
modulePath: data.modulePath,
|
|
390
|
+
width: data.width,
|
|
391
|
+
height: data.height,
|
|
392
|
+
variants: data.variants,
|
|
393
|
+
contactSheet: data.contactSheet,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
245
397
|
// --- Panels ---
|
|
246
398
|
|
|
247
|
-
async showViewport(tab:
|
|
399
|
+
async showViewport(tab: ViewportTab): Promise<void> {
|
|
248
400
|
await this.command({ type: 'viewport-tab', tab });
|
|
249
401
|
}
|
|
250
402
|
|
|
@@ -252,6 +404,18 @@ export class EditorClient {
|
|
|
252
404
|
await this.command({ type: 'active-tab', key: '__inspector__' });
|
|
253
405
|
}
|
|
254
406
|
|
|
407
|
+
/** Show several instances of the running game split-screen — multiplayer
|
|
408
|
+
* authoring. Pass a total `count` (default "Player N" labels) or an array of
|
|
409
|
+
* `names` (its length is the count; index 0 is the primary). Requires a live
|
|
410
|
+
* play session. */
|
|
411
|
+
async setInstanceCount(countOrNames: number | string[]): Promise<void> {
|
|
412
|
+
await this.command(
|
|
413
|
+
Array.isArray(countOrNames)
|
|
414
|
+
? { type: 'set-instance-count', names: countOrNames }
|
|
415
|
+
: { type: 'set-instance-count', count: countOrNames },
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
255
419
|
async openAsset(path: string, kind: AssetKind): Promise<void> {
|
|
256
420
|
await this.command({ type: 'open-asset-tab', path, kind });
|
|
257
421
|
}
|
|
@@ -278,6 +442,27 @@ export class EditorClient {
|
|
|
278
442
|
return { view: presented.view, url: presented.url, warnings: presented.warnings };
|
|
279
443
|
}
|
|
280
444
|
|
|
445
|
+
/**
|
|
446
|
+
* The INSPECTION SUBJECT the editor is showing right now, as data — the
|
|
447
|
+
* serialized projection of the inspection model (design:
|
|
448
|
+
* `docs/ARCHITECTURE-CORE.md` §Editor chrome, "The Inspection Model").
|
|
449
|
+
*
|
|
450
|
+
* The same subject a human reads in the inspector: identity, presentation,
|
|
451
|
+
* verbs, and the identified sections in display order — with a `fields`
|
|
452
|
+
* section's CURRENT VALUES read through the same io the field rows edit
|
|
453
|
+
* through. With nothing selected it answers the active surface's own
|
|
454
|
+
* no-selection subject when it has one, exactly as the panel does; it never
|
|
455
|
+
* reports another surface's, and when the panel itself is unmounted it
|
|
456
|
+
* answers `{none: true}` rather than a subject nobody is looking at. A
|
|
457
|
+
* `custom` section body is a named opaque (`{kind, id, title}`) — the editor
|
|
458
|
+
* renders those with React — plus its displayed values under `data` when it
|
|
459
|
+
* has any (the Transform section's position/rotation/scale).
|
|
460
|
+
*/
|
|
461
|
+
async inspect(): Promise<InspectedInspection> {
|
|
462
|
+
const data = await this.command<{ subject: InspectedInspection }>({ type: 'inspect' });
|
|
463
|
+
return data.subject;
|
|
464
|
+
}
|
|
465
|
+
|
|
281
466
|
/** Read the editor's actual current durable projection. */
|
|
282
467
|
async currentView(): Promise<EditorView> {
|
|
283
468
|
const data = await this.command<{ view: EditorView }>({ type: 'current-view' });
|
|
@@ -329,10 +514,10 @@ export class EditorClient {
|
|
|
329
514
|
}
|
|
330
515
|
|
|
331
516
|
// --- Scene ---
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
517
|
+
//
|
|
518
|
+
// `openScene(path)` lived here, relaying `{type:'open-scene', path}`. That
|
|
519
|
+
// verb now rejects — the `.vscn.json` format it opened is deleted, and a three
|
|
520
|
+
// root is opened by activating its root.
|
|
336
521
|
|
|
337
522
|
// --- Project management ---
|
|
338
523
|
|
|
@@ -397,12 +582,20 @@ export class EditorClient {
|
|
|
397
582
|
async runProjectTool(
|
|
398
583
|
name: string,
|
|
399
584
|
input: unknown = {},
|
|
400
|
-
options: { confirm?: boolean } = {},
|
|
585
|
+
options: { confirm?: boolean; instance?: string } = {},
|
|
401
586
|
): Promise<ProjectToolOutcome> {
|
|
402
587
|
const res = await fetch(`${this.baseUrl}/__editor/project-tools/run`, {
|
|
403
588
|
method: 'POST',
|
|
404
589
|
headers: { 'Content-Type': 'application/json' },
|
|
405
|
-
body: JSON.stringify({
|
|
590
|
+
body: JSON.stringify({
|
|
591
|
+
name,
|
|
592
|
+
input,
|
|
593
|
+
confirm: options.confirm === true,
|
|
594
|
+
// Omitted (not null) when unset — the wire body is JSON and the tool
|
|
595
|
+
// host reads absence as "the sole instance", same convention as the
|
|
596
|
+
// relay's `instance`.
|
|
597
|
+
...(options.instance !== undefined ? { instance: options.instance } : {}),
|
|
598
|
+
}),
|
|
406
599
|
});
|
|
407
600
|
const body = (await res.json()) as ProjectToolOutcome;
|
|
408
601
|
if (!body || typeof body !== 'object' || typeof body.ok !== 'boolean') {
|
package/src/contributions.ts
CHANGED
|
@@ -26,7 +26,6 @@ export interface ToolContributionNode {
|
|
|
26
26
|
readonly kind: string;
|
|
27
27
|
readonly parentId: string | null;
|
|
28
28
|
readonly childIds: string[];
|
|
29
|
-
readonly flags: object;
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
/**
|
|
@@ -97,8 +96,28 @@ export interface ToolObject3DPreviewProps {
|
|
|
97
96
|
export interface ToolObject3DAuthoringProps {
|
|
98
97
|
readonly documentId: string;
|
|
99
98
|
readonly sourcePath: string;
|
|
99
|
+
/**
|
|
100
|
+
* Produce the graph to author. Called ONCE PER ACTIVATION, not once per
|
|
101
|
+
* document: the host tears its scene down whenever the document goes inactive
|
|
102
|
+
* (an ordinary tab switch) and calls this again on the way back, disposing
|
|
103
|
+
* whatever the previous call returned. So a caller must pick one of two
|
|
104
|
+
* shapes, and there is no third:
|
|
105
|
+
*
|
|
106
|
+
* - a FACTORY — build a fresh graph every call, and let the returned
|
|
107
|
+
* `dispose` free it (what the model/entity asset documents do); or
|
|
108
|
+
* - an OWNED graph — return the same root every call with an EMPTY `dispose`,
|
|
109
|
+
* and free it from the caller's own lifetime instead (what the 3D
|
|
110
|
+
* components board and the story turntable do, because their graph is
|
|
111
|
+
* async to create).
|
|
112
|
+
*
|
|
113
|
+
* Returning a graph you cannot rebuild synchronously AND a `dispose` that
|
|
114
|
+
* really frees it is the third shape, and it renders a black panel on the
|
|
115
|
+
* second activation.
|
|
116
|
+
*/
|
|
100
117
|
readonly build: () => ToolObject3DPreviewSource;
|
|
101
118
|
readonly displayName?: string;
|
|
119
|
+
/** Explicit flat scene background. Omit it to inherit the editor's standard
|
|
120
|
+
* viewport dressing (environment, gradient backdrop, key light). */
|
|
102
121
|
readonly background?: ColorRepresentation;
|
|
103
122
|
readonly cameraDirection?: readonly [number, number, number];
|
|
104
123
|
readonly autoplay?: boolean;
|
package/src/extension.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Extension contract (W3a) — the published surface a game project uses to
|
|
3
3
|
* contribute to the editor, with the degradation ladder made API.
|
|
4
4
|
*
|
|
5
|
-
* A project extends the editor through exactly
|
|
5
|
+
* A project extends the editor through exactly three surfaces:
|
|
6
6
|
*
|
|
7
7
|
* (a) **Editor panels** — registered into the Dockview workspace as documents
|
|
8
8
|
* or utilities via a tool contribution (`package.json#vgai.tools` →
|
|
@@ -12,11 +12,7 @@
|
|
|
12
12
|
* (b) **Inspector sections** — `point: 'selection.inspector'` /
|
|
13
13
|
* `'asset.inspector'` tool contributions with an exported `match`.
|
|
14
14
|
* Types: `ToolInspectorContributionProps` and friends, same module.
|
|
15
|
-
* (c) **
|
|
16
|
-
* subclass declares `static editorGizmo: ComponentGizmoHook`; the editor
|
|
17
|
-
* renders its objects for every scene entity carrying that component,
|
|
18
|
-
* on the editor-only layer, alongside the built-in gizmos.
|
|
19
|
-
* (d) **System adapters** — runtime capabilities (networking, debug, …)
|
|
15
|
+
* (c) **System adapters** — runtime capabilities (networking, debug, …)
|
|
20
16
|
* registered from game code via `ctx.registerSystemAdapter?.(kind, impl)`
|
|
21
17
|
* (`SystemAdapters` in `@vgai/engine`'s `runtime/types` /
|
|
22
18
|
* `adapter/system-adapter`). Deliberately NOT re-exported here: the
|
|
@@ -36,74 +32,8 @@
|
|
|
36
32
|
* console) — never silently swallowed into fake output.
|
|
37
33
|
*/
|
|
38
34
|
|
|
39
|
-
import type * as THREE from 'three';
|
|
40
|
-
|
|
41
35
|
/**
|
|
42
36
|
* Capability tier of a single extension contribution — the degradation
|
|
43
37
|
* ladder made API (see the module doc above for the exact semantics).
|
|
44
38
|
*/
|
|
45
39
|
export type ExtensionContributionTier = 'active' | 'absent' | 'failed';
|
|
46
|
-
|
|
47
|
-
/** Resolved status of one per-component gizmo contribution. */
|
|
48
|
-
export interface ComponentGizmoStatus {
|
|
49
|
-
tier: ExtensionContributionTier;
|
|
50
|
-
/** Present only for `'failed'` — the contained error's message. */
|
|
51
|
-
error?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* What a component gizmo hook receives. Draw with `ctx.three` — the EDITOR's
|
|
56
|
-
* three module — so returned objects render in the editor viewport
|
|
57
|
-
* regardless of which physical `three` copy the project itself resolves.
|
|
58
|
-
*/
|
|
59
|
-
export interface ComponentGizmoContext {
|
|
60
|
-
/** The component's registry name (the key in `entity.components`). */
|
|
61
|
-
componentName: string;
|
|
62
|
-
/**
|
|
63
|
-
* The entity's authored component data. When the component class declares
|
|
64
|
-
* a `static schema` (Zod), this is the schema-parsed value — defaults
|
|
65
|
-
* applied, invalid data already rejected (a parse failure fails the
|
|
66
|
-
* contribution loudly instead of fabricating defaults).
|
|
67
|
-
*/
|
|
68
|
-
props: Record<string, unknown>;
|
|
69
|
-
/** The editor's `three` module — build all gizmo objects with this. */
|
|
70
|
-
three: typeof THREE;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Per-component viewport gizmo contribution (critique O11 — the
|
|
75
|
-
* `OnDrawGizmos` half of the extension contract). Declared as a STATIC on a
|
|
76
|
-
* `GameComponent` subclass:
|
|
77
|
-
*
|
|
78
|
-
* ```ts
|
|
79
|
-
* import type { ComponentGizmoHook } from '@vgai/editor-sdk/extension';
|
|
80
|
-
*
|
|
81
|
-
* export class TriggerZone extends GameComponent {
|
|
82
|
-
* static schema = z.object({ radius: z.number().default(2) });
|
|
83
|
-
* static editorGizmo: ComponentGizmoHook = ({ props, three }) => {
|
|
84
|
-
* const ring = new three.LineLoop(circleGeometry(props.radius as number),
|
|
85
|
-
* new three.LineBasicMaterial({ color: 0x44ff88 }));
|
|
86
|
-
* return ring;
|
|
87
|
-
* };
|
|
88
|
-
* // …
|
|
89
|
-
* }
|
|
90
|
-
* ```
|
|
91
|
-
*
|
|
92
|
-
* Contract:
|
|
93
|
-
* - Runs at DESIGN TIME in the editor (never in the running game). Called
|
|
94
|
-
* whenever the editor (re)builds gizmos for an entity carrying the
|
|
95
|
-
* component; treat it as pure — read `props`, return fresh objects.
|
|
96
|
-
* - Returned objects are parented to the entity's `Object3D` (local space),
|
|
97
|
-
* tagged as editor helpers on the editor-only render layer, excluded from
|
|
98
|
-
* raycast selection, and toggled by the viewport's Helpers ▸ Components
|
|
99
|
-
* filter. The editor disposes them with the entity's other gizmos.
|
|
100
|
-
* - Return `null` (or an empty array) to draw nothing for this entity.
|
|
101
|
-
* - Draw-only, v1: gizmos visualize; they do not edit. Interactive handles
|
|
102
|
-
* stay editor-internal until a consumer needs them (no dead surface).
|
|
103
|
-
* - Throwing (or returning a non-Object3D) marks the contribution
|
|
104
|
-
* `'failed'`: contained, hidden, reported loudly. The editor never
|
|
105
|
-
* substitutes a fallback drawing.
|
|
106
|
-
*/
|
|
107
|
-
export type ComponentGizmoHook = (
|
|
108
|
-
ctx: ComponentGizmoContext,
|
|
109
|
-
) => THREE.Object3D | readonly THREE.Object3D[] | null;
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,8 @@ export type {
|
|
|
4
4
|
GenerationJobStatus,
|
|
5
5
|
GenerationJobsDocument,
|
|
6
6
|
} from '@vgai/sdk/generations';
|
|
7
|
-
export {
|
|
7
|
+
export type { GameDebugDoor } from './client.js';
|
|
8
|
+
export { EditorClient, EditorCommandError } from './client.js';
|
|
8
9
|
export type {
|
|
9
10
|
ToolAssetInspectorContributionMatch,
|
|
10
11
|
ToolAssetInspectorContributionProps,
|
|
@@ -16,12 +17,7 @@ export type {
|
|
|
16
17
|
ToolInspectorContributionProps,
|
|
17
18
|
} from './contributions.js';
|
|
18
19
|
export { editorViewFromUrl, editorViewUrl } from './editor-view.js';
|
|
19
|
-
export type {
|
|
20
|
-
ComponentGizmoContext,
|
|
21
|
-
ComponentGizmoHook,
|
|
22
|
-
ComponentGizmoStatus,
|
|
23
|
-
ExtensionContributionTier,
|
|
24
|
-
} from './extension.js';
|
|
20
|
+
export type { ExtensionContributionTier } from './extension.js';
|
|
25
21
|
export type {
|
|
26
22
|
ActiveDocumentCapture,
|
|
27
23
|
AssetCompareCapture,
|
|
@@ -32,7 +28,9 @@ export type {
|
|
|
32
28
|
AssetPreviewCapture,
|
|
33
29
|
AssetPreviewOptions,
|
|
34
30
|
AssetPreviewShotSetDefinition,
|
|
31
|
+
AssetPreviewShotWarning,
|
|
35
32
|
AssetPreviewSource,
|
|
33
|
+
AssetPreviewStage,
|
|
36
34
|
AssetPreviewView,
|
|
37
35
|
EditorCameraState,
|
|
38
36
|
EditorEntitySummary,
|
|
@@ -41,6 +39,15 @@ export type {
|
|
|
41
39
|
EditorViewDocument,
|
|
42
40
|
GameCapture,
|
|
43
41
|
HelperVisibility,
|
|
42
|
+
InspectedAction,
|
|
43
|
+
InspectedField,
|
|
44
|
+
InspectedInspection,
|
|
45
|
+
InspectedNothing,
|
|
46
|
+
InspectedSection,
|
|
47
|
+
InspectedSectionBody,
|
|
48
|
+
InspectedSubject,
|
|
49
|
+
InspectionPresentationKind,
|
|
50
|
+
InspectionSurface,
|
|
44
51
|
LabeledShotSetCapture,
|
|
45
52
|
PresentedEditorView,
|
|
46
53
|
ProjectInfo,
|
|
@@ -53,9 +60,13 @@ export type {
|
|
|
53
60
|
ShadingMode,
|
|
54
61
|
ShotSetPoseRotation,
|
|
55
62
|
ShotSetShot,
|
|
63
|
+
StoryCaptureOptions,
|
|
64
|
+
StoryVariantCapture,
|
|
65
|
+
StoryVariantImage,
|
|
56
66
|
TransformMode,
|
|
57
67
|
TransformSpace,
|
|
58
68
|
Vec3Value,
|
|
59
69
|
ViewPreset,
|
|
60
70
|
ViewportCapture,
|
|
71
|
+
ViewportTab,
|
|
61
72
|
} from './types.js';
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two viewport tabs — the editor's two MODES, named for what the user is
|
|
3
|
+
* doing rather than for what happens to be mounted (ARCHITECTURE-CORE
|
|
4
|
+
* §Vocabulary, decided 2026-08-02). They were `'scene' | 'game'` until WO-LR1:
|
|
5
|
+
* `'scene'` outlived the `.vscn` scene format that named it (a three root is
|
|
6
|
+
* TSX now, and asset/story/tool documents live on that tab too), and `'game'`
|
|
7
|
+
* named the mounted artifact rather than the mode. Not to be confused with the
|
|
8
|
+
* PLAY TRANSPORT (the five stable controls) — this is which tab is showing.
|
|
9
|
+
*/
|
|
10
|
+
export type ViewportTab = 'edit' | 'play';
|
|
11
|
+
|
|
1
12
|
export type AssetKind =
|
|
2
13
|
| 'model'
|
|
3
14
|
| 'image'
|
|
@@ -19,8 +30,6 @@ export interface HelperVisibility {
|
|
|
19
30
|
splines: boolean;
|
|
20
31
|
navmesh: boolean;
|
|
21
32
|
skeletons: boolean;
|
|
22
|
-
/** Per-component gizmo contributions (W3a extension contract). */
|
|
23
|
-
components: boolean;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
export interface Vec3Value {
|
|
@@ -60,25 +69,74 @@ export interface ViewportCapture {
|
|
|
60
69
|
* container, no DOM `Image`/`XMLSerializer`, or a rasterization failure) —
|
|
61
70
|
* never a silently HUD-less image passed off as the whole game. `layers` is
|
|
62
71
|
* only present on the composite leg.
|
|
72
|
+
*
|
|
73
|
+
* `flatness` and `hiddenFrame` are the same honesty contract applied to the
|
|
74
|
+
* PIXELS: how much of the frame is one flat surface (a near-blank capture
|
|
75
|
+
* carries its own "weak evidence" warning), and whether the frame exists only
|
|
76
|
+
* because the capture asked a hidden tab's runtime for one deterministic tick.
|
|
63
77
|
*/
|
|
64
78
|
export interface GameCapture {
|
|
65
79
|
base64: string;
|
|
66
80
|
mimeType: 'image/png';
|
|
67
81
|
composite: boolean;
|
|
68
82
|
layers?: { canvases: number; domOverlays: number };
|
|
83
|
+
/** Degeneracy measure — `packages/editor/src/composite-screenshot.ts`'s
|
|
84
|
+
* `measureFlatness`. Absent when pixel readback was unavailable. */
|
|
85
|
+
flatness?: {
|
|
86
|
+
dominantFraction: number;
|
|
87
|
+
dominantColor: string;
|
|
88
|
+
distinctRegions: number;
|
|
89
|
+
degenerate: boolean;
|
|
90
|
+
/** Present iff `degenerate`; the sentence to show the caller verbatim. */
|
|
91
|
+
warning?: string;
|
|
92
|
+
};
|
|
93
|
+
/** True when the editor tab was hidden-paused and the runtime rendered one
|
|
94
|
+
* deterministic tick to produce this frame. */
|
|
95
|
+
hiddenFrame?: boolean;
|
|
69
96
|
}
|
|
70
97
|
|
|
71
98
|
export type AssetPreviewView = 'front' | 'right' | 'top' | 'perspective';
|
|
72
99
|
export type AssetPreviewBackground = 'neutral' | 'transparent';
|
|
73
100
|
|
|
101
|
+
/**
|
|
102
|
+
* What the Asset Lab is being asked to photograph: a same-origin project
|
|
103
|
+
* model path, a live scene entity, or RAW GLB BYTES that travel with the
|
|
104
|
+
* command.
|
|
105
|
+
*
|
|
106
|
+
* The bytes form exists because rasterization only happens where there is a
|
|
107
|
+
* GPU. A Node host that built a model in memory — the module-look lane's
|
|
108
|
+
* `project.bake.preview`, which compiles a project TS module's `Object3D` and
|
|
109
|
+
* exports it to an in-memory GLB without writing anything — has pixels
|
|
110
|
+
* nowhere until the live editor session renders them. It is four-view,
|
|
111
|
+
* lab-stage only: bytes stand nowhere, so `stage: 'scene'`, the shot-set /
|
|
112
|
+
* source-review modes and compare are all refused by name at the relay.
|
|
113
|
+
*/
|
|
74
114
|
export type AssetPreviewSource =
|
|
75
|
-
| { assetPath: string; entityId?: never }
|
|
76
|
-
| { entityId: string; assetPath?: never }
|
|
115
|
+
| { assetPath: string; entityId?: never; glbBase64?: never }
|
|
116
|
+
| { entityId: string; assetPath?: never; glbBase64?: never }
|
|
117
|
+
| { glbBase64: string; assetPath?: never; entityId?: never };
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Where an ENTITY capture is staged.
|
|
121
|
+
*
|
|
122
|
+
* `'lab'` (the default on every surface) is the editor's neutral Asset Lab
|
|
123
|
+
* stage: an isolated, yaw-normalized snapshot under fixed studio lighting,
|
|
124
|
+
* identical whatever the entity's surroundings are. `'scene'` photographs the
|
|
125
|
+
* entity where it stands in the live scene, under the scene's own lighting,
|
|
126
|
+
* with the editor's own grid/gizmos/helpers excluded — see
|
|
127
|
+
* `packages/editor/src/asset-preview.ts`'s `captureSceneStageAssetPreview`.
|
|
128
|
+
*
|
|
129
|
+
* `'scene'` is an ENTITY-only, four-view option: the relay refuses it by name
|
|
130
|
+
* for an `assetPath` source (a model loaded from disk stands nowhere) and for
|
|
131
|
+
* the shot-set / source-review / compare modes (each stages its own subject).
|
|
132
|
+
*/
|
|
133
|
+
export type AssetPreviewStage = 'lab' | 'scene';
|
|
77
134
|
|
|
78
135
|
export interface AssetPreviewOptions {
|
|
79
136
|
width?: number;
|
|
80
137
|
height?: number;
|
|
81
138
|
background?: AssetPreviewBackground;
|
|
139
|
+
stage?: AssetPreviewStage;
|
|
82
140
|
}
|
|
83
141
|
|
|
84
142
|
export interface AssetPreviewCapture {
|
|
@@ -89,8 +147,39 @@ export interface AssetPreviewCapture {
|
|
|
89
147
|
}
|
|
90
148
|
|
|
91
149
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
150
|
+
* The STORY lane (`vgai screenshot <file>.stories.tsx`): a project CSF file's
|
|
151
|
+
* exports rendered in the live session's DOM and captured through the same
|
|
152
|
+
* composite leg the game lane uses, as ONE variant sheet per file. `story`
|
|
153
|
+
* narrows to a single export. See `packages/editor/src/story-capture.ts`.
|
|
154
|
+
*/
|
|
155
|
+
export interface StoryCaptureOptions {
|
|
156
|
+
/** Narrow the sheet to one CSF export name (`--story <export>`). */
|
|
157
|
+
story?: string;
|
|
158
|
+
/** Per-variant cell size in CSS pixels; defaults to a 960x540 rectangle,
|
|
159
|
+
* because a UI story is not the Asset Lab's square 3D view. */
|
|
160
|
+
width?: number;
|
|
161
|
+
height?: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface StoryVariantImage extends ViewportCapture {
|
|
165
|
+
/** The CSF export name — the file name each variant PNG is written under. */
|
|
166
|
+
name: string;
|
|
167
|
+
/** Storybook's human-facing story name. */
|
|
168
|
+
label: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface StoryVariantCapture {
|
|
172
|
+
/** Project-relative path of the CSF module that was photographed. */
|
|
173
|
+
modulePath: string;
|
|
174
|
+
width: number;
|
|
175
|
+
height: number;
|
|
176
|
+
variants: StoryVariantImage[];
|
|
177
|
+
contactSheet: ViewportCapture & { width: number; height: number };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* B8.4 — the Asset Lab compare mode (`vgai screenshot <model.glb>
|
|
182
|
+
* --compare <ref.glb>`): the asset and a caller-supplied reference GLB rendered with
|
|
94
183
|
* matched orthographic front + side framing (equal-height bounding-box
|
|
95
184
|
* normalization, both yaw-normalized to face the camera), scored by
|
|
96
185
|
* silhouette IoU with per-view overlay evidence (orange asset / cyan
|
|
@@ -121,7 +210,7 @@ export interface AssetCompareCapture {
|
|
|
121
210
|
}
|
|
122
211
|
|
|
123
212
|
/**
|
|
124
|
-
* A project-defined labeled shot set (`vgai
|
|
213
|
+
* A project-defined labeled shot set (`vgai screenshot <target> --shots <set>`).
|
|
125
214
|
* The DEFINITION is project data: a registered project tool named
|
|
126
215
|
* `project.<set>.previewShots` returns it (installed capabilities register
|
|
127
216
|
* theirs — e.g. the humanoid capability contributes its canonical verify set),
|
|
@@ -135,6 +224,20 @@ export interface ShotSetPoseRotation {
|
|
|
135
224
|
radians: number;
|
|
136
225
|
}
|
|
137
226
|
|
|
227
|
+
/** The other half of a pose: a named morph target driven to an influence.
|
|
228
|
+
* A rig whose meshes carry no such morph is posed by the rotations alone —
|
|
229
|
+
* the same degrade-don't-throw rule the rotations follow for a missing
|
|
230
|
+
* joint. */
|
|
231
|
+
export interface ShotSetPoseMorph {
|
|
232
|
+
morph: string;
|
|
233
|
+
influence: number;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** One step of a named pose. A pose is a flat list so a single expression
|
|
237
|
+
* (a jaw ROTATION plus a brow MORPH) is one entry in `poses`, not two
|
|
238
|
+
* parallel lists the capture engine would have to zip. */
|
|
239
|
+
export type ShotSetPoseStep = ShotSetPoseRotation | ShotSetPoseMorph;
|
|
240
|
+
|
|
138
241
|
export type ShotSetShot =
|
|
139
242
|
| { label: string; view: 'turntable'; yaw: number; pose?: string | undefined }
|
|
140
243
|
| {
|
|
@@ -142,6 +245,13 @@ export type ShotSetShot =
|
|
|
142
245
|
view: 'bone-zoom';
|
|
143
246
|
bones: string[];
|
|
144
247
|
spanFraction: number;
|
|
248
|
+
/** The angle the crop is taken FROM, in the same radian convention as
|
|
249
|
+
* a turntable shot's `yaw` (0 the front camera, -PI/2 the subject's
|
|
250
|
+
* left). Omitted means 0. The front camera is not a general answer
|
|
251
|
+
* for a long subject: a crop anchored on the tail root of an 8 m
|
|
252
|
+
* quadruped otherwise photographs the hind legs standing in front of
|
|
253
|
+
* it. */
|
|
254
|
+
yaw?: number | undefined;
|
|
145
255
|
pose?: string | undefined;
|
|
146
256
|
};
|
|
147
257
|
|
|
@@ -155,16 +265,37 @@ export interface AssetPreviewShotSetDefinition {
|
|
|
155
265
|
/** Optional clause appended to rig-requirement errors,
|
|
156
266
|
* e.g. "a Mixamo-named humanoid skeleton". */
|
|
157
267
|
rigRequirementHint?: string;
|
|
158
|
-
/** Named poses (bone rotations applied to a disposable
|
|
159
|
-
* shots opt in via their `pose` field. */
|
|
160
|
-
poses?: Record<string,
|
|
268
|
+
/** Named poses (bone rotations and morph influences applied to a disposable
|
|
269
|
+
* snapshot only); shots opt in via their `pose` field. */
|
|
270
|
+
poses?: Record<string, ShotSetPoseStep[]>;
|
|
161
271
|
shots: ShotSetShot[];
|
|
162
272
|
}
|
|
163
273
|
|
|
274
|
+
/**
|
|
275
|
+
* A shot the capture engine rendered but does not vouch for.
|
|
276
|
+
*
|
|
277
|
+
* `'empty-frame'` — the shot's frame contained no renderable geometry, so
|
|
278
|
+
* the PNG is background only. It is reported rather than thrown because one
|
|
279
|
+
* mis-aimed crop must not kill a 20-shot render; it is reported LOUDLY
|
|
280
|
+
* because a background tile on a contact sheet otherwise reads as coverage.
|
|
281
|
+
*/
|
|
282
|
+
export interface AssetPreviewShotWarning {
|
|
283
|
+
label: string;
|
|
284
|
+
reason: 'empty-frame';
|
|
285
|
+
/** Already names the shot, its anchor joints and its pose — surfaces print
|
|
286
|
+
* this string rather than re-composing one. */
|
|
287
|
+
message: string;
|
|
288
|
+
bones?: string[];
|
|
289
|
+
pose?: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
164
292
|
export interface LabeledShotSetCapture {
|
|
165
293
|
width: number;
|
|
166
294
|
height: number;
|
|
167
295
|
shots: Array<ViewportCapture & { label: string }>;
|
|
296
|
+
/** Empty when every shot framed geometry. Absent against an editor that
|
|
297
|
+
* predates the empty-frame guard. */
|
|
298
|
+
warnings: AssetPreviewShotWarning[];
|
|
168
299
|
contactSheet: ViewportCapture & { width: number; height: number };
|
|
169
300
|
}
|
|
170
301
|
|
|
@@ -182,18 +313,29 @@ export interface EditorState {
|
|
|
182
313
|
*/
|
|
183
314
|
loopLiveness?: 'running' | 'hidden-paused' | 'stopped' | null;
|
|
184
315
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
316
|
+
* The connected editor tab's OWN reported visibility/focus
|
|
317
|
+
* (`command-listener.ts`'s `collectPresence`, straight off
|
|
318
|
+
* `document.visibilityState`/`document.hasFocus()`). This is the only field
|
|
319
|
+
* that distinguishes a usable session from a merely attached one:
|
|
320
|
+
* `connected` answers "is an SSE client holding the session open", which a
|
|
321
|
+
* BACKGROUNDED tab satisfies perfectly while the engine hidden-pauses its
|
|
322
|
+
* loop underneath. `null` when the page has no `document` at all; absent
|
|
323
|
+
* against an older server that predates the field.
|
|
324
|
+
*/
|
|
325
|
+
presence?: { visibility: 'visible' | 'hidden' | 'prerender'; focused: boolean } | null;
|
|
326
|
+
/**
|
|
327
|
+
* The pending-restart reason when source changed while the game was
|
|
328
|
+
* RUNNING and the running session is now stale (e.g. an R3F entry-file
|
|
329
|
+
* write-back during play, a registry.ts edit). The editor's Restart button
|
|
330
|
+
* surfaces the same reason; one restart (`vgai play`, or the button)
|
|
331
|
+
* remounts every root from fresh source and clears it. `null` when the
|
|
332
|
+
* running session is fresh; absent against an older server that predates
|
|
333
|
+
* the field.
|
|
192
334
|
*/
|
|
193
335
|
restartRequired?: string | null;
|
|
194
336
|
selectedEntityId: string | null;
|
|
195
337
|
selectedEntityIds: string[];
|
|
196
|
-
activeViewportTab:
|
|
338
|
+
activeViewportTab: ViewportTab;
|
|
197
339
|
/** The actual active center document, including tool/source documents. */
|
|
198
340
|
activeDocumentId?: string | null;
|
|
199
341
|
activeTabKey: string;
|
|
@@ -233,7 +375,8 @@ export interface EditorState {
|
|
|
233
375
|
stateUpdatedAt?: number;
|
|
234
376
|
/**
|
|
235
377
|
* Validate-on-change (#103): per-file validation status for every
|
|
236
|
-
*
|
|
378
|
+
* Project `src/**` source / `vgai.project.json` the dev
|
|
379
|
+
* server has seen
|
|
237
380
|
* change since it booted (or since the last project switch). Server-
|
|
238
381
|
* computed — unlike the rest of `EditorState`, it is NOT part of the
|
|
239
382
|
* browser-POSTed snapshot, so it is always current. A file appears here
|
|
@@ -243,6 +386,32 @@ export interface EditorState {
|
|
|
243
386
|
* read it unconditionally.
|
|
244
387
|
*/
|
|
245
388
|
projectValidation?: Record<string, { errors: string[]; at: number }>;
|
|
389
|
+
/**
|
|
390
|
+
* PD-13: the WARNING half of the same server-computed validation pass —
|
|
391
|
+
* authoring-convention findings (the R3F00x codes, OID surface conflicts)
|
|
392
|
+
* that do not make a file invalid but do make it unauthorable. Same shape,
|
|
393
|
+
* same lifecycle and same freshness guarantee as `projectValidation` above:
|
|
394
|
+
* whole-project (not just the open document), keyed by project-relative
|
|
395
|
+
* path, present only while the file currently warns, `{}` when clean.
|
|
396
|
+
*
|
|
397
|
+
* The server has sent this since the R3F authoring diagnostics landed; it
|
|
398
|
+
* was missing from this interface, so every typed consumer — `vgai status`
|
|
399
|
+
* included — could only reach it through a cast. Declared here so a caller
|
|
400
|
+
* that wants to react to authoring warnings can see they exist.
|
|
401
|
+
*/
|
|
402
|
+
projectWarnings?: Record<string, { warnings: string[]; at: number }>;
|
|
403
|
+
/**
|
|
404
|
+
* PD-14: whether the SOURCE half of the validation pass above is running at
|
|
405
|
+
* all. `'active'` is the normal state; `'awaiting-src'` means the project
|
|
406
|
+
* has no `src/` directory yet, so nothing under `src/` is being validated —
|
|
407
|
+
* an empty `projectValidation`/`projectWarnings` says nothing about source
|
|
408
|
+
* files while this reads `'awaiting-src'`. It is not terminal: the watcher
|
|
409
|
+
* is armed on the not-yet-existing path and flips to `'active'` (running
|
|
410
|
+
* the boot-equivalent scan) the moment `src/` appears, with no restart.
|
|
411
|
+
* `'no-project'` when no project is open. Server-computed, like its
|
|
412
|
+
* neighbors above.
|
|
413
|
+
*/
|
|
414
|
+
sourceValidation?: 'active' | 'awaiting-src' | 'no-project';
|
|
246
415
|
/**
|
|
247
416
|
* #124: the absolute path of the project this editor server currently has
|
|
248
417
|
* open — server-computed (never part of the browser-POSTed snapshot,
|
|
@@ -257,7 +426,7 @@ export interface EditorState {
|
|
|
257
426
|
projectRoot?: string | null;
|
|
258
427
|
/**
|
|
259
428
|
* #124: the open project's declared name, alongside `projectRoot` above
|
|
260
|
-
* (`vgai.
|
|
429
|
+
* (`vgai.project.json`'s `name`). `null` when no project is open, or the open
|
|
261
430
|
* project has no readable manifest name.
|
|
262
431
|
*/
|
|
263
432
|
projectName?: string | null;
|
|
@@ -366,3 +535,104 @@ export type {
|
|
|
366
535
|
ProjectToolContribution,
|
|
367
536
|
ToolContributionPoint,
|
|
368
537
|
} from '@vgai/sdk/project-tool-catalog';
|
|
538
|
+
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
// Inspection — the serialized inspection subject (`EditorClient.inspect`)
|
|
541
|
+
// ---------------------------------------------------------------------------
|
|
542
|
+
//
|
|
543
|
+
// The wire mirror of the editor's own `SerializedInspectionSubject`
|
|
544
|
+
// (`packages/editor/src/inspection/serialize.ts`, which owns the contract and
|
|
545
|
+
// carries the reasoning). `command-listener.ts` annotates its `inspect`
|
|
546
|
+
// payload with this type, so `tsc` checks the two sides against each other on
|
|
547
|
+
// every build rather than letting them drift silently.
|
|
548
|
+
|
|
549
|
+
/** Where the inspector's subject lives; `asset-lab` is an open asset
|
|
550
|
+
* document — the three paradigm scoped to a subtree, inspected in the same
|
|
551
|
+
* box as the scene. */
|
|
552
|
+
export type InspectionSurface = 'three' | 'canvas' | 'dom' | 'asset-lab';
|
|
553
|
+
|
|
554
|
+
/** Which LAYOUT the one inspector box is in: the compact box over the
|
|
555
|
+
* viewport, or the same sections stacked in the dock column. */
|
|
556
|
+
export type InspectionPresentationKind = 'card' | 'column';
|
|
557
|
+
|
|
558
|
+
/** One inspected field: a stable scriptable `path` and the value at it. */
|
|
559
|
+
export interface InspectedField {
|
|
560
|
+
path: string;
|
|
561
|
+
label: string;
|
|
562
|
+
type: 'string' | 'number' | 'boolean' | 'vec3' | 'color' | 'enum' | 'asset' | 'json';
|
|
563
|
+
/** Absent when nothing is at that address, or when `mixed` is set. */
|
|
564
|
+
value?: unknown;
|
|
565
|
+
/** The inspected subjects disagree about this field. */
|
|
566
|
+
mixed?: true;
|
|
567
|
+
/** The value shown is the declared default — the document does not carry it. */
|
|
568
|
+
defaulted?: boolean;
|
|
569
|
+
readonly?: boolean;
|
|
570
|
+
resettable?: boolean;
|
|
571
|
+
revertsTo?: string;
|
|
572
|
+
group?: string;
|
|
573
|
+
options?: readonly unknown[];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** A section's content. An opaque body is a NAMED OPAQUE — the editor renders
|
|
577
|
+
* it with a React component the wire deliberately does not describe. The two
|
|
578
|
+
* opaque kinds are distinguished because "this subject has a live preview"
|
|
579
|
+
* is a real fact about it: `custom` is a contributed block, `preview` is the
|
|
580
|
+
* subject's own square view of itself.
|
|
581
|
+
*
|
|
582
|
+
* A custom body carries `data` when it can say what it DISPLAYS — the keys
|
|
583
|
+
* are the section's own vocabulary, not a shared schema. The shipped case is
|
|
584
|
+
* `transform`: `{position, rotation, scale}`, three numbers each, with
|
|
585
|
+
* rotation in Euler XYZ DEGREES exactly as the rotation inputs show it (the
|
|
586
|
+
* quaternion behind them is not on this wire). */
|
|
587
|
+
export type InspectedSectionBody =
|
|
588
|
+
| { kind: 'fields'; fields: readonly InspectedField[] }
|
|
589
|
+
| { kind: 'custom'; id: string; title: string; data?: Record<string, unknown> }
|
|
590
|
+
| { kind: 'preview'; id: string; title: string };
|
|
591
|
+
|
|
592
|
+
export interface InspectedSection {
|
|
593
|
+
id: string;
|
|
594
|
+
title: string;
|
|
595
|
+
order: number;
|
|
596
|
+
description?: string;
|
|
597
|
+
body: InspectedSectionBody;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** A verb on the subject (the visibility eye, the Asset Editor jump). */
|
|
601
|
+
export interface InspectedAction {
|
|
602
|
+
id: string;
|
|
603
|
+
title: string;
|
|
604
|
+
label?: string;
|
|
605
|
+
/** Toggle state, for verbs that have one — how visibility is read. */
|
|
606
|
+
pressed?: boolean;
|
|
607
|
+
disabled?: boolean;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** The whole inspection subject, as data — what a human sees in the
|
|
611
|
+
* inspector, for an agent (`vgai eval 'editor.inspect()'`). */
|
|
612
|
+
export interface InspectedSubject {
|
|
613
|
+
id: string;
|
|
614
|
+
title: string;
|
|
615
|
+
kindLabel?: string;
|
|
616
|
+
/** The quiet line a subject with nothing to edit explains itself with. */
|
|
617
|
+
hint?: string;
|
|
618
|
+
presentation: {
|
|
619
|
+
preferred: InspectionPresentationKind;
|
|
620
|
+
resolved?: InspectionPresentationKind;
|
|
621
|
+
surface?: InspectionSurface;
|
|
622
|
+
};
|
|
623
|
+
quickActions: readonly InspectedAction[];
|
|
624
|
+
/** Already in display order. */
|
|
625
|
+
sections: readonly InspectedSection[];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** NOTHING is being inspected: the inspector is unmounted, so the honest
|
|
629
|
+
* answer is not an empty subject but the absence of one. Distinct from a
|
|
630
|
+
* missing reply, which means nobody answered
|
|
631
|
+
* (`editor.inspection.get`'s `INSPECTION_UNAVAILABLE`). */
|
|
632
|
+
export interface InspectedNothing {
|
|
633
|
+
none: true;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** What `editor.inspect()` answers: the subject showing, or nothing at all.
|
|
637
|
+
* Narrow with `'none' in result`. */
|
|
638
|
+
export type InspectedInspection = InspectedSubject | InspectedNothing;
|