@vgai/live 0.5.41 → 0.5.44

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
 
@@ -69,6 +69,41 @@ export class LiveEditorDocument {
69
69
  });
70
70
  }
71
71
 
72
+ /**
73
+ * A real pointer DRAG across one matched element — press at `from`, move,
74
+ * release at `to`, as fractions of the element's box (`[0.5, 0.5]` is its
75
+ * center). The gesture a direct-manipulation canvas needs; a zero-length
76
+ * drag is a click at that fraction, which `click` (always the center)
77
+ * cannot place.
78
+ */
79
+ async drag(
80
+ selector: string,
81
+ options: DocumentGestureOptions & {
82
+ from: [number, number];
83
+ to: [number, number];
84
+ via?: [number, number][];
85
+ steps?: number;
86
+ altKey?: boolean;
87
+ ctrlKey?: boolean;
88
+ metaKey?: boolean;
89
+ shiftKey?: boolean;
90
+ },
91
+ ): Promise<DocumentProbeResult> {
92
+ return this.#probe({
93
+ action: 'drag',
94
+ selector,
95
+ from: options.from,
96
+ to: options.to,
97
+ ...(options.via === undefined ? {} : { via: options.via }),
98
+ ...(options.steps === undefined ? {} : { steps: options.steps }),
99
+ ...(options.index === undefined ? {} : { index: options.index }),
100
+ ...(options.altKey === undefined ? {} : { altKey: options.altKey }),
101
+ ...(options.ctrlKey === undefined ? {} : { ctrlKey: options.ctrlKey }),
102
+ ...(options.metaKey === undefined ? {} : { metaKey: options.metaKey }),
103
+ ...(options.shiftKey === undefined ? {} : { shiftKey: options.shiftKey }),
104
+ });
105
+ }
106
+
72
107
  /** A real keydown/keyup on the target, or on whatever inside the document has focus. */
73
108
  async key(key: string, options?: DocumentKeyOptions): Promise<DocumentProbeResult> {
74
109
  return this.#probe({ action: 'key', key, ...(options ?? {}) });
@@ -80,6 +115,41 @@ export class LiveEditorDocument {
80
115
  return this.#probe({ action: 'paste', text, ...(options ?? {}) });
81
116
  }
82
117
 
118
+ /**
119
+ * Choose `value` on a `<select>` — a native dropdown's options are drawn by
120
+ * the OS, so `click` has nothing in the document to resolve, and a plain
121
+ * `element.value =` is invisible to React. Set through the prototype's own
122
+ * value setter plus `input`/`change`; `value` is the option's `value`, not
123
+ * its label. An unknown value is refused with the options it does offer.
124
+ */
125
+ async select(
126
+ selector: string,
127
+ value: string,
128
+ options?: DocumentGestureOptions,
129
+ ): Promise<DocumentProbeResult> {
130
+ return this.#probe({
131
+ action: 'select',
132
+ selector,
133
+ value,
134
+ ...(options?.index === undefined ? {} : { index: options.index }),
135
+ });
136
+ }
137
+
138
+ /**
139
+ * THE REPL over the open document: run `step` in the editor page against the
140
+ * object the ACTIVE document published as its context (the mesh document
141
+ * publishes its `MeshEditSession`, whose `ctx` is the bpy-shaped edit
142
+ * context — `ctx.ops.mesh.bevel({ offset: 0.1 })`, `ctx.selection`,
143
+ * `ctx.history`, `session.commit()`). Edit mode, no play. Serialized like
144
+ * `game.page`: the step's own source travels, so inline every value it
145
+ * needs and return plain data.
146
+ */
147
+ async run<T = unknown>(
148
+ step: (ctx: unknown, info: { documentId: string }) => T | Promise<T>,
149
+ ): Promise<T> {
150
+ return this.#client.documentScript<T>(step.toString());
151
+ }
152
+
83
153
  #probe(step: DocumentProbeStep): Promise<DocumentProbeResult> {
84
154
  return this.#client.documentProbe(step);
85
155
  }
