@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.
@@ -0,0 +1,234 @@
1
+ /**
2
+ * `editor.document` — the session binding for the scoped editor-chrome door.
3
+ *
4
+ * WHY IT IS A SEPARATE OBJECT, and why the verbs are these, is recorded once
5
+ * in the implementation's header
6
+ * (`packages/editor/src/editor-document-probe.ts`); the short version is that
7
+ * `game.page()` is play-mode-gated and rooted at the GAME container, so an
8
+ * editor surface that is not a running game could be neither read nor driven
9
+ * through the product.
10
+ *
11
+ * WHAT IT ADDRESSES is a closed VOCABULARY of named surfaces, passed as
12
+ * `{ scope }` on every verb:
13
+ *
14
+ * `document` (default) the active centre document's whole box
15
+ * `header` / `shelf` that document's toolbar strip / tool rail
16
+ * `rail` the PROPERTIES view — its tabs, sections and fields
17
+ * `outliner` the OUTLINER view — its rows and their controls
18
+ * `content` the CONTENT view — its categories and asset rows
19
+ *
20
+ * await editor.document.query('[role=tab]', { scope: 'rail' });
21
+ * await editor.document.click('[data-testid=properties-tab-modifiers]', { scope: 'rail' });
22
+ * await editor.document.click('[data-ingest-name=Cube]', { scope: 'outliner', clicks: 2 });
23
+ * await editor.document.type('Wheel', { scope: 'outliner' });
24
+ *
25
+ * It is not page automation: a selector resolving outside the named surface is
26
+ * refused by a message naming the scope and listing the others. `rail` and
27
+ * `outliner`, and `content` are editor chrome and stay readable while the Game document is
28
+ * active; `editor.hierarchy()` / `editor.inspect()` keep answering what those
29
+ * panels RESOLVED, where this door answers what they DREW.
30
+ *
31
+ * A field on `LiveEditor` rather than methods on it, so `vgai eval --list`
32
+ * shows the verbs as one named surface — the same reason `game.input`
33
+ * and `game.events` are instance fields.
34
+ */
35
+
36
+ import type {
37
+ DocumentProbeResult,
38
+ DocumentProbeScope,
39
+ DocumentProbeStep,
40
+ EditorClient,
41
+ } from '@volter/editor-sdk';
42
+
43
+ /** Which named surface a verb runs against; `document` when omitted. */
44
+ export interface DocumentScopeOption {
45
+ scope?: DocumentProbeScope;
46
+ }
47
+
48
+ /** Modifier/targeting options shared by the gesture verbs. */
49
+ export interface DocumentGestureOptions extends DocumentScopeOption {
50
+ /** Which match to drive when the selector matches several (default 0). */
51
+ index?: number;
52
+ }
53
+
54
+ export interface DocumentKeyOptions extends DocumentGestureOptions {
55
+ /** Target one element instead of whatever inside the scope has focus. */
56
+ selector?: string;
57
+ code?: string;
58
+ ctrlKey?: boolean;
59
+ metaKey?: boolean;
60
+ shiftKey?: boolean;
61
+ altKey?: boolean;
62
+ }
63
+
64
+ export interface DocumentPasteOptions extends DocumentGestureOptions {
65
+ selector?: string;
66
+ }
67
+
68
+ export interface DocumentTypeOptions extends DocumentGestureOptions {
69
+ /** The field; omitted types into whatever inside the scope has focus. */
70
+ selector?: string;
71
+ /** Replace what the field holds first (default true). */
72
+ replace?: boolean;
73
+ /** Press Enter after the text (default true). */
74
+ enter?: boolean;
75
+ }
76
+
77
+ export class LiveEditorDocument {
78
+ readonly #client: EditorClient;
79
+
80
+ constructor(client: EditorClient) {
81
+ this.#client = client;
82
+ }
83
+
84
+ /**
85
+ * Read matching elements inside the active document: tag, text, attributes,
86
+ * value/checked/disabled and rect. `matched` is the total before `limit`.
87
+ *
88
+ * `styles` additionally resolves named properties per match — and resolving
89
+ * is the point, because a theme token is an expression until an element
90
+ * paints it. Ask for the standard property to learn the colour a person
91
+ * sees; ask for a `--vgai-…` custom property to learn what a rule WOULD
92
+ * paint, which is the only way to measure a `:hover` colour (`:hover` is a
93
+ * browser state no synthetic event can enter, so there is deliberately no
94
+ * hover verb on this door).
95
+ *
96
+ * await editor.document.query('.vgai-tree-row', {
97
+ * styles: ['backgroundColor', '--vgai-widget-regular-hover'],
98
+ * });
99
+ */
100
+ async query(
101
+ selector: string,
102
+ options?: DocumentScopeOption & { limit?: number; styles?: readonly string[] },
103
+ ): Promise<DocumentProbeResult> {
104
+ return this.#probe({
105
+ action: 'query',
106
+ selector,
107
+ ...(options?.scope === undefined ? {} : { scope: options.scope }),
108
+ ...(options?.limit === undefined ? {} : { limit: options.limit }),
109
+ ...(options?.styles === undefined ? {} : { styles: [...options.styles] }),
110
+ });
111
+ }
112
+
113
+ /** A REAL pointer gesture (pointerdown/mousedown/focus/pointerup/mouseup/click)
114
+ * — not `element.click()`, which a `pointerdown` listener never sees. */
115
+ async click(
116
+ selector: string,
117
+ options?: DocumentGestureOptions & { clicks?: number },
118
+ ): Promise<DocumentProbeResult> {
119
+ return this.#probe({
120
+ action: 'click',
121
+ selector,
122
+ ...(options?.scope === undefined ? {} : { scope: options.scope }),
123
+ ...(options?.index === undefined ? {} : { index: options.index }),
124
+ ...(options?.clicks === undefined ? {} : { clicks: options.clicks }),
125
+ });
126
+ }
127
+
128
+ /**
129
+ * TYPE into a field and commit with Enter, the way a person does — one
130
+ * character at a time through the prototype's value setter, between real
131
+ * `keydown`/`keyup`.
132
+ *
133
+ * `paste` is not a substitute: an untrusted `ClipboardEvent` performs no
134
+ * default action, so a plain `<input>` with no paste handler keeps its old
135
+ * value. Omit `selector` to type into whatever inside the scope has focus —
136
+ * which is what a rename field is, one gesture after
137
+ * `click(row, { clicks: 2 })`.
138
+ */
139
+ async type(text: string, options?: DocumentTypeOptions): Promise<DocumentProbeResult> {
140
+ return this.#probe({ action: 'type', text, ...(options ?? {}) });
141
+ }
142
+
143
+ /**
144
+ * A real pointer DRAG across one matched element — press at `from`, move,
145
+ * release at `to`. The gesture a direct-manipulation canvas needs; a
146
+ * zero-length drag is a click at that fraction, which `click` (always the
147
+ * center) cannot place.
148
+ *
149
+ * `from`, `to` and every point in `via` are `[x, y]` FRACTIONS OF THE
150
+ * MATCHED ELEMENT'S BOX, 0..1 from its top-left — NEVER pixels and never
151
+ * page coordinates. `[0.5, 0.5]` is its center, `[1, 0]` its top-right.
152
+ * Compute a pixel target by measuring the element first: `query` answers
153
+ * its `rect`, and `(px - rect.x) / rect.width` is the fraction to pass.
154
+ */
155
+ async drag(
156
+ selector: string,
157
+ options: DocumentGestureOptions & {
158
+ from: [number, number];
159
+ to: [number, number];
160
+ via?: [number, number][];
161
+ steps?: number;
162
+ altKey?: boolean;
163
+ ctrlKey?: boolean;
164
+ metaKey?: boolean;
165
+ shiftKey?: boolean;
166
+ },
167
+ ): Promise<DocumentProbeResult> {
168
+ return this.#probe({
169
+ action: 'drag',
170
+ selector,
171
+ ...(options.scope === undefined ? {} : { scope: options.scope }),
172
+ from: options.from,
173
+ to: options.to,
174
+ ...(options.via === undefined ? {} : { via: options.via }),
175
+ ...(options.steps === undefined ? {} : { steps: options.steps }),
176
+ ...(options.index === undefined ? {} : { index: options.index }),
177
+ ...(options.altKey === undefined ? {} : { altKey: options.altKey }),
178
+ ...(options.ctrlKey === undefined ? {} : { ctrlKey: options.ctrlKey }),
179
+ ...(options.metaKey === undefined ? {} : { metaKey: options.metaKey }),
180
+ ...(options.shiftKey === undefined ? {} : { shiftKey: options.shiftKey }),
181
+ });
182
+ }
183
+
184
+ /** A real keydown/keyup on the target, or on whatever inside the document has focus. */
185
+ async key(key: string, options?: DocumentKeyOptions): Promise<DocumentProbeResult> {
186
+ return this.#probe({ action: 'key', key, ...(options ?? {}) });
187
+ }
188
+
189
+ /** A real `ClipboardEvent` carrying `text/plain` — the gesture nothing else
190
+ * in the product can produce. */
191
+ async paste(text: string, options?: DocumentPasteOptions): Promise<DocumentProbeResult> {
192
+ return this.#probe({ action: 'paste', text, ...(options ?? {}) });
193
+ }
194
+
195
+ /**
196
+ * Choose `value` on a `<select>` — a native dropdown's options are drawn by
197
+ * the OS, so `click` has nothing in the document to resolve, and a plain
198
+ * `element.value =` is invisible to React. Set through the prototype's own
199
+ * value setter plus `input`/`change`; `value` is the option's `value`, not
200
+ * its label. An unknown value is refused with the options it does offer.
201
+ */
202
+ async select(
203
+ selector: string,
204
+ value: string,
205
+ options?: DocumentGestureOptions,
206
+ ): Promise<DocumentProbeResult> {
207
+ return this.#probe({
208
+ action: 'select',
209
+ selector,
210
+ value,
211
+ ...(options?.scope === undefined ? {} : { scope: options.scope }),
212
+ ...(options?.index === undefined ? {} : { index: options.index }),
213
+ });
214
+ }
215
+
216
+ /**
217
+ * THE REPL over the open document: run `step` in the editor page against the
218
+ * object the ACTIVE document published as its context (the mesh document
219
+ * publishes its `MeshEditSession`, whose `ctx` is the bpy-shaped edit
220
+ * context — `ctx.ops.mesh.bevel({ offset: 0.1 })`, `ctx.selection`,
221
+ * `ctx.history`, `session.commit()`). Edit mode, no play. Serialized like
222
+ * `game.page`: the step's own source travels, so inline every value it
223
+ * needs and return plain data.
224
+ */
225
+ async run<T = unknown>(
226
+ step: (ctx: unknown, info: { documentId: string }) => T | Promise<T>,
227
+ ): Promise<T> {
228
+ return this.#client.documentScript<T>(step.toString());
229
+ }
230
+
231
+ #probe(step: DocumentProbeStep): Promise<DocumentProbeResult> {
232
+ return this.#client.documentProbe(step);
233
+ }
234
+ }