@vgai/live 0.5.42 → 0.5.45

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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `editor.document` — the session binding for the scoped editor-chrome door.
3
3
  *
4
- * WHY IT IS A SEPARATE OBJECT, and why the verbs are these four, is recorded
4
+ * WHY IT IS A SEPARATE OBJECT, and why the verbs are these five, is recorded
5
5
  * once in the implementation's header
6
6
  * (`packages/editor/src/editor-document-probe.ts`); the short version is that
7
7
  * `game.page()` is play-mode-gated and rooted at the GAME container, so an
@@ -16,7 +16,7 @@
16
16
  * RENDERED rather than what a DOM scrape can find.
17
17
  *
18
18
  * A field on `LiveEditor` rather than methods on it, so `vgai eval --list`
19
- * shows the four verbs as one named surface — the same reason `game.input`
19
+ * shows the six verbs as one named surface — the same reason `game.input`
20
20
  * and `game.events` are instance fields.
21
21
  */
22
22
  import type { DocumentProbeResult, EditorClient } from '@vgai/editor-sdk';
@@ -48,9 +48,46 @@ export declare class LiveEditorDocument {
48
48
  /** A REAL pointer gesture (pointerdown/mousedown/focus/pointerup/mouseup/click)
49
49
  * — not `element.click()`, which a `pointerdown` listener never sees. */
50
50
  click(selector: string, options?: DocumentGestureOptions): Promise<DocumentProbeResult>;
51
+ /**
52
+ * A real pointer DRAG across one matched element — press at `from`, move,
53
+ * release at `to`, as fractions of the element's box (`[0.5, 0.5]` is its
54
+ * center). The gesture a direct-manipulation canvas needs; a zero-length
55
+ * drag is a click at that fraction, which `click` (always the center)
56
+ * cannot place.
57
+ */
58
+ drag(selector: string, options: DocumentGestureOptions & {
59
+ from: [number, number];
60
+ to: [number, number];
61
+ via?: [number, number][];
62
+ steps?: number;
63
+ altKey?: boolean;
64
+ ctrlKey?: boolean;
65
+ metaKey?: boolean;
66
+ shiftKey?: boolean;
67
+ }): Promise<DocumentProbeResult>;
51
68
  /** A real keydown/keyup on the target, or on whatever inside the document has focus. */
52
69
  key(key: string, options?: DocumentKeyOptions): Promise<DocumentProbeResult>;
53
70
  /** A real `ClipboardEvent` carrying `text/plain` — the gesture nothing else
54
71
  * in the product can produce. */
55
72
  paste(text: string, options?: DocumentPasteOptions): Promise<DocumentProbeResult>;
73
+ /**
74
+ * Choose `value` on a `<select>` — a native dropdown's options are drawn by
75
+ * the OS, so `click` has nothing in the document to resolve, and a plain
76
+ * `element.value =` is invisible to React. Set through the prototype's own
77
+ * value setter plus `input`/`change`; `value` is the option's `value`, not
78
+ * its label. An unknown value is refused with the options it does offer.
79
+ */
80
+ select(selector: string, value: string, options?: DocumentGestureOptions): Promise<DocumentProbeResult>;
81
+ /**
82
+ * THE REPL over the open document: run `step` in the editor page against the
83
+ * object the ACTIVE document published as its context (the mesh document
84
+ * publishes its `MeshEditSession`, whose `ctx` is the bpy-shaped edit
85
+ * context — `ctx.ops.mesh.bevel({ offset: 0.1 })`, `ctx.selection`,
86
+ * `ctx.history`, `session.commit()`). Edit mode, no play. Serialized like
87
+ * `game.page`: the step's own source travels, so inline every value it
88
+ * needs and return plain data.
89
+ */
90
+ run<T = unknown>(step: (ctx: unknown, info: {
91
+ documentId: string;
92
+ }) => T | Promise<T>): Promise<T>;
56
93
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `editor.document` — the session binding for the scoped editor-chrome door.
3
3
  *
4
- * WHY IT IS A SEPARATE OBJECT, and why the verbs are these four, is recorded
4
+ * WHY IT IS A SEPARATE OBJECT, and why the verbs are these five, is recorded
5
5
  * once in the implementation's header
6
6
  * (`packages/editor/src/editor-document-probe.ts`); the short version is that
7
7
  * `game.page()` is play-mode-gated and rooted at the GAME container, so an
@@ -16,7 +16,7 @@
16
16
  * RENDERED rather than what a DOM scrape can find.
17
17
  *
18
18
  * A field on `LiveEditor` rather than methods on it, so `vgai eval --list`
19
- * shows the four verbs as one named surface — the same reason `game.input`
19
+ * shows the six verbs as one named surface — the same reason `game.input`
20
20
  * and `game.events` are instance fields.
21
21
  */
22
22
  export class LiveEditorDocument {
@@ -42,6 +42,28 @@ export class LiveEditorDocument {
42
42
  ...(options?.index === undefined ? {} : { index: options.index }),
43
43
  });
44
44
  }
