@volter/editor-sdk 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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +19 -0
- package/package.json +90 -0
- package/src/account.ts +210 -0
- package/src/chrome.ts +83 -0
- package/src/client.ts +1547 -0
- package/src/commands.ts +66 -0
- package/src/contributions.ts +985 -0
- package/src/document-probe.ts +237 -0
- package/src/editor-view.ts +220 -0
- package/src/extension.ts +40 -0
- package/src/generations.ts +178 -0
- package/src/host.ts +1167 -0
- package/src/http-transport.browser.ts +14 -0
- package/src/http-transport.node.ts +19 -0
- package/src/index.ts +128 -0
- package/src/layout-arrangements.ts +5 -0
- package/src/layouts.tsx +108 -0
- package/src/looks.ts +14 -0
- package/src/project/output-roots.ts +73 -0
- package/src/project/tab-census.ts +149 -0
- package/src/project-tool-catalog.ts +96 -0
- package/src/selection.tsx +108 -0
- package/src/services.ts +18 -0
- package/src/session/build-report.ts +19 -0
- package/src/session/collaboration-types.ts +262 -0
- package/src/session/command-table.ts +333 -0
- package/src/session/discovery.ts +90 -0
- package/src/session/editor-brand.ts +73 -0
- package/src/session/editor-compatibility.ts +248 -0
- package/src/session/editor-control-lifecycle.ts +68 -0
- package/src/session/editor-control-protocol.ts +5 -0
- package/src/session/entrypoint-selection-readers.ts +66 -0
- package/src/session/entrypoint-selection-source.ts +120 -0
- package/src/session/game-css-scope.ts +30 -0
- package/src/session/product-create.ts +24 -0
- package/src/session/product-locator.ts +389 -0
- package/src/session/project-module-url.ts +245 -0
- package/src/session/registry-format.ts +203 -0
- package/src/session/relative-path-guard.ts +56 -0
- package/src/session/source-glob.ts +15 -0
- package/src/session/tool-contribution-convention.ts +116 -0
- package/src/session/workbench-locator.ts +650 -0
- package/src/session.ts +41 -0
- package/src/share.ts +160 -0
- package/src/tools/errors.ts +91 -0
- package/src/tools/provider-execution.ts +70 -0
- package/src/tools/registry.ts +341 -0
- package/src/tools/types.ts +159 -0
- package/src/transport.ts +97 -0
- package/src/types.ts +1581 -0
- package/src/views.ts +164 -0
- package/src/widgets/design-system.ts +93 -0
- package/src/widgets/editor-appearance.ts +149 -0
- package/src/widgets/editor-material.ts +83 -0
- package/src/widgets/icon-set-registry.ts +105 -0
- package/src/widgets/index.ts +71 -0
- package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
- package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
- package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
- package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
- package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
- package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
- package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
- package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
- package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
- package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
- package/src/widgets/inspector-widgets/color-utils.ts +201 -0
- package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
- package/src/widgets/inspector-widgets/index.ts +24 -0
- package/src/widgets/inspector-widgets/shared.tsx +140 -0
- package/src/widgets/interactive-edit-scope.ts +33 -0
- package/src/widgets/patterns/Dialog.tsx +129 -0
- package/src/widgets/patterns/Fields.tsx +44 -0
- package/src/widgets/patterns/List.tsx +25 -0
- package/src/widgets/patterns/StateSurface.tsx +40 -0
- package/src/widgets/patterns/Surfaces.tsx +122 -0
- package/src/widgets/patterns/Tabs.tsx +80 -0
- package/src/widgets/patterns/Toolbar.tsx +72 -0
- package/src/widgets/patterns/Tree.tsx +72 -0
- package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
- package/src/widgets/primitives/Button.tsx +62 -0
- package/src/widgets/primitives/ColorInput.tsx +78 -0
- package/src/widgets/primitives/DraftTextInput.tsx +63 -0
- package/src/widgets/primitives/EditorIcon.tsx +157 -0
- package/src/widgets/primitives/FormControls.tsx +88 -0
- package/src/widgets/primitives/HoverPreview.tsx +96 -0
- package/src/widgets/primitives/JsonInput.tsx +113 -0
- package/src/widgets/primitives/Layout.tsx +100 -0
- package/src/widgets/primitives/Menu.tsx +140 -0
- package/src/widgets/primitives/NumberInput.tsx +169 -0
- package/src/widgets/primitives/Panel.tsx +80 -0
- package/src/widgets/primitives/SectionHeader.tsx +77 -0
- package/src/widgets/primitives/Text.tsx +54 -0
- package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
- package/src/widgets/primitives/Tooltip.tsx +204 -0
- package/src/widgets/primitives/Vec3Input.tsx +70 -0
- package/src/widgets/primitives/banner-tones.ts +32 -0
- package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
- package/src/widgets/primitives/editor-icons.ts +245 -0
- package/src/widgets/primitives/panel-header-styles.ts +42 -0
- package/src/widgets/theme.ts +2633 -0
- package/src/widgets/z-index.ts +25 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire vocabulary of the EDITOR-SURFACE probe — the scoped editor-chrome
|
|
3
|
+
* door (`packages/editor/src/editor-document-probe.ts` implements it,
|
|
4
|
+
* `@vgai/live`'s `editor.document` binds it, and that module's header carries
|
|
5
|
+
* the design decision and the scope contract).
|
|
6
|
+
*
|
|
7
|
+
* Deliberately NOT a page-automation vocabulary: there is no navigation, no
|
|
8
|
+
* waiting, no frame/window addressing, and no selector rooted anywhere but one
|
|
9
|
+
* of the NAMED surfaces in {@link DocumentProbeScope}. Eight actions, each one
|
|
10
|
+
* a gesture or a read an agent cannot otherwise perform through the product.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* WHICH NAMED SURFACE a step runs against — the whole addressing vocabulary of
|
|
15
|
+
* this door, and the reason it is a vocabulary rather than a free selector.
|
|
16
|
+
*
|
|
17
|
+
* Every name here resolves to ONE live element the person is looking at, found
|
|
18
|
+
* by a stamp that surface's own owner wrote (`data-vgai-part` on the element
|
|
19
|
+
* the workbench handed over for a registered view; the document surface's own
|
|
20
|
+
* `data-testid`), never by walking the page for a class that looks right. A
|
|
21
|
+
* name whose surface is not on screen is REFUSED by name — an agent learns the
|
|
22
|
+
* Properties view is closed instead of reading an empty match list.
|
|
23
|
+
*
|
|
24
|
+
* - `document` — the ACTIVE centre document's whole box: its content, its
|
|
25
|
+
* header strip and its shelf rail. The default, and what this door meant
|
|
26
|
+
* before there were names.
|
|
27
|
+
* - `header` / `shelf` — the active document's toolbar strip and its tool
|
|
28
|
+
* rail, each on its own. Both are INSIDE `document`; they exist as names so
|
|
29
|
+
* a selector that also matches in the content (`button`, `[role=tab]`) can
|
|
30
|
+
* be aimed without an index.
|
|
31
|
+
* - `rail` — the Properties view (`vgai.properties`): its vertical tab rail,
|
|
32
|
+
* the active tab's sections and their fields.
|
|
33
|
+
* - `outliner` — the Outliner view (`vgai.outliner`): its rows, their
|
|
34
|
+
* expand/eye/camera controls, and its header.
|
|
35
|
+
* - `content` — the Content view (`vgai.content`): its categories and asset
|
|
36
|
+
* rows.
|
|
37
|
+
*
|
|
38
|
+
* `rail`, `outliner`, and `content` are EDITOR CHROME and do not belong to the active
|
|
39
|
+
* document, so they stay reachable while the Game document is active — the
|
|
40
|
+
* Game refusal is about driving a game through synthetic gestures, and reading
|
|
41
|
+
* the panel that reports its selection is not that.
|
|
42
|
+
*/
|
|
43
|
+
export type DocumentProbeScope = 'document' | 'header' | 'shelf' | 'rail' | 'outliner' | 'content';
|
|
44
|
+
|
|
45
|
+
/** One element as the probe reports it — everything a caller needs to assert
|
|
46
|
+
* on, and nothing that requires a second round trip. */
|
|
47
|
+
export interface ProbedElement {
|
|
48
|
+
/** Position within the match list this element came from. */
|
|
49
|
+
index: number;
|
|
50
|
+
tag: string;
|
|
51
|
+
/** `innerText`, trimmed and capped. */
|
|
52
|
+
text: string;
|
|
53
|
+
attributes: Record<string, string>;
|
|
54
|
+
rect: { x: number; y: number; width: number; height: number };
|
|
55
|
+
/** Present for form controls. */
|
|
56
|
+
value?: string;
|
|
57
|
+
checked?: boolean;
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
/** Resolved style, present only for the names {@link DocumentQueryStep.styles}
|
|
60
|
+
* asked for. A standard property carries the browser's own resolved value
|
|
61
|
+
* (`backgroundColor` → `rgb(52, 52, 52)`); a custom property carries what it
|
|
62
|
+
* PAINTS when it resolves as a colour, and its declared text otherwise. */
|
|
63
|
+
styles?: Record<string, string>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface DocumentProbeResult {
|
|
67
|
+
/** The surface the step ran against — echoed so a transcript proves WHICH
|
|
68
|
+
* one was read or driven, not merely that something was. `name` is the
|
|
69
|
+
* vocabulary word; `id`/`title` are that surface's own identity (a
|
|
70
|
+
* document's id and title, or the registered view's). */
|
|
71
|
+
scope: { name: DocumentProbeScope; id: string; title: string };
|
|
72
|
+
/** Total matches inside the scope, before any `limit`. */
|
|
73
|
+
matched: number;
|
|
74
|
+
elements: ProbedElement[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Which named surface a step runs against; `document` when omitted. */
|
|
78
|
+
interface ScopedStep {
|
|
79
|
+
scope?: DocumentProbeScope;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Read what a surface rendered. */
|
|
83
|
+
export interface DocumentQueryStep extends ScopedStep {
|
|
84
|
+
action: 'query';
|
|
85
|
+
selector: string;
|
|
86
|
+
/** Cap on returned elements (default 25); `matched` still reports the total. */
|
|
87
|
+
limit?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Style property names to resolve on each match — standard
|
|
90
|
+
* (`backgroundColor`, `borderInlineStartWidth`) or custom (`--vgai-…`).
|
|
91
|
+
*
|
|
92
|
+
* Ask for the STANDARD property to learn what a surface PAINTS: a theme
|
|
93
|
+
* token is an expression (`color-mix(…)`), and the element that uses it is
|
|
94
|
+
* where the browser turns that expression into a colour. A custom property
|
|
95
|
+
* is resolved here too, through a real paint property, so a token answers
|
|
96
|
+
* with the rgb it would paint rather than its own algebra — which is the
|
|
97
|
+
* only way a `:hover` colour can be measured at all, `:hover` being a
|
|
98
|
+
* browser state no synthetic event can enter. An UNDECLARED custom
|
|
99
|
+
* property answers `""`, so "does this palette declare this group" is a
|
|
100
|
+
* question this door can be trusted with.
|
|
101
|
+
*
|
|
102
|
+
* The value is the browser's, in the browser's own spelling: a token built
|
|
103
|
+
* by `color-mix()` answers as `color(srgb …)` with float channels, a plain
|
|
104
|
+
* one as `rgb(…)`. Both are the paint; multiply a float by 255 to compare
|
|
105
|
+
* against a reference frame's 8-bit levels.
|
|
106
|
+
*/
|
|
107
|
+
styles?: string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A real pointer gesture on one matched element. */
|
|
111
|
+
export interface DocumentClickStep extends ScopedStep {
|
|
112
|
+
action: 'click';
|
|
113
|
+
selector: string;
|
|
114
|
+
/** Which match (default 0). */
|
|
115
|
+
index?: number;
|
|
116
|
+
/**
|
|
117
|
+
* Clicks in the gesture (default 1). `2` is a REAL double click: the second
|
|
118
|
+
* press carries `detail: 2` and is followed by a `dblclick`, which is the
|
|
119
|
+
* only event `onDoubleClick` listens for.
|
|
120
|
+
*
|
|
121
|
+
* It is on `click` rather than a verb of its own because it is the same
|
|
122
|
+
* gesture with the same target — and it is here at all because a double
|
|
123
|
+
* click is the OUTLINER'S OWN rename gesture (`GameHierarchy.tsx`'s
|
|
124
|
+
* `onDoubleClick` → `onStartEditing`), so without it a rename could only be
|
|
125
|
+
* driven through `editor.setField`, which is not the control.
|
|
126
|
+
*/
|
|
127
|
+
clicks?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* TYPE into a focused field, character by character, and commit with Enter the
|
|
132
|
+
* way a person does.
|
|
133
|
+
*
|
|
134
|
+
* `paste` cannot stand in for this: an untrusted `ClipboardEvent` performs no
|
|
135
|
+
* default action, so a plain `<input>` with no paste handler keeps its old
|
|
136
|
+
* value. And a direct `element.value = x` is invisible to React, which caches
|
|
137
|
+
* the last value it wrote on the node — the same trap `select` documents. So
|
|
138
|
+
* each character goes through the prototype's own value setter (leaving
|
|
139
|
+
* React's tracker stale, which is what makes the following `input` read as a
|
|
140
|
+
* real edit), between a real `keydown`/`keyup` for that character, so a field
|
|
141
|
+
* that commits on a KEY rather than on `input` sees the keys too.
|
|
142
|
+
*/
|
|
143
|
+
export interface DocumentTypeStep extends ScopedStep {
|
|
144
|
+
action: 'type';
|
|
145
|
+
text: string;
|
|
146
|
+
/** The field; omitted means whatever inside the scope has focus. */
|
|
147
|
+
selector?: string;
|
|
148
|
+
index?: number;
|
|
149
|
+
/** Replace what the field holds first, the way a person selects-all and
|
|
150
|
+
* types over it (default true). `false` appends at the end. */
|
|
151
|
+
replace?: boolean;
|
|
152
|
+
/** Press Enter after the text (default true) — the commit gesture for the
|
|
153
|
+
* Outliner's rename field and every numeric field in the Properties rail. */
|
|
154
|
+
enter?: boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A real pointer DRAG across one matched element: `pointerdown` at `from`,
|
|
159
|
+
* `steps` `pointermove`s along the way, `pointerup` at `to` — the sequence a
|
|
160
|
+
* mouse produces, so a canvas that begins a gesture on press and previews on
|
|
161
|
+
* move (an Asset Lab document's direct manipulation) sees the whole gesture.
|
|
162
|
+
* `from`/`to` are FRACTIONS of the element's box (`[0.5, 0.5]` is its
|
|
163
|
+
* center), so a caller reasons in the element's own space, not the screen's.
|
|
164
|
+
* A zero-length drag is a click at that fraction.
|
|
165
|
+
*/
|
|
166
|
+
export interface DocumentDragStep extends ScopedStep {
|
|
167
|
+
action: 'drag';
|
|
168
|
+
selector: string;
|
|
169
|
+
/** Which match (default 0). */
|
|
170
|
+
index?: number;
|
|
171
|
+
from: [number, number];
|
|
172
|
+
to: [number, number];
|
|
173
|
+
/** Waypoints the pointer passes through between `from` and `to`, in
|
|
174
|
+
* order — what a lasso or a knife stroke needs, since a straight drag
|
|
175
|
+
* encloses nothing. Each leg gets `steps` moves. */
|
|
176
|
+
via?: [number, number][];
|
|
177
|
+
/** Intermediate `pointermove`s between consecutive points (default 8). */
|
|
178
|
+
steps?: number;
|
|
179
|
+
/** Modifier keys held for the whole gesture (a Shift-extend, an Alt-click). */
|
|
180
|
+
altKey?: boolean;
|
|
181
|
+
ctrlKey?: boolean;
|
|
182
|
+
metaKey?: boolean;
|
|
183
|
+
shiftKey?: boolean;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A real key on the explicit target, else whatever inside the document has focus. */
|
|
187
|
+
export interface DocumentKeyStep extends ScopedStep {
|
|
188
|
+
action: 'key';
|
|
189
|
+
key: string;
|
|
190
|
+
code?: string;
|
|
191
|
+
selector?: string;
|
|
192
|
+
index?: number;
|
|
193
|
+
ctrlKey?: boolean;
|
|
194
|
+
metaKey?: boolean;
|
|
195
|
+
shiftKey?: boolean;
|
|
196
|
+
altKey?: boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** A real `ClipboardEvent` carrying `text/plain` — the only way to exercise a
|
|
200
|
+
* paste handler, and the gesture the Sheets build could not verify. */
|
|
201
|
+
export interface DocumentPasteStep extends ScopedStep {
|
|
202
|
+
action: 'paste';
|
|
203
|
+
text: string;
|
|
204
|
+
selector?: string;
|
|
205
|
+
index?: number;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Choose a value on a `<select>` — the gesture `click` cannot make.
|
|
210
|
+
*
|
|
211
|
+
* A native dropdown's option list is rendered by the OS, not by the DOM, so
|
|
212
|
+
* there is nothing inside the document's container for a pointer gesture to
|
|
213
|
+
* resolve against: `click` on the `<select>` opens a menu no synthetic event
|
|
214
|
+
* can reach. Assigning `element.value` is equally useless on a React
|
|
215
|
+
* controlled component — React caches the last value it wrote on the node, so
|
|
216
|
+
* a direct write is not seen as a change and the next render puts the old
|
|
217
|
+
* value straight back. The implementation goes through the prototype's own
|
|
218
|
+
* value setter and then dispatches `input`/`change`, which is the ONE spelling
|
|
219
|
+
* React's synthetic-event layer honours.
|
|
220
|
+
*/
|
|
221
|
+
export interface DocumentSelectStep extends ScopedStep {
|
|
222
|
+
action: 'select';
|
|
223
|
+
selector: string;
|
|
224
|
+
/** The option's `value` (not its label). */
|
|
225
|
+
value: string;
|
|
226
|
+
/** Which match (default 0). */
|
|
227
|
+
index?: number;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export type DocumentProbeStep =
|
|
231
|
+
| DocumentQueryStep
|
|
232
|
+
| DocumentClickStep
|
|
233
|
+
| DocumentDragStep
|
|
234
|
+
| DocumentKeyStep
|
|
235
|
+
| DocumentPasteStep
|
|
236
|
+
| DocumentSelectStep
|
|
237
|
+
| DocumentTypeStep;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { AssetKind, EditorCameraState, EditorView, ViewPreset } from './types.js';
|
|
2
|
+
import { EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS, isEditorViewUtility } from './types.js';
|
|
3
|
+
|
|
4
|
+
const PREFIX = 'view.';
|
|
5
|
+
const MAX_QUERY_LENGTH = 4096;
|
|
6
|
+
const WORKSPACE_DOCUMENT_IDS = new Set<string>(EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS);
|
|
7
|
+
const ASSET_KINDS = new Set<AssetKind>([
|
|
8
|
+
'model',
|
|
9
|
+
'image',
|
|
10
|
+
'audio',
|
|
11
|
+
'animation',
|
|
12
|
+
'json',
|
|
13
|
+
'prefab',
|
|
14
|
+
'source',
|
|
15
|
+
]);
|
|
16
|
+
const CAMERAS = new Set<ViewPreset | 'isometric'>([
|
|
17
|
+
'top',
|
|
18
|
+
'front',
|
|
19
|
+
'right',
|
|
20
|
+
'perspective',
|
|
21
|
+
'isometric',
|
|
22
|
+
]);
|
|
23
|
+
type EditorViewDiagnostic = NonNullable<NonNullable<EditorView['viewport']>['diagnostic']>;
|
|
24
|
+
const DIAGNOSTICS = new Set<EditorViewDiagnostic>([
|
|
25
|
+
'solid',
|
|
26
|
+
'unlit',
|
|
27
|
+
'wireframe',
|
|
28
|
+
'normals',
|
|
29
|
+
'overdraw',
|
|
30
|
+
'uv',
|
|
31
|
+
'vertex-colors',
|
|
32
|
+
'bounds',
|
|
33
|
+
'skeleton',
|
|
34
|
+
]);
|
|
35
|
+
function nonEmpty(value: string | null): string | null {
|
|
36
|
+
const trimmed = value?.trim();
|
|
37
|
+
return trimmed ? trimmed : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeDocument(params: URLSearchParams, document: EditorView['document']): void {
|
|
41
|
+
if (!document) return;
|
|
42
|
+
params.set(`${PREFIX}docKind`, document.kind);
|
|
43
|
+
if (document.kind === 'asset' && document.entityId) {
|
|
44
|
+
params.set(`${PREFIX}doc`, document.entityId);
|
|
45
|
+
params.set(`${PREFIX}assetSource`, 'entity');
|
|
46
|
+
} else if (document.kind === 'asset') {
|
|
47
|
+
params.set(`${PREFIX}doc`, document.path!);
|
|
48
|
+
} else if (document.kind === 'scene') {
|
|
49
|
+
params.set(`${PREFIX}doc`, document.path);
|
|
50
|
+
} else if (document.kind === 'story') {
|
|
51
|
+
params.set(`${PREFIX}doc`, document.modulePath);
|
|
52
|
+
params.set(`${PREFIX}story`, document.storyName);
|
|
53
|
+
if (document.mode === 'docs') params.set(`${PREFIX}storyMode`, 'docs');
|
|
54
|
+
} else if (document.kind === 'project-tool') {
|
|
55
|
+
params.set(`${PREFIX}doc`, document.name);
|
|
56
|
+
} else {
|
|
57
|
+
params.set(`${PREFIX}doc`, document.id);
|
|
58
|
+
}
|
|
59
|
+
if (document.kind === 'asset' && document.assetKind) {
|
|
60
|
+
params.set(`${PREFIX}assetKind`, document.assetKind);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeViewport(params: URLSearchParams, viewport: EditorView['viewport']): void {
|
|
65
|
+
if (!viewport) return;
|
|
66
|
+
if (typeof viewport.camera === 'string') params.set(`${PREFIX}camera`, viewport.camera);
|
|
67
|
+
else if (viewport.camera) {
|
|
68
|
+
params.set(`${PREFIX}camera`, 'pose');
|
|
69
|
+
params.set(
|
|
70
|
+
`${PREFIX}cameraPosition`,
|
|
71
|
+
[viewport.camera.position.x, viewport.camera.position.y, viewport.camera.position.z].join(
|
|
72
|
+
',',
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
params.set(
|
|
76
|
+
`${PREFIX}cameraTarget`,
|
|
77
|
+
[viewport.camera.target.x, viewport.camera.target.y, viewport.camera.target.z].join(','),
|
|
78
|
+
);
|
|
79
|
+
if (viewport.camera.fov !== undefined)
|
|
80
|
+
params.set(`${PREFIX}cameraFov`, String(viewport.camera.fov));
|
|
81
|
+
}
|
|
82
|
+
if (viewport.diagnostic) params.set(`${PREFIX}diagnostic`, viewport.diagnostic);
|
|
83
|
+
if (viewport.frame) params.set(`${PREFIX}frame`, viewport.frame);
|
|
84
|
+
if (viewport.grid !== undefined) params.set(`${PREFIX}grid`, viewport.grid ? '1' : '0');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseVec3(value: string | null): EditorCameraState['position'] | null {
|
|
88
|
+
if (!value) return null;
|
|
89
|
+
const parts = value.split(',').map(Number);
|
|
90
|
+
if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) return null;
|
|
91
|
+
return { x: parts[0]!, y: parts[1]!, z: parts[2]! };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseCamera(params: URLSearchParams): NonNullable<EditorView['viewport']>['camera'] {
|
|
95
|
+
const camera = params.get(`${PREFIX}camera`);
|
|
96
|
+
if (camera && CAMERAS.has(camera as ViewPreset | 'isometric')) {
|
|
97
|
+
return camera as ViewPreset | 'isometric';
|
|
98
|
+
}
|
|
99
|
+
if (camera !== 'pose') return undefined;
|
|
100
|
+
const position = parseVec3(params.get(`${PREFIX}cameraPosition`));
|
|
101
|
+
const target = parseVec3(params.get(`${PREFIX}cameraTarget`));
|
|
102
|
+
const fovValue = params.get(`${PREFIX}cameraFov`);
|
|
103
|
+
const fov = fovValue === null ? undefined : Number(fovValue);
|
|
104
|
+
if (!position || !target || (fov !== undefined && !Number.isFinite(fov))) return undefined;
|
|
105
|
+
return { position, target, ...(fov !== undefined ? { fov } : {}) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseStoryDocument(params: URLSearchParams, modulePath: string): EditorView['document'] {
|
|
109
|
+
const storyName = nonEmpty(params.get(`${PREFIX}story`));
|
|
110
|
+
if (!storyName) return undefined;
|
|
111
|
+
return {
|
|
112
|
+
kind: 'story',
|
|
113
|
+
modulePath,
|
|
114
|
+
storyName,
|
|
115
|
+
...(params.get(`${PREFIX}storyMode`) === 'docs' ? { mode: 'docs' as const } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseAssetDocument(params: URLSearchParams, value: string): EditorView['document'] {
|
|
120
|
+
const assetKind = params.get(`${PREFIX}assetKind`);
|
|
121
|
+
if (params.get(`${PREFIX}assetSource`) === 'entity') {
|
|
122
|
+
return { kind: 'asset', entityId: value, assetKind: 'model' };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
kind: 'asset',
|
|
126
|
+
path: value,
|
|
127
|
+
...(assetKind && ASSET_KINDS.has(assetKind as AssetKind)
|
|
128
|
+
? { assetKind: assetKind as AssetKind }
|
|
129
|
+
: {}),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseDocument(params: URLSearchParams): EditorView['document'] {
|
|
134
|
+
const kind = params.get(`${PREFIX}docKind`);
|
|
135
|
+
const value = nonEmpty(params.get(`${PREFIX}doc`));
|
|
136
|
+
if (!value) return undefined;
|
|
137
|
+
if (kind === 'scene') return { kind, path: value };
|
|
138
|
+
if (kind === 'tool') return { kind, id: value };
|
|
139
|
+
if (kind === 'document') return { kind, id: value };
|
|
140
|
+
if (kind === 'world') return { kind, id: value };
|
|
141
|
+
if (kind === 'project-tool') return { kind, name: value };
|
|
142
|
+
if (kind === 'generation') return { kind, id: value };
|
|
143
|
+
if (kind === 'story') return parseStoryDocument(params, value);
|
|
144
|
+
if (kind === 'workspace' && WORKSPACE_DOCUMENT_IDS.has(value)) {
|
|
145
|
+
return {
|
|
146
|
+
kind,
|
|
147
|
+
id: value as Extract<NonNullable<EditorView['document']>, { kind: 'workspace' }>['id'],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return kind === 'asset' ? parseAssetDocument(params, value) : undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseViewport(params: URLSearchParams): EditorView['viewport'] {
|
|
154
|
+
const camera = parseCamera(params);
|
|
155
|
+
const diagnostic = params.get(`${PREFIX}diagnostic`);
|
|
156
|
+
const frame = params.get(`${PREFIX}frame`);
|
|
157
|
+
const grid = params.get(`${PREFIX}grid`);
|
|
158
|
+
const viewport = {
|
|
159
|
+
...(camera ? { camera } : {}),
|
|
160
|
+
...(diagnostic && DIAGNOSTICS.has(diagnostic as EditorViewDiagnostic)
|
|
161
|
+
? { diagnostic: diagnostic as EditorViewDiagnostic }
|
|
162
|
+
: {}),
|
|
163
|
+
...(frame === 'document' || frame === 'selection'
|
|
164
|
+
? { frame: frame as 'document' | 'selection' }
|
|
165
|
+
: {}),
|
|
166
|
+
...(grid === '0' || grid === '1' ? { grid: grid === '1' } : {}),
|
|
167
|
+
};
|
|
168
|
+
return Object.keys(viewport).length > 0 ? viewport : undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Apply only the view-owned query parameters, preserving project/scene routing params. */
|
|
172
|
+
export function editorViewUrl(view: EditorView, baseUrl: string | URL): string {
|
|
173
|
+
const url = new URL(baseUrl);
|
|
174
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
175
|
+
if (key.startsWith(PREFIX)) url.searchParams.delete(key);
|
|
176
|
+
}
|
|
177
|
+
url.searchParams.set(`${PREFIX}v`, '1');
|
|
178
|
+
writeDocument(url.searchParams, view.document);
|
|
179
|
+
if (view.selection?.ids.length) {
|
|
180
|
+
for (const id of view.selection.ids) url.searchParams.append(`${PREFIX}select`, id);
|
|
181
|
+
if (view.selection.focus) url.searchParams.set(`${PREFIX}focus`, '1');
|
|
182
|
+
}
|
|
183
|
+
writeViewport(url.searchParams, view.viewport);
|
|
184
|
+
if (view.workspace) url.searchParams.set(`${PREFIX}workspace`, view.workspace);
|
|
185
|
+
if (view.panel) url.searchParams.set(`${PREFIX}panel`, view.panel);
|
|
186
|
+
if (view.utility) url.searchParams.set(`${PREFIX}utility`, view.utility);
|
|
187
|
+
if (url.search.length > MAX_QUERY_LENGTH) {
|
|
188
|
+
throw new Error(`Editor view URL exceeds the ${MAX_QUERY_LENGTH}-character share limit.`);
|
|
189
|
+
}
|
|
190
|
+
return url.toString();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Parse a shareable editor projection. Malformed projections are ignored at boot. */
|
|
194
|
+
export function editorViewFromUrl(value: string | URL): EditorView | null {
|
|
195
|
+
const url = value instanceof URL ? value : new URL(value, 'http://editor.invalid');
|
|
196
|
+
const params = url.searchParams;
|
|
197
|
+
if (params.get(`${PREFIX}v`) !== '1') return null;
|
|
198
|
+
const view: EditorView = { version: 1 };
|
|
199
|
+
const document = parseDocument(params);
|
|
200
|
+
if (document) view.document = document;
|
|
201
|
+
const ids = params
|
|
202
|
+
.getAll(`${PREFIX}select`)
|
|
203
|
+
.filter((id) => id.length > 0)
|
|
204
|
+
.slice(0, 32);
|
|
205
|
+
if (ids.length) view.selection = { ids, focus: params.get(`${PREFIX}focus`) === '1' };
|
|
206
|
+
const viewport = parseViewport(params);
|
|
207
|
+
if (viewport) view.viewport = viewport;
|
|
208
|
+
// Shape check only: a `tool:` id names a utility the OPEN PROJECT
|
|
209
|
+
// contributes, so the URL cannot know whether it exists. The editor
|
|
210
|
+
// validates it against its live registry when the view is presented.
|
|
211
|
+
const workspace = params.get(`${PREFIX}workspace`);
|
|
212
|
+
if (workspace) view.workspace = workspace;
|
|
213
|
+
// Shape check only, like `workspace` above: which panels exist is the open
|
|
214
|
+
// editor's registry, and it answers at present-time.
|
|
215
|
+
const panel = params.get(`${PREFIX}panel`);
|
|
216
|
+
if (panel) view.panel = panel;
|
|
217
|
+
const utility = params.get(`${PREFIX}utility`);
|
|
218
|
+
if (utility && isEditorViewUtility(utility)) view.utility = utility;
|
|
219
|
+
return view;
|
|
220
|
+
}
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension contract (W3a) — the published surface a game project uses to
|
|
3
|
+
* contribute to the editor, with the degradation ladder made API.
|
|
4
|
+
*
|
|
5
|
+
* A project extends the editor through exactly three surfaces:
|
|
6
|
+
*
|
|
7
|
+
* (a) **Editor panels** — registered into the workspace as documents
|
|
8
|
+
* or utilities via a tool contribution (`package.json#vgai.tools` →
|
|
9
|
+
* `contributes: [{ point: 'workspace.document' | 'workspace.utility' | 'workspace.analytics' }]`).
|
|
10
|
+
* Types: `ToolContributionProps` in `@volter/editor-sdk/contributions`.
|
|
11
|
+
* Panels are never a parallel rail — the frame owns all layout.
|
|
12
|
+
* (b) **Inspector sections** — `point: 'selection.inspector'` /
|
|
13
|
+
* `'asset.inspector'` tool contributions with an exported `match`.
|
|
14
|
+
* Types: `ToolInspectorContributionProps` and friends, same module.
|
|
15
|
+
* (c) **System adapters** — runtime capabilities (networking, debug, …)
|
|
16
|
+
* registered from game code via `ctx.registerSystemAdapter?.(kind, impl)`
|
|
17
|
+
* (`SystemAdapters` in `@volter/editor-project`'s
|
|
18
|
+
* `adapter/system-adapter`). Deliberately NOT re-exported here: the
|
|
19
|
+
* engine already publishes that seam and every consumer of it also
|
|
20
|
+
* imports the engine — an alias would be a dead surface.
|
|
21
|
+
*
|
|
22
|
+
* Outcomes (the anti-shim rule, as API): every contribution resolves to an
|
|
23
|
+
* {@link ExtensionContributionState} —
|
|
24
|
+
*
|
|
25
|
+
* - `'active'` — the contribution loaded and produced its surface.
|
|
26
|
+
* - `'absent'` — nothing was contributed. The editor HIDES the surface
|
|
27
|
+
* entirely; it never fabricates placeholder data for a missing
|
|
28
|
+
* contribution.
|
|
29
|
+
* - `'failed'` — the contribution exists but threw / violated the contract.
|
|
30
|
+
* The failure is contained per-contribution (the editor never crashes),
|
|
31
|
+
* the surface is hidden, and the error is reported LOUDLY (editor
|
|
32
|
+
* console) — never silently swallowed into fake output.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The terminal state of a single extension contribution (see the module doc
|
|
37
|
+
* above for the exact semantics). Not a grade and not an ordering: `absent`
|
|
38
|
+
* and `failed` both hide the surface, and `failed` is reported loudly.
|
|
39
|
+
*/
|
|
40
|
+
export type ExtensionContributionState = 'active' | 'absent' | 'failed';
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* First-party generation JOB vocabulary.
|
|
5
|
+
*
|
|
6
|
+
* Providers keep their native request and result schemas. This deliberately
|
|
7
|
+
* normalizes only the lifecycle that is genuinely shared by Fal, Tripo,
|
|
8
|
+
* World Labs, and future generators: durable identity, state, resumption,
|
|
9
|
+
* acceptance, and the charge shown to the user.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const GenerationJobStatusSchema = z.enum([
|
|
13
|
+
'queued',
|
|
14
|
+
'running',
|
|
15
|
+
'succeeded',
|
|
16
|
+
'failed',
|
|
17
|
+
'cancelled',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export const GenerationBillingSchema = z.discriminatedUnion('route', [
|
|
21
|
+
z.object({ route: z.literal('mock') }).strict(),
|
|
22
|
+
z
|
|
23
|
+
.object({
|
|
24
|
+
route: z.literal('managed'),
|
|
25
|
+
estimatedCredits: z.number().nonnegative().optional(),
|
|
26
|
+
settledCredits: z.number().nonnegative().optional(),
|
|
27
|
+
})
|
|
28
|
+
.strict(),
|
|
29
|
+
z
|
|
30
|
+
.object({
|
|
31
|
+
route: z.literal('byok'),
|
|
32
|
+
currency: z.literal('USD'),
|
|
33
|
+
estimatedAmount: z.number().nonnegative().optional(),
|
|
34
|
+
settledAmount: z.number().nonnegative().optional(),
|
|
35
|
+
})
|
|
36
|
+
.strict(),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const GenerationOperationReferenceSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
tool: z.string().min(1),
|
|
42
|
+
input: z.unknown(),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
|
|
46
|
+
export const GenerationJobSchema = z
|
|
47
|
+
.object({
|
|
48
|
+
id: z.string().min(1),
|
|
49
|
+
provider: z.string().min(1),
|
|
50
|
+
externalId: z.string().min(1),
|
|
51
|
+
label: z.string().min(1),
|
|
52
|
+
operation: z.string().min(1),
|
|
53
|
+
mode: z.enum(['mock', 'direct', 'managed']),
|
|
54
|
+
status: GenerationJobStatusSchema,
|
|
55
|
+
createdAt: z.string().datetime(),
|
|
56
|
+
updatedAt: z.string().datetime(),
|
|
57
|
+
progress: z.number().min(0).max(1).optional(),
|
|
58
|
+
providerStatus: z.string().optional(),
|
|
59
|
+
queuePosition: z.number().int().nonnegative().optional(),
|
|
60
|
+
message: z.string().optional(),
|
|
61
|
+
lastPolledAt: z.string().datetime().optional(),
|
|
62
|
+
lastProviderChangeAt: z.string().datetime().optional(),
|
|
63
|
+
pollError: z.string().optional(),
|
|
64
|
+
pollErrorAt: z.string().datetime().optional(),
|
|
65
|
+
pollFailureCount: z.number().int().nonnegative().optional(),
|
|
66
|
+
nextPollAt: z.string().datetime().optional(),
|
|
67
|
+
billing: GenerationBillingSchema,
|
|
68
|
+
poll: GenerationOperationReferenceSchema,
|
|
69
|
+
cancel: GenerationOperationReferenceSchema.optional(),
|
|
70
|
+
accept: GenerationOperationReferenceSchema.optional(),
|
|
71
|
+
acceptedAt: z.string().datetime().optional(),
|
|
72
|
+
/** Last time a human/agent opened the native result. A later provider
|
|
73
|
+
* update makes the terminal result unread again without changing status. */
|
|
74
|
+
readAt: z.string().datetime().optional(),
|
|
75
|
+
provenanceOperationId: z.string().optional(),
|
|
76
|
+
outputPaths: z.array(z.string()).optional(),
|
|
77
|
+
})
|
|
78
|
+
.strict();
|
|
79
|
+
|
|
80
|
+
export const GenerationJobsDocumentSchema = z
|
|
81
|
+
.object({
|
|
82
|
+
version: z.literal(1),
|
|
83
|
+
jobs: z.array(GenerationJobSchema),
|
|
84
|
+
})
|
|
85
|
+
.strict();
|
|
86
|
+
|
|
87
|
+
export type GenerationJobStatus = z.infer<typeof GenerationJobStatusSchema>;
|
|
88
|
+
export type GenerationBilling = z.infer<typeof GenerationBillingSchema>;
|
|
89
|
+
export type GenerationJob = z.infer<typeof GenerationJobSchema>;
|
|
90
|
+
export type GenerationJobsDocument = z.infer<typeof GenerationJobsDocumentSchema>;
|
|
91
|
+
|
|
92
|
+
/** The MANAGED arm of {@link GenerationBilling} — vgai credits, not currency. */
|
|
93
|
+
export type ManagedGenerationBilling = Extract<GenerationBilling, { route: 'managed' }>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The managed-route billing leg of a provider tool's `toUpdate`, spread-ready.
|
|
97
|
+
*
|
|
98
|
+
* Every provider poll tool needs the same three-part decision — is this run on
|
|
99
|
+
* the managed route at all, did the provider report either credit figure, and
|
|
100
|
+
* which of the two are present — and Fal, Tripo, World Labs and OpenRouter had
|
|
101
|
+
* each spelled it out identically, fourteen lines apiece. The conditions are
|
|
102
|
+
* not obvious enough to retype safely: reporting `route: 'managed'` with both
|
|
103
|
+
* figures absent claims a charge nobody measured, and writing
|
|
104
|
+
* `estimatedCredits: undefined` is NOT the same as omitting it under
|
|
105
|
+
* `exactOptionalPropertyTypes` — it fails the schema rather than leaving the
|
|
106
|
+
* field unset.
|
|
107
|
+
*
|
|
108
|
+
* Call it with the parsed result itself; only the two credit fields are read.
|
|
109
|
+
*
|
|
110
|
+
* ...managedBilling(input.mode, result),
|
|
111
|
+
*/
|
|
112
|
+
export function managedBilling(
|
|
113
|
+
mode: string,
|
|
114
|
+
credits: {
|
|
115
|
+
readonly estimatedCredits?: number | undefined;
|
|
116
|
+
readonly settledCredits?: number | undefined;
|
|
117
|
+
},
|
|
118
|
+
): { billing?: ManagedGenerationBilling } {
|
|
119
|
+
if (mode !== 'managed') return {};
|
|
120
|
+
const { estimatedCredits, settledCredits } = credits;
|
|
121
|
+
if (estimatedCredits === undefined && settledCredits === undefined) return {};
|
|
122
|
+
return {
|
|
123
|
+
billing: {
|
|
124
|
+
route: 'managed',
|
|
125
|
+
...(estimatedCredits === undefined ? {} : { estimatedCredits }),
|
|
126
|
+
...(settledCredits === undefined ? {} : { settledCredits }),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface GenerationJobDraft {
|
|
132
|
+
provider: string;
|
|
133
|
+
externalId: string;
|
|
134
|
+
label: string;
|
|
135
|
+
operation: string;
|
|
136
|
+
mode: 'mock' | 'direct' | 'managed';
|
|
137
|
+
status: GenerationJobStatus;
|
|
138
|
+
progress?: number;
|
|
139
|
+
providerStatus?: string;
|
|
140
|
+
queuePosition?: number;
|
|
141
|
+
message?: string;
|
|
142
|
+
billing: GenerationBilling;
|
|
143
|
+
poll: { tool: string; input: unknown };
|
|
144
|
+
cancel?: { tool: string; input: unknown };
|
|
145
|
+
accept?: { tool: string; input: unknown };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface GenerationJobUpdate {
|
|
149
|
+
provider: string;
|
|
150
|
+
externalId: string;
|
|
151
|
+
status?: GenerationJobStatus;
|
|
152
|
+
progress?: number;
|
|
153
|
+
providerStatus?: string;
|
|
154
|
+
queuePosition?: number;
|
|
155
|
+
message?: string;
|
|
156
|
+
billing?: GenerationBilling;
|
|
157
|
+
cancel?: { tool: string; input: unknown };
|
|
158
|
+
/** `null` withdraws an acceptance action after polling proves that a
|
|
159
|
+
* successful operation produced no downloadable project output. */
|
|
160
|
+
accept?: { tool: string; input: unknown } | null;
|
|
161
|
+
accepted?: {
|
|
162
|
+
provenanceOperationId: string;
|
|
163
|
+
outputPaths: string[];
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Provider-owned mapping attached to a registered native operation module. */
|
|
168
|
+
export type GenerationToolContribution =
|
|
169
|
+
| {
|
|
170
|
+
role: 'submit';
|
|
171
|
+
provider: string;
|
|
172
|
+
toJob(input: unknown, result: unknown): GenerationJobDraft;
|
|
173
|
+
}
|
|
174
|
+
| {
|
|
175
|
+
role: 'poll' | 'cancel' | 'accept';
|
|
176
|
+
provider: string;
|
|
177
|
+
toUpdate(input: unknown, result: unknown): GenerationJobUpdate;
|
|
178
|
+
};
|