@volter/live 0.0.0-stage → 0.5.204
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 +458 -0
- package/LICENSE +202 -0
- package/README.md +19 -3
- package/dist/editor-document.d.ts +170 -0
- package/dist/editor.d.ts +441 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +2456 -0
- package/dist/index.js.map +7 -0
- package/dist/lazy-proxy.d.ts +31 -0
- package/dist/member-hints.d.ts +29 -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 +44 -4
- package/src/editor-document.ts +270 -0
- package/src/editor.ts +720 -0
- package/src/index.ts +40 -0
- package/src/lazy-proxy.ts +77 -0
- package/src/member-hints.ts +64 -0
- package/src/session.ts +287 -0
- package/src/singleton.ts +30 -0
- package/src/tools.ts +45 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NAMES AN AGENT REACHES FOR THAT THE FAÇADE DOES NOT HAVE, and where the
|
|
3
|
+
* thing it wanted actually lives.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists (measured 2026-10, an agent building an obby in the Model
|
|
6
|
+
* Editor): it called `editor.setCamera(...)`, got the engine's bare
|
|
7
|
+
* "editor.setCamera is not a function", searched for a camera verb, and in the
|
|
8
|
+
* end improvised a pose from `frame()` and `orbit()`. The camera was reachable
|
|
9
|
+
* the whole time — through `editor.present`'s `viewport.camera` — but the
|
|
10
|
+
* failure said nothing about it. The refusal is the one moment the caller is
|
|
11
|
+
* certainly reading, so that is where the pointer goes.
|
|
12
|
+
*
|
|
13
|
+
* TARGETED, NOT A CATCH-ALL. Only the names below are intercepted, each with
|
|
14
|
+
* the door it was looking for. Every other missing member keeps JavaScript's
|
|
15
|
+
* own behaviour (`undefined`, then the engine's TypeError), so this table can
|
|
16
|
+
* never turn a typo into a wrong suggestion — it only ever adds what is known.
|
|
17
|
+
*/
|
|
18
|
+
/** The pointer for a member `editor` does not have, or undefined when there is none to give. */
|
|
19
|
+
export declare function editorMemberHint(member: PropertyKey): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* `editor` as `eval` binds it: the same object, except that reading one of the
|
|
22
|
+
* hinted names it does NOT have throws the hint, naming `binding.member`.
|
|
23
|
+
* Thrown on READ, not on call, so `editor.camera.position` is caught as well as
|
|
24
|
+
* `editor.setCamera(...)`.
|
|
25
|
+
*
|
|
26
|
+
* Methods come back bound to the real object: `LiveEditor` keeps its client in
|
|
27
|
+
* a `#`-private field, which a call whose `this` is the proxy cannot read.
|
|
28
|
+
*/
|
|
29
|
+
export declare function withEditorMemberHints<T extends object>(target: T, binding?: string): T;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Resolve an existing session for exactly one project.
|
|
2
|
+
* Prefer the project's hint, verify it against the served canonical path,
|
|
3
|
+
* then consult the shared registry. Never fall back to another project.
|
|
4
|
+
* Storage identifiers move with the server in the coordinated migration. */
|
|
5
|
+
import { type SessionListingTransport as SessionDiscoveryTransport } from '@volter/sdk/session/discovery';
|
|
6
|
+
/** Nearest ancestor containing the project's canonical manifest. */
|
|
7
|
+
export declare function findProjectRootFrom(dir: string): string | null;
|
|
8
|
+
/** The minimal slice of `EditorTransport` `resolveSession` actually needs — narrower than the full interface so a test double only has to implement one method. */
|
|
9
|
+
export type SessionListingTransport = SessionDiscoveryTransport;
|
|
10
|
+
export interface ResolvedSession {
|
|
11
|
+
/** Editor dev-server port the resolved session is listening on. */
|
|
12
|
+
port: number;
|
|
13
|
+
/** Absolute project root — the nearest ancestor of the requested directory containing `volter.project.json`. */
|
|
14
|
+
projectRoot: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SessionResolutionDeps {
|
|
17
|
+
/** Session listing + liveness verification — defaults to a real `HttpSessionDiscovery`. Overridable for tests. */
|
|
18
|
+
transport?: SessionListingTransport;
|
|
19
|
+
/** Project-root discovery — defaults to the real fs walk (`findProjectRootFrom` above). Overridable for tests. */
|
|
20
|
+
findProjectRootFrom?: (dir: string) => string | null;
|
|
21
|
+
/** Project-local session hint reader. Defaults to `.volter/session.json`. */
|
|
22
|
+
readProjectSession?: (projectRoot: string) => ProjectSessionHint | null;
|
|
23
|
+
/** Exact-session verifier. Defaults to GET /__editor/project + canonical project matching. */
|
|
24
|
+
verifyProjectSession?: (hint: ProjectSessionHint, projectRoot: string) => Promise<boolean>;
|
|
25
|
+
}
|
|
26
|
+
export interface ProjectSessionHint {
|
|
27
|
+
port: number;
|
|
28
|
+
pid: number;
|
|
29
|
+
url: string;
|
|
30
|
+
startedAt: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function resolveSession(projectDir?: string, deps?: SessionResolutionDeps): Promise<ResolvedSession>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny memoized-async-factory primitive — the machinery behind
|
|
3
|
+
* `index.ts`'s lazy top-level `editor`/`tools` singletons. Pulled into
|
|
4
|
+
* its own module (rather than inlined) so it's independently unit-testable
|
|
5
|
+
* with a fake factory, with no need to exercise a real `connect()` (session
|
|
6
|
+
* discovery, network) just to prove "two accesses, one connect".
|
|
7
|
+
*
|
|
8
|
+
* Caches the in-flight PROMISE, not just the resolved value — two callers
|
|
9
|
+
* racing `ensure()` before the first resolves still share the SAME
|
|
10
|
+
* connection attempt, not two independent ones.
|
|
11
|
+
*/
|
|
12
|
+
export interface LazySession<T> {
|
|
13
|
+
/** Returns the memoized promise, creating it via `factory()` on first call. */
|
|
14
|
+
ensure(): Promise<T>;
|
|
15
|
+
/** Clears the memo — the NEXT `ensure()` calls `factory()` again. Exposed for tests (and for a caller that deliberately wants to reconnect); not needed in ordinary use. */
|
|
16
|
+
reset(): void;
|
|
17
|
+
}
|
|
18
|
+
export declare function createLazySession<T>(factory: () => Promise<T>): LazySession<T>;
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Registered project tools over the current shared editor session. */
|
|
2
|
+
import type { EditorClient, ProjectToolCatalog, ProjectToolCatalogEntry, ProjectToolOutcome } from '@volter/sdk';
|
|
3
|
+
export declare class LiveTools {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(client: EditorClient);
|
|
6
|
+
/** Enumerate the exact `package.json#volter.tools` catalog without executing it. */
|
|
7
|
+
list(): Promise<ProjectToolCatalog>;
|
|
8
|
+
/** Return one tool's discoverable metadata, or `null` when it is not registered. */
|
|
9
|
+
describe(name: string): Promise<ProjectToolCatalogEntry | null>;
|
|
10
|
+
/**
|
|
11
|
+
* Invoke the same validated callable used by the editor and CLI.
|
|
12
|
+
*
|
|
13
|
+
* `instance` names WHICH mounted instance the tool should drive when several
|
|
14
|
+
* are live (multiplayer authoring) — it reaches the tool as `ctx.instance`,
|
|
15
|
+
* and a tool that drives the game binds
|
|
16
|
+
* `game.instance(ctx.instance)` from it. Omitted is the single-instance case;
|
|
17
|
+
* the tool then targets the sole live instance, exactly as before.
|
|
18
|
+
*/
|
|
19
|
+
run(name: string, input?: unknown, options?: {
|
|
20
|
+
confirm?: boolean;
|
|
21
|
+
instance?: string;
|
|
22
|
+
}): Promise<ProjectToolOutcome>;
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/live",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"author": "Volter AI, Inc.",
|
|
4
|
+
"version": "0.5.204",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"description": "Attach to Volter Editor sessions for document and modeling automation.",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/volter-ai/editor.git",
|
|
14
|
+
"directory": "packages/live"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"README.md",
|
|
21
|
+
"BUNDLED_NOTICES"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@volter/sdk": "0.5.204",
|
|
34
|
+
"@volter/project": "0.5.204",
|
|
35
|
+
"undici": "^7.28.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"esbuild": "0.25.12"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
42
|
+
"build": "tsc -p tsconfig.build.json && node build.mjs",
|
|
43
|
+
"prepack": "npm run build",
|
|
44
|
+
"test": "npm run build && node --test test/session.test.mjs"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
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/sdk/src/kit/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
|
+
* `menubar` the APPLICATION MENUS (Edit, View, …) and the menus they open
|
|
20
|
+
*
|
|
21
|
+
* await editor.document.click('[data-testid=app-menu-edit]', { scope: 'menubar' });
|
|
22
|
+
* await editor.document.query('[role=tab]', { scope: 'rail' });
|
|
23
|
+
* await editor.document.click('[data-testid=properties-tab-modifiers]', { scope: 'rail' });
|
|
24
|
+
* await editor.document.click('[data-ingest-name=Cube]', { scope: 'outliner', clicks: 2 });
|
|
25
|
+
* await editor.document.type('Wheel', { scope: 'outliner' });
|
|
26
|
+
*
|
|
27
|
+
* It is not page automation: a selector resolving outside the named surface is
|
|
28
|
+
* refused by a message naming the scope and listing the others. `rail` and
|
|
29
|
+
* `outliner`, and `content` are editor chrome and stay readable while the Game document is
|
|
30
|
+
* active; `editor.hierarchy()` / `editor.inspect()` keep answering what those
|
|
31
|
+
* panels RESOLVED, where this door answers what they DREW.
|
|
32
|
+
*
|
|
33
|
+
* A field on `LiveEditor` rather than methods on it, so the editor's `eval --list` command
|
|
34
|
+
* shows the verbs as one named surface — the same reason `game.input`
|
|
35
|
+
* and `game.events` are instance fields.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import type {
|
|
39
|
+
DocumentProbeResult,
|
|
40
|
+
DocumentProbeScope,
|
|
41
|
+
DocumentProbeStep,
|
|
42
|
+
EditorClient,
|
|
43
|
+
} from '@volter/sdk';
|
|
44
|
+
|
|
45
|
+
/** Which named surface a verb runs against; `document` when omitted. */
|
|
46
|
+
export interface DocumentScopeOption {
|
|
47
|
+
scope?: DocumentProbeScope;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Modifier/targeting options shared by the gesture verbs. */
|
|
51
|
+
export interface DocumentGestureOptions extends DocumentScopeOption {
|
|
52
|
+
/** Which match to drive when the selector matches several (default 0). */
|
|
53
|
+
index?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DocumentKeyOptions extends DocumentGestureOptions {
|
|
57
|
+
/** Target one element instead of whatever inside the scope has focus. */
|
|
58
|
+
selector?: string;
|
|
59
|
+
code?: string;
|
|
60
|
+
ctrlKey?: boolean;
|
|
61
|
+
metaKey?: boolean;
|
|
62
|
+
shiftKey?: boolean;
|
|
63
|
+
altKey?: boolean;
|
|
64
|
+
/** Hold the key this long before releasing it (a person holding W).
|
|
65
|
+
* Finite, nonnegative, and at most 120,000 ms. */
|
|
66
|
+
holdMs?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface DocumentPasteOptions extends DocumentGestureOptions {
|
|
70
|
+
selector?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface DocumentTypeOptions extends DocumentGestureOptions {
|
|
74
|
+
/** The field; omitted types into whatever inside the scope has focus. */
|
|
75
|
+
selector?: string;
|
|
76
|
+
/** Replace what the field holds first (default true). */
|
|
77
|
+
replace?: boolean;
|
|
78
|
+
/** Press Enter after the text (default true). */
|
|
79
|
+
enter?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class LiveEditorDocument {
|
|
83
|
+
readonly #client: EditorClient;
|
|
84
|
+
|
|
85
|
+
constructor(client: EditorClient) {
|
|
86
|
+
this.#client = client;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read matching elements inside the active document: tag, text, attributes,
|
|
91
|
+
* value/checked/disabled and rect. `matched` is the total before `limit`.
|
|
92
|
+
*
|
|
93
|
+
* `styles` additionally resolves named properties per match — and resolving
|
|
94
|
+
* is the point, because a theme token is an expression until an element
|
|
95
|
+
* paints it. Ask for the standard property to learn the colour a person
|
|
96
|
+
* sees; ask for a `--volter-…` custom property to learn what a rule WOULD
|
|
97
|
+
* paint, which is the only way to measure a `:hover` colour (`:hover` is a
|
|
98
|
+
* browser state no synthetic event can enter, so there is deliberately no
|
|
99
|
+
* hover verb on this door).
|
|
100
|
+
*
|
|
101
|
+
* await editor.document.query('.volter-tree-row', {
|
|
102
|
+
* styles: ['backgroundColor', '--volter-widget-regular-hover'],
|
|
103
|
+
* });
|
|
104
|
+
*/
|
|
105
|
+
async query(
|
|
106
|
+
selector: string,
|
|
107
|
+
options?: DocumentScopeOption & { limit?: number; styles?: readonly string[] },
|
|
108
|
+
): Promise<DocumentProbeResult> {
|
|
109
|
+
return this.#probe({
|
|
110
|
+
action: 'query',
|
|
111
|
+
selector,
|
|
112
|
+
...(options?.scope === undefined ? {} : { scope: options.scope }),
|
|
113
|
+
...(options?.limit === undefined ? {} : { limit: options.limit }),
|
|
114
|
+
...(options?.styles === undefined ? {} : { styles: [...options.styles] }),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A REAL pointer gesture (pointerdown/mousedown/focus/pointerup/mouseup/click)
|
|
119
|
+
* — not `element.click()`, which a `pointerdown` listener never sees. */
|
|
120
|
+
async click(
|
|
121
|
+
selector: string,
|
|
122
|
+
options?: DocumentGestureOptions & {
|
|
123
|
+
clicks?: number;
|
|
124
|
+
at?: [number, number];
|
|
125
|
+
altKey?: boolean;
|
|
126
|
+
ctrlKey?: boolean;
|
|
127
|
+
metaKey?: boolean;
|
|
128
|
+
shiftKey?: boolean;
|
|
129
|
+
},
|
|
130
|
+
): Promise<DocumentProbeResult> {
|
|
131
|
+
// An option this door does not know is refused, never dropped: a dropped `position` clicked
|
|
132
|
+
// the element's centre and answered as if it had clicked where it was asked.
|
|
133
|
+
const known = ['scope', 'index', 'clicks', 'at', 'altKey', 'ctrlKey', 'metaKey', 'shiftKey'];
|
|
134
|
+
const unknown = Object.keys(options ?? {}).filter((key) => !known.includes(key));
|
|
135
|
+
if (unknown.length > 0) throw new Error(`click has no option ${unknown.map((key) => `\`${key}\``).join(', ')}; its options are scope, index, clicks, at ([x, y] fractions of the element's box) and the modifier keys altKey, ctrlKey, metaKey and shiftKey.`);
|
|
136
|
+
return this.#probe({
|
|
137
|
+
action: 'click',
|
|
138
|
+
selector,
|
|
139
|
+
...(options?.scope === undefined ? {} : { scope: options.scope }),
|
|
140
|
+
...(options?.index === undefined ? {} : { index: options.index }),
|
|
141
|
+
...(options?.clicks === undefined ? {} : { clicks: options.clicks }),
|
|
142
|
+
...(options?.at === undefined ? {} : { at: options.at }),
|
|
143
|
+
...(options?.altKey ? { altKey: true } : {}),
|
|
144
|
+
...(options?.ctrlKey ? { ctrlKey: true } : {}),
|
|
145
|
+
...(options?.metaKey ? { metaKey: true } : {}),
|
|
146
|
+
...(options?.shiftKey ? { shiftKey: true } : {}),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* TYPE into a field and commit with Enter, the way a person does — one
|
|
152
|
+
* character at a time through the prototype's value setter, between real
|
|
153
|
+
* `keydown`/`keyup`.
|
|
154
|
+
*
|
|
155
|
+
* `paste` is not a substitute: an untrusted `ClipboardEvent` performs no
|
|
156
|
+
* default action, so a plain `<input>` with no paste handler keeps its old
|
|
157
|
+
* value. Omit `selector` to type into whatever inside the scope has focus —
|
|
158
|
+
* which is what a rename field is, one gesture after
|
|
159
|
+
* `click(row, { clicks: 2 })`.
|
|
160
|
+
*/
|
|
161
|
+
async type(text: string, options?: DocumentTypeOptions): Promise<DocumentProbeResult> {
|
|
162
|
+
return this.#probe({ action: 'type', text, ...(options ?? {}) });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A real pointer DRAG across one matched element — press at `from`, move,
|
|
167
|
+
* release at `to`. The gesture a direct-manipulation canvas needs; a
|
|
168
|
+
* zero-length drag is a click at that fraction, which `click` (always the
|
|
169
|
+
* center) cannot place.
|
|
170
|
+
*
|
|
171
|
+
* `from`, `to` and every point in `via` are `[x, y]` FRACTIONS OF THE
|
|
172
|
+
* MATCHED ELEMENT'S BOX, 0..1 from its top-left — NEVER pixels and never
|
|
173
|
+
* page coordinates. `[0.5, 0.5]` is its center, `[1, 0]` its top-right.
|
|
174
|
+
* Compute a pixel target by measuring the element first: `query` answers
|
|
175
|
+
* its `rect`, and `(px - rect.x) / rect.width` is the fraction to pass.
|
|
176
|
+
*/
|
|
177
|
+
async drag(
|
|
178
|
+
selector: string,
|
|
179
|
+
options: DocumentGestureOptions & {
|
|
180
|
+
from: [number, number];
|
|
181
|
+
to: [number, number];
|
|
182
|
+
via?: [number, number][];
|
|
183
|
+
steps?: number;
|
|
184
|
+
altKey?: boolean;
|
|
185
|
+
ctrlKey?: boolean;
|
|
186
|
+
metaKey?: boolean;
|
|
187
|
+
shiftKey?: boolean;
|
|
188
|
+
/** 0 primary (default), 1 middle, 2 secondary. */
|
|
189
|
+
button?: 0 | 1 | 2;
|
|
190
|
+
/** `via`/`to` may leave the element's box after the press (a handle drag). */
|
|
191
|
+
leave?: boolean;
|
|
192
|
+
/** Keep the button down at `to`, for readings taken mid-gesture; {@link release} lets go. */
|
|
193
|
+
hold?: boolean;
|
|
194
|
+
},
|
|
195
|
+
): Promise<DocumentProbeResult> {
|
|
196
|
+
return this.#probe({
|
|
197
|
+
action: 'drag',
|
|
198
|
+
selector,
|
|
199
|
+
...(options.scope === undefined ? {} : { scope: options.scope }),
|
|
200
|
+
from: options.from,
|
|
201
|
+
to: options.to,
|
|
202
|
+
...(options.via === undefined ? {} : { via: options.via }),
|
|
203
|
+
...(options.steps === undefined ? {} : { steps: options.steps }),
|
|
204
|
+
...(options.index === undefined ? {} : { index: options.index }),
|
|
205
|
+
...(options.altKey === undefined ? {} : { altKey: options.altKey }),
|
|
206
|
+
...(options.ctrlKey === undefined ? {} : { ctrlKey: options.ctrlKey }),
|
|
207
|
+
...(options.metaKey === undefined ? {} : { metaKey: options.metaKey }),
|
|
208
|
+
...(options.shiftKey === undefined ? {} : { shiftKey: options.shiftKey }),
|
|
209
|
+
...(options.button === undefined ? {} : { button: options.button }),
|
|
210
|
+
...(options.leave === undefined ? {} : { leave: options.leave }),
|
|
211
|
+
...(options.hold === undefined ? {} : { hold: options.hold }),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Lets go of the drag `hold: true` left pressed, where it was left. */
|
|
216
|
+
async release(options?: DocumentGestureOptions): Promise<DocumentProbeResult> {
|
|
217
|
+
return this.#probe({ action: 'release', ...(options?.scope === undefined ? {} : { scope: options.scope }) });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A real keydown/keyup on the target, or on whatever inside the document has focus. */
|
|
221
|
+
async key(key: string, options?: DocumentKeyOptions): Promise<DocumentProbeResult> {
|
|
222
|
+
return this.#probe({ action: 'key', key, ...(options ?? {}) });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** A real `ClipboardEvent` carrying `text/plain` — the gesture nothing else
|
|
226
|
+
* in the product can produce. */
|
|
227
|
+
async paste(text: string, options?: DocumentPasteOptions): Promise<DocumentProbeResult> {
|
|
228
|
+
return this.#probe({ action: 'paste', text, ...(options ?? {}) });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Choose `value` on a `<select>` — a native dropdown's options are drawn by
|
|
233
|
+
* the OS, so `click` has nothing in the document to resolve, and a plain
|
|
234
|
+
* `element.value =` is invisible to React. Set through the prototype's own
|
|
235
|
+
* value setter plus `input`/`change`; `value` is the option's `value`, not
|
|
236
|
+
* its label. An unknown value is refused with the options it does offer.
|
|
237
|
+
*/
|
|
238
|
+
async select(
|
|
239
|
+
selector: string,
|
|
240
|
+
value: string,
|
|
241
|
+
options?: DocumentGestureOptions,
|
|
242
|
+
): Promise<DocumentProbeResult> {
|
|
243
|
+
return this.#probe({
|
|
244
|
+
action: 'select',
|
|
245
|
+
selector,
|
|
246
|
+
value,
|
|
247
|
+
...(options?.scope === undefined ? {} : { scope: options.scope }),
|
|
248
|
+
...(options?.index === undefined ? {} : { index: options.index }),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* THE REPL over the open document: run `step` in the editor page against the
|
|
254
|
+
* object the ACTIVE document published as its context (the mesh document
|
|
255
|
+
* publishes its `MeshEditSession`, whose `ctx` is the bpy-shaped edit
|
|
256
|
+
* context — `ctx.ops.mesh.bevel({ offset: 0.1 })`, `ctx.selection`,
|
|
257
|
+
* `ctx.history`, `session.commit()`). Edit mode, no play. Serialized like
|
|
258
|
+
* `game.page`: the step's own source travels, so inline every value it
|
|
259
|
+
* needs and return plain data.
|
|
260
|
+
*/
|
|
261
|
+
async run<T = unknown>(
|
|
262
|
+
step: (ctx: unknown, info: { documentId: string }) => T | Promise<T>,
|
|
263
|
+
): Promise<T> {
|
|
264
|
+
return this.#client.documentScript<T>(step.toString());
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#probe(step: DocumentProbeStep): Promise<DocumentProbeResult> {
|
|
268
|
+
return this.#client.documentProbe(step);
|
|
269
|
+
}
|
|
270
|
+
}
|