package/src/editor.ts CHANGED
@@ -17,18 +17,27 @@ import type {
17
17
  AssetKind,
18
18
  AssetPreviewCapture,
19
19
  AssetPreviewOptions,
20
+ AssetPreviewShotSetDefinition,
20
21
  AssetPreviewSource,
22
+ CaptureDimensions,
23
+ DocumentLookOutcome,
24
+ EditorChromeCapture,
21
25
  EditorClient,
22
26
  EditorState,
23
27
  EditorView,
28
+ EditorWorkspaceName,
24
29
  HistoryStep,
25
30
  InspectedFieldWrite,
26
31
  InspectedHierarchy,
27
32
  InspectedInspection,
33
+ LabeledShotSetCapture,
28
34
  OpenedDocument,
29
35
  PresentedEditorView,
30
36
  RagdollGenerationResult,
31
37
  ShadingMode,
38
+ StructureOp,
39
+ StructureOpOptions,
40
+ StructureOpResult,
32
41
  ViewPreset,
33
42
  ViewportCapture,
34
43
  } from '@vgai/editor-sdk';
@@ -56,6 +65,23 @@ const EXTENSION_KIND: Record<string, AssetKind> = {
56
65
  '.glsl': 'source',
57
66
  '.vert': 'source',
58
67
  '.frag': 'source',
68
+ // PROJECT SCRIPTS ARE SOURCE. Without these the guess below falls through to
69
+ // `'json'`, the asset-document router sends the file to the generic JSON
70
+ // viewer (`asset-documents.tsx#assetDocumentViewerRoute`: `spec.kind ===
71
+ // 'json'` is decided before any content routing), and the LIVE MODELING
72
+ // DOCUMENT never mounts — `editor.openAsset('src/lib/fox/fox.model.ts')`
73
+ // silently shows a text pane instead of the model. Only `kind: 'source'`
74
+ // reaches `SourceAssetViewer`, which is what content-routes a project script
75
+ // to `LiveModuleDocument`. The set matches that viewer's own
76
+ // `isProjectScriptPath` regex, `/\.(?:[cm]?[jt]sx?)$/`.
77
+ '.ts': 'source',
78
+ '.tsx': 'source',
79
+ '.mts': 'source',
80
+ '.cts': 'source',
81
+ '.js': 'source',
82
+ '.jsx': 'source',
83
+ '.mjs': 'source',
84
+ '.cjs': 'source',
59
85
  };
60
86
 