45
+ /**
46
+ * A real pointer DRAG across one matched element — press at `from`, move,
47
+ * release at `to`, as fractions of the element's box (`[0.5, 0.5]` is its
48
+ * center). The gesture a direct-manipulation canvas needs; a zero-length
49
+ * drag is a click at that fraction, which `click` (always the center)
50
+ * cannot place.
51
+ */
52
+ async drag(selector, options) {
53
+ return this.#probe({
54
+ action: 'drag',
55
+ selector,
56
+ from: options.from,
57
+ to: options.to,
58
+ ...(options.via === undefined ? {} : { via: options.via }),
59
+ ...(options.steps === undefined ? {} : { steps: options.steps }),
60
+ ...(options.index === undefined ? {} : { index: options.index }),
61
+ ...(options.altKey === undefined ? {} : { altKey: options.altKey }),
62
+ ...(options.ctrlKey === undefined ? {} : { ctrlKey: options.ctrlKey }),
63
+ ...(options.metaKey === undefined ? {} : { metaKey: options.metaKey }),
64
+ ...(options.shiftKey === undefined ? {} : { shiftKey: options.shiftKey }),
65
+ });
66
+ }
45
67
  /** A real keydown/keyup on the target, or on whatever inside the document has focus. */
46
68
  async key(key, options) {
47
69
  return this.#probe({ action: 'key', key, ...(options ?? {}) });
@@ -51,6 +73,33 @@ export class LiveEditorDocument {
51
73
  async paste(text, options) {
52
74
  return this.#probe({ action: 'paste', text, ...(options ?? {}) });
53
75
  }
76
+ /**
77
+ * Choose `value` on a `<select>` — a native dropdown's options are drawn by
78
+ * the OS, so `click` has nothing in the document to resolve, and a plain
79
+ * `element.value =` is invisible to React. Set through the prototype's own
80
+ * value setter plus `input`/`change`; `value` is the option's `value`, not
81
+ * its label. An unknown value is refused with the options it does offer.
82
+ */
83
+ async select(selector, value, options) {
84
+ return this.#probe({
85
+ action: 'select',
86
+ selector,
87
+ value,
88
+ ...(options?.index === undefined ? {} : { index: options.index }),
89
+ });
90
+ }
91
+ /**
92
+ * THE REPL over the open document: run `step` in the editor page against the
93
+ * object the ACTIVE document published as its context (the mesh document
94
+ * publishes its `MeshEditSession`, whose `ctx` is the bpy-shaped edit
95
+ * context — `ctx.ops.mesh.bevel({ offset: 0.1 })`, `ctx.selection`,
96
+ * `ctx.history`, `session.commit()`). Edit mode, no play. Serialized like
97
+ * `game.page`: the step's own source travels, so inline every value it
98
+ * needs and return plain data.
99
+ */
100
+ async run(step) {
101
+ return this.#client.documentScript(step.toString());
102
+ }
54
103
  #probe(step) {
55
104
  return this.#client.documentProbe(step);
56
105
  }
package/dist/editor.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * hand-rolls a `fetch` to `/__editor/command` itself, and has no FILE-mode
11
11
  * escape hatch: every method goes over the wire.
12
12
  */
13
- import type { ActiveDocumentCapture, AnimationCaptureAction, AssetKind, AssetPreviewCapture, AssetPreviewOptions, AssetPreviewSource, EditorClient, EditorState, EditorView, HistoryStep, InspectedFieldWrite, InspectedHierarchy, InspectedInspection, OpenedDocument, PresentedEditorView, RagdollGenerationResult, ShadingMode, ViewPreset, ViewportCapture } from '@vgai/editor-sdk';
13
+ import type { ActiveDocumentCapture, AnimationCaptureAction, AssetKind, AssetPreviewCapture, AssetPreviewOptions, AssetPreviewShotSetDefinition, AssetPreviewSource, CaptureDimensions, DocumentLookOutcome, EditorChromeCapture, EditorClient, EditorState, EditorView, EditorWorkspaceName, HistoryStep, InspectedFieldWrite, InspectedHierarchy, InspectedInspection, LabeledShotSetCapture, OpenedDocument, PresentedEditorView, RagdollGenerationResult, ShadingMode, StructureOp, StructureOpOptions, StructureOpResult, ViewPreset, ViewportCapture } from '@vgai/editor-sdk';
14
14
  import { LiveEditorDocument } from './editor-document.js';
