@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.
Files changed (104) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +19 -0
  4. package/package.json +90 -0
  5. package/src/account.ts +210 -0
  6. package/src/chrome.ts +83 -0
  7. package/src/client.ts +1547 -0
  8. package/src/commands.ts +66 -0
  9. package/src/contributions.ts +985 -0
  10. package/src/document-probe.ts +237 -0
  11. package/src/editor-view.ts +220 -0
  12. package/src/extension.ts +40 -0
  13. package/src/generations.ts +178 -0
  14. package/src/host.ts +1167 -0
  15. package/src/http-transport.browser.ts +14 -0
  16. package/src/http-transport.node.ts +19 -0
  17. package/src/index.ts +128 -0
  18. package/src/layout-arrangements.ts +5 -0
  19. package/src/layouts.tsx +108 -0
  20. package/src/looks.ts +14 -0
  21. package/src/project/output-roots.ts +73 -0
  22. package/src/project/tab-census.ts +149 -0
  23. package/src/project-tool-catalog.ts +96 -0
  24. package/src/selection.tsx +108 -0
  25. package/src/services.ts +18 -0
  26. package/src/session/build-report.ts +19 -0
  27. package/src/session/collaboration-types.ts +262 -0
  28. package/src/session/command-table.ts +333 -0
  29. package/src/session/discovery.ts +90 -0
  30. package/src/session/editor-brand.ts +73 -0
  31. package/src/session/editor-compatibility.ts +248 -0
  32. package/src/session/editor-control-lifecycle.ts +68 -0
  33. package/src/session/editor-control-protocol.ts +5 -0
  34. package/src/session/entrypoint-selection-readers.ts +66 -0
  35. package/src/session/entrypoint-selection-source.ts +120 -0
  36. package/src/session/game-css-scope.ts +30 -0
  37. package/src/session/product-create.ts +24 -0
  38. package/src/session/product-locator.ts +389 -0
  39. package/src/session/project-module-url.ts +245 -0
  40. package/src/session/registry-format.ts +203 -0
  41. package/src/session/relative-path-guard.ts +56 -0
  42. package/src/session/source-glob.ts +15 -0
  43. package/src/session/tool-contribution-convention.ts +116 -0
  44. package/src/session/workbench-locator.ts +650 -0
  45. package/src/session.ts +41 -0
  46. package/src/share.ts +160 -0
  47. package/src/tools/errors.ts +91 -0
  48. package/src/tools/provider-execution.ts +70 -0
  49. package/src/tools/registry.ts +341 -0
  50. package/src/tools/types.ts +159 -0
  51. package/src/transport.ts +97 -0
  52. package/src/types.ts +1581 -0
  53. package/src/views.ts +164 -0
  54. package/src/widgets/design-system.ts +93 -0
  55. package/src/widgets/editor-appearance.ts +149 -0
  56. package/src/widgets/editor-material.ts +83 -0
  57. package/src/widgets/icon-set-registry.ts +105 -0
  58. package/src/widgets/index.ts +71 -0
  59. package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
  60. package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
  61. package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
  62. package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
  63. package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
  64. package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
  65. package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
  66. package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
  67. package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
  68. package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
  69. package/src/widgets/inspector-widgets/color-utils.ts +201 -0
  70. package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
  71. package/src/widgets/inspector-widgets/index.ts +24 -0
  72. package/src/widgets/inspector-widgets/shared.tsx +140 -0
  73. package/src/widgets/interactive-edit-scope.ts +33 -0
  74. package/src/widgets/patterns/Dialog.tsx +129 -0
  75. package/src/widgets/patterns/Fields.tsx +44 -0
  76. package/src/widgets/patterns/List.tsx +25 -0
  77. package/src/widgets/patterns/StateSurface.tsx +40 -0
  78. package/src/widgets/patterns/Surfaces.tsx +122 -0
  79. package/src/widgets/patterns/Tabs.tsx +80 -0
  80. package/src/widgets/patterns/Toolbar.tsx +72 -0
  81. package/src/widgets/patterns/Tree.tsx +72 -0
  82. package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
  83. package/src/widgets/primitives/Button.tsx +62 -0
  84. package/src/widgets/primitives/ColorInput.tsx +78 -0
  85. package/src/widgets/primitives/DraftTextInput.tsx +63 -0
  86. package/src/widgets/primitives/EditorIcon.tsx +157 -0
  87. package/src/widgets/primitives/FormControls.tsx +88 -0
  88. package/src/widgets/primitives/HoverPreview.tsx +96 -0
  89. package/src/widgets/primitives/JsonInput.tsx +113 -0
  90. package/src/widgets/primitives/Layout.tsx +100 -0
  91. package/src/widgets/primitives/Menu.tsx +140 -0
  92. package/src/widgets/primitives/NumberInput.tsx +169 -0
  93. package/src/widgets/primitives/Panel.tsx +80 -0
  94. package/src/widgets/primitives/SectionHeader.tsx +77 -0
  95. package/src/widgets/primitives/Text.tsx +54 -0
  96. package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
  97. package/src/widgets/primitives/Tooltip.tsx +204 -0
  98. package/src/widgets/primitives/Vec3Input.tsx +70 -0
  99. package/src/widgets/primitives/banner-tones.ts +32 -0
  100. package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
  101. package/src/widgets/primitives/editor-icons.ts +245 -0
  102. package/src/widgets/primitives/panel-header-styles.ts +42 -0
  103. package/src/widgets/theme.ts +2633 -0
  104. 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
+ }
@@ -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
+ };