61
87
  /**
@@ -125,9 +151,32 @@ export class LiveEditor {
125
151
  return this.#client.currentView();
126
152
  }
127
153
 
128
- /** Capture the same center document the human is currently looking at. */
129
- async captureActiveDocument(size?: number): Promise<ActiveDocumentCapture> {
130
- return this.#client.captureActiveDocument(size);
154
+ /**
155
+ * Capture the same center document the human is currently looking at.
156
+ *
157
+ * A number is a SQUARE of that size — the default, and the right shape for
158
+ * an unstaged look at a model. `{width, height}` asks for a shaped frame, so
159
+ * a video-aspect look needs no crop afterwards. Both are bounded by the
160
+ * relay budget (64-1024 per side, total no larger than a 1024 square); see
161
+ * `@vgai/editor-sdk`'s `CaptureDimensions`.
162
+ * Supply a view to present and photograph it in one editor request.
163
+ */
164
+ async captureActiveDocument(
165
+ size?: CaptureDimensions,
166
+ view?: EditorView,
167
+ ): Promise<ActiveDocumentCapture> {
168
+ return this.#client.captureActiveDocument(size, view);
169
+ }
170
+
171
+ /**
172
+ * Photograph the editor PAGE — every panel, tab strip and viewport as the
173
+ * person sees it. `vgai screenshot editor` is this verb from the shell. The
174
+ * one door for judging chrome sighted: a skin, a workspace arrangement or a
175
+ * contributed panel is looked at through this, never guessed at from DOM
176
+ * probes. Returns the page at its own size.
177
+ */
178
+ async captureEditorChrome(): Promise<EditorChromeCapture> {
179
+ return this.#client.captureEditorChrome();
131
180
  }
132
181
 
133
182
  async play(opts?: { seed?: number }): Promise<void> {
@@ -184,8 +233,57 @@ export class LiveEditor {
184
233
  * `assetPreview(..., { stage: 'scene' })`): a framing that silently missed
185
234
  * would otherwise be indistinguishable from one that worked.
186
235
  */
187
- async frame(entityId: string): Promise<void> {
188
- await this.#client.frameEntity(entityId);
236
+ async frame(entityId: string): Promise<void>;
237
+ /**
238
+ * Bare `frame()` frames the OPEN Object3D document's subject instead — its
239
+ * selection if it has one, else the whole model: the toolbar's own Frame
240
+ * button, reachable from a script. `fit` scales the fitted distance (1 is
241
+ * that button's tight fit, 1.5 stands back a little for a shot).
242
+ */
243
+ async frame(options?: { readonly fit?: number }): Promise<void>;
244
+ async frame(target?: string | { readonly fit?: number }): Promise<void> {
245
+ if (typeof target === 'string') {
246
+ await this.#client.frameEntity(target);
247
+ return;
248
+ }
249
+ await this.#client.frameDocument(target?.fit);
250
+ }
251
+
252
+ /**
253
+ * WATCH THE AGENT LOOK AROUND THE MODEL.
254
+ *
255
+ * Swings the open Object3D document's camera — the camera the human's tab is
256
+ * showing — around the framed subject by `azimuth`/`elevation` RADIANS,
257
+ * animated over `duration` seconds (default 0.6), and resolves when the move
258
+ * ends. This is deliberately not a jump cut: the point of the verb is that a
259
+ * person watching sees the agent walk around the thing it is working on.
260
+ *
261
+ * `await editor.orbit({ azimuth: Math.PI / 2 })` — a quarter turn to the right.
262
+ *
263
+ * There is ONE camera, and the human owns it: a drag during the move cancels
264
+ * it exactly where it is, and the resolved outcome says `cancelledBy:
265
+ * 'human'` rather than throwing. A second look verb supersedes the first.
266
+ * The move is drawn by the document's own frame loop, so a document that
267
+ * isn't being drawn (background tab, inactive panel) doesn't orbit.
268
+ */
269
+ async orbit(options: {
270
+ readonly azimuth?: number;
271
+ readonly elevation?: number;
272
+ readonly duration?: number;
273
+ }): Promise<DocumentLookOutcome> {
274
+ return this.#client.orbitDocument(options);
275
+ }
276
+
277
+ /**
278
+ * A slow full revolution of the open document's subject — {@link orbit} with
279
+ * the turns spelled out and a constant angular rate. Resolves at the end of
280
+ * the last revolution.
281
+ */
282
+ async turntable(options?: {
283
+ readonly seconds?: number;
284
+ readonly revolutions?: number;
285
+ }): Promise<DocumentLookOutcome> {
286
+ return this.#client.turntableDocument(options);
189
287
  }
190
288
 
191
289
  async view(preset: ViewPreset): Promise<void> {
@@ -206,6 +304,25 @@ export class LiveEditor {
206
304
  await this.#client.setInstanceCount(countOrNames);
207
305
  }
208
306
 
307
+ /**
308
+ * Switch the editor's NAMED WORKSPACE — `await editor.workspace('model')`.
309
+ *
310
+ * A workspace is a task-named LAYOUT MEMORY over the one dock
311
+ * (ARCHITECTURE-CORE §Editor chrome): `game` (the default, the editor's
312
+ * standing arrangement), `model`, `sculpt`, `texture`, `animate`, `look`.
313
+ * Switching is an EXPLICIT act — nothing in the editor moves chrome on its
314
+ * own, opening a document included — and this is the session door to it,
315
+ * beside `Window → Workspace` and the registered actions.
316
+ *
317
+ * Resolves once the dock has finished rebuilding, so a capture taken
318
+ * immediately after photographs the arrangement that was asked for. Each
319
+ * workspace remembers the user's own hand-tuning per project, so switching
320
+ * away and back is lossless.
321
+ */
322
+ async workspace(id: EditorWorkspaceName): Promise<void> {
323
+ await this.#client.setWorkspace(id);
324
+ }
325
+
209
326
  /** `vgai show <viewport <edit|play>|inspector|console|build>`'s four sub-verbs, folded into one action name. */
210
327
  async showPanel(name: PanelName): Promise<void> {
211
328
  switch (name) {
@@ -232,6 +349,20 @@ export class LiveEditor {
232
349
  await this.#client.openAsset(path, kind ?? inferAssetKind(path));
233
350
  }
234
351
 
352
+ /**
353
+ * SELECT a project asset — the browser's single click, which fills the
354
+ * Inspector without opening a document. `openAsset` is the double click.
355
+ *
356
+ * This is how a project's own `asset.inspector` section is reached: select
357
+ * the file it matches, then `inspect()` lists the verbs that section
358
+ * declares and `runAction(id)` runs one. Selecting a path nothing matches
359
+ * is not an error — the Inspector shows what it has, exactly as it does
360
+ * for a human.
361
+ */
362
+ async selectAsset(path: string): Promise<void> {
363
+ await this.#client.selectAsset(path);
364
+ }
365
+
235
366
  /**
236
367
  * Fit native Rapier bodies and joints to a rigged GLB/glTF, copy the reusable
237
368
  * project capability, and open the generated TSX prefab's Setup story.
@@ -261,6 +392,27 @@ export class LiveEditor {
261
392
  );
262
393
  }
263
394
 
395
+ /**
396
+ * The same subject photographed as a LABELED SHOT SET instead of the four
397
+ * views — a caller-supplied definition of turntable yaws and bone-anchored
398
+ * crops, rendered against the asset's own skeleton, with a contact sheet.
399
+ * Every source {@link assetPreview} takes works here, GLB bytes included:
400
+ * a shot set stages its own subject, so it needs no place to stand.
401
+ *
402
+ * Sole in-repo caller today: `project.bake.preview`'s `--orbit` lane.
403
+ */
404
+ async assetPreviewShots(
405
+ source: string | AssetPreviewSource,
406
+ definition: AssetPreviewShotSetDefinition,
407
+ options?: AssetPreviewOptions,
408
+ ): Promise<LabeledShotSetCapture> {
409
+ return this.#client.captureShotSetPreview(
410
+ typeof source === 'string' ? { assetPath: source } : source,
411
+ definition,
412
+ options,
413
+ );
414
+ }
415
+
264
416
  async grid(on: boolean): Promise<void> {
265
417
  await this.#client.setGrid(on);
266
418
  }
@@ -322,6 +474,53 @@ export class LiveEditor {
322
474
  return this.#client.runInspectionAction(actionId);
323
475
  }
324
476
 
477
+ /**
478
+ * RESTRUCTURE the authored tree — the hierarchy context menu's own verbs.
479
+ *
480
+ * `create`, `delete`, `duplicate`, `reparent`, `reorder`, `wrap`, `unwrap`,
481
+ * `group`, `ungroup`, `copy`, `cut`, `paste`; `extractComponent` and
482
+ * `forkComponent` are the two that write whole new files and have their own
483
+ * doors below. All of them run the SAME `authoring/consumer-actions.ts`
484
+ * helpers the menu items call, so there is one implementation of each op and
485
+ * not a second that can disagree with what a human gets.
486
+ *
487
+ * It exists because the menu is a POINTER surface: every one of these ops was
488
+ * reachable only by right-clicking a hierarchy row, which is nothing an agent
489
+ * can do — so for an ingest root, whose only authoring surface IS the editor,
490
+ * structure was closed entirely.
491
+ *
492
+ * `id`/`ids` default to the current selection. The answer carries the same
493
+ * per-edit `write` ack `setField` does, so `write.persisted` tells a saved
494
+ * restructure from a live-only one. An op the active adapter does not provide
495
+ * REJECTS by name — never a silent no-op.
496
+ */
497
+ async structure(op: StructureOp, options?: StructureOpOptions): Promise<StructureOpResult> {
498
+ return this.#client.structureOp(op, options ?? {});
499
+ }
500
+
501
+ /**
502
+ * "Extract Component…" — lift the selected native subtree into its own
503
+ * component file (plus a story) and replace the callsite with it.
504
+ *
505
+ * Answers the action's own sentence, which NAMES both new files, because
506
+ * undo owns the callsite edit and will not remove them.
507
+ */
508
+ async extractComponent(options?: { id?: string; name?: string }): Promise<string> {
509
+ return (await this.#client.extractComponent(options ?? {})).hint;
510
+ }
511
+
512
+ /**
513
+ * "Fork Component…" — copy the selected instance's component definition to a
514
+ * new file and retarget THIS CALLSITE at it.
515
+ *
516
+ * One callsite is the unit of the edit; when that callsite sits inside a
517
+ * component rendered many times, every one of those renders now renders the
518
+ * fork.
519
+ */
520
+ async forkComponent(options?: { id?: string }): Promise<string> {
521
+ return (await this.#client.forkComponent(options ?? {})).hint;
522
+ }
523
+
325
524
  /**
326
525
  * READ the hierarchy panel, as data — the rows a human is looking at right
327
526
  * now, nested exactly as the panel nests them.
@@ -353,6 +552,11 @@ export class LiveEditor {
353
552
  return this.#client.hierarchy();
354
553
  }
355
554
 
555
+ /** Expand every branch through the Hierarchy panel's own action. */
556
+ async expandHierarchyAll(): Promise<void> {
557
+ await this.#client.expandHierarchyAll();
558
+ }
559
+
356
560
  /**
357
561
  * Write one editable field from `inspect()` by its stable path, through the
358
562
  * same Inspector IO and persistence boundary the human control uses.
@@ -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
@@ -21,7 +29,7 @@
21
29
  * record carries beside the frame cannot drift apart.
22
30
  */
23
31
 
24
- /** The conditions a capture was taken under, as facts. Both fields are
32
+ /** The conditions a capture was taken under, as facts. Every field is
25
33
  * optional and absent means "not so": an ordinary frame carries no notes. */
26
34
  export interface CaptureNotes {
27
35
  /** The host loop was starved, so the frame exists only because the runtime
@@ -30,6 +38,16 @@ export interface CaptureNotes {
30
38
  /** The page's own near-blank-frame sentence, verbatim (it owns the wording;
31
39
  * see `composite-screenshot.ts`'s `CaptureFlatness.warning`). */
32
40
  readonly flatnessWarning?: string;
41
+ /** The clip this frame is one frame OF — every `vgai play` records
42
+ * (`play-recording.ts`). Not a degradation: the still is delivered and is
43
+ * the right instrument for a look question. It is here because a still of a
44
+ * MOVING game answers a temporal question only by luck, and a reader of the
45
+ * persisted record has to be able to find the door that answers it. */
46
+ readonly recordingPath?: string;
47
+ /** The SERVED project root this capture was written under, when it was.
48
+ * Not a degradation of the frame — a cost of taking it; see
49
+ * {@link WATCHED_CAPTURE_PATH_CAVEAT}. */
50
+ readonly watchedProjectRoot?: string;
33
51
  }
34
52
 
35
53
  /** One capture, as reported to a {@link CaptureListener} after the bytes are
@@ -53,6 +71,20 @@ export type CaptureListener = (capture: CaptureRecord) => void | Promise<void>;
53
71
  /** The loop-recovery sentence. Spelled once because it is said in two places
54
72
  * (a live console warning and a persisted record) and a second copy is a
55
73
  * second wording. */
74
+ /**
75
+ * The cost of writing a capture INSIDE the served project.
76
+ *
77
+ * The dev server watches the project root, and its ignore list names build
78
+ * output only — nothing about capture output — so every PNG written under the
79
+ * root goes through the watcher. Measured cost class: ~3x slower frame rates
80
+ * while a capture loop wrote there. Named, not fixed: where a capture goes is
81
+ * the caller's decision, and this door's job is to stop that decision being
82
+ * made blind.
83
+ */
84
+ const WATCHED_CAPTURE_PATH_CAVEAT =
85
+ 'writing captures under the project root triggers the dev server’s file watcher; expect ~3× ' +
86
+ 'slower frame rates — write outside the project or to the session’s own capture dir';
87
+
56
88
  const LOOP_RECOVERY_FRAME_CAVEAT =
57
89
  'LOOP-RECOVERY FRAME — the host loop was starved, so the runtime rendered one ' +
58
90
  'deterministic tick on demand. It is current, not stale; it was not produced by ordinary presentation.';
@@ -61,9 +93,9 @@ const LOOP_RECOVERY_FRAME_CAVEAT =
61
93
  * What a reader must be told about this frame, or `null` when there is nothing
62
94
  * to tell.
63
95
  *
64
- * Both notes can be true at once (an on-demand recovery tick that also came
65
- * out near-blank), and both are said — a capture that is degraded twice over
66
- * must not report only the first reason.
96
+ * Any of them can be true at once (an on-demand recovery tick that also came
97
+ * out near-blank, in a recorded run), and each is said — a capture that is
98
+ * degraded twice over must not report only the first reason.
67
99
  */
68
100
  export function describeCaptureCaveat(notes: CaptureNotes): string | null {
69
101
  const parts: string[] = [];
@@ -71,5 +103,14 @@ export function describeCaptureCaveat(notes: CaptureNotes): string | null {
71
103
  if (typeof notes.flatnessWarning === 'string' && notes.flatnessWarning !== '') {
72
104
  parts.push(notes.flatnessWarning);
73
105
  }
106
+ if (typeof notes.recordingPath === 'string' && notes.recordingPath !== '') {
107
+ parts.push(
108
+ `ONE FRAME of a recorded run — ${notes.recordingPath} holds the whole of it. A still ` +
109
+ 'answers what it looks like; a temporal question (did the jump land) needs the clip.',
110
+ );
111
+ }
112
+ if (typeof notes.watchedProjectRoot === 'string' && notes.watchedProjectRoot !== '') {
113
+ parts.push(`${WATCHED_CAPTURE_PATH_CAVEAT} (${notes.watchedProjectRoot}).`);
114
+ }
74
115
  return parts.length === 0 ? null : parts.join(' ');
75
116
  }