15
15
  import { LiveGameplayRecording } from './recording.js';
16
16
  /** `vgai show <...>`'s four sub-verbs folded into one action name — see `showPanel`. */
@@ -52,8 +52,25 @@ export declare class LiveEditor {
52
52
  present(view: EditorView): Promise<PresentedEditorView>;
53
53
  /** The human editor's actual active document, selection, camera and utility. */
54
54
  currentView(): Promise<EditorView>;
55
- /** Capture the same center document the human is currently looking at. */
56
- captureActiveDocument(size?: number): Promise<ActiveDocumentCapture>;
55
+ /**
56
+ * Capture the same center document the human is currently looking at.
57
+ *
58
+ * A number is a SQUARE of that size — the default, and the right shape for
59
+ * an unstaged look at a model. `{width, height}` asks for a shaped frame, so
60
+ * a video-aspect look needs no crop afterwards. Both are bounded by the
61
+ * relay budget (64-1024 per side, total no larger than a 1024 square); see
62
+ * `@vgai/editor-sdk`'s `CaptureDimensions`.
63
+ * Supply a view to present and photograph it in one editor request.
64
+ */
65
+ captureActiveDocument(size?: CaptureDimensions, view?: EditorView): Promise<ActiveDocumentCapture>;
66
+ /**
67
+ * Photograph the editor PAGE — every panel, tab strip and viewport as the
68
+ * person sees it. `vgai screenshot editor` is this verb from the shell. The
69
+ * one door for judging chrome sighted: a skin, a workspace arrangement or a
70
+ * contributed panel is looked at through this, never guessed at from DOM
71
+ * probes. Returns the page at its own size.
72
+ */
73
+ captureEditorChrome(): Promise<EditorChromeCapture>;
57
74
  play(opts?: {
58
75
  seed?: number;
59
76
  }): Promise<void>;
@@ -77,6 +94,46 @@ export declare class LiveEditor {
77
94
  * would otherwise be indistinguishable from one that worked.
78
95
  */
79
96
  frame(entityId: string): Promise<void>;
97
+ /**
98
+ * Bare `frame()` frames the OPEN Object3D document's subject instead — its
99
+ * selection if it has one, else the whole model: the toolbar's own Frame
100
+ * button, reachable from a script. `fit` scales the fitted distance (1 is
101
+ * that button's tight fit, 1.5 stands back a little for a shot).
102
+ */
103
+ frame(options?: {
104
+ readonly fit?: number;
105
+ }): Promise<void>;
106
+ /**
107
+ * WATCH THE AGENT LOOK AROUND THE MODEL.
108
+ *
109
+ * Swings the open Object3D document's camera — the camera the human's tab is
110
+ * showing — around the framed subject by `azimuth`/`elevation` RADIANS,
111
+ * animated over `duration` seconds (default 0.6), and resolves when the move
112
+ * ends. This is deliberately not a jump cut: the point of the verb is that a
113
+ * person watching sees the agent walk around the thing it is working on.
114
+ *
115
+ * `await editor.orbit({ azimuth: Math.PI / 2 })` — a quarter turn to the right.
116
+ *
117
+ * There is ONE camera, and the human owns it: a drag during the move cancels
118
+ * it exactly where it is, and the resolved outcome says `cancelledBy:
119
+ * 'human'` rather than throwing. A second look verb supersedes the first.
120
+ * The move is drawn by the document's own frame loop, so a document that
121
+ * isn't being drawn (background tab, inactive panel) doesn't orbit.
122
+ */
123
+ orbit(options: {
124
+ readonly azimuth?: number;
125
+ readonly elevation?: number;
126
+ readonly duration?: number;
127
+ }): Promise<DocumentLookOutcome>;
128
+ /**
129
+ * A slow full revolution of the open document's subject — {@link orbit} with
130
+ * the turns spelled out and a constant angular rate. Resolves at the end of
131
+ * the last revolution.
132
+ */
133
+ turntable(options?: {
134
+ readonly seconds?: number;
135
+ readonly revolutions?: number;
136
+ }): Promise<DocumentLookOutcome>;
80
137
  view(preset: ViewPreset): Promise<void>;
81
138
  /**
82
139
  * Show several instances of the running game split-screen — the whole point
@@ -89,10 +146,37 @@ export declare class LiveEditor {
89
146
  * session.
90
147
  */
91
148
  instances(countOrNames: number | string[]): Promise<void>;
149
+ /**
150
+ * Switch the editor's NAMED WORKSPACE — `await editor.workspace('model')`.
151
+ *
152
+ * A workspace is a task-named LAYOUT MEMORY over the one dock
153
+ * (ARCHITECTURE-CORE §Editor chrome): `game` (the default, the editor's
154
+ * standing arrangement), `model`, `sculpt`, `texture`, `animate`, `look`.
155
+ * Switching is an EXPLICIT act — nothing in the editor moves chrome on its
156
+ * own, opening a document included — and this is the session door to it,
157
+ * beside `Window → Workspace` and the registered actions.
158
+ *
159
+ * Resolves once the dock has finished rebuilding, so a capture taken
160
+ * immediately after photographs the arrangement that was asked for. Each
161
+ * workspace remembers the user's own hand-tuning per project, so switching
162
+ * away and back is lossless.
163
+ */
164
+ workspace(id: EditorWorkspaceName): Promise<void>;
92
165
  /** `vgai show <viewport <edit|play>|inspector|console|build>`'s four sub-verbs, folded into one action name. */
93
166
  showPanel(name: PanelName): Promise<void>;
94
167
  /** `kind` inferred from `path`'s extension when omitted (`inferAssetKind`) — pass it explicitly to override. */
95
168
  openAsset(path: string, kind?: AssetKind): Promise<void>;
169
+ /**
170
+ * SELECT a project asset — the browser's single click, which fills the
171
+ * Inspector without opening a document. `openAsset` is the double click.
172
+ *
173
+ * This is how a project's own `asset.inspector` section is reached: select
174
+ * the file it matches, then `inspect()` lists the verbs that section
175
+ * declares and `runAction(id)` runs one. Selecting a path nothing matches
176
+ * is not an error — the Inspector shows what it has, exactly as it does
177
+ * for a human.
178
+ */
179
+ selectAsset(path: string): Promise<void>;
96
180
  /**
97
181
  * Fit native Rapier bodies and joints to a rigged GLB/glTF, copy the reusable
98
182
  * project capability, and open the generated TSX prefab's Setup story.
@@ -110,6 +194,16 @@ export declare class LiveEditor {
110
194
  * always produced, and the bytes form is lab-only.
111
195
  */
112
196
  assetPreview(source: string | AssetPreviewSource, options?: AssetPreviewOptions): Promise<AssetPreviewCapture>;
197
+ /**
198
+ * The same subject photographed as a LABELED SHOT SET instead of the four
199
+ * views — a caller-supplied definition of turntable yaws and bone-anchored
200
+ * crops, rendered against the asset's own skeleton, with a contact sheet.
201
+ * Every source {@link assetPreview} takes works here, GLB bytes included:
202
+ * a shot set stages its own subject, so it needs no place to stand.
203
+ *
204
+ * Sole in-repo caller today: `project.bake.preview`'s `--orbit` lane.
205
+ */
206
+ assetPreviewShots(source: string | AssetPreviewSource, definition: AssetPreviewShotSetDefinition, options?: AssetPreviewOptions): Promise<LabeledShotSetCapture>;
113
207
  grid(on: boolean): Promise<void>;
114
208
  helpers(on: boolean): Promise<void>;
115
209
  stats(on: boolean): Promise<void>;
@@ -150,6 +244,49 @@ export declare class LiveEditor {
150
244
  /** Run one verb listed by `inspect().quickActions`, through the same action
151
245
  * the human Inspector button invokes. */
152
246
  runAction(actionId: string): Promise<InspectedInspection>;
247
+ /**
248
+ * RESTRUCTURE the authored tree — the hierarchy context menu's own verbs.
249
+ *
250
+ * `create`, `delete`, `duplicate`, `reparent`, `reorder`, `wrap`, `unwrap`,
251
+ * `group`, `ungroup`, `copy`, `cut`, `paste`; `extractComponent` and
252
+ * `forkComponent` are the two that write whole new files and have their own
253
+ * doors below. All of them run the SAME `authoring/consumer-actions.ts`
254
+ * helpers the menu items call, so there is one implementation of each op and
255
+ * not a second that can disagree with what a human gets.
256
+ *
257
+ * It exists because the menu is a POINTER surface: every one of these ops was
258
+ * reachable only by right-clicking a hierarchy row, which is nothing an agent
259
+ * can do — so for an ingest root, whose only authoring surface IS the editor,
260
+ * structure was closed entirely.
261
+ *
262
+ * `id`/`ids` default to the current selection. The answer carries the same
263
+ * per-edit `write` ack `setField` does, so `write.persisted` tells a saved
264
+ * restructure from a live-only one. An op the active adapter does not provide
265
+ * REJECTS by name — never a silent no-op.
266
+ */
267
+ structure(op: StructureOp, options?: StructureOpOptions): Promise<StructureOpResult>;
268
+ /**
269
+ * "Extract Component…" — lift the selected native subtree into its own
270
+ * component file (plus a story) and replace the callsite with it.
271
+ *
272
+ * Answers the action's own sentence, which NAMES both new files, because
273
+ * undo owns the callsite edit and will not remove them.
274
+ */
275
+ extractComponent(options?: {
276
+ id?: string;
277
+ name?: string;
278
+ }): Promise<string>;
279
+ /**
280
+ * "Fork Component…" — copy the selected instance's component definition to a
281
+ * new file and retarget THIS CALLSITE at it.
282
+ *
283
+ * One callsite is the unit of the edit; when that callsite sits inside a
284
+ * component rendered many times, every one of those renders now renders the
285
+ * fork.
286
+ */
287
+ forkComponent(options?: {
288
+ id?: string;
289
+ }): Promise<string>;
153
290
  /**
154
291
  * READ the hierarchy panel, as data — the rows a human is looking at right
155
292
  * now, nested exactly as the panel nests them.
package/dist/editor.js CHANGED
@@ -30,6 +30,23 @@ const EXTENSION_KIND = {
30
30
  '.glsl': 'source',
31
31
  '.vert': 'source',
32
32
  '.frag': 'source',
33
+ // PROJECT SCRIPTS ARE SOURCE. Without these the guess below falls through to
34
+ // `'json'`, the asset-document router sends the file to the generic JSON
35
+ // viewer (`asset-documents.tsx#assetDocumentViewerRoute`: `spec.kind ===
36
+ // 'json'` is decided before any content routing), and the LIVE MODELING
37
+ // DOCUMENT never mounts — `editor.openAsset('src/lib/fox/fox.model.ts')`
38
+ // silently shows a text pane instead of the model. Only `kind: 'source'`
39
+ // reaches `SourceAssetViewer`, which is what content-routes a project script
40
+ // to `LiveModuleDocument`. The set matches that viewer's own
41
+ // `isProjectScriptPath` regex, `/\.(?:[cm]?[jt]sx?)$/`.
42
+ '.ts': 'source',
43
+ '.tsx': 'source',
44
+ '.mts': 'source',
45
+ '.cts': 'source',
46
+ '.js': 'source',
47
+ '.jsx': 'source',
48
+ '.mjs': 'source',
49
+ '.cjs': 'source',
33
50
  };
34
51
  /**
35
52
  * Lightweight extension-based `AssetKind` guess for `openAsset`'s optional
@@ -91,9 +108,28 @@ export class LiveEditor {
91
108
  async currentView() {
92
109
  return this.#client.currentView();
93
110
  }
94
- /** Capture the same center document the human is currently looking at. */
95
- async captureActiveDocument(size) {
96
- return this.#client.captureActiveDocument(size);
111
+ /**
112
+ * Capture the same center document the human is currently looking at.
113
+ *
114
+ * A number is a SQUARE of that size — the default, and the right shape for
115
+ * an unstaged look at a model. `{width, height}` asks for a shaped frame, so
116
+ * a video-aspect look needs no crop afterwards. Both are bounded by the
117
+ * relay budget (64-1024 per side, total no larger than a 1024 square); see
118
+ * `@vgai/editor-sdk`'s `CaptureDimensions`.
119
+ * Supply a view to present and photograph it in one editor request.
120
+ */
121
+ async captureActiveDocument(size, view) {
122
+ return this.#client.captureActiveDocument(size, view);
123
+ }
124
+ /**
125
+ * Photograph the editor PAGE — every panel, tab strip and viewport as the
126
+ * person sees it. `vgai screenshot editor` is this verb from the shell. The
127
+ * one door for judging chrome sighted: a skin, a workspace arrangement or a
128
+ * contributed panel is looked at through this, never guessed at from DOM
129
+ * probes. Returns the page at its own size.
130
+ */
131
+ async captureEditorChrome() {
132
+ return this.#client.captureEditorChrome();
97
133
  }
98
134
  async play(opts) {
99
135
  await this.#client.play(opts);
@@ -133,16 +169,40 @@ export class LiveEditor {
133
169
  }
134
170
  await this.#client.focusSelection();
135
171
  }
172
+ async frame(target) {
173
+ if (typeof target === 'string') {
174
+ await this.#client.frameEntity(target);
175
+ return;
176
+ }
177
+ await this.#client.frameDocument(target?.fit);
178
+ }
136
179
  /**
137
- * Frame the edit viewport camera on one entity. Same framing as
138
- * `focus(id)`, but an entity id the scene does not know THROWS, naming the
139
- * id where `focus` quietly does nothing. Reach for this whenever the next
140
- * step reads the viewport (`screenshot()`, an
141
- * `assetPreview(..., { stage: 'scene' })`): a framing that silently missed
142
- * would otherwise be indistinguishable from one that worked.
180
+ * WATCH THE AGENT LOOK AROUND THE MODEL.
181
+ *
182
+ * Swings the open Object3D document's camera the camera the human's tab is
183
+ * showing around the framed subject by `azimuth`/`elevation` RADIANS,
184
+ * animated over `duration` seconds (default 0.6), and resolves when the move
185
+ * ends. This is deliberately not a jump cut: the point of the verb is that a
186
+ * person watching sees the agent walk around the thing it is working on.
187
+ *
188
+ * `await editor.orbit({ azimuth: Math.PI / 2 })` — a quarter turn to the right.
189
+ *
190
+ * There is ONE camera, and the human owns it: a drag during the move cancels
191
+ * it exactly where it is, and the resolved outcome says `cancelledBy:
192
+ * 'human'` rather than throwing. A second look verb supersedes the first.
193
+ * The move is drawn by the document's own frame loop, so a document that
194
+ * isn't being drawn (background tab, inactive panel) doesn't orbit.
143
195
  */
144
- async frame(entityId) {
145
- await this.#client.frameEntity(entityId);
196
+ async orbit(options) {
197
+ return this.#client.orbitDocument(options);
198
+ }
199
+ /**
200
+ * A slow full revolution of the open document's subject — {@link orbit} with
201
+ * the turns spelled out and a constant angular rate. Resolves at the end of
202
+ * the last revolution.
203
+ */
204
+ async turntable(options) {
205
+ return this.#client.turntableDocument(options);
146
206
  }
147
207
  async view(preset) {
148
208
  await this.#client.viewPreset(preset);
@@ -160,6 +220,24 @@ export class LiveEditor {
160
220
  async instances(countOrNames) {
161
221
  await this.#client.setInstanceCount(countOrNames);
162
222
  }
223
+ /**
224
+ * Switch the editor's NAMED WORKSPACE — `await editor.workspace('model')`.
225
+ *
226
+ * A workspace is a task-named LAYOUT MEMORY over the one dock
227
+ * (ARCHITECTURE-CORE §Editor chrome): `game` (the default, the editor's
228
+ * standing arrangement), `model`, `sculpt`, `texture`, `animate`, `look`.
229
+ * Switching is an EXPLICIT act — nothing in the editor moves chrome on its
230
+ * own, opening a document included — and this is the session door to it,
231
+ * beside `Window → Workspace` and the registered actions.
232
+ *
233
+ * Resolves once the dock has finished rebuilding, so a capture taken
234
+ * immediately after photographs the arrangement that was asked for. Each
235
+ * workspace remembers the user's own hand-tuning per project, so switching
236
+ * away and back is lossless.
237
+ */
238
+ async workspace(id) {
239
+ await this.#client.setWorkspace(id);
240
+ }
163
241
  /** `vgai show <viewport <edit|play>|inspector|console|build>`'s four sub-verbs, folded into one action name. */
164
242
  async showPanel(name) {
165
243
  switch (name) {
@@ -184,6 +262,19 @@ export class LiveEditor {
184
262
  async openAsset(path, kind) {
185
263
  await this.#client.openAsset(path, kind ?? inferAssetKind(path));
186
264
  }
265
+ /**
266
+ * SELECT a project asset — the browser's single click, which fills the
267
+ * Inspector without opening a document. `openAsset` is the double click.
268
+ *
269
+ * This is how a project's own `asset.inspector` section is reached: select
270
+ * the file it matches, then `inspect()` lists the verbs that section
271
+ * declares and `runAction(id)` runs one. Selecting a path nothing matches
272
+ * is not an error — the Inspector shows what it has, exactly as it does
273
+ * for a human.
274
+ */
275
+ async selectAsset(path) {
276
+ await this.#client.selectAsset(path);
277
+ }
187
278
  /**
188
279
  * Fit native Rapier bodies and joints to a rigged GLB/glTF, copy the reusable
189
280
  * project capability, and open the generated TSX prefab's Setup story.
@@ -205,6 +296,18 @@ export class LiveEditor {
205
296
  async assetPreview(source, options) {
206
297
  return this.#client.captureAssetPreview(typeof source === 'string' ? { assetPath: source } : source, options);
207
298
  }
299
+ /**
300
+ * The same subject photographed as a LABELED SHOT SET instead of the four
301
+ * views — a caller-supplied definition of turntable yaws and bone-anchored
302
+ * crops, rendered against the asset's own skeleton, with a contact sheet.
303
+ * Every source {@link assetPreview} takes works here, GLB bytes included:
304
+ * a shot set stages its own subject, so it needs no place to stand.
305
+ *
306
+ * Sole in-repo caller today: `project.bake.preview`'s `--orbit` lane.
307
+ */
308
+ async assetPreviewShots(source, definition, options) {
309
+ return this.#client.captureShotSetPreview(typeof source === 'string' ? { assetPath: source } : source, definition, options);
310
+ }
208
311
  async grid(on) {
209
312
  await this.#client.setGrid(on);
210
313
  }
@@ -259,6 +362,50 @@ export class LiveEditor {
259
362
  async runAction(actionId) {
260
363
  return this.#client.runInspectionAction(actionId);
261
364
  }
365
+ /**
366
+ * RESTRUCTURE the authored tree — the hierarchy context menu's own verbs.
367
+ *
368
+ * `create`, `delete`, `duplicate`, `reparent`, `reorder`, `wrap`, `unwrap`,
369
+ * `group`, `ungroup`, `copy`, `cut`, `paste`; `extractComponent` and
370
+ * `forkComponent` are the two that write whole new files and have their own
371
+ * doors below. All of them run the SAME `authoring/consumer-actions.ts`
372
+ * helpers the menu items call, so there is one implementation of each op and
373
+ * not a second that can disagree with what a human gets.
374
+ *
375
+ * It exists because the menu is a POINTER surface: every one of these ops was
376
+ * reachable only by right-clicking a hierarchy row, which is nothing an agent
377
+ * can do — so for an ingest root, whose only authoring surface IS the editor,
378
+ * structure was closed entirely.
379
+ *
380
+ * `id`/`ids` default to the current selection. The answer carries the same
381
+ * per-edit `write` ack `setField` does, so `write.persisted` tells a saved
382
+ * restructure from a live-only one. An op the active adapter does not provide
383
+ * REJECTS by name — never a silent no-op.
384
+ */
385
+ async structure(op, options) {
386
+ return this.#client.structureOp(op, options ?? {});
387
+ }
388
+ /**
389
+ * "Extract Component…" — lift the selected native subtree into its own
390
+ * component file (plus a story) and replace the callsite with it.
391
+ *
392
+ * Answers the action's own sentence, which NAMES both new files, because
393
+ * undo owns the callsite edit and will not remove them.
394
+ */
395
+ async extractComponent(options) {
396
+ return (await this.#client.extractComponent(options ?? {})).hint;
397
+ }
398
+ /**
399
+ * "Fork Component…" — copy the selected instance's component definition to a
400
+ * new file and retarget THIS CALLSITE at it.
401
+ *
402
+ * One callsite is the unit of the edit; when that callsite sits inside a
403
+ * component rendered many times, every one of those renders now renders the
404
+ * fork.
405
+ */
406
+ async forkComponent(options) {
407
+ return (await this.#client.forkComponent(options ?? {})).hint;
408
+ }
262
409
  /**
263
410
  * READ the hierarchy panel, as data — the rows a human is looking at right
264
411
  * now, nested exactly as the panel nests them.
@@ -2,17 +2,25 @@
2
2
  * What a capture knows about ITSELF beyond its pixels — and the one place the
3
3
  * caveat sentence is spelled.
4
4
  *
5
- * A PNG is silent about the conditions it was taken under. Two of those
6
- * conditions change what the frame is worth as evidence, and both are already
7
- * measured elsewhere in the stack: the editor page reports
5
+ * A PNG is silent about the conditions it was taken under. Four of those
6
+ * conditions matter, and each is already measured elsewhere in the stack.
7
+ * Three change what the frame is worth as EVIDENCE: the editor page reports
8
8
  * `loopRecoveryFrame` when the host loop was starved and the runtime had to
9
9
  * render one deterministic tick on demand (`command-listener.ts`'s
10
10
  * `handleBridgeScreenshot`), and it reports
11
11
  * a `flatness.warning` sentence when the frame is nine-tenths one flat surface
12
- * (`composite-screenshot.ts`'s `measureFlatness`). Until now both stopped at a
12
+ * (`composite-screenshot.ts`'s `measureFlatness`), and it names the CLIP a
13
+ * frame of a recorded run belongs to (`command-listener.ts`'s
14
+ * `screenshotRecordingNotice`). Until now these stopped at a
13
15
  * `console.warn` inside the relay transport — visible to a human watching a
14
16
  * terminal, invisible to anything that later reads the file.
15
17
  *
18
+ * The fourth is not about the frame at all but about what taking it COST: a
19
+ * capture written under the served project root goes through the dev server's
20
+ * file watcher on every shot (measured ~3x slower frame rates), and the
21
+ * out-path resolver is the only place that knows
22
+ * (`screenshot-target.ts`'s `underWatchedProjectRoot`).
23
+ *
16
24
  * So the transport seam carries them back as {@link CaptureNotes}, and
17
25
  * {@link describeCaptureCaveat} turns them into the ONE sentence every surface
18
26
  * says. Its two callers are the transport's own console warning and
@@ -20,7 +28,7 @@
20
28
  * listener — so the words a human reads in the terminal and the words a run
21
29
  * record carries beside the frame cannot drift apart.
22
30
  */
23
- /** The conditions a capture was taken under, as facts. Both fields are
31
+ /** The conditions a capture was taken under, as facts. Every field is
24
32
  * optional and absent means "not so": an ordinary frame carries no notes. */
25
33
  export interface CaptureNotes {
26
34
  /** The host loop was starved, so the frame exists only because the runtime
@@ -29,6 +37,16 @@ export interface CaptureNotes {
29
37
  /** The page's own near-blank-frame sentence, verbatim (it owns the wording;
30
38
  * see `composite-screenshot.ts`'s `CaptureFlatness.warning`). */
31
39
  readonly flatnessWarning?: string;
40
+ /** The clip this frame is one frame OF — every `vgai play` records
41
+ * (`play-recording.ts`). Not a degradation: the still is delivered and is
42
+ * the right instrument for a look question. It is here because a still of a
43
+ * MOVING game answers a temporal question only by luck, and a reader of the
44
+ * persisted record has to be able to find the door that answers it. */
45
+ readonly recordingPath?: string;
46
+ /** The SERVED project root this capture was written under, when it was.
47
+ * Not a degradation of the frame — a cost of taking it; see
48
+ * {@link WATCHED_CAPTURE_PATH_CAVEAT}. */
49
+ readonly watchedProjectRoot?: string;
32
50
  }
33
51
  /** One capture, as reported to a {@link CaptureListener} after the bytes are
34
52
  * on disk. `caveat` is already composed — see {@link describeCaptureCaveat} —
@@ -50,8 +68,8 @@ export type CaptureListener = (capture: CaptureRecord) => void | Promise<void>;
50
68
  * What a reader must be told about this frame, or `null` when there is nothing
51
69
  * to tell.
52
70
  *
53
- * Both notes can be true at once (an on-demand recovery tick that also came
54
- * out near-blank), and both are said — a capture that is degraded twice over
55
- * must not report only the first reason.
71
+ * Any of them can be true at once (an on-demand recovery tick that also came
72
+ * out near-blank, in a recorded run), and each is said — a capture that is
73
+ * degraded twice over must not report only the first reason.
56
74
  */
57
75
  export declare function describeCaptureCaveat(notes: CaptureNotes): string | null;