@volter/editor-live 0.5.57
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/BUNDLED_NOTICES +434 -0
- package/LICENSE +202 -0
- package/README.md +20 -0
- package/dist/editor-document.d.ts +152 -0
- package/dist/editor.d.ts +399 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +2080 -0
- package/dist/index.js.map +7 -0
- package/dist/lazy-proxy.d.ts +26 -0
- package/dist/session.d.ts +32 -0
- package/dist/singleton.d.ts +18 -0
- package/dist/tools.d.ts +23 -0
- package/package.json +46 -0
- package/src/editor-document.ts +234 -0
- package/src/editor.ts +663 -0
- package/src/index.ts +38 -0
- package/src/lazy-proxy.ts +68 -0
- package/src/session.ts +267 -0
- package/src/singleton.ts +30 -0
- package/src/tools.ts +45 -0
package/src/editor.ts
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
/** Editor and document automation over the shared session command wire.
|
|
2
|
+
* Modeling executes in the editor tab; this client owns no model state. */
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
ActiveDocumentCapture,
|
|
6
|
+
AssetKind,
|
|
7
|
+
AssetPreviewCapture,
|
|
8
|
+
AssetPreviewOptions,
|
|
9
|
+
AssetPreviewShotSetDefinition,
|
|
10
|
+
AssetPreviewSource,
|
|
11
|
+
CaptureDimensions,
|
|
12
|
+
DocumentLookOutcome,
|
|
13
|
+
EditorChromeCapture,
|
|
14
|
+
EditorChromeCaptureOptions,
|
|
15
|
+
EditorClient,
|
|
16
|
+
EditorState,
|
|
17
|
+
EditorView,
|
|
18
|
+
EditorWorkspaceName,
|
|
19
|
+
HistoryStep,
|
|
20
|
+
InspectedFieldWrite,
|
|
21
|
+
InspectedHierarchy,
|
|
22
|
+
InspectedInspection,
|
|
23
|
+
LabeledShotSetCapture,
|
|
24
|
+
OpenedDocument,
|
|
25
|
+
PresentedEditorView,
|
|
26
|
+
ShadingMode,
|
|
27
|
+
StructureOp,
|
|
28
|
+
StructureOpOptions,
|
|
29
|
+
StructureOpResult,
|
|
30
|
+
ViewPreset,
|
|
31
|
+
ViewportCapture,
|
|
32
|
+
} from '@volter/editor-sdk';
|
|
33
|
+
import { LiveEditorDocument } from './editor-document.js';
|
|
34
|
+
|
|
35
|
+
/** A panel `showPanel` can focus: the viewport tabs, the console, the build
|
|
36
|
+
* surface, or any key the editor's own static-panel registry holds
|
|
37
|
+
* (`hierarchy`, `assets`, `asset-library`, `inspector`, `history`, …) — which
|
|
38
|
+
* is why this is open: the registry, not this union, is the vocabulary. */
|
|
39
|
+
export type PanelName =
|
|
40
|
+
| 'viewport-edit'
|
|
41
|
+
| 'console'
|
|
42
|
+
// Keeps the literals above in autocomplete while admitting every key the
|
|
43
|
+
// editor's registry holds — the registry answers, this union only hints.
|
|
44
|
+
| (string & {});
|
|
45
|
+
|
|
46
|
+
const EXTENSION_KIND: Record<string, AssetKind> = {
|
|
47
|
+
'.glb': 'model',
|
|
48
|
+
'.gltf': 'model',
|
|
49
|
+
'.png': 'image',
|
|
50
|
+
'.jpg': 'image',
|
|
51
|
+
'.jpeg': 'image',
|
|
52
|
+
'.webp': 'image',
|
|
53
|
+
'.gif': 'image',
|
|
54
|
+
'.svg': 'image',
|
|
55
|
+
'.hdr': 'image',
|
|
56
|
+
'.exr': 'image',
|
|
57
|
+
'.mp4': 'video',
|
|
58
|
+
'.webm': 'video',
|
|
59
|
+
'.mp3': 'audio',
|
|
60
|
+
'.ogg': 'audio',
|
|
61
|
+
'.wav': 'audio',
|
|
62
|
+
'.flac': 'audio',
|
|
63
|
+
'.glsl': 'source',
|
|
64
|
+
'.vert': 'source',
|
|
65
|
+
'.frag': 'source',
|
|
66
|
+
// PROJECT SCRIPTS ARE SOURCE. Without these the guess below falls through to
|
|
67
|
+
// `'json'`, the asset-document router sends the file to the generic JSON
|
|
68
|
+
// viewer (`asset-documents.tsx#assetDocumentViewerRoute`: `spec.kind ===
|
|
69
|
+
// 'json'` is decided before any content routing), and the LIVE MODELING
|
|
70
|
+
// DOCUMENT never mounts — `editor.openAsset('src/lib/fox/fox.model.ts')`
|
|
71
|
+
// silently shows a text pane instead of the model. Only `kind: 'source'`
|
|
72
|
+
// reaches `SourceAssetViewer`, which is what content-routes a project script
|
|
73
|
+
// to `LiveModuleDocument`. The set matches that viewer's own
|
|
74
|
+
// `isProjectScriptPath` regex, `/\.(?:[cm]?[jt]sx?)$/`.
|
|
75
|
+
'.ts': 'source',
|
|
76
|
+
'.tsx': 'source',
|
|
77
|
+
'.mts': 'source',
|
|
78
|
+
'.cts': 'source',
|
|
79
|
+
'.js': 'source',
|
|
80
|
+
'.jsx': 'source',
|
|
81
|
+
'.mjs': 'source',
|
|
82
|
+
'.cjs': 'source',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Lightweight extension-based `AssetKind` guess for `openAsset`'s optional
|
|
87
|
+
* `kind` argument. Deliberately independent of (not shared with) the
|
|
88
|
+
* editor's own `AssetBrowser.tsx#getAssetKind` — that function is a private,
|
|
89
|
+
* React-component-local helper of a package `@volter/editor-live` has no dependency
|
|
90
|
+
* on. Callers with an unusual extension can always pass `kind` explicitly.
|
|
91
|
+
*/
|
|
92
|
+
export function inferAssetKind(path: string): AssetKind {
|
|
93
|
+
// `.prefab.json` is retired (PRs #578/#581/#589) but the `'prefab'`
|
|
94
|
+
// AssetKind itself remains in `@volter/editor-sdk` for view-link compatibility.
|
|
95
|
+
if (path.endsWith('.prefab.json')) return 'prefab';
|
|
96
|
+
const dot = path.lastIndexOf('.');
|
|
97
|
+
const ext = dot >= 0 ? path.slice(dot).toLowerCase() : '';
|
|
98
|
+
return EXTENSION_KIND[ext] ?? 'json';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class LiveEditor {
|
|
102
|
+
/** `#`-private, not `private`: `volter-editor eval --list` enumerates this object's
|
|
103
|
+
* real runtime members, and TypeScript's erased `private` would leave the
|
|
104
|
+
* raw `EditorClient` advertised beside them. */
|
|
105
|
+
readonly #client: EditorClient;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The ACTIVE center document's own DOM: read it, click it, key it, paste
|
|
109
|
+
* into it. The one door onto editor chrome that is not play-mode gated, and
|
|
110
|
+
* deliberately scoped to that document alone —
|
|
111
|
+
* `packages/editor/src/editor-document-probe.ts` carries the design and the
|
|
112
|
+
* refusal contract. Screenshotting the same subject is
|
|
113
|
+
* {@link LiveEditor.captureActiveDocument}, not a fifth verb here.
|
|
114
|
+
*/
|
|
115
|
+
readonly document: LiveEditorDocument;
|
|
116
|
+
|
|
117
|
+
constructor(client: EditorClient) {
|
|
118
|
+
this.#client = client;
|
|
119
|
+
this.document = new LiveEditorDocument(client);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* THE BLENDER LANE'S VERBS, from `volter-editor eval`.
|
|
124
|
+
*
|
|
125
|
+
* Blender runs headless in the editor tab's worker (ARCHITECTURE-CORE, "THE
|
|
126
|
+
* BLENDER IN THE TAB IS BLENDER") and answers `blender-start`,
|
|
127
|
+
* `blender-execute`, `blender-scene-info`, `blender-object-info`,
|
|
128
|
+
* `blender-screenshot-view`, `blender-read-file`, `blender-write-file`,
|
|
129
|
+
* `blender-list-files`, `blender-stop` and `blender-status`. They were
|
|
130
|
+
* reachable from `@volter/editor-sdk` and through `vgai blender-mcp` but from
|
|
131
|
+
* no GENERAL door, so driving a session meant writing an MCP client script
|
|
132
|
+
* per question — the same discovery failure `eval-surface.ts`'s header
|
|
133
|
+
* records, in a lane that had not noticed it yet.
|
|
134
|
+
*
|
|
135
|
+
* volter-editor eval "await editor.blender('blender-execute', { code: 'import bpy; print(len(bpy.data.objects))' })"
|
|
136
|
+
*
|
|
137
|
+
* `blender-status` is the only verb that creates nothing: it answers whether
|
|
138
|
+
* this tab already has a session without starting one.
|
|
139
|
+
*/
|
|
140
|
+
async blender<T extends object = Record<string, unknown>>(
|
|
141
|
+
type: `blender-${string}`,
|
|
142
|
+
fields: Record<string, unknown> = {},
|
|
143
|
+
): Promise<T> {
|
|
144
|
+
return this.#client.blender<T>(type, fields);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The active authoring adapter's persistence destination — where a save would
|
|
149
|
+
* land (`status().savePath`). A read only: a three root has no scene document
|
|
150
|
+
* to open, and its root is activated instead.
|
|
151
|
+
*/
|
|
152
|
+
async scene(): Promise<string | null> {
|
|
153
|
+
const state = await this.#client.getState();
|
|
154
|
+
return state.savePath;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Make the connected human editor show the same subject/view as the agent.
|
|
159
|
+
* The returned URL is a compact, shareable projection — not a serialized
|
|
160
|
+
* workspace or document payload.
|
|
161
|
+
*/
|
|
162
|
+
async present(view: EditorView): Promise<PresentedEditorView> {
|
|
163
|
+
return this.#client.present(view);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The human editor's actual active document, selection, camera and utility. */
|
|
167
|
+
async currentView(): Promise<EditorView> {
|
|
168
|
+
return this.#client.currentView();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Capture the same center document the human is currently looking at.
|
|
173
|
+
*
|
|
174
|
+
* A number is a SQUARE of that size — the default, and the right shape for
|
|
175
|
+
* an unstaged look at a model. `{width, height}` asks for a shaped frame, so
|
|
176
|
+
* a video-aspect look needs no crop afterwards. Both are bounded by the
|
|
177
|
+
* relay budget (64-1024 per side, total no larger than a 1024 square); see
|
|
178
|
+
* `@volter/editor-sdk`'s `CaptureDimensions`.
|
|
179
|
+
* Supply a view to present and photograph it in one editor request.
|
|
180
|
+
*/
|
|
181
|
+
async captureActiveDocument(
|
|
182
|
+
size?: CaptureDimensions,
|
|
183
|
+
view?: EditorView,
|
|
184
|
+
): Promise<ActiveDocumentCapture> {
|
|
185
|
+
return this.#client.captureActiveDocument(size, view);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Photograph the editor PAGE — every panel, tab strip and viewport as the
|
|
190
|
+
* person sees it. `vgai screenshot editor` is this verb from the shell. The
|
|
191
|
+
* one door for judging chrome sighted: a skin, a workspace arrangement or a
|
|
192
|
+
* contributed panel is looked at through this, never guessed at from DOM
|
|
193
|
+
* probes. The page at its own layout, `scale` output pixels per CSS pixel
|
|
194
|
+
* (default `devicePixelRatio`) — a 1 px border or a glyph stroke is only
|
|
195
|
+
* judgeable at the scale the reference it is compared against was captured
|
|
196
|
+
* at, and the result reports its own `size` and `scale`.
|
|
197
|
+
*/
|
|
198
|
+
async captureEditorChrome(options?: EditorChromeCaptureOptions): Promise<EditorChromeCapture> {
|
|
199
|
+
return this.#client.captureEditorChrome(options);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** `'all'` -> `EditorClient.selectAll()` (mirrors `vgai select --all`); otherwise `EditorClient.select(id)` (mirrors `vgai select <entityId>`). */
|
|
203
|
+
async select(id: string | 'all'): Promise<void> {
|
|
204
|
+
if (id === 'all') {
|
|
205
|
+
await this.#client.selectAll();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
await this.#client.select(id);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Mirrors `vgai deselect`. */
|
|
212
|
+
async deselect(): Promise<void> {
|
|
213
|
+
await this.#client.select(null);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** No `id` -> focus the current selection (mirrors bare `vgai focus`); `id` given -> focus that entity. */
|
|
217
|
+
async focus(id?: string): Promise<void> {
|
|
218
|
+
if (id !== undefined) {
|
|
219
|
+
await this.#client.focusEntity(id);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
await this.#client.focusSelection();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Frame the edit viewport camera on one entity. Same framing as
|
|
227
|
+
* `focus(id)`, but an entity id the scene does not know THROWS, naming the
|
|
228
|
+
* id — where `focus` quietly does nothing. Reach for this whenever the next
|
|
229
|
+
* step reads the viewport (`screenshot()`, an
|
|
230
|
+
* `assetPreview(..., { stage: 'scene' })`): a framing that silently missed
|
|
231
|
+
* would otherwise be indistinguishable from one that worked.
|
|
232
|
+
*/
|
|
233
|
+
async frame(entityId: string): Promise<void>;
|
|
234
|
+
/**
|
|
235
|
+
* Bare `frame()` frames the OPEN Object3D document's subject instead — its
|
|
236
|
+
* selection if it has one, else the whole model: the toolbar's own Frame
|
|
237
|
+
* button, reachable from a script. `fit` scales the fitted distance (1 is
|
|
238
|
+
* that button's tight fit, 1.5 stands back a little for a shot).
|
|
239
|
+
*/
|
|
240
|
+
async frame(options?: { readonly fit?: number }): Promise<void>;
|
|
241
|
+
async frame(target?: string | { readonly fit?: number }): Promise<void> {
|
|
242
|
+
if (typeof target === 'string') {
|
|
243
|
+
await this.#client.frameEntity(target);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
await this.#client.frameDocument(target?.fit);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* WATCH THE AGENT LOOK AROUND THE MODEL.
|
|
251
|
+
*
|
|
252
|
+
* Swings the open Object3D document's camera — the camera the human's tab is
|
|
253
|
+
* showing — around the framed subject by `azimuth`/`elevation` RADIANS,
|
|
254
|
+
* animated over `duration` seconds (default 0.6), and resolves when the move
|
|
255
|
+
* ends. This is deliberately not a jump cut: the point of the verb is that a
|
|
256
|
+
* person watching sees the agent walk around the thing it is working on.
|
|
257
|
+
*
|
|
258
|
+
* `await editor.orbit({ azimuth: Math.PI / 2 })` — a quarter turn to the right.
|
|
259
|
+
*
|
|
260
|
+
* There is ONE camera, and the human owns it: a drag during the move cancels
|
|
261
|
+
* it exactly where it is, and the resolved outcome says `cancelledBy:
|
|
262
|
+
* 'human'` rather than throwing. A second look verb supersedes the first.
|
|
263
|
+
* The move is drawn by the document's own frame loop, so a document that
|
|
264
|
+
* isn't being drawn (background tab, inactive panel) doesn't orbit.
|
|
265
|
+
*/
|
|
266
|
+
async orbit(options: {
|
|
267
|
+
/** RADIANS, relative to where the camera is now. `Math.PI / 2` is a
|
|
268
|
+
* quarter turn; degrees are not accepted and `90` is fourteen turns. */
|
|
269
|
+
readonly azimuth?: number;
|
|
270
|
+
/** RADIANS, relative to where the camera is now. */
|
|
271
|
+
readonly elevation?: number;
|
|
272
|
+
/** SECONDS the move takes (default 0.6). */
|
|
273
|
+
readonly duration?: number;
|
|
274
|
+
}): Promise<DocumentLookOutcome> {
|
|
275
|
+
// AN UNKNOWN KEY IS REFUSED BY NAME. Every member here is optional, so a
|
|
276
|
+
// misspelling — `yaw` for `azimuth`, `pitch` for `elevation` — used to
|
|
277
|
+
// orbit by nothing at all while the outcome still reported a plausible
|
|
278
|
+
// ABSOLUTE azimuth, which reads exactly like a move that happened
|
|
279
|
+
// (measured 2026-09-21, and it cost a round). The keys and their unit are
|
|
280
|
+
// in the refusal because that is the moment the caller needs them.
|
|
281
|
+
const known = ['azimuth', 'elevation', 'duration'];
|
|
282
|
+
const unknown = Object.keys(options ?? {}).filter((key) => !known.includes(key));
|
|
283
|
+
if (unknown.length > 0)
|
|
284
|
+
throw new Error(
|
|
285
|
+
`editor.orbit: ${unknown.join(', ')} ${unknown.length === 1 ? 'is not a key' : 'are not keys'} ` +
|
|
286
|
+
'this verb takes. It takes azimuth and elevation in RADIANS (relative to where the ' +
|
|
287
|
+
'camera is now) and duration in SECONDS.',
|
|
288
|
+
);
|
|
289
|
+
return this.#client.orbitDocument(options);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* A slow full revolution of the open document's subject — {@link orbit} with
|
|
294
|
+
* the turns spelled out and a constant angular rate. Resolves at the end of
|
|
295
|
+
* the last revolution.
|
|
296
|
+
*/
|
|
297
|
+
async turntable(options?: {
|
|
298
|
+
readonly seconds?: number;
|
|
299
|
+
readonly revolutions?: number;
|
|
300
|
+
}): Promise<DocumentLookOutcome> {
|
|
301
|
+
return this.#client.turntableDocument(options);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async view(preset: ViewPreset): Promise<void> {
|
|
305
|
+
await this.#client.viewPreset(preset);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Switch the editor's NAMED WORKSPACE — `await editor.workspace('model')`.
|
|
310
|
+
*
|
|
311
|
+
* A workspace is a task-named LAYOUT MEMORY over the one dock
|
|
312
|
+
* (ARCHITECTURE-CORE §Editor chrome): `game` (the default, the editor's
|
|
313
|
+
* standing arrangement), `model`, `sculpt`, `texture`, `animate`, `look`.
|
|
314
|
+
* Switching is an EXPLICIT act — nothing in the editor moves chrome on its
|
|
315
|
+
* own, opening a document included — and this is the session door to it,
|
|
316
|
+
* beside `Window → Workspace` and the registered actions.
|
|
317
|
+
*
|
|
318
|
+
* Resolves once the dock has finished rebuilding, so a capture taken
|
|
319
|
+
* immediately after photographs the arrangement that was asked for. Each
|
|
320
|
+
* workspace remembers the user's own hand-tuning per project, so switching
|
|
321
|
+
* away and back is lossless.
|
|
322
|
+
*/
|
|
323
|
+
async workspace(id: EditorWorkspaceName): Promise<void> {
|
|
324
|
+
await this.#client.setWorkspace(id);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Apply a STYLE BUNDLE by id — the chrome's palette, material, icon set and
|
|
329
|
+
* region defaults in one gesture, the session door beside
|
|
330
|
+
* `View → <Style> Style`. A bundle the open project does not offer refuses
|
|
331
|
+
* and names the vocabulary; `currentView().style` reports the one worn.
|
|
332
|
+
*/
|
|
333
|
+
async style(id: string): Promise<void> {
|
|
334
|
+
await this.#client.setStyle(id);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Set the MATERIAL apart from the bundle that usually carries it.
|
|
339
|
+
* Appearance is palette × material, independent axes by ruling, so
|
|
340
|
+
* `style()` alone can never say whether a cost belongs to the blur or to
|
|
341
|
+
* the palette. This is the door that measures them apart; it answers with
|
|
342
|
+
* what the chrome wears afterwards (`style` is `null` when the mix matches
|
|
343
|
+
* no registered bundle).
|
|
344
|
+
*/
|
|
345
|
+
async appearance(appearance: {
|
|
346
|
+
readonly material?: string;
|
|
347
|
+
}): Promise<{ material: string; style: string | null }> {
|
|
348
|
+
return this.#client.setAppearance(appearance);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Focus an editor panel: a viewport tab, the console, the build surface, or
|
|
352
|
+
* any key the editor's static-panel registry holds — an unknown key refuses
|
|
353
|
+
* naming the ones it does. */
|
|
354
|
+
async showPanel(name: PanelName): Promise<void> {
|
|
355
|
+
switch (name) {
|
|
356
|
+
case 'viewport-edit':
|
|
357
|
+
await this.#client.showViewport('edit');
|
|
358
|
+
return;
|
|
359
|
+
case 'console':
|
|
360
|
+
await this.#client.toggleConsole();
|
|
361
|
+
return;
|
|
362
|
+
default:
|
|
363
|
+
// Every other name is the EDITOR's to resolve, against its live panel
|
|
364
|
+
// registry. A list kept here could only ever be a copy going stale.
|
|
365
|
+
await this.#client.showPanel(name);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** `kind` inferred from `path`'s extension when omitted (`inferAssetKind`) — pass it explicitly to override. */
|
|
371
|
+
async openAsset(path: string, kind?: AssetKind): Promise<void> {
|
|
372
|
+
await this.#client.openAsset(path, kind ?? inferAssetKind(path));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* SELECT a project asset — the browser's single click, which fills the
|
|
377
|
+
* Inspector without opening a document. `openAsset` is the double click.
|
|
378
|
+
*
|
|
379
|
+
* This is how a project's own `asset.inspector` section is reached: select
|
|
380
|
+
* the file it matches, then `inspect()` lists the verbs that section
|
|
381
|
+
* declares and `runAction(id)` runs one. Selecting a path nothing matches
|
|
382
|
+
* is not an error — the Inspector shows what it has, exactly as it does
|
|
383
|
+
* for a human.
|
|
384
|
+
*/
|
|
385
|
+
async selectAsset(path: string): Promise<void> {
|
|
386
|
+
await this.#client.selectAsset(path);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Captures the editor's native four-view preview. A bare string is a
|
|
391
|
+
* project-relative asset path (the common case); an explicit source object
|
|
392
|
+
* targets a path, a LIVE SCENE ENTITY (`assetPreview({ entityId }, …)`) —
|
|
393
|
+
* which is what makes `options.stage: 'scene'`, the entity photographed
|
|
394
|
+
* where it stands under the scene's own lighting, reachable from here — or
|
|
395
|
+
* RAW GLB BYTES (`assetPreview({ glbBase64 }, …)`), for a model that exists
|
|
396
|
+
* only in the calling Node process's memory and has never been written to
|
|
397
|
+
* disk. `stage` defaults to `'lab'`, the neutral Asset Lab staging this has
|
|
398
|
+
* always produced, and the bytes form is lab-only.
|
|
399
|
+
*/
|
|
400
|
+
async assetPreview(
|
|
401
|
+
source: string | AssetPreviewSource,
|
|
402
|
+
options?: AssetPreviewOptions,
|
|
403
|
+
): Promise<AssetPreviewCapture> {
|
|
404
|
+
return this.#client.captureAssetPreview(
|
|
405
|
+
typeof source === 'string' ? { assetPath: source } : source,
|
|
406
|
+
options,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The same subject photographed as a LABELED SHOT SET instead of the four
|
|
412
|
+
* views — a caller-supplied definition of turntable yaws and bone-anchored
|
|
413
|
+
* crops, rendered against the asset's own skeleton, with a contact sheet.
|
|
414
|
+
* Every source {@link assetPreview} takes works here, GLB bytes included:
|
|
415
|
+
* a shot set stages its own subject, so it needs no place to stand.
|
|
416
|
+
*
|
|
417
|
+
* Sole in-repo caller today: `project.bake.preview`'s `--orbit` lane.
|
|
418
|
+
*/
|
|
419
|
+
async assetPreviewShots(
|
|
420
|
+
source: string | AssetPreviewSource,
|
|
421
|
+
definition: AssetPreviewShotSetDefinition,
|
|
422
|
+
options?: AssetPreviewOptions,
|
|
423
|
+
): Promise<LabeledShotSetCapture> {
|
|
424
|
+
return this.#client.captureShotSetPreview(
|
|
425
|
+
typeof source === 'string' ? { assetPath: source } : source,
|
|
426
|
+
definition,
|
|
427
|
+
options,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async grid(on: boolean): Promise<void> {
|
|
432
|
+
await this.#client.setGrid(on);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async helpers(on: boolean): Promise<void> {
|
|
436
|
+
await this.#client.setHelpers(on);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async stats(on: boolean): Promise<void> {
|
|
440
|
+
await this.#client.setStats(on);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async shading(mode: ShadingMode): Promise<void> {
|
|
444
|
+
await this.#client.setShadingMode(mode);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* READ the inspector, as data — the serialized inspection subject
|
|
449
|
+
* (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
|
|
450
|
+
* "The Inspection Model"). This is the Figma-Inspect analog: whatever a
|
|
451
|
+
* human would see in the inspector right now — the subject's identity, its
|
|
452
|
+
* verbs, and every identified section in display order, with a `fields`
|
|
453
|
+
* section's CURRENT VALUES at their scriptable `path`s.
|
|
454
|
+
*
|
|
455
|
+
* Reach for it whenever the next step depends on what an object actually
|
|
456
|
+
* IS: `await editor.select(id)` then `await editor.inspect()` answers "what
|
|
457
|
+
* properties does this thing have, and what are they set to" in one call,
|
|
458
|
+
* against the same model the panel renders — no scene-graph reads, no
|
|
459
|
+
* guessing at property names.
|
|
460
|
+
*
|
|
461
|
+
* When the inspector is showing NOTHING — nothing selected on a surface
|
|
462
|
+
* with no empty-state subject of its own, which is most of them — the answer
|
|
463
|
+
* is `{none: true}`, so "the human sees no inspector" and "the read failed"
|
|
464
|
+
* are never the same value. A surface whose empty space IS a real thing (an
|
|
465
|
+
* open Asset Lab document) still answers with that subject, and never with
|
|
466
|
+
* another surface's.
|
|
467
|
+
*
|
|
468
|
+
* A `custom` section body is a named opaque: the editor renders it with
|
|
469
|
+
* React, so the wire reports its identity rather than pretending to describe
|
|
470
|
+
* its rendering — plus, when the section can say what it DISPLAYS, a `data`
|
|
471
|
+
* payload in its own vocabulary (`transform` carries
|
|
472
|
+
* `{position, rotation, scale}`, rotation in Euler XYZ degrees).
|
|
473
|
+
*/
|
|
474
|
+
async inspect(): Promise<InspectedInspection> {
|
|
475
|
+
return this.#client.inspect();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Run one verb listed by `inspect().quickActions`, through the same action
|
|
479
|
+
* the human Inspector button invokes. */
|
|
480
|
+
async runAction(actionId: string): Promise<InspectedInspection> {
|
|
481
|
+
return this.#client.runInspectionAction(actionId);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Run ONE command by id — the door to everything the command palette lists.
|
|
486
|
+
*
|
|
487
|
+
* ONE NAME (orchestrator ruling 2026-09-19). There were briefly TWO doors
|
|
488
|
+
* onto the one view-verb table — this one and `editor.viewVerb(view, verb)`,
|
|
489
|
+
* which addressed the same registry by its two halves. A second addressing
|
|
490
|
+
* of one table is a second name for one thing, and an agent reading
|
|
491
|
+
* `--list` had to choose between them with nothing to choose on. This door
|
|
492
|
+
* stays because it is strictly wider: it addresses a COMMAND ID, so under
|
|
493
|
+
* the frame it reaches everything the workbench knows — a `vgai.action.<id>`
|
|
494
|
+
* editor action, one of VS Code's own — and not only a view. A VIEW is
|
|
495
|
+
* reached by spelling its verb's command id:
|
|
496
|
+
*
|
|
497
|
+
* await editor.command('vgai.blender-uv-view.state')
|
|
498
|
+
* await editor.command('vgai.blender-uv-view.zoom', { to: 600 })
|
|
499
|
+
*
|
|
500
|
+
* await editor.command('vgai.blender-node-view.view-all')
|
|
501
|
+
* await editor.command('vgai.blender-node-view.look', { node: 'Principled BSDF' })
|
|
502
|
+
*
|
|
503
|
+
* Under the Code-OSS frame this is the workbench's own command service, so
|
|
504
|
+
* any command id works — ours and VS Code's alike. Standalone `vgai edit`
|
|
505
|
+
* has no command service and answers the `vgai.<view>.<verb>` shape off the
|
|
506
|
+
* SAME verb table the frame's commands call, refusing any other id by name.
|
|
507
|
+
* One table, two doors, exactly like the keymap's.
|
|
508
|
+
*
|
|
509
|
+
* Answers with whatever the command returned — a view verb's own state, or
|
|
510
|
+
* `null` for a command that returns nothing.
|
|
511
|
+
*/
|
|
512
|
+
async command(commandId: string, args?: unknown): Promise<unknown> {
|
|
513
|
+
return this.#client.runCommand(commandId, args);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* RESTRUCTURE the authored tree — the hierarchy context menu's own verbs.
|
|
518
|
+
*
|
|
519
|
+
* `create`, `delete`, `duplicate`, `reparent`, `reorder`, `wrap`, `unwrap`,
|
|
520
|
+
* `group`, `ungroup`, `copy`, `cut`, `paste`; `extractComponent` and
|
|
521
|
+
* `forkComponent` are the two that write whole new files and have their own
|
|
522
|
+
* doors below. All of them run the SAME `authoring/consumer-actions.ts`
|
|
523
|
+
* helpers the menu items call, so there is one implementation of each op and
|
|
524
|
+
* not a second that can disagree with what a human gets.
|
|
525
|
+
*
|
|
526
|
+
* It exists because the menu is a POINTER surface: every one of these ops was
|
|
527
|
+
* reachable only by right-clicking a hierarchy row, which is nothing an agent
|
|
528
|
+
* can do — so for an ingest root, whose only authoring surface IS the editor,
|
|
529
|
+
* structure was closed entirely.
|
|
530
|
+
*
|
|
531
|
+
* `id`/`ids` default to the current selection. The answer carries the same
|
|
532
|
+
* per-edit `write` ack `setField` does, so `write.persisted` tells a saved
|
|
533
|
+
* restructure from a live-only one. An op the active adapter does not provide
|
|
534
|
+
* REJECTS by name — never a silent no-op.
|
|
535
|
+
*/
|
|
536
|
+
async structure(op: StructureOp, options?: StructureOpOptions): Promise<StructureOpResult> {
|
|
537
|
+
return this.#client.structureOp(op, options ?? {});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* "Extract Component…" — lift the selected native subtree into its own
|
|
542
|
+
* component file (plus a story) and replace the callsite with it.
|
|
543
|
+
*
|
|
544
|
+
* Answers the action's own sentence, which NAMES both new files, because
|
|
545
|
+
* undo owns the callsite edit and will not remove them.
|
|
546
|
+
*/
|
|
547
|
+
async extractComponent(options?: { id?: string; name?: string }): Promise<string> {
|
|
548
|
+
return (await this.#client.extractComponent(options ?? {})).hint;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* "Fork Component…" — copy the selected instance's component definition to a
|
|
553
|
+
* new file and retarget THIS CALLSITE at it.
|
|
554
|
+
*
|
|
555
|
+
* One callsite is the unit of the edit; when that callsite sits inside a
|
|
556
|
+
* component rendered many times, every one of those renders now renders the
|
|
557
|
+
* fork.
|
|
558
|
+
*/
|
|
559
|
+
async forkComponent(options?: { id?: string }): Promise<string> {
|
|
560
|
+
return (await this.#client.forkComponent(options ?? {})).hint;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* READ the hierarchy panel, as data — the rows a human is looking at right
|
|
565
|
+
* now, nested exactly as the panel nests them.
|
|
566
|
+
*
|
|
567
|
+
* The companion to {@link inspect}: that one answers "what IS the selected
|
|
568
|
+
* thing", this one answers "what does the tree LOOK LIKE". It is the panel's
|
|
569
|
+
* own output, not a fresh walk of the scene — the adapter's tree after the
|
|
570
|
+
* component marks fold implementation subtrees (bones, particle renderers,
|
|
571
|
+
* instanced pools), after the internals reveal, the document promotion, the
|
|
572
|
+
* child cap, the collapse state, the search filter and the selection scope.
|
|
573
|
+
*
|
|
574
|
+
* Works in play mode and edit mode; the answer says which (`playState`,
|
|
575
|
+
* `activeViewportTab`), because the two are different adapters and a tree
|
|
576
|
+
* that looks wrong is very often the wrong adapter's tree.
|
|
577
|
+
*
|
|
578
|
+
* Prefer this over `status().entities`, which is deliberately a different
|
|
579
|
+
* question — the RAW adapter tree, unprojected. A panel that renders the
|
|
580
|
+
* wrong rows looks perfectly healthy in that facet.
|
|
581
|
+
*
|
|
582
|
+
* Each row carries `childCount` (what its caret opens), `internalChildCount`
|
|
583
|
+
* (what is folded behind "Reveal Internals") and `expandable` (whether the
|
|
584
|
+
* panel draws a caret at all), so "this subtree exists but nothing in the UI
|
|
585
|
+
* opens it" is a fact you can read rather than one you have to notice.
|
|
586
|
+
*
|
|
587
|
+
* Rejects, naming the panel, when no hierarchy panel is mounted — an empty
|
|
588
|
+
* tree would be a fabricated answer about a surface nobody is being shown.
|
|
589
|
+
*/
|
|
590
|
+
async hierarchy(): Promise<InspectedHierarchy> {
|
|
591
|
+
return this.#client.hierarchy();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Expand every branch through the Hierarchy panel's own action. */
|
|
595
|
+
async expandHierarchyAll(): Promise<void> {
|
|
596
|
+
await this.#client.expandHierarchyAll();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Collapse every branch through the same panel action. Expanding is
|
|
600
|
+
* persisted per project, so without this the tree's REST STATE — what a
|
|
601
|
+
* person sees on opening the project — is unreachable once any reader has
|
|
602
|
+
* expanded it. */
|
|
603
|
+
async collapseHierarchyAll(): Promise<void> {
|
|
604
|
+
await this.#client.collapseHierarchyAll();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Write one editable field from `inspect()` by its stable path, through the
|
|
609
|
+
* same Inspector IO and persistence boundary the human control uses.
|
|
610
|
+
*
|
|
611
|
+
* The answer is `{ subject, write }`, and `write` is the half worth reading
|
|
612
|
+
* first: a write with no persistence route open still succeeds — it lands on
|
|
613
|
+
* the live object and journals live-only — so `write.persisted` is how you
|
|
614
|
+
* tell a saved edit from one that will not survive the session, without
|
|
615
|
+
* diffing the tree. `write.destination` is the adapter's own words for where
|
|
616
|
+
* it went ("live-only (not saved)" is a destination, never silence).
|
|
617
|
+
*/
|
|
618
|
+
async setField(path: string, value: unknown): Promise<InspectedFieldWrite> {
|
|
619
|
+
return this.#client.setInspectionField(path, value);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* REMOVE one field's authored override — the revert arrow, as a command.
|
|
624
|
+
*
|
|
625
|
+
* Reach for this instead of `setField` whenever you are UNDOING an edit that
|
|
626
|
+
* added a property the source did not carry: `setField` can only write a
|
|
627
|
+
* value, so setting the default back leaves `position={[0, 0, 0]}` in the
|
|
628
|
+
* file where there was nothing before. Only this door restores the bytes.
|
|
629
|
+
*
|
|
630
|
+
* The answer is the same `{ subject, write }` shape, awaited past the bytes.
|
|
631
|
+
* It rejects with `code: 'REMOVAL_UNAVAILABLE'` when the field is not
|
|
632
|
+
* declared removable or the lane has no removal door — which is a missing
|
|
633
|
+
* seam to report, not a removal that failed.
|
|
634
|
+
*/
|
|
635
|
+
async removeField(path: string): Promise<InspectedFieldWrite> {
|
|
636
|
+
return this.#client.removeInspectionField(path);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Open a document by its adapter-declared id, through its registered owner. */
|
|
640
|
+
async open(id: string): Promise<OpenedDocument> {
|
|
641
|
+
return this.#client.open(id);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Undo / redo one project transaction, through the session's own history
|
|
645
|
+
* queue — the same one the keyboard shortcut drives. */
|
|
646
|
+
async undo(): Promise<HistoryStep> {
|
|
647
|
+
return this.#client.undo();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async redo(): Promise<HistoryStep> {
|
|
651
|
+
return this.#client.redo();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** Mirrors `vgai status` — the full live editor state as JSON. */
|
|
655
|
+
async status(): Promise<EditorState> {
|
|
656
|
+
return this.#client.getState();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** A live viewport PNG (`EditorClient.captureViewport`) — no direct CLI verb exists; this is the closest wire read. */
|
|
660
|
+
async screenshot(size?: number): Promise<ViewportCapture> {
|
|
661
|
+
return this.#client.captureViewport(size);
|
|
662
|
+
}
|
|
663
|
+
}
|