@vgai/editor-sdk 0.4.1 → 0.5.0-canary.20260719.0
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/README.md +77 -9
- package/package.json +11 -2
- package/src/client.ts +89 -1
- package/src/contributions.ts +158 -0
- package/src/editor-view.ts +223 -0
- package/src/extension.ts +109 -0
- package/src/index.ts +24 -0
- package/src/types.ts +116 -36
package/README.md
CHANGED
|
@@ -12,7 +12,18 @@ to the caller.
|
|
|
12
12
|
import { EditorClient } from '@vgai/editor-sdk';
|
|
13
13
|
|
|
14
14
|
const editor = new EditorClient(); // default http://localhost:5173
|
|
15
|
+
const projectEditor = new EditorClient({ url: 'http://localhost:25786' });
|
|
15
16
|
await editor.play();
|
|
17
|
+
|
|
18
|
+
// Present the same editor subject/view to the connected human and get a
|
|
19
|
+
// compact share URL. This does not serialize document contents or layout.
|
|
20
|
+
const shown = await editor.present({
|
|
21
|
+
version: 1,
|
|
22
|
+
document: { kind: 'tool', id: 'walking-castle-builder' },
|
|
23
|
+
viewport: { camera: 'isometric', frame: 'document' },
|
|
24
|
+
utility: 'profiler',
|
|
25
|
+
});
|
|
26
|
+
console.log(shown.url);
|
|
16
27
|
await editor.waitForState((s) => s.playState === 'playing');
|
|
17
28
|
const entries = await editor.getLogEntries(); // proves frames actually ran
|
|
18
29
|
```
|
|
@@ -22,14 +33,15 @@ const entries = await editor.getLogEntries(); // proves frames actually ran
|
|
|
22
33
|
One export, `EditorClient`, plus its types (`EditorState`, `ProjectInfo`,
|
|
23
34
|
`AssetKind`, `ShadingMode`, ...). Methods, by group:
|
|
24
35
|
|
|
25
|
-
- Play control: `play`, `stop`, `pause`, `resume`, `step`
|
|
36
|
+
- Play control: `play`, `restart`, `stop`, `pause`, `resume`, `step`
|
|
26
37
|
- Selection: `select(id | null)`, `selectMultiple`, `selectAll`
|
|
27
38
|
- Viewport: `focusEntity`, `focusSelection`, `viewPreset`, `setCamera`,
|
|
28
39
|
`captureViewport`
|
|
29
40
|
- Asset preview: `captureAssetPreview` for deterministic front, right, top,
|
|
30
41
|
and three-quarter captures of a project model or authored entity hierarchy
|
|
31
42
|
- Panels: `showViewport('scene'|'game')`, `showInspector`, `openAsset`,
|
|
32
|
-
`closeAsset`, `toggleConsole`, `toggleCommandPalette`, `showBuild
|
|
43
|
+
`closeAsset`, `toggleConsole`, `toggleCommandPalette`, `showBuild`, and
|
|
44
|
+
`present(EditorView)` for an atomic human-visible view plus share URL
|
|
33
45
|
- Display: `setGrid`, `setHelpers`, `setStats`, `setShadingMode` (`solid`,
|
|
34
46
|
`unlit`, `wireframe`, `normals`, or `overdraw`), `setHelperType` (including
|
|
35
47
|
the independent `bounds` category). Shading targets the active Scene/Game
|
|
@@ -54,11 +66,12 @@ registered tool and an already-configured client:
|
|
|
54
66
|
import { Button } from '@editor/widgets';
|
|
55
67
|
import type { ToolContributionProps } from '@vgai/editor-sdk/contributions';
|
|
56
68
|
|
|
57
|
-
export default function MapBuilder({ tool, client }: ToolContributionProps) {
|
|
69
|
+
export default function MapBuilder({ tool, client, account }: ToolContributionProps) {
|
|
58
70
|
return (
|
|
59
71
|
<Button
|
|
60
72
|
variant="solid"
|
|
61
73
|
onClick={() => void client.runProjectTool(tool.name, { seed: 42 }, { confirm: true })}
|
|
74
|
+
title={`Default execution: ${account.preferredRoute}`}
|
|
62
75
|
>
|
|
63
76
|
Build map
|
|
64
77
|
</Button>
|
|
@@ -66,16 +79,71 @@ export default function MapBuilder({ tool, client }: ToolContributionProps) {
|
|
|
66
79
|
}
|
|
67
80
|
```
|
|
68
81
|
|
|
69
|
-
The package registration chooses `workspace.document`, `workspace.utility`,
|
|
70
|
-
`selection.inspector
|
|
71
|
-
`node`/`nodeId` and export
|
|
72
|
-
|
|
82
|
+
The package registration chooses `workspace.document`, `workspace.utility`,
|
|
83
|
+
`selection.inspector`, `asset.inspector`, or `generation.result`. Selection
|
|
84
|
+
Inspector contributions receive `node`/`nodeId` and export
|
|
85
|
+
`match(node, adapter)`; asset Inspector contributions receive `asset` and
|
|
86
|
+
export `match(asset)`. A generation-result contribution receives the durable
|
|
87
|
+
job and raw native poll result inside the editor-owned result document and
|
|
88
|
+
must export `match(job, result)`; registration order never selects a renderer.
|
|
89
|
+
All contributions receive `account`, a validated global projection containing
|
|
90
|
+
plan, credits, spend policy, and provider-specific route availability. It never
|
|
91
|
+
contains an access token. There is no extension class, lifecycle, or
|
|
92
|
+
proprietary UI description.
|
|
73
93
|
|
|
74
94
|
Use ordinary React/CSS for composition and `@editor/widgets` for controls.
|
|
75
95
|
Shape the UI for its contribution point: a bounded workspace for documents, a
|
|
76
96
|
dense single column for inspector sections, and a compact row/status block for
|
|
77
97
|
utilities. Badges are for short statuses and counts, not headings.
|
|
78
98
|
|
|
99
|
+
### Extension contract (`@vgai/editor-sdk/extension`)
|
|
100
|
+
|
|
101
|
+
The typed contract for everything a game project contributes TO the editor —
|
|
102
|
+
four surfaces, one degradation ladder (`ExtensionContributionTier`:
|
|
103
|
+
`active` / `absent` / `failed`). An absent contribution hides its surface
|
|
104
|
+
(the editor never fabricates placeholder data); a failing one is contained
|
|
105
|
+
per-contribution and reported loudly on the editor console — never a crashed
|
|
106
|
+
editor, never silent fake output.
|
|
107
|
+
|
|
108
|
+
1. **Editor panels** — `workspace.document` / `workspace.utility` tool
|
|
109
|
+
contributions (previous section). Panels join the Dockview workspace;
|
|
110
|
+
there is no parallel rail.
|
|
111
|
+
2. **Inspector sections** — `selection.inspector` / `asset.inspector` tool
|
|
112
|
+
contributions with an exported `match`.
|
|
113
|
+
3. **Per-component viewport gizmos** — a `GameComponent` subclass declares
|
|
114
|
+
`static editorGizmo: ComponentGizmoHook` (the Unity `OnDrawGizmos`
|
|
115
|
+
analog). The editor renders its objects for every scene entity carrying
|
|
116
|
+
that component — editor-only layer, Helpers ▸ Components toggle,
|
|
117
|
+
raycast-excluded, draw-only. Props arrive schema-parsed through the
|
|
118
|
+
component's own Zod `static schema`; build objects with `ctx.three` (the
|
|
119
|
+
editor's three module):
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import type { ComponentGizmoHook } from '@vgai/editor-sdk/extension';
|
|
123
|
+
|
|
124
|
+
export class TriggerZone extends GameComponent {
|
|
125
|
+
static schema = z.object({ radius: z.number().default(2) });
|
|
126
|
+
static editorGizmo: ComponentGizmoHook = ({ props, three }) => {
|
|
127
|
+
const geo = new three.RingGeometry(0.98 * (props.radius as number), props.radius as number);
|
|
128
|
+
return new three.Mesh(geo, new three.MeshBasicMaterial({ wireframe: true }));
|
|
129
|
+
};
|
|
130
|
+
update() {}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The scaffold template's `SceneCamera` (`src/scripts/components/
|
|
135
|
+
scene-camera.ts`) is the worked reference.
|
|
136
|
+
|
|
137
|
+
Editing a component file in edit mode re-captures the registry and
|
|
138
|
+
redraws contributed gizmos (script-HMR); registry restructures may need a
|
|
139
|
+
project reopen. Recorded deferral: in the HOSTED (browser) editor,
|
|
140
|
+
contributed component gizmos resolve `absent` — example registries are
|
|
141
|
+
evaluated for play but not captured at design time.
|
|
142
|
+
4. **System adapters** — runtime capabilities registered from game code via
|
|
143
|
+
`ctx.registerSystemAdapter?.(kind, impl)` (`SystemAdapters` in
|
|
144
|
+
`@vgai/engine`). Deliberately not re-exported here: the engine already
|
|
145
|
+
publishes that seam and every consumer of it also imports the engine.
|
|
146
|
+
|
|
79
147
|
### Asset Lab capture
|
|
80
148
|
|
|
81
149
|
`captureAssetPreview` accepts exactly one source: an authored entity hierarchy
|
|
@@ -100,12 +168,12 @@ buffers and images must resolve on the same project origin. The thin CLI
|
|
|
100
168
|
equivalent writes the four views and contact sheet to disk:
|
|
101
169
|
|
|
102
170
|
```bash
|
|
103
|
-
vgai asset-preview --asset /models/storefront.glb --out artifacts/storefront
|
|
171
|
+
npm run vgai -- asset-preview --asset /models/storefront.glb --out artifacts/storefront
|
|
104
172
|
```
|
|
105
173
|
|
|
106
174
|
## Limitations
|
|
107
175
|
|
|
108
176
|
- The published package exports raw TypeScript (`exports: "./src/index.ts"`),
|
|
109
177
|
so consumers need a TypeScript-aware runner/bundler such as tsx or Vite.
|
|
110
|
-
- Requires the editor dev server (`npm run dev` or `vgai edit
|
|
178
|
+
- Requires the editor dev server (`npm run dev` or `npx @vgai/cli@latest edit <project>`); there is no
|
|
111
179
|
offline mode.
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@vgai/editor-sdk",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.5.0-canary.20260719.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -17,6 +17,15 @@
|
|
|
17
17
|
],
|
|
18
18
|
"exports": {
|
|
19
19
|
".": "./src/index.ts",
|
|
20
|
-
"./contributions": "./src/contributions.ts"
|
|
20
|
+
"./contributions": "./src/contributions.ts",
|
|
21
|
+
"./extension": "./src/extension.ts"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@types/three": "^0.180.0",
|
|
25
|
+
"@vgai/sdk": "0.5.0-canary.20260719.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": "^19.0.0",
|
|
29
|
+
"three": "^0.180.0"
|
|
21
30
|
}
|
|
22
31
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import type { GenerationJobsDocument } from '@vgai/sdk/generations';
|
|
1
2
|
import type {
|
|
3
|
+
ActiveDocumentCapture,
|
|
2
4
|
AssetKind,
|
|
3
5
|
AssetPreviewCapture,
|
|
4
6
|
AssetPreviewOptions,
|
|
5
7
|
AssetPreviewSource,
|
|
6
8
|
EditorState,
|
|
9
|
+
EditorView,
|
|
7
10
|
GameCapture,
|
|
8
11
|
HelperVisibility,
|
|
12
|
+
HumanoidShotSetCapture,
|
|
13
|
+
PresentedEditorView,
|
|
9
14
|
ProjectInfo,
|
|
10
15
|
ProjectTemplate,
|
|
11
16
|
ProjectToolCatalog,
|
|
@@ -25,6 +30,17 @@ export class EditorClient {
|
|
|
25
30
|
private readonly baseUrl: string;
|
|
26
31
|
|
|
27
32
|
constructor(opts?: { url?: string }) {
|
|
33
|
+
if (opts !== undefined && (typeof opts !== 'object' || opts === null || Array.isArray(opts))) {
|
|
34
|
+
throw new TypeError(
|
|
35
|
+
'EditorClient options must be an object. Use new EditorClient({ url: "http://localhost:5173" }), not new EditorClient("...").',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const unknownOptions = Object.keys(opts ?? {}).filter((key) => key !== 'url');
|
|
39
|
+
if (unknownOptions.length > 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`EditorClient: unknown option${unknownOptions.length === 1 ? '' : 's'} ${unknownOptions.map((key) => `"${key}"`).join(', ')}. Use { url: "http://localhost:<port>" } to target an editor.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
28
44
|
this.baseUrl = (opts?.url ?? DEFAULT_URL).replace(/\/$/, '');
|
|
29
45
|
}
|
|
30
46
|
|
|
@@ -54,6 +70,11 @@ export class EditorClient {
|
|
|
54
70
|
await this.command({ type: 'play', ...(opts?.seed !== undefined ? { seed: opts.seed } : {}) });
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
/** Dispose the current play session and mount it again from fresh project entry source. */
|
|
74
|
+
async restart(): Promise<void> {
|
|
75
|
+
await this.command({ type: 'play' });
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
async stop(): Promise<void> {
|
|
58
79
|
await this.command({ type: 'stop' });
|
|
59
80
|
}
|
|
@@ -118,7 +139,7 @@ export class EditorClient {
|
|
|
118
139
|
/**
|
|
119
140
|
* Unit 4 (live-front-door wave) — capture the RUNNING GAME (`vgai
|
|
120
141
|
* screenshot`'s wire leg). Sends the SAME `bridge-screenshot` relay op
|
|
121
|
-
* `@vgai/
|
|
142
|
+
* `@vgai/e2e`'s `RelayTransport.screenshot` (and therefore
|
|
122
143
|
* `game.screenshot()` on the relay path) already sends, so all three
|
|
123
144
|
* surfaces composite the identical full game stack — canvas(es) plus the
|
|
124
145
|
* HUD/react DOM layers — rather than any of them inventing a second,
|
|
@@ -166,6 +187,33 @@ export class EditorClient {
|
|
|
166
187
|
};
|
|
167
188
|
}
|
|
168
189
|
|
|
190
|
+
/**
|
|
191
|
+
* B7.6 — the canonical humanoid verify shot set (`vgai asset-preview
|
|
192
|
+
* --shots humanoid`): four turntable angles, tight zooms on the
|
|
193
|
+
* historical seam sites (head/hands/feet/shoulder), and a deep-bend pose
|
|
194
|
+
* pair, all framed by the editor from the loaded GLB's OWN skeleton — see
|
|
195
|
+
* `packages/editor/src/asset-preview.ts`'s `captureHumanoidShotSetAssetPreview`.
|
|
196
|
+
* Throws (via `command`'s `{ok:false}` unwrap) with a clear message naming
|
|
197
|
+
* the missing joint(s) when the asset has no Mixamo-named skeleton.
|
|
198
|
+
*/
|
|
199
|
+
async captureHumanoidShotSetPreview(
|
|
200
|
+
source: AssetPreviewSource,
|
|
201
|
+
options: AssetPreviewOptions = {},
|
|
202
|
+
): Promise<HumanoidShotSetCapture> {
|
|
203
|
+
const data = await this.command<HumanoidShotSetCapture>({
|
|
204
|
+
type: 'capture-asset-preview',
|
|
205
|
+
...source,
|
|
206
|
+
...options,
|
|
207
|
+
shots: 'humanoid',
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
width: data.width,
|
|
211
|
+
height: data.height,
|
|
212
|
+
shots: data.shots,
|
|
213
|
+
contactSheet: data.contactSheet,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
169
217
|
// --- Panels ---
|
|
170
218
|
|
|
171
219
|
async showViewport(tab: 'scene' | 'game'): Promise<void> {
|
|
@@ -196,6 +244,26 @@ export class EditorClient {
|
|
|
196
244
|
await this.command({ type: 'show-build' });
|
|
197
245
|
}
|
|
198
246
|
|
|
247
|
+
/** Atomically present a durable editor view and return its shareable URL. */
|
|
248
|
+
async present(view: EditorView): Promise<PresentedEditorView> {
|
|
249
|
+
const presented = await this.command<PresentedEditorView>({ type: 'present-view', view });
|
|
250
|
+
return { view: presented.view, url: presented.url, warnings: presented.warnings };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Read the editor's actual current durable projection. */
|
|
254
|
+
async currentView(): Promise<EditorView> {
|
|
255
|
+
const data = await this.command<{ view: EditorView }>({ type: 'current-view' });
|
|
256
|
+
return data.view;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Capture the active center document exactly as presented to the user. */
|
|
260
|
+
async captureActiveDocument(size?: number): Promise<ActiveDocumentCapture> {
|
|
261
|
+
return this.command<ActiveDocumentCapture>({
|
|
262
|
+
type: 'capture-active-document',
|
|
263
|
+
...(size === undefined ? {} : { size }),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
199
267
|
// --- Display (set semantics) ---
|
|
200
268
|
|
|
201
269
|
async setGrid(enabled: boolean): Promise<void> {
|
|
@@ -315,6 +383,26 @@ export class EditorClient {
|
|
|
315
383
|
return body;
|
|
316
384
|
}
|
|
317
385
|
|
|
386
|
+
// --- First-party generation job activity ---
|
|
387
|
+
|
|
388
|
+
/** Read the one project-local generation job ledger. Provider-native
|
|
389
|
+
* request/result shapes remain on their registered operations. */
|
|
390
|
+
async listGenerationJobs(): Promise<GenerationJobsDocument> {
|
|
391
|
+
const res = await fetch(`${this.baseUrl}/__editor/generations`);
|
|
392
|
+
if (!res.ok) throw new Error(`Failed to list generation jobs: ${res.status}`);
|
|
393
|
+
return (await res.json()) as GenerationJobsDocument;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Forget operational job state. Accepted provenance and project assets
|
|
397
|
+
* are deliberately unaffected. */
|
|
398
|
+
async forgetGenerationJob(id: string): Promise<boolean> {
|
|
399
|
+
const res = await fetch(`${this.baseUrl}/__editor/generations/${encodeURIComponent(id)}`, {
|
|
400
|
+
method: 'DELETE',
|
|
401
|
+
});
|
|
402
|
+
if (!res.ok) throw new Error(`Failed to forget generation job: ${res.status}`);
|
|
403
|
+
return ((await res.json()) as { removed: boolean }).removed;
|
|
404
|
+
}
|
|
405
|
+
|
|
318
406
|
// --- Logs ---
|
|
319
407
|
|
|
320
408
|
async getLogEntries(): Promise<
|
package/src/contributions.ts
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
* metadata names a module and the editor renders its default export.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { GenerationAccountProjection } from '@vgai/sdk/account';
|
|
8
|
+
import type { GenerationJob } from '@vgai/sdk/generations';
|
|
9
|
+
import type {
|
|
10
|
+
AnimationClip,
|
|
11
|
+
Camera,
|
|
12
|
+
ColorRepresentation,
|
|
13
|
+
Object3D,
|
|
14
|
+
Scene,
|
|
15
|
+
WebGLRenderer,
|
|
16
|
+
} from 'three';
|
|
7
17
|
import type { EditorClient } from './client.js';
|
|
8
18
|
import type { ProjectToolCatalogEntry } from './types.js';
|
|
9
19
|
|
|
@@ -19,11 +29,122 @@ export interface ToolContributionNode {
|
|
|
19
29
|
readonly flags: object;
|
|
20
30
|
}
|
|
21
31
|
|
|
32
|
+
/**
|
|
33
|
+
* One disposable native Three.js build supplied to the editor's generic
|
|
34
|
+
* preview surface. This is a host-lifetime boundary, not a model format or
|
|
35
|
+
* procedural-asset base class: project code still constructs ordinary
|
|
36
|
+
* Object3D and AnimationClip instances directly.
|
|
37
|
+
*/
|
|
38
|
+
export interface ToolObject3DPreviewSource {
|
|
39
|
+
readonly root: Object3D;
|
|
40
|
+
readonly animations?: readonly AnimationClip[];
|
|
41
|
+
dispose(): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Direct native handles for optional project-owned preview presentation. */
|
|
45
|
+
export interface ToolObject3DPreviewContext {
|
|
46
|
+
readonly scene: Scene;
|
|
47
|
+
readonly camera: Camera;
|
|
48
|
+
readonly renderer: WebGLRenderer;
|
|
49
|
+
/**
|
|
50
|
+
* A cloned Object3D graph, never the source root itself. Geometry,
|
|
51
|
+
* materials, and textures are shared read-only with the disposable source;
|
|
52
|
+
* clone a resource before applying preview-only mutations to it.
|
|
53
|
+
*/
|
|
54
|
+
readonly model: Object3D;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ToolObject3DPreviewExtension {
|
|
58
|
+
/** When present, replaces the viewer's default render call for this frame. */
|
|
59
|
+
render?(deltaSeconds: number): void;
|
|
60
|
+
/** Resize project-owned composers and render targets with the host viewport. */
|
|
61
|
+
resize?(width: number, height: number, pixelRatio: number): void;
|
|
62
|
+
/** Release every light, ground mesh, pass, target, and listener added here. */
|
|
63
|
+
dispose(): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ToolObject3DPreviewProps {
|
|
67
|
+
/** Build a fresh source. The host disposes it after its preview snapshot. */
|
|
68
|
+
readonly build: () => ToolObject3DPreviewSource;
|
|
69
|
+
readonly displayName?: string;
|
|
70
|
+
readonly height?: number | string;
|
|
71
|
+
readonly background?: ColorRepresentation;
|
|
72
|
+
readonly showSkeleton?: boolean;
|
|
73
|
+
readonly autoplay?: boolean;
|
|
74
|
+
readonly cameraDirection?: readonly [number, number, number];
|
|
75
|
+
/** Host diagnostic grid. Defaults off with setupPreview, on otherwise. */
|
|
76
|
+
readonly showGrid?: boolean;
|
|
77
|
+
/** Host studio lights. Defaults off with setupPreview, on otherwise. */
|
|
78
|
+
readonly useDefaultLighting?: boolean;
|
|
79
|
+
/** Explicit renderer exposure. Custom presentation otherwise owns it. */
|
|
80
|
+
readonly exposure?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Optional raw Three.js presentation hook for project-specific lights,
|
|
83
|
+
* ground, or postprocessing. Host grid/studio lights default off when this
|
|
84
|
+
* is present. It is preview chrome and is never exported.
|
|
85
|
+
*/
|
|
86
|
+
readonly setupPreview?: (
|
|
87
|
+
context: ToolObject3DPreviewContext,
|
|
88
|
+
) => ToolObject3DPreviewExtension | undefined;
|
|
89
|
+
readonly active?: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A native source graph hosted by the editor's normal Three.js authoring
|
|
94
|
+
* viewport, hierarchy, selection, and Inspector. Unlike Object3DPreview this
|
|
95
|
+
* surface exposes the live graph as the active workspace document context.
|
|
96
|
+
*/
|
|
97
|
+
export interface ToolObject3DAuthoringProps {
|
|
98
|
+
readonly documentId: string;
|
|
99
|
+
readonly sourcePath: string;
|
|
100
|
+
readonly build: () => ToolObject3DPreviewSource;
|
|
101
|
+
readonly displayName?: string;
|
|
102
|
+
readonly background?: ColorRepresentation;
|
|
103
|
+
readonly cameraDirection?: readonly [number, number, number];
|
|
104
|
+
readonly autoplay?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Optional binding from live native clips back to ordinary project source.
|
|
107
|
+
* Project code only serializes its own TypeScript shape; the editor owns the
|
|
108
|
+
* checksum-guarded write and records it in the canonical project history.
|
|
109
|
+
* Without this binding the animation workspace remains honestly read-only.
|
|
110
|
+
*/
|
|
111
|
+
readonly animationSource?: ToolAnimationSourceBinding;
|
|
112
|
+
readonly active?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ToolAnimationSourceBinding {
|
|
116
|
+
/** Project-relative TypeScript file below src/. */
|
|
117
|
+
readonly path: string;
|
|
118
|
+
/** Undo/redo label shown by the editor history. */
|
|
119
|
+
readonly label?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Serialize the same native clips returned by build into project-owned source.
|
|
122
|
+
* The source loader must accept replacement typed arrays when key counts change.
|
|
123
|
+
*/
|
|
124
|
+
readonly serialize: (animations: readonly AnimationClip[]) => string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Editor-owned native surfaces available to ordinary project React tools. */
|
|
128
|
+
export interface ToolContributionSurfaces {
|
|
129
|
+
readonly Object3DPreview: import('react').ComponentType<ToolObject3DPreviewProps>;
|
|
130
|
+
readonly Object3DAuthoring: import('react').ComponentType<ToolObject3DAuthoringProps>;
|
|
131
|
+
}
|
|
132
|
+
|
|
22
133
|
export interface ToolContributionProps {
|
|
23
134
|
/** The exact registered callable this contribution presents. */
|
|
24
135
|
readonly tool: ProjectToolCatalogEntry;
|
|
136
|
+
/** Stable id of this presentation within the registered callable. */
|
|
137
|
+
readonly contributionId?: string;
|
|
25
138
|
/** Direct editor SDK client; invoke with `client.runProjectTool(tool.name, ...)`. */
|
|
26
139
|
readonly client: EditorClient;
|
|
140
|
+
/** Generic editor-owned presentation surfaces; project source remains native. */
|
|
141
|
+
readonly surfaces: ToolContributionSurfaces;
|
|
142
|
+
/** Sanitized product-account projection. Never contains an access token. */
|
|
143
|
+
readonly account: GenerationAccountProjection;
|
|
144
|
+
/** Present when mounted as a workspace document contribution. */
|
|
145
|
+
readonly documentId?: string;
|
|
146
|
+
/** Whether that workspace document is the active center subject. */
|
|
147
|
+
readonly active?: boolean;
|
|
27
148
|
}
|
|
28
149
|
|
|
29
150
|
export interface ToolInspectorContributionProps extends ToolContributionProps {
|
|
@@ -31,6 +152,43 @@ export interface ToolInspectorContributionProps extends ToolContributionProps {
|
|
|
31
152
|
readonly nodeId: string | null;
|
|
32
153
|
}
|
|
33
154
|
|
|
155
|
+
/** Stable editor projection of one selected project or external-library asset. */
|
|
156
|
+
export interface ToolContributionAsset {
|
|
157
|
+
readonly path: string;
|
|
158
|
+
readonly name: string;
|
|
159
|
+
readonly kind: string;
|
|
160
|
+
readonly origin: 'project' | 'library';
|
|
161
|
+
readonly sourcePath?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface ToolAssetInspectorContributionProps extends ToolContributionProps {
|
|
165
|
+
readonly asset: ToolContributionAsset | null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Optional named export required by `asset.inspector` contributions. */
|
|
169
|
+
export type ToolAssetInspectorContributionMatch = (asset: ToolContributionAsset | null) => boolean;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Provider-owned presentation mounted inside the editor-owned generation
|
|
173
|
+
* result document. The host owns job state, billing, and acceptance; this
|
|
174
|
+
* component only interprets the native poll result.
|
|
175
|
+
*/
|
|
176
|
+
export interface ToolGenerationResultContributionProps extends ToolContributionProps {
|
|
177
|
+
readonly job: GenerationJob;
|
|
178
|
+
readonly result: unknown;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Required named export for `generation.result` contributions. A single poll
|
|
183
|
+
* callable may serve many native operations, so presentation selection is
|
|
184
|
+
* based on the durable job and the provider's unmodified poll result rather
|
|
185
|
+
* than registration order or a host-owned media taxonomy.
|
|
186
|
+
*/
|
|
187
|
+
export type ToolGenerationResultContributionMatch = (
|
|
188
|
+
job: GenerationJob,
|
|
189
|
+
result: unknown,
|
|
190
|
+
) => boolean;
|
|
191
|
+
|
|
34
192
|
/** Optional named export required by `selection.inspector` contributions. */
|
|
35
193
|
export type ToolInspectorContributionMatch = (
|
|
36
194
|
node: ToolContributionNode | null,
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { AssetKind, EditorCameraState, EditorView, ViewPreset } from './types.js';
|
|
2
|
+
|
|
3
|
+
const PREFIX = 'view.';
|
|
4
|
+
const MAX_QUERY_LENGTH = 4096;
|
|
5
|
+
const ASSET_KINDS = new Set<AssetKind>([
|
|
6
|
+
'model',
|
|
7
|
+
'image',
|
|
8
|
+
'audio',
|
|
9
|
+
'animation',
|
|
10
|
+
'json',
|
|
11
|
+
'scene',
|
|
12
|
+
'prefab',
|
|
13
|
+
'material',
|
|
14
|
+
]);
|
|
15
|
+
const CAMERAS = new Set<ViewPreset | 'isometric'>([
|
|
16
|
+
'top',
|
|
17
|
+
'front',
|
|
18
|
+
'right',
|
|
19
|
+
'perspective',
|
|
20
|
+
'isometric',
|
|
21
|
+
]);
|
|
22
|
+
type EditorViewDiagnostic = NonNullable<NonNullable<EditorView['viewport']>['diagnostic']>;
|
|
23
|
+
type EditorViewUtility = NonNullable<EditorView['utility']>;
|
|
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
|
+
const UTILITIES = new Set<EditorViewUtility>(['profiler', 'console', 'animation', 'debugger']);
|
|
36
|
+
|
|
37
|
+
function nonEmpty(value: string | null): string | null {
|
|
38
|
+
const trimmed = value?.trim();
|
|
39
|
+
return trimmed ? trimmed : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeDocument(params: URLSearchParams, document: EditorView['document']): void {
|
|
43
|
+
if (!document) return;
|
|
44
|
+
params.set(`${PREFIX}docKind`, document.kind);
|
|
45
|
+
if (document.kind === 'asset' && document.entityId) {
|
|
46
|
+
params.set(`${PREFIX}doc`, document.entityId);
|
|
47
|
+
params.set(`${PREFIX}assetSource`, 'entity');
|
|
48
|
+
} else if (document.kind === 'asset') {
|
|
49
|
+
params.set(`${PREFIX}doc`, document.path!);
|
|
50
|
+
} else if (document.kind === 'scene' || document.kind === 'data') {
|
|
51
|
+
params.set(`${PREFIX}doc`, document.path);
|
|
52
|
+
} else if (document.kind === 'story') {
|
|
53
|
+
params.set(`${PREFIX}doc`, document.modulePath);
|
|
54
|
+
params.set(`${PREFIX}story`, document.storyName);
|
|
55
|
+
if (document.mode === 'docs') params.set(`${PREFIX}storyMode`, 'docs');
|
|
56
|
+
} else if (document.kind === 'project-tool') {
|
|
57
|
+
params.set(`${PREFIX}doc`, document.name);
|
|
58
|
+
} else {
|
|
59
|
+
params.set(`${PREFIX}doc`, document.id);
|
|
60
|
+
}
|
|
61
|
+
if (document.kind === 'asset' && document.assetKind) {
|
|
62
|
+
params.set(`${PREFIX}assetKind`, document.assetKind);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function writeViewport(params: URLSearchParams, viewport: EditorView['viewport']): void {
|
|
67
|
+
if (!viewport) return;
|
|
68
|
+
if (typeof viewport.camera === 'string') params.set(`${PREFIX}camera`, viewport.camera);
|
|
69
|
+
else if (viewport.camera) {
|
|
70
|
+
params.set(`${PREFIX}camera`, 'pose');
|
|
71
|
+
params.set(
|
|
72
|
+
`${PREFIX}cameraPosition`,
|
|
73
|
+
[viewport.camera.position.x, viewport.camera.position.y, viewport.camera.position.z].join(
|
|
74
|
+
',',
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
params.set(
|
|
78
|
+
`${PREFIX}cameraTarget`,
|
|
79
|
+
[viewport.camera.target.x, viewport.camera.target.y, viewport.camera.target.z].join(','),
|
|
80
|
+
);
|
|
81
|
+
if (viewport.camera.fov !== undefined)
|
|
82
|
+
params.set(`${PREFIX}cameraFov`, String(viewport.camera.fov));
|
|
83
|
+
}
|
|
84
|
+
if (viewport.diagnostic) params.set(`${PREFIX}diagnostic`, viewport.diagnostic);
|
|
85
|
+
if (viewport.frame) params.set(`${PREFIX}frame`, viewport.frame);
|
|
86
|
+
if (viewport.grid !== undefined) params.set(`${PREFIX}grid`, viewport.grid ? '1' : '0');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseVec3(value: string | null): EditorCameraState['position'] | null {
|
|
90
|
+
if (!value) return null;
|
|
91
|
+
const parts = value.split(',').map(Number);
|
|
92
|
+
if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) return null;
|
|
93
|
+
return { x: parts[0]!, y: parts[1]!, z: parts[2]! };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseCamera(params: URLSearchParams): NonNullable<EditorView['viewport']>['camera'] {
|
|
97
|
+
const camera = params.get(`${PREFIX}camera`);
|
|
98
|
+
if (camera && CAMERAS.has(camera as ViewPreset | 'isometric')) {
|
|
99
|
+
return camera as ViewPreset | 'isometric';
|
|
100
|
+
}
|
|
101
|
+
if (camera !== 'pose') return undefined;
|
|
102
|
+
const position = parseVec3(params.get(`${PREFIX}cameraPosition`));
|
|
103
|
+
const target = parseVec3(params.get(`${PREFIX}cameraTarget`));
|
|
104
|
+
const fovValue = params.get(`${PREFIX}cameraFov`);
|
|
105
|
+
const fov = fovValue === null ? undefined : Number(fovValue);
|
|
106
|
+
if (!position || !target || (fov !== undefined && !Number.isFinite(fov))) return undefined;
|
|
107
|
+
return { position, target, ...(fov !== undefined ? { fov } : {}) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseStoryDocument(params: URLSearchParams, modulePath: string): EditorView['document'] {
|
|
111
|
+
const storyName = nonEmpty(params.get(`${PREFIX}story`));
|
|
112
|
+
if (!storyName) return undefined;
|
|
113
|
+
return {
|
|
114
|
+
kind: 'story',
|
|
115
|
+
modulePath,
|
|
116
|
+
storyName,
|
|
117
|
+
...(params.get(`${PREFIX}storyMode`) === 'docs' ? { mode: 'docs' as const } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseAssetDocument(params: URLSearchParams, value: string): EditorView['document'] {
|
|
122
|
+
const assetKind = params.get(`${PREFIX}assetKind`);
|
|
123
|
+
if (params.get(`${PREFIX}assetSource`) === 'entity') {
|
|
124
|
+
return { kind: 'asset', entityId: value, assetKind: 'model' };
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
kind: 'asset',
|
|
128
|
+
path: value,
|
|
129
|
+
...(assetKind && ASSET_KINDS.has(assetKind as AssetKind)
|
|
130
|
+
? { assetKind: assetKind as AssetKind }
|
|
131
|
+
: {}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseDocument(params: URLSearchParams): EditorView['document'] {
|
|
136
|
+
const kind = params.get(`${PREFIX}docKind`);
|
|
137
|
+
const value = nonEmpty(params.get(`${PREFIX}doc`));
|
|
138
|
+
if (!value) return undefined;
|
|
139
|
+
if (kind === 'scene') return { kind, path: value };
|
|
140
|
+
if (kind === 'tool') return { kind, id: value };
|
|
141
|
+
if (kind === 'world') return { kind, id: value };
|
|
142
|
+
if (kind === 'data') return { kind, path: value };
|
|
143
|
+
if (kind === 'project-tool') return { kind, name: value };
|
|
144
|
+
if (kind === 'generation') return { kind, id: value };
|
|
145
|
+
if (kind === 'story') return parseStoryDocument(params, value);
|
|
146
|
+
if (
|
|
147
|
+
kind === 'workspace' &&
|
|
148
|
+
[
|
|
149
|
+
'workspace:scene',
|
|
150
|
+
'workspace:game',
|
|
151
|
+
'workspace:data-assets',
|
|
152
|
+
'workspace:build-profiles',
|
|
153
|
+
'account',
|
|
154
|
+
'project-tools',
|
|
155
|
+
].includes(value)
|
|
156
|
+
) {
|
|
157
|
+
return {
|
|
158
|
+
kind,
|
|
159
|
+
id: value as Extract<NonNullable<EditorView['document']>, { kind: 'workspace' }>['id'],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return kind === 'asset' ? parseAssetDocument(params, value) : undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseViewport(params: URLSearchParams): EditorView['viewport'] {
|
|
166
|
+
const camera = parseCamera(params);
|
|
167
|
+
const diagnostic = params.get(`${PREFIX}diagnostic`);
|
|
168
|
+
const frame = params.get(`${PREFIX}frame`);
|
|
169
|
+
const grid = params.get(`${PREFIX}grid`);
|
|
170
|
+
const viewport = {
|
|
171
|
+
...(camera ? { camera } : {}),
|
|
172
|
+
...(diagnostic && DIAGNOSTICS.has(diagnostic as EditorViewDiagnostic)
|
|
173
|
+
? { diagnostic: diagnostic as EditorViewDiagnostic }
|
|
174
|
+
: {}),
|
|
175
|
+
...(frame === 'document' || frame === 'selection'
|
|
176
|
+
? { frame: frame as 'document' | 'selection' }
|
|
177
|
+
: {}),
|
|
178
|
+
...(grid === '0' || grid === '1' ? { grid: grid === '1' } : {}),
|
|
179
|
+
};
|
|
180
|
+
return Object.keys(viewport).length > 0 ? viewport : undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Apply only the view-owned query parameters, preserving project/scene routing params. */
|
|
184
|
+
export function editorViewUrl(view: EditorView, baseUrl: string | URL): string {
|
|
185
|
+
const url = new URL(baseUrl);
|
|
186
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
187
|
+
if (key.startsWith(PREFIX)) url.searchParams.delete(key);
|
|
188
|
+
}
|
|
189
|
+
url.searchParams.set(`${PREFIX}v`, '1');
|
|
190
|
+
writeDocument(url.searchParams, view.document);
|
|
191
|
+
if (view.selection?.ids.length) {
|
|
192
|
+
for (const id of view.selection.ids) url.searchParams.append(`${PREFIX}select`, id);
|
|
193
|
+
if (view.selection.focus) url.searchParams.set(`${PREFIX}focus`, '1');
|
|
194
|
+
}
|
|
195
|
+
writeViewport(url.searchParams, view.viewport);
|
|
196
|
+
if (view.utility) url.searchParams.set(`${PREFIX}utility`, view.utility);
|
|
197
|
+
if (url.search.length > MAX_QUERY_LENGTH) {
|
|
198
|
+
throw new Error(`Editor view URL exceeds the ${MAX_QUERY_LENGTH}-character share limit.`);
|
|
199
|
+
}
|
|
200
|
+
return url.toString();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Parse a shareable editor projection. Malformed projections are ignored at boot. */
|
|
204
|
+
export function editorViewFromUrl(value: string | URL): EditorView | null {
|
|
205
|
+
const url = value instanceof URL ? value : new URL(value, 'http://editor.invalid');
|
|
206
|
+
const params = url.searchParams;
|
|
207
|
+
if (params.get(`${PREFIX}v`) !== '1') return null;
|
|
208
|
+
const view: EditorView = { version: 1 };
|
|
209
|
+
const document = parseDocument(params);
|
|
210
|
+
if (document) view.document = document;
|
|
211
|
+
const ids = params
|
|
212
|
+
.getAll(`${PREFIX}select`)
|
|
213
|
+
.filter((id) => id.length > 0)
|
|
214
|
+
.slice(0, 32);
|
|
215
|
+
if (ids.length) view.selection = { ids, focus: params.get(`${PREFIX}focus`) === '1' };
|
|
216
|
+
const viewport = parseViewport(params);
|
|
217
|
+
if (viewport) view.viewport = viewport;
|
|
218
|
+
const utility = params.get(`${PREFIX}utility`);
|
|
219
|
+
if (utility && UTILITIES.has(utility as EditorViewUtility)) {
|
|
220
|
+
view.utility = utility as EditorViewUtility;
|
|
221
|
+
}
|
|
222
|
+
return view;
|
|
223
|
+
}
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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 four surfaces:
|
|
6
|
+
*
|
|
7
|
+
* (a) **Editor panels** — registered into the Dockview workspace as documents
|
|
8
|
+
* or utilities via a tool contribution (`package.json#vgai.tools` →
|
|
9
|
+
* `contributes: [{ point: 'workspace.document' | 'workspace.utility' }]`).
|
|
10
|
+
* Types: `ToolContributionProps` in `@vgai/editor-sdk/contributions`.
|
|
11
|
+
* Panels are never a parallel rail — Dockview 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) **Per-component viewport gizmos** (this module) — a `GameComponent`
|
|
16
|
+
* subclass declares `static editorGizmo: ComponentGizmoHook`; the editor
|
|
17
|
+
* renders its objects for every scene entity carrying that component,
|
|
18
|
+
* on the editor-only layer, alongside the built-in gizmos.
|
|
19
|
+
* (d) **System adapters** — runtime capabilities (networking, debug, …)
|
|
20
|
+
* registered from game code via `ctx.registerSystemAdapter?.(kind, impl)`
|
|
21
|
+
* (`SystemAdapters` in `@vgai/engine`'s `runtime/types` /
|
|
22
|
+
* `adapter/system-adapter`). Deliberately NOT re-exported here: the
|
|
23
|
+
* engine already publishes that seam and every consumer of it also
|
|
24
|
+
* imports the engine — an alias would be a dead surface.
|
|
25
|
+
*
|
|
26
|
+
* Degradation ladder (the anti-shim rule, as API): every contribution
|
|
27
|
+
* resolves to a {@link ExtensionContributionTier} —
|
|
28
|
+
*
|
|
29
|
+
* - `'active'` — the contribution loaded and produced its surface.
|
|
30
|
+
* - `'absent'` — nothing was contributed. The editor HIDES the surface
|
|
31
|
+
* entirely; it never fabricates placeholder data for a missing
|
|
32
|
+
* contribution.
|
|
33
|
+
* - `'failed'` — the contribution exists but threw / violated the contract.
|
|
34
|
+
* The failure is contained per-contribution (the editor never crashes),
|
|
35
|
+
* the surface is hidden, and the error is reported LOUDLY (editor
|
|
36
|
+
* console) — never silently swallowed into fake output.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type * as THREE from 'three';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Capability tier of a single extension contribution — the degradation
|
|
43
|
+
* ladder made API (see the module doc above for the exact semantics).
|
|
44
|
+
*/
|
|
45
|
+
export type ExtensionContributionTier = 'active' | 'absent' | 'failed';
|
|
46
|
+
|
|
47
|
+
/** Resolved status of one per-component gizmo contribution. */
|
|
48
|
+
export interface ComponentGizmoStatus {
|
|
49
|
+
tier: ExtensionContributionTier;
|
|
50
|
+
/** Present only for `'failed'` — the contained error's message. */
|
|
51
|
+
error?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What a component gizmo hook receives. Draw with `ctx.three` — the EDITOR's
|
|
56
|
+
* three module — so returned objects render in the editor viewport
|
|
57
|
+
* regardless of which physical `three` copy the project itself resolves.
|
|
58
|
+
*/
|
|
59
|
+
export interface ComponentGizmoContext {
|
|
60
|
+
/** The component's registry name (the key in `entity.components`). */
|
|
61
|
+
componentName: string;
|
|
62
|
+
/**
|
|
63
|
+
* The entity's authored component data. When the component class declares
|
|
64
|
+
* a `static schema` (Zod), this is the schema-parsed value — defaults
|
|
65
|
+
* applied, invalid data already rejected (a parse failure fails the
|
|
66
|
+
* contribution loudly instead of fabricating defaults).
|
|
67
|
+
*/
|
|
68
|
+
props: Record<string, unknown>;
|
|
69
|
+
/** The editor's `three` module — build all gizmo objects with this. */
|
|
70
|
+
three: typeof THREE;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Per-component viewport gizmo contribution (critique O11 — the
|
|
75
|
+
* `OnDrawGizmos` half of the extension contract). Declared as a STATIC on a
|
|
76
|
+
* `GameComponent` subclass:
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* import type { ComponentGizmoHook } from '@vgai/editor-sdk/extension';
|
|
80
|
+
*
|
|
81
|
+
* export class TriggerZone extends GameComponent {
|
|
82
|
+
* static schema = z.object({ radius: z.number().default(2) });
|
|
83
|
+
* static editorGizmo: ComponentGizmoHook = ({ props, three }) => {
|
|
84
|
+
* const ring = new three.LineLoop(circleGeometry(props.radius as number),
|
|
85
|
+
* new three.LineBasicMaterial({ color: 0x44ff88 }));
|
|
86
|
+
* return ring;
|
|
87
|
+
* };
|
|
88
|
+
* // …
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* Contract:
|
|
93
|
+
* - Runs at DESIGN TIME in the editor (never in the running game). Called
|
|
94
|
+
* whenever the editor (re)builds gizmos for an entity carrying the
|
|
95
|
+
* component; treat it as pure — read `props`, return fresh objects.
|
|
96
|
+
* - Returned objects are parented to the entity's `Object3D` (local space),
|
|
97
|
+
* tagged as editor helpers on the editor-only render layer, excluded from
|
|
98
|
+
* raycast selection, and toggled by the viewport's Helpers ▸ Components
|
|
99
|
+
* filter. The editor disposes them with the entity's other gizmos.
|
|
100
|
+
* - Return `null` (or an empty array) to draw nothing for this entity.
|
|
101
|
+
* - Draw-only, v1: gizmos visualize; they do not edit. Interactive handles
|
|
102
|
+
* stay editor-internal until a consumer needs them (no dead surface).
|
|
103
|
+
* - Throwing (or returning a non-Object3D) marks the contribution
|
|
104
|
+
* `'failed'`: contained, hidden, reported loudly. The editor never
|
|
105
|
+
* substitutes a fallback drawing.
|
|
106
|
+
*/
|
|
107
|
+
export type ComponentGizmoHook = (
|
|
108
|
+
ctx: ComponentGizmoContext,
|
|
109
|
+
) => THREE.Object3D | readonly THREE.Object3D[] | null;
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
GenerationBilling,
|
|
3
|
+
GenerationJob,
|
|
4
|
+
GenerationJobStatus,
|
|
5
|
+
GenerationJobsDocument,
|
|
6
|
+
} from '@vgai/sdk/generations';
|
|
1
7
|
export { EditorClient } from './client.js';
|
|
2
8
|
export type {
|
|
9
|
+
ToolAssetInspectorContributionMatch,
|
|
10
|
+
ToolAssetInspectorContributionProps,
|
|
11
|
+
ToolContributionAsset,
|
|
3
12
|
ToolContributionNode,
|
|
4
13
|
ToolContributionProps,
|
|
14
|
+
ToolGenerationResultContributionProps,
|
|
5
15
|
ToolInspectorContributionMatch,
|
|
6
16
|
ToolInspectorContributionProps,
|
|
7
17
|
} from './contributions.js';
|
|
18
|
+
export { editorViewFromUrl, editorViewUrl } from './editor-view.js';
|
|
19
|
+
export type {
|
|
20
|
+
ComponentGizmoContext,
|
|
21
|
+
ComponentGizmoHook,
|
|
22
|
+
ComponentGizmoStatus,
|
|
23
|
+
ExtensionContributionTier,
|
|
24
|
+
} from './extension.js';
|
|
8
25
|
export type {
|
|
26
|
+
ActiveDocumentCapture,
|
|
9
27
|
AssetKind,
|
|
10
28
|
AssetPreviewBackground,
|
|
11
29
|
AssetPreviewCapture,
|
|
@@ -15,8 +33,13 @@ export type {
|
|
|
15
33
|
EditorCameraState,
|
|
16
34
|
EditorEntitySummary,
|
|
17
35
|
EditorState,
|
|
36
|
+
EditorView,
|
|
37
|
+
EditorViewDocument,
|
|
18
38
|
GameCapture,
|
|
19
39
|
HelperVisibility,
|
|
40
|
+
HumanoidShotLabel,
|
|
41
|
+
HumanoidShotSetCapture,
|
|
42
|
+
PresentedEditorView,
|
|
20
43
|
ProjectInfo,
|
|
21
44
|
ProjectTemplate,
|
|
22
45
|
ProjectToolCatalog,
|
|
@@ -31,3 +54,4 @@ export type {
|
|
|
31
54
|
ViewPreset,
|
|
32
55
|
ViewportCapture,
|
|
33
56
|
} from './types.js';
|
|
57
|
+
export { HUMANOID_SHOT_LABELS } from './types.js';
|
package/src/types.ts
CHANGED
|
@@ -13,10 +13,14 @@ export interface HelperVisibility {
|
|
|
13
13
|
lights: boolean;
|
|
14
14
|
cameras: boolean;
|
|
15
15
|
colliders: boolean;
|
|
16
|
+
joints: boolean;
|
|
17
|
+
lod: boolean;
|
|
16
18
|
audio: boolean;
|
|
17
19
|
splines: boolean;
|
|
18
20
|
navmesh: boolean;
|
|
19
21
|
skeletons: boolean;
|
|
22
|
+
/** Per-component gizmo contributions (W3a extension contract). */
|
|
23
|
+
components: boolean;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
export interface Vec3Value {
|
|
@@ -84,6 +88,33 @@ export interface AssetPreviewCapture {
|
|
|
84
88
|
contactSheet: ViewportCapture & { width: number; height: number };
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
/**
|
|
92
|
+
* B7.6 — the canonical humanoid verify shot set (`vgai asset-preview --shots
|
|
93
|
+
* humanoid`): four turntable angles, tight zooms on the historical seam
|
|
94
|
+
* sites (head/hands/feet/shoulder), and a deep-bend pose pair, all framed
|
|
95
|
+
* from the loaded GLB's OWN skeleton (see `packages/editor/src/asset-preview.ts`).
|
|
96
|
+
*/
|
|
97
|
+
export const HUMANOID_SHOT_LABELS = [
|
|
98
|
+
'front',
|
|
99
|
+
'back',
|
|
100
|
+
'left',
|
|
101
|
+
'right',
|
|
102
|
+
'zoom-head',
|
|
103
|
+
'zoom-hands',
|
|
104
|
+
'zoom-feet',
|
|
105
|
+
'zoom-shoulder',
|
|
106
|
+
'bend-front',
|
|
107
|
+
'bend-quarter',
|
|
108
|
+
] as const;
|
|
109
|
+
export type HumanoidShotLabel = (typeof HUMANOID_SHOT_LABELS)[number];
|
|
110
|
+
|
|
111
|
+
export interface HumanoidShotSetCapture {
|
|
112
|
+
width: number;
|
|
113
|
+
height: number;
|
|
114
|
+
shots: Array<ViewportCapture & { label: HumanoidShotLabel }>;
|
|
115
|
+
contactSheet: ViewportCapture & { width: number; height: number };
|
|
116
|
+
}
|
|
117
|
+
|
|
87
118
|
export interface EditorState {
|
|
88
119
|
playState: 'stopped' | 'playing' | 'paused';
|
|
89
120
|
/**
|
|
@@ -97,9 +128,21 @@ export interface EditorState {
|
|
|
97
128
|
* or against an older server that predates this field.
|
|
98
129
|
*/
|
|
99
130
|
loopLiveness?: 'running' | 'hidden-paused' | 'stopped' | null;
|
|
131
|
+
/**
|
|
132
|
+
* R1 (docs/R3F-FOLLOW-THROUGH-SPEC.md) — the pending-restart reason when
|
|
133
|
+
* source changed while the game was RUNNING and the running session is now
|
|
134
|
+
* stale (e.g. an R3F entry-file write-back during play, a registry.ts
|
|
135
|
+
* edit). The editor's Restart button surfaces the same reason; one restart
|
|
136
|
+
* (`vgai play`, or the button) remounts every root from fresh source and
|
|
137
|
+
* clears it. `null` when the running session is fresh; absent against an
|
|
138
|
+
* older server that predates the field.
|
|
139
|
+
*/
|
|
140
|
+
restartRequired?: string | null;
|
|
100
141
|
selectedEntityId: string | null;
|
|
101
142
|
selectedEntityIds: string[];
|
|
102
143
|
activeViewportTab: 'scene' | 'game';
|
|
144
|
+
/** The actual active center document, including tool/source documents. */
|
|
145
|
+
activeDocumentId?: string | null;
|
|
103
146
|
activeTabKey: string;
|
|
104
147
|
showGrid: boolean;
|
|
105
148
|
showHelpers: boolean;
|
|
@@ -172,6 +215,69 @@ export type ShadingMode = 'solid' | 'unlit' | 'wireframe' | 'normals' | 'overdra
|
|
|
172
215
|
export type TransformMode = 'translate' | 'rotate' | 'scale';
|
|
173
216
|
export type TransformSpace = 'world' | 'local';
|
|
174
217
|
|
|
218
|
+
/**
|
|
219
|
+
* A durable, intentionally small projection of what an editor is presenting.
|
|
220
|
+
* This is not workspace persistence: panel sizes, transient tool state, and
|
|
221
|
+
* authored document contents remain outside the URL.
|
|
222
|
+
*/
|
|
223
|
+
export type EditorViewDocument =
|
|
224
|
+
| { kind: 'scene'; path: string }
|
|
225
|
+
| { kind: 'asset'; path: string; entityId?: never; assetKind?: AssetKind }
|
|
226
|
+
| { kind: 'asset'; entityId: string; path?: never; assetKind?: 'model' }
|
|
227
|
+
| { kind: 'tool'; id: string }
|
|
228
|
+
| { kind: 'world'; id: string }
|
|
229
|
+
| { kind: 'story'; modulePath: string; storyName: string; mode?: 'preview' | 'docs' }
|
|
230
|
+
| { kind: 'data'; path: string }
|
|
231
|
+
| { kind: 'project-tool'; name: string }
|
|
232
|
+
| { kind: 'generation'; id: string }
|
|
233
|
+
| {
|
|
234
|
+
kind: 'workspace';
|
|
235
|
+
id:
|
|
236
|
+
| 'workspace:scene'
|
|
237
|
+
| 'workspace:game'
|
|
238
|
+
| 'workspace:data-assets'
|
|
239
|
+
| 'workspace:build-profiles'
|
|
240
|
+
| 'account'
|
|
241
|
+
| 'project-tools';
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export interface EditorView {
|
|
245
|
+
version: 1;
|
|
246
|
+
document?: EditorViewDocument;
|
|
247
|
+
selection?: { ids: string[]; focus?: boolean };
|
|
248
|
+
viewport?: {
|
|
249
|
+
camera?: ViewPreset | 'isometric' | EditorCameraState;
|
|
250
|
+
diagnostic?: ShadingMode | 'uv' | 'vertex-colors' | 'bounds' | 'skeleton';
|
|
251
|
+
frame?: 'document' | 'selection';
|
|
252
|
+
grid?: boolean;
|
|
253
|
+
};
|
|
254
|
+
utility?: 'profiler' | 'console' | 'animation' | 'debugger';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface PresentedEditorView {
|
|
258
|
+
view: EditorView;
|
|
259
|
+
/** Shareable URL for the durable projection that was applied. */
|
|
260
|
+
url: string;
|
|
261
|
+
/** Honest degradations; an unsupported requested view is never silent. */
|
|
262
|
+
warnings: string[];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The pixels of the active center document, with enough provenance for an
|
|
266
|
+
* agent to prove which user-visible subject it captured. Editor chrome is
|
|
267
|
+
* deliberately excluded. */
|
|
268
|
+
export interface ActiveDocumentCapture extends ViewportCapture {
|
|
269
|
+
document: {
|
|
270
|
+
id: string;
|
|
271
|
+
title: string;
|
|
272
|
+
kind: string;
|
|
273
|
+
sourcePath?: string;
|
|
274
|
+
rootId?: string;
|
|
275
|
+
};
|
|
276
|
+
view: EditorView;
|
|
277
|
+
source: 'scene-viewport' | 'game-composite' | 'object3d-document' | 'document-composite';
|
|
278
|
+
layers?: { canvases: number; domOverlays: number };
|
|
279
|
+
}
|
|
280
|
+
|
|
175
281
|
/** Template ids accepted by the editor's create-project endpoint. */
|
|
176
282
|
export type ProjectTemplate = 'default' | '2d' | 'react' | 'example';
|
|
177
283
|
|
|
@@ -187,43 +293,8 @@ export interface RecentProject {
|
|
|
187
293
|
thumbnail?: string;
|
|
188
294
|
}
|
|
189
295
|
|
|
190
|
-
export interface ProjectToolCatalogEntry {
|
|
191
|
-
name: string;
|
|
192
|
-
summary: string;
|
|
193
|
-
description: string;
|
|
194
|
-
sourcePath: string;
|
|
195
|
-
inputSchema: unknown;
|
|
196
|
-
resultSchema: unknown;
|
|
197
|
-
errors: Array<{ code: string; summary: string; dataSchema?: unknown }>;
|
|
198
|
-
requires: Record<string, boolean | undefined>;
|
|
199
|
-
host: 'node' | 'editor-browser' | 'runtime-page';
|
|
200
|
-
mutates: boolean;
|
|
201
|
-
supportsDryRun: boolean;
|
|
202
|
-
longRunning: boolean;
|
|
203
|
-
permission: { risk: 'read' | 'write' | 'destructive'; summary: string };
|
|
204
|
-
contributions: ProjectToolContribution[];
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export type ToolContributionPoint =
|
|
208
|
-
| 'workspace.document'
|
|
209
|
-
| 'selection.inspector'
|
|
210
|
-
| 'workspace.utility';
|
|
211
|
-
|
|
212
|
-
export interface ProjectToolContribution {
|
|
213
|
-
id: string;
|
|
214
|
-
point: ToolContributionPoint;
|
|
215
|
-
title: string;
|
|
216
|
-
/** Project-relative or package-absolute browser module path. */
|
|
217
|
-
entryPath: string;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export interface ProjectToolCatalog {
|
|
221
|
-
tools: ProjectToolCatalogEntry[];
|
|
222
|
-
loadErrors: Array<{ sourcePath: string; message: string }>;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
296
|
export type ProjectToolOutcome =
|
|
226
|
-
| { ok: true; data: unknown }
|
|
297
|
+
| { ok: true; data: unknown; generation?: GenerationJob; generationWarning?: string }
|
|
227
298
|
| {
|
|
228
299
|
ok: false;
|
|
229
300
|
error: {
|
|
@@ -233,3 +304,12 @@ export type ProjectToolOutcome =
|
|
|
233
304
|
issues?: Array<{ path?: PropertyKey[]; message?: string; [key: string]: unknown }>;
|
|
234
305
|
};
|
|
235
306
|
};
|
|
307
|
+
|
|
308
|
+
import type { GenerationJob } from '@vgai/sdk/generations';
|
|
309
|
+
|
|
310
|
+
export type {
|
|
311
|
+
ProjectToolCatalog,
|
|
312
|
+
ProjectToolCatalogEntry,
|
|
313
|
+
ProjectToolContribution,
|
|
314
|
+
ToolContributionPoint,
|
|
315
|
+
} from '@vgai/sdk/project-tool-catalog';
|