# @vgai/editor-sdk

Typed client for the editor dev server's control API. This is the layer the
`vgai` CLI and tooling use to drive a running editor: commands are POSTed to
`/__editor/command`, relayed to the browser editor over SSE, executed there,
and the real result (or an explicit "no editor connected" timeout) comes back
to the caller.

## Usage

```ts
import { EditorClient } from '@vgai/editor-sdk';

const editor = new EditorClient(); // default http://127.0.0.1:20173
const projectEditor = new EditorClient({ url: 'http://127.0.0.1:25786' });
await editor.play();

// Present the same editor subject/view to the connected human and get a
// compact share URL. This does not serialize document contents or layout.
const shown = await editor.present({
  version: 1,
  document: { kind: 'tool', id: 'walking-castle-builder' },
  viewport: { camera: 'isometric', frame: 'document' },
  utility: 'profiler',
});
console.log(shown.url);
await editor.waitForState((s) => s.playState === 'playing');
const entries = await editor.getLogEntries(); // proves frames actually ran
```

## Surface

One export, `EditorClient`, plus its types (`EditorState`, `ProjectInfo`,
`AssetKind`, `ShadingMode`, ...). Methods, by group:

- Play control: `play`, `restart`, `stop`, `pause`, `resume`, `step`
- Selection: `select(id | null)`, `selectMultiple`, `selectAll`
- Viewport: `focusEntity`, `focusSelection`, `viewPreset`, `setCamera`,
  `captureViewport`
- Asset preview: `captureAssetPreview` for deterministic front, right, top,
  and three-quarter captures of a project model or authored entity hierarchy
- Panels: `showViewport('scene'|'game')`, `showInspector`, `openAsset`,
  `closeAsset`, `toggleConsole`, `toggleCommandPalette`, `showBuild`, and
  `present(EditorView)` for an atomic human-visible view plus share URL
- Display: `setGrid`, `setHelpers`, `setStats`, `setShadingMode` (`solid` for
  authored materials, `clay` for neutral flat shading, `unlit`, `wireframe`,
  `normals`, or `overdraw`), `setHelperType` (including
  the independent `bounds` category). Shading targets the active Scene/Game
  viewport and remains render-only, session-local state.
- Transform tools: `setTransformMode`, `setTransformSpace`, `setSnap`
- Project: `createProject`, `openProject`, `getProject`, `listRecentProjects`
- State/logs: `getState`, `waitForState(predicate, timeoutMs)`,
  `getLogEntries`
- Project tools: `listProjectTools`, `runProjectTool`

Commands throw on `{ ok: false }` responses, including the server's timeout
when no browser editor is connected — failures are never silently swallowed.

### Project-owned editor layouts

`vgai.adapter.ts` imports a React layout component. The component owns both the
visible surfaces and their behavior; no layout name is resolved through a registry.

```tsx
// vgai.adapter.ts
import { defineAdapter } from '@vgai/engine/adapter/adapter-module';
import { ProjectLayout } from './src/editor/ProjectLayout';

export default defineAdapter({
  editor: { Layout: ProjectLayout },
  // roots, documents and source regions remain the project's normal declarations
});
```

```tsx
// src/editor/ProjectLayout.tsx
import {
  EditorFrame, EditorHeader, EditorFooter, Workspace,
  type EditorLayoutProps,
} from '@vgai/editor-sdk/layouts';

export function ProjectLayout({ playing, onReturnToProjectScreen }: EditorLayoutProps) {
  return (
    <EditorFrame>
      <EditorHeader onReturnToProjectScreen={onReturnToProjectScreen} />
      <Workspace
        immersivePlay={false}
        playUtilities={playing ? ['tool:analytics.analytics'] : []}
      />
      <EditorFooter />
    </EditorFrame>
  );
}
```

The editor supplies `playing` and `paused`. Use normal React state, effects and
composition for mode-dependent behavior. `Workspace` hosts the native dock; its
`immersivePlay` policy decides whether Play takes over the workspace, and its
`playUtilities` list temporarily reveals registered utilities during Play. Stop
restores their prior presentation without overriding a later manual utility choice.
Keep the same `Workspace` mounted when changing policy to retain its documents.

Import `GameLayout`, `StudioLayout`, `ModelLayout`, `DesignLayout` or `StageLayout`
when their implementation fits. New starters use these imported components.
`Workspace` also accepts an imported `WorkspaceArrangement` object containing
native dock geometry and region presentation. Its `id` identifies saved state;
it does not resolve an implementation. Existing `editor.workspace` and
`editor.playUtilities` declarations remain compatibility defaults.

A bounded editor can instead compose native `DocumentView` surfaces directly:

```tsx
import { DocumentView, EditorFrame } from '@vgai/editor-sdk/layouts';

export function PrefabLayout() {
  return (
    <EditorFrame pageScroll>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', height: '100%' }}>
        <DocumentView
          document={{ kind: 'story', modulePath: 'src/prefabs/Bridge.stories.tsx', storyName: 'Default' }}
          chrome={false}
          onReady={(document) => { document.select('Bridge Pivot'); document.transform('rotate'); }}
        />
        <DocumentView
          document={{ kind: 'asset', path: 'src/prefabs/Bridge.tsx', assetKind: 'source' }}
          active={false}
          chrome={false}
        />
      </div>
    </EditorFrame>
  );
}
```

These are the editor's existing documents, with the same source authoring and
history. `chrome={false}` omits each document's header and shelf. Global header,
footer and docking are present only when the layout composes them. `pageScroll`
allows the enclosing page to scroll over a viewport. `onReady` exposes native
object selection, transform mode and framing for an Object3D document; a missing
or ambiguous object name reports an error. Source documents show their actual
file and refresh when the native authoring tools write it, briefly highlighting the
changed text without recentering visible code. Opening a specific story loads that module first; unrelated stories
remain background catalog work and do not hold its first frame.

Layout imports reload with their adapter dependency graph. Executable components
remain runtime values; status facets report only their name. Existing projects
without `editor.Layout` retain the standard editor shell.

### Editor contributions for project tools

An optional React contribution is a normal default-exported component. Import
its props from `@vgai/editor-sdk/contributions`; the editor supplies the exact
registered tool and an already-configured client:

```tsx
import { Button } from '@editor/widgets';
import type { ToolContributionProps } from '@vgai/editor-sdk/contributions';

export default function MapBuilder({ tool, client, account }: ToolContributionProps) {
  return (
    <Button
      variant="primary"
      onClick={() => void client.runProjectTool(tool.name, { seed: 42 }, { confirm: true })}
      title={`Default execution: ${account.preferredRoute}`}
    >
      Build map
    </Button>
  );
}
```

The package registration chooses `workspace.document`, `workspace.utility`, `workspace.analytics`,
`selection.inspector`, `asset.inspector`, or `generation.result`. Selection
Inspector contributions receive `node`/`nodeId` and export
`match(node, adapter)`; asset Inspector contributions receive `asset` and
export `match(asset)`. A generation-result contribution receives the durable
job and raw native poll result inside the editor-owned result document and
must export `match(job, result)`; registration order never selects a renderer.
All contributions receive `account`, a validated global projection containing
plan, credits, spend policy, and provider-specific route availability. It never
contains an access token. There is no extension class, lifecycle, or
proprietary UI description.

Use ordinary React/CSS for composition and `@editor/widgets` for controls.
Shape the UI for its contribution point: a bounded workspace for documents, a
dense single column for inspector sections, and a compact row/status block for
utilities. Badges are for short statuses and counts, not headings.

### Extension contract (`@vgai/editor-sdk/extension`)

The typed contract for everything a game project contributes TO the editor —
three surfaces, one outcome vocabulary (`ExtensionContributionState`:
`active` / `absent` / `failed`). An absent contribution hides its surface
(the editor never fabricates placeholder data); a failing one is contained
per-contribution and reported loudly on the editor console — never a crashed
editor, never silent fake output.

1. **Editor panels** — `workspace.document` / `workspace.utility` tool
   contributions (previous section). Panels join the Dockview workspace;
   there is no parallel rail.
2. **Inspector sections** — `selection.inspector` / `asset.inspector` tool
   contributions with an exported `match`.
3. **System adapters** — runtime capabilities registered from game code via
   `ctx.registerSystemAdapter?.(kind, impl)` (`SystemAdapters` in
   `@vgai/engine`). Deliberately not re-exported here: the engine already
   publishes that seam and every consumer of it also imports the engine.

### Asset Lab capture

`captureAssetPreview` accepts exactly one source: an authored entity hierarchy
already loaded in the editor, or a same-origin project `.glb`/`.gltf` path.
Rendering happens in an isolated native editor scene and returns deterministic
front, right, top, and three-quarter PNGs plus a labeled contact sheet:

```ts
const sketch = await editor.captureAssetPreview(
  { entityId: 'storefront-sketch' },
  { width: 768, height: 768, background: 'neutral' },
);

const model = await editor.captureAssetPreview(
  { assetPath: '/models/storefront.glb' },
  { background: 'transparent' },
);
```

Project model input is bounded to 64 MiB and 25 seconds. External `.gltf`
buffers and images must resolve on the same project origin. The thin CLI
equivalent writes the four views and contact sheet to disk:

```bash
npm run vgai -- screenshot /models/storefront.glb --out artifacts/storefront
```

## Limitations

- The published package exports raw TypeScript (`exports: "./src/index.ts"`),
  so consumers need a TypeScript-aware runner/bundler such as tsx or Vite.
- Requires the editor dev server (`npm run dev` or `npx @vgai/cli@latest edit <project>`); there is no
  offline mode.
