@vgai/sdk 0.4.0-canary.20260715.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/package.json +27 -0
- package/src/cinematic/capabilities-operations.ts +128 -0
- package/src/cinematic/cue-operations.ts +198 -0
- package/src/cinematic/gsap-operations.ts +126 -0
- package/src/cinematic/index.ts +59 -0
- package/src/cinematic/preview-operations.ts +279 -0
- package/src/cinematic/preview-transport.ts +244 -0
- package/src/cinematic/render-operations.ts +409 -0
- package/src/cinematic/render-transport.ts +238 -0
- package/src/cinematic/theatre-operations.ts +306 -0
- package/src/editor/camera-operations.ts +169 -0
- package/src/editor/console-operations.ts +87 -0
- package/src/editor/hierarchy-operations.ts +95 -0
- package/src/editor/index.ts +62 -0
- package/src/editor/open-operations.ts +209 -0
- package/src/editor/screenshot-operations.ts +99 -0
- package/src/editor/selection-operations.ts +144 -0
- package/src/editor/session-operations.ts +73 -0
- package/src/editor/source-location-operations.ts +106 -0
- package/src/editor/transport.ts +647 -0
- package/src/errors.ts +72 -0
- package/src/http/http-projection.ts +349 -0
- package/src/http/index.ts +11 -0
- package/src/index.ts +67 -0
- package/src/mcp/index.ts +16 -0
- package/src/mcp/mcp-projection.ts +288 -0
- package/src/operations.ts +83 -0
- package/src/play/control-operations.ts +205 -0
- package/src/play/debug-command-operations.ts +245 -0
- package/src/play/index.ts +66 -0
- package/src/play/input-operations.ts +316 -0
- package/src/play/lifecycle-operations.ts +271 -0
- package/src/play/log-operations.ts +279 -0
- package/src/play/run-ticks-operations.ts +141 -0
- package/src/play/state-operations.ts +210 -0
- package/src/play/status-operations.ts +160 -0
- package/src/play/transport.ts +728 -0
- package/src/project/asset-operations.ts +243 -0
- package/src/project/component-operations.ts +337 -0
- package/src/project/discovery-operations.ts +269 -0
- package/src/project/entity-operations.ts +366 -0
- package/src/project/index.ts +55 -0
- package/src/project/input-map-operations.ts +233 -0
- package/src/project/manifest-operations.ts +355 -0
- package/src/project/scene-operations.ts +426 -0
- package/src/project/shared.ts +299 -0
- package/src/registry.ts +285 -0
- package/src/render/capabilities/ffmpeg.ts +141 -0
- package/src/render/index.ts +15 -0
- package/src/render/render-cinematic.ts +1847 -0
- package/src/types.ts +101 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cinematic.project.list` / `cinematic.sheet.list` / `cinematic.sequence.inspect`
|
|
3
|
+
* (B5, §8 B5 "list Theatre projects/sheets", "inspect sequence length and objects").
|
|
4
|
+
*
|
|
5
|
+
* STATIC, file-native ops (host: 'node') — they read the COMMITTED Theatre
|
|
6
|
+
* project-state JSON directly off disk, exactly like B2's
|
|
7
|
+
* `project.theatre.discover`/`project.theatre.validate`
|
|
8
|
+
* (`../project/discovery-operations.ts`), which they deliberately reuse
|
|
9
|
+
* (`listProjectFiles`) rather than duplicate. The key difference from B2's
|
|
10
|
+
* `project.*` versions: `cinematic.*` targets "a controlled render runtime"
|
|
11
|
+
* (§5.7), which in practice is very often NOT inside a vgai game project at
|
|
12
|
+
* all — the committed reference fixtures this unit proves against
|
|
13
|
+
* (`packages/engine/e2e/{reference,render}-cinematic/theatre-project.json`)
|
|
14
|
+
* are engine e2e fixtures with no `vgai.game.json` anywhere above them. So
|
|
15
|
+
* these ops take an explicit `root` (falling back to `ctx.projectRoot` when
|
|
16
|
+
* omitted) rather than being HARD-confined to `ctx.projectRoot` the way
|
|
17
|
+
* `project.*` ops are — there is deliberately no `PATH_OUTSIDE_PROJECT`-style
|
|
18
|
+
* confinement here; `cinematic.render`'s own `entry`/`out` fields have the
|
|
19
|
+
* same unconfined shape (arbitrary filesystem paths), for the same reason.
|
|
20
|
+
*
|
|
21
|
+
* `sheetsById`'s outer shape is validated the same "shape-only" way B2's
|
|
22
|
+
* `project.theatre.validate` does (§3.2/§5.3: Theatre's internal per-track
|
|
23
|
+
* keyframe format is a real, versioned, proprietary format this repo does
|
|
24
|
+
* not own) — PLUS one layer deeper into `sequence` (`length`,
|
|
25
|
+
* `subUnitsPerUnit`, `tracksByObject`'s KEYS), which is exactly the "sequence
|
|
26
|
+
* length and objects" the B5 AC asks for and nothing past that (never a
|
|
27
|
+
* single keyframe/track's internal content).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
31
|
+
import { isAbsolute, join } from 'node:path';
|
|
32
|
+
import { z } from 'zod';
|
|
33
|
+
import { OperationError } from '../errors.js';
|
|
34
|
+
import { listProjectFiles } from '../project/shared.js';
|
|
35
|
+
import { defineOperation, type ErrorDefinition, type OperationRegistry } from '../registry.js';
|
|
36
|
+
import type { OperationContext } from '../types.js';
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Shared path resolution: `root` (explicit) > `ctx.projectRoot` (fallback).
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export const NO_ROOT_ERROR: ErrorDefinition = {
|
|
43
|
+
code: 'NO_ROOT',
|
|
44
|
+
summary: 'Neither an explicit `root` input nor ctx.projectRoot was given.',
|
|
45
|
+
data: z.object({}).describe('No additional data.'),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const THEATRE_FILE_NOT_FOUND_ERROR: ErrorDefinition = {
|
|
49
|
+
code: 'FILE_NOT_FOUND',
|
|
50
|
+
summary: 'The referenced Theatre project-state file does not exist on disk.',
|
|
51
|
+
data: z.object({ path: z.string() }),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const INVALID_THEATRE_PROJECT_ERROR: ErrorDefinition = {
|
|
55
|
+
code: 'INVALID_THEATRE_PROJECT',
|
|
56
|
+
summary:
|
|
57
|
+
"The file's outer shape (sheetsById/definitionVersion/revisionHistory, and — for " +
|
|
58
|
+
'sequence.inspect — sheetsById[sheet].sequence) does not match Theatre project state.',
|
|
59
|
+
data: z.object({ issues: z.array(z.string()) }),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const SHEET_NOT_FOUND_ERROR: ErrorDefinition = {
|
|
63
|
+
code: 'SHEET_NOT_FOUND',
|
|
64
|
+
summary: 'No sheet with the given name exists in this Theatre project’s sheetsById.',
|
|
65
|
+
data: z.object({ sheetName: z.string(), availableSheets: z.array(z.string()) }),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Resolve `root` input against `ctx.projectRoot`, requiring at least one. */
|
|
69
|
+
function resolveRoot(root: string | undefined, ctx: OperationContext): string {
|
|
70
|
+
const resolved = root ?? ctx.projectRoot;
|
|
71
|
+
if (!resolved) {
|
|
72
|
+
throw new OperationError(
|
|
73
|
+
'NO_ROOT',
|
|
74
|
+
'Neither an explicit `root` input nor ctx.projectRoot was given.',
|
|
75
|
+
{},
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return resolved;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Resolve a Theatre project-state path against `root` — absolute paths pass through unconfined (see module jsdoc); relative paths join `root`. */
|
|
82
|
+
function resolveTheatreProjectPath(root: string, theatreProjectPath: string): string {
|
|
83
|
+
return isAbsolute(theatreProjectPath) ? theatreProjectPath : join(root, theatreProjectPath);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readTheatreProjectFile(absPath: string): unknown {
|
|
87
|
+
if (!existsSync(absPath)) {
|
|
88
|
+
throw new OperationError('FILE_NOT_FOUND', `File not found: ${absPath}`, { path: absPath });
|
|
89
|
+
}
|
|
90
|
+
return JSON.parse(readFileSync(absPath, 'utf-8'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Same "outer shape only" convention as B2's project.theatre.validate — see
|
|
94
|
+
// module jsdoc for why this repo does not own the internal keyframe format.
|
|
95
|
+
const TheatreProjectOuterShape = z.object({
|
|
96
|
+
sheetsById: z.record(z.string(), z.unknown()),
|
|
97
|
+
definitionVersion: z.string(),
|
|
98
|
+
revisionHistory: z.array(z.unknown()),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// One layer deeper — sequence.length/subUnitsPerUnit + tracksByObject KEYS
|
|
102
|
+
// only (never a track/keyframe's own content).
|
|
103
|
+
const TheatreSequenceShape = z.object({
|
|
104
|
+
length: z.number(),
|
|
105
|
+
subUnitsPerUnit: z.number().optional(),
|
|
106
|
+
tracksByObject: z.record(z.string(), z.unknown()),
|
|
107
|
+
});
|
|
108
|
+
const TheatreSheetShape = z.object({
|
|
109
|
+
sequence: TheatreSequenceShape,
|
|
110
|
+
staticOverrides: z.unknown().optional(),
|
|
111
|
+
});
|
|
112
|
+
const TheatreProjectDeepShape = z.object({
|
|
113
|
+
sheetsById: z.record(z.string(), TheatreSheetShape),
|
|
114
|
+
definitionVersion: z.string(),
|
|
115
|
+
revisionHistory: z.array(z.unknown()),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// cinematic.project.list
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
const CinematicProjectListInput = z.object({
|
|
123
|
+
root: z
|
|
124
|
+
.string()
|
|
125
|
+
.optional()
|
|
126
|
+
.describe('Directory to search for *.theatre-project.json files. Defaults to ctx.projectRoot.'),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const CinematicProjectListResult = z.object({
|
|
130
|
+
root: z.string().describe('The directory actually searched.'),
|
|
131
|
+
theatreProjectFiles: z
|
|
132
|
+
.array(z.string())
|
|
133
|
+
.describe(
|
|
134
|
+
'Root-relative paths to every committed *.theatre-project.json / theatre-project.json file found, sorted.',
|
|
135
|
+
),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export const cinematicProjectList = defineOperation({
|
|
139
|
+
name: 'cinematic.project.list',
|
|
140
|
+
summary: 'List every committed Theatre project-state file under a directory.',
|
|
141
|
+
description:
|
|
142
|
+
'Static, file-native discovery (§8 B5 "list Theatre projects"). Matches both the ' +
|
|
143
|
+
'`*.theatre-project.json` naming convention (packages/engine/test/fixtures/) and the plain ' +
|
|
144
|
+
'`theatre-project.json` convention used by the render-mode e2e fixtures ' +
|
|
145
|
+
'(packages/engine/e2e/{reference,render,audio}-cinematic/).',
|
|
146
|
+
input: CinematicProjectListInput,
|
|
147
|
+
result: CinematicProjectListResult,
|
|
148
|
+
errors: [NO_ROOT_ERROR],
|
|
149
|
+
requires: {},
|
|
150
|
+
host: 'node',
|
|
151
|
+
mutates: false,
|
|
152
|
+
supportsDryRun: false,
|
|
153
|
+
permission: { risk: 'read', summary: 'Walks a file tree; no writes.' },
|
|
154
|
+
async impl(input, ctx) {
|
|
155
|
+
const root = resolveRoot(input.root, ctx);
|
|
156
|
+
const theatreProjectFiles = listProjectFiles(root, (p) => p.endsWith('theatre-project.json'));
|
|
157
|
+
return { root, theatreProjectFiles };
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// cinematic.sheet.list
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
const CinematicSheetListInput = z.object({
|
|
166
|
+
theatreProjectPath: z
|
|
167
|
+
.string()
|
|
168
|
+
.describe('Path to a committed Theatre project-state JSON file (absolute, or root-relative).'),
|
|
169
|
+
root: z
|
|
170
|
+
.string()
|
|
171
|
+
.optional()
|
|
172
|
+
.describe('Base directory for a relative theatreProjectPath. Defaults to ctx.projectRoot.'),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const CinematicSheetListResult = z.object({
|
|
176
|
+
sheets: z.array(z.string()).describe('Every sheet name in sheetsById, sorted.'),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export const cinematicSheetList = defineOperation({
|
|
180
|
+
name: 'cinematic.sheet.list',
|
|
181
|
+
summary: 'List every sheet name in a committed Theatre project-state file.',
|
|
182
|
+
description: 'Static, file-native (§8 B5 "list Theatre ... sheets"). Outer-shape validated only.',
|
|
183
|
+
input: CinematicSheetListInput,
|
|
184
|
+
result: CinematicSheetListResult,
|
|
185
|
+
errors: [NO_ROOT_ERROR, THEATRE_FILE_NOT_FOUND_ERROR, INVALID_THEATRE_PROJECT_ERROR],
|
|
186
|
+
requires: {},
|
|
187
|
+
host: 'node',
|
|
188
|
+
mutates: false,
|
|
189
|
+
supportsDryRun: false,
|
|
190
|
+
permission: { risk: 'read', summary: 'Reads one Theatre project-state file; no writes.' },
|
|
191
|
+
async impl(input, ctx) {
|
|
192
|
+
const root = resolveRoot(input.root, ctx);
|
|
193
|
+
const absPath = resolveTheatreProjectPath(root, input.theatreProjectPath);
|
|
194
|
+
const json = readTheatreProjectFile(absPath);
|
|
195
|
+
const parsed = TheatreProjectOuterShape.safeParse(json);
|
|
196
|
+
if (!parsed.success) {
|
|
197
|
+
throw new OperationError(
|
|
198
|
+
'INVALID_THEATRE_PROJECT',
|
|
199
|
+
`${absPath} does not match Theatre project-state's outer shape.`,
|
|
200
|
+
{ issues: parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`) },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return { sheets: Object.keys(parsed.data.sheetsById).sort() };
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// cinematic.sequence.inspect
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
const CinematicSequenceInspectInput = z.object({
|
|
212
|
+
theatreProjectPath: z
|
|
213
|
+
.string()
|
|
214
|
+
.describe('Path to a committed Theatre project-state JSON file (absolute, or root-relative).'),
|
|
215
|
+
sheetName: z.string().describe('Which sheet in sheetsById to inspect.'),
|
|
216
|
+
root: z
|
|
217
|
+
.string()
|
|
218
|
+
.optional()
|
|
219
|
+
.describe('Base directory for a relative theatreProjectPath. Defaults to ctx.projectRoot.'),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const CinematicSequenceInspectResult = z.object({
|
|
223
|
+
sheetName: z.string(),
|
|
224
|
+
length: z.number().describe('Sequence length in seconds (Theatre’s `sequence.length`).'),
|
|
225
|
+
subUnitsPerUnit: z
|
|
226
|
+
.number()
|
|
227
|
+
.optional()
|
|
228
|
+
.describe('Theatre’s `sequence.subUnitsPerUnit`, when present.'),
|
|
229
|
+
objects: z
|
|
230
|
+
.array(z.string())
|
|
231
|
+
.describe('Every tracked Sheet Object name (tracksByObject’s keys), sorted.'),
|
|
232
|
+
trackCountByObject: z
|
|
233
|
+
.record(z.string(), z.number())
|
|
234
|
+
.describe(
|
|
235
|
+
'Number of property tracks declared per object (trackIdByPropPath’s size), where present.',
|
|
236
|
+
),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
export const cinematicSequenceInspect = defineOperation({
|
|
240
|
+
name: 'cinematic.sequence.inspect',
|
|
241
|
+
summary:
|
|
242
|
+
'Inspect one sheet’s sequence length and tracked objects in a Theatre project-state file.',
|
|
243
|
+
description:
|
|
244
|
+
'Static, file-native (§8 B5 "inspect sequence length and objects"). Descends one layer past ' +
|
|
245
|
+
'the outer shape (sequence.length/subUnitsPerUnit/tracksByObject KEYS) — never a track/keyframe’s ' +
|
|
246
|
+
'own content, which stays Theatre Studio’s proprietary authoring concern (§3.2/§5.3).',
|
|
247
|
+
input: CinematicSequenceInspectInput,
|
|
248
|
+
result: CinematicSequenceInspectResult,
|
|
249
|
+
errors: [
|
|
250
|
+
NO_ROOT_ERROR,
|
|
251
|
+
THEATRE_FILE_NOT_FOUND_ERROR,
|
|
252
|
+
INVALID_THEATRE_PROJECT_ERROR,
|
|
253
|
+
SHEET_NOT_FOUND_ERROR,
|
|
254
|
+
],
|
|
255
|
+
requires: {},
|
|
256
|
+
host: 'node',
|
|
257
|
+
mutates: false,
|
|
258
|
+
supportsDryRun: false,
|
|
259
|
+
permission: { risk: 'read', summary: 'Reads one Theatre project-state file; no writes.' },
|
|
260
|
+
async impl(input, ctx) {
|
|
261
|
+
const root = resolveRoot(input.root, ctx);
|
|
262
|
+
const absPath = resolveTheatreProjectPath(root, input.theatreProjectPath);
|
|
263
|
+
const json = readTheatreProjectFile(absPath);
|
|
264
|
+
const parsed = TheatreProjectDeepShape.safeParse(json);
|
|
265
|
+
if (!parsed.success) {
|
|
266
|
+
throw new OperationError(
|
|
267
|
+
'INVALID_THEATRE_PROJECT',
|
|
268
|
+
`${absPath} does not match Theatre project-state's shape (incl. per-sheet sequence).`,
|
|
269
|
+
{ issues: parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`) },
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const sheet = parsed.data.sheetsById[input.sheetName];
|
|
273
|
+
if (!sheet) {
|
|
274
|
+
throw new OperationError(
|
|
275
|
+
'SHEET_NOT_FOUND',
|
|
276
|
+
`No sheet named "${input.sheetName}" in ${absPath}.`,
|
|
277
|
+
{ sheetName: input.sheetName, availableSheets: Object.keys(parsed.data.sheetsById).sort() },
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const objects = Object.keys(sheet.sequence.tracksByObject).sort();
|
|
281
|
+
const trackCountByObject: Record<string, number> = {};
|
|
282
|
+
for (const [objectName, trackEntry] of Object.entries(sheet.sequence.tracksByObject)) {
|
|
283
|
+
const trackIdByPropPath = (trackEntry as { trackIdByPropPath?: Record<string, unknown> })
|
|
284
|
+
.trackIdByPropPath;
|
|
285
|
+
trackCountByObject[objectName] = trackIdByPropPath
|
|
286
|
+
? Object.keys(trackIdByPropPath).length
|
|
287
|
+
: 0;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
sheetName: input.sheetName,
|
|
291
|
+
length: sheet.sequence.length,
|
|
292
|
+
...(sheet.sequence.subUnitsPerUnit !== undefined
|
|
293
|
+
? { subUnitsPerUnit: sheet.sequence.subUnitsPerUnit }
|
|
294
|
+
: {}),
|
|
295
|
+
objects,
|
|
296
|
+
trackCountByObject,
|
|
297
|
+
};
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
/** Register cinematic.project.list / cinematic.sheet.list / cinematic.sequence.inspect onto `registry`. */
|
|
302
|
+
export function registerTheatreOperations(registry: OperationRegistry): void {
|
|
303
|
+
registry.register(cinematicProjectList);
|
|
304
|
+
registry.register(cinematicSheetList);
|
|
305
|
+
registry.register(cinematicSequenceInspect);
|
|
306
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `editor.viewport.camera.get` / `editor.viewport.camera.set`
|
|
3
|
+
* (B3, §8 B3 "viewport camera read/set").
|
|
4
|
+
*
|
|
5
|
+
* B3-followup: both directions are now genuinely wired to the live viewport
|
|
6
|
+
* camera, not just the three relay verbs.
|
|
7
|
+
*
|
|
8
|
+
* GET reads the REAL pose: `collectState` in `packages/editor/src/command-listener.ts`
|
|
9
|
+
* now reports `camera: {position, target, fov}` straight off the bound Three.js
|
|
10
|
+
* viewport camera + orbit target (`EditorStore.cameraPose`, populated once
|
|
11
|
+
* `ViewportPanel.tsx` binds a camera). `HttpEditorTransport.getCamera` reads it
|
|
12
|
+
* off `GET /__editor/state`. The declared `CAMERA_STATE_UNAVAILABLE` error still
|
|
13
|
+
* exists for the narrow window before any browser tab has bound/reported a
|
|
14
|
+
* camera (a session IS connected — this is a timing gap, not a fabricated pose).
|
|
15
|
+
*
|
|
16
|
+
* SET now supports a genuine arbitrary pose in addition to the three original
|
|
17
|
+
* relay verbs (focus-entity/focus-selection/view-preset): a `{mode:'pose',
|
|
18
|
+
* position, target, fov?}` input relays `{type:'set-camera', position, target,
|
|
19
|
+
* fov}` — a real, handled case in `command-listener.ts`'s `handleCommand`
|
|
20
|
+
* switch that calls `EditorViewport.setPose` (moves `camera.position` +
|
|
21
|
+
* `orbitControls.target`, updates `fov`/projection matrix when given).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
import { OperationError } from '../errors.js';
|
|
26
|
+
import { defineOperation, type OperationRegistry } from '../registry.js';
|
|
27
|
+
import {
|
|
28
|
+
EDITOR_COMMAND_TIMEOUT_MS,
|
|
29
|
+
EDITOR_NOT_RUNNING_ERROR,
|
|
30
|
+
EDITOR_READ_TIMEOUT_MS,
|
|
31
|
+
getTransport,
|
|
32
|
+
resolveEditorSession,
|
|
33
|
+
withTimeout,
|
|
34
|
+
} from './transport.js';
|
|
35
|
+
|
|
36
|
+
const CAMERA_STATE_UNAVAILABLE_ERROR = {
|
|
37
|
+
code: 'CAMERA_STATE_UNAVAILABLE',
|
|
38
|
+
summary:
|
|
39
|
+
'A session is connected, but no browser tab has bound/reported a viewport camera pose yet ' +
|
|
40
|
+
'(e.g. immediately after page load, before ViewportPanel mounts) — not a fabricated value.',
|
|
41
|
+
data: z.object({}),
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
const COMMAND_FAILED_ERROR = {
|
|
45
|
+
code: 'COMMAND_FAILED',
|
|
46
|
+
summary: 'The connected editor rejected or failed to execute the relayed camera command.',
|
|
47
|
+
data: z.object({ message: z.string() }),
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
const Vector3Schema = z.object({ x: z.number(), y: z.number(), z: z.number() });
|
|
51
|
+
|
|
52
|
+
const CameraPoseSchema = z.object({
|
|
53
|
+
position: Vector3Schema,
|
|
54
|
+
target: Vector3Schema,
|
|
55
|
+
fov: z.number(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// editor.viewport.camera.get
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
const EditorViewportCameraGetInput = z
|
|
63
|
+
.object({})
|
|
64
|
+
.describe('No input — reads the viewport camera pose from the target editor session.');
|
|
65
|
+
|
|
66
|
+
const EditorViewportCameraGetResult = CameraPoseSchema.describe('Live viewport camera pose.');
|
|
67
|
+
|
|
68
|
+
export const editorViewportCameraGet = defineOperation({
|
|
69
|
+
name: 'editor.viewport.camera.get',
|
|
70
|
+
summary: 'Read the viewport camera pose (position/target/fov) from a connected editor session.',
|
|
71
|
+
description:
|
|
72
|
+
'Resolves a target session then reads its live camera pose (position, orbit target, fov) off ' +
|
|
73
|
+
'the bound Three.js viewport camera. Throws the declared CAMERA_STATE_UNAVAILABLE only in the ' +
|
|
74
|
+
'narrow window before any browser tab has bound/reported one — never a fabricated pose.',
|
|
75
|
+
input: EditorViewportCameraGetInput,
|
|
76
|
+
result: EditorViewportCameraGetResult,
|
|
77
|
+
errors: [EDITOR_NOT_RUNNING_ERROR, CAMERA_STATE_UNAVAILABLE_ERROR],
|
|
78
|
+
requires: { editor: true },
|
|
79
|
+
host: 'editor-browser',
|
|
80
|
+
mutates: false,
|
|
81
|
+
supportsDryRun: false,
|
|
82
|
+
permission: { risk: 'read', summary: 'Reads live editor viewport camera state only.' },
|
|
83
|
+
async impl(_input, ctx) {
|
|
84
|
+
const transport = getTransport(ctx);
|
|
85
|
+
const session = await resolveEditorSession(ctx, transport);
|
|
86
|
+
const camera = await withTimeout(
|
|
87
|
+
transport.getCamera(session, EDITOR_READ_TIMEOUT_MS),
|
|
88
|
+
EDITOR_READ_TIMEOUT_MS,
|
|
89
|
+
'editor.viewport.camera.get',
|
|
90
|
+
).catch(() => undefined);
|
|
91
|
+
if (!camera) {
|
|
92
|
+
throw new OperationError(
|
|
93
|
+
'CAMERA_STATE_UNAVAILABLE',
|
|
94
|
+
'No browser tab has bound/reported a viewport camera pose yet.',
|
|
95
|
+
{},
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return camera;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// editor.viewport.camera.set
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
const EditorViewportCameraSetInput = z
|
|
107
|
+
.discriminatedUnion('mode', [
|
|
108
|
+
z.object({ mode: z.literal('focusEntity'), entityId: z.string() }),
|
|
109
|
+
z.object({ mode: z.literal('focusSelection') }),
|
|
110
|
+
z.object({
|
|
111
|
+
mode: z.literal('viewPreset'),
|
|
112
|
+
preset: z.enum(['top', 'front', 'right', 'perspective']),
|
|
113
|
+
}),
|
|
114
|
+
z.object({
|
|
115
|
+
mode: z.literal('pose'),
|
|
116
|
+
position: Vector3Schema,
|
|
117
|
+
target: Vector3Schema,
|
|
118
|
+
fov: z.number().optional(),
|
|
119
|
+
}),
|
|
120
|
+
])
|
|
121
|
+
.describe(
|
|
122
|
+
'Either one of the three named relay verbs, or an arbitrary xyz position/target (+ optional fov) pose.',
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const EditorViewportCameraSetResult = z.object({ ok: z.literal(true) });
|
|
126
|
+
|
|
127
|
+
export const editorViewportCameraSet = defineOperation({
|
|
128
|
+
name: 'editor.viewport.camera.set',
|
|
129
|
+
summary:
|
|
130
|
+
'Move the viewport camera via focus-entity, focus-selection, a view preset, or an arbitrary pose.',
|
|
131
|
+
description:
|
|
132
|
+
'Relays focus-entity/focus-selection/view-preset, or an arbitrary {position, target, fov?} ' +
|
|
133
|
+
'pose via `set-camera`, through POST /__editor/command — all four are real, handled cases in ' +
|
|
134
|
+
"the browser's command listener (EditorViewport.setPose backs the arbitrary-pose mode).",
|
|
135
|
+
input: EditorViewportCameraSetInput,
|
|
136
|
+
result: EditorViewportCameraSetResult,
|
|
137
|
+
errors: [EDITOR_NOT_RUNNING_ERROR, COMMAND_FAILED_ERROR],
|
|
138
|
+
requires: { editor: true },
|
|
139
|
+
host: 'editor-browser',
|
|
140
|
+
mutates: true,
|
|
141
|
+
supportsDryRun: false,
|
|
142
|
+
permission: {
|
|
143
|
+
risk: 'write',
|
|
144
|
+
summary: 'Moves the live editor viewport camera only (no file/document writes).',
|
|
145
|
+
},
|
|
146
|
+
async impl(input, ctx) {
|
|
147
|
+
const transport = getTransport(ctx);
|
|
148
|
+
const session = await resolveEditorSession(ctx, transport);
|
|
149
|
+
const result = await withTimeout(
|
|
150
|
+
transport.setCamera(session, input, EDITOR_COMMAND_TIMEOUT_MS),
|
|
151
|
+
EDITOR_COMMAND_TIMEOUT_MS,
|
|
152
|
+
'editor.viewport.camera.set',
|
|
153
|
+
).catch((err: unknown) => ({
|
|
154
|
+
ok: false,
|
|
155
|
+
error: err instanceof Error ? err.message : String(err),
|
|
156
|
+
}));
|
|
157
|
+
if (!result.ok) {
|
|
158
|
+
throw new OperationError('COMMAND_FAILED', result.error ?? 'camera command failed', {
|
|
159
|
+
message: result.error ?? 'camera command failed',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return { ok: true as const };
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
export function registerCameraOperations(registry: OperationRegistry): void {
|
|
167
|
+
registry.register(editorViewportCameraGet);
|
|
168
|
+
registry.register(editorViewportCameraSet);
|
|
169
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `editor.console.subscribe` (B3, §8 B3 "editor console/log subscription metadata").
|
|
3
|
+
*
|
|
4
|
+
* Deliberately METADATA-ONLY, per the spec bullet's own wording — a
|
|
5
|
+
* request/response SDK operation cannot itself BE a stream. This describes
|
|
6
|
+
* the real, existing subscription mechanics: the general SSE endpoint
|
|
7
|
+
* (`GET /__editor/events`, broadcasting `editor-command`/`project-changed`/
|
|
8
|
+
* `asset-moved`/`scene-changed`) and the play-mode log persistence pair
|
|
9
|
+
* (`POST /__editor/log-session`, `GET/POST /__editor/log-entries`), plus a
|
|
10
|
+
* live count of currently-buffered log entries as a liveness signal. Editor
|
|
11
|
+
* console messages themselves (`packages/editor/src/editor-console.ts`) are
|
|
12
|
+
* in-browser, in-memory only today — not broadcast server-side — so this op
|
|
13
|
+
* cannot report live console entries, only the real subscription surface
|
|
14
|
+
* that does exist.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import { OperationError } from '../errors.js';
|
|
19
|
+
import { defineOperation, type OperationRegistry } from '../registry.js';
|
|
20
|
+
import {
|
|
21
|
+
EDITOR_NOT_RUNNING_ERROR,
|
|
22
|
+
EDITOR_READ_TIMEOUT_MS,
|
|
23
|
+
getTransport,
|
|
24
|
+
resolveEditorSession,
|
|
25
|
+
withTimeout,
|
|
26
|
+
} from './transport.js';
|
|
27
|
+
|
|
28
|
+
const CONSOLE_METADATA_UNAVAILABLE_ERROR = {
|
|
29
|
+
code: 'CONSOLE_METADATA_UNAVAILABLE',
|
|
30
|
+
summary: 'A session is connected but did not answer the log-entries read in time.',
|
|
31
|
+
data: z.object({}),
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const EditorConsoleSubscribeInput = z
|
|
35
|
+
.object({})
|
|
36
|
+
.describe("No input — describes how to subscribe to the target editor session's events/logs.");
|
|
37
|
+
|
|
38
|
+
const EditorConsoleSubscribeResult = z.object({
|
|
39
|
+
sseUrl: z.string().describe('The general editor SSE endpoint.'),
|
|
40
|
+
events: z.array(z.string()).describe('Event names broadcast on sseUrl.'),
|
|
41
|
+
logEntriesUrl: z.string().describe('GET (read) / POST (append) persisted play-mode log entries.'),
|
|
42
|
+
logSessionUrl: z
|
|
43
|
+
.string()
|
|
44
|
+
.describe('POST {action:"start"|"end"} to bracket a play-mode log-persistence session.'),
|
|
45
|
+
bufferedLogCount: z
|
|
46
|
+
.number()
|
|
47
|
+
.describe('Number of play-mode log entries currently buffered/persisted.'),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const editorConsoleSubscribe = defineOperation({
|
|
51
|
+
name: 'editor.console.subscribe',
|
|
52
|
+
summary: "Describe how to subscribe to a connected editor session's events and logs.",
|
|
53
|
+
description:
|
|
54
|
+
'Metadata only (a request/response op cannot itself be a stream): reports the real SSE ' +
|
|
55
|
+
'endpoint + event names and the real play-mode log-persistence endpoints, plus a live ' +
|
|
56
|
+
'buffered-entry count. General editor console messages are in-browser/in-memory only today ' +
|
|
57
|
+
'(not broadcast server-side), so they are not part of this metadata.',
|
|
58
|
+
input: EditorConsoleSubscribeInput,
|
|
59
|
+
result: EditorConsoleSubscribeResult,
|
|
60
|
+
errors: [EDITOR_NOT_RUNNING_ERROR, CONSOLE_METADATA_UNAVAILABLE_ERROR],
|
|
61
|
+
requires: { editor: true },
|
|
62
|
+
host: 'editor-browser',
|
|
63
|
+
mutates: false,
|
|
64
|
+
supportsDryRun: false,
|
|
65
|
+
permission: { risk: 'read', summary: 'Reads live log-entry metadata only.' },
|
|
66
|
+
async impl(_input, ctx) {
|
|
67
|
+
const transport = getTransport(ctx);
|
|
68
|
+
const session = await resolveEditorSession(ctx, transport);
|
|
69
|
+
const metadata = await withTimeout(
|
|
70
|
+
transport.getConsoleSubscriptionMetadata(session, EDITOR_READ_TIMEOUT_MS),
|
|
71
|
+
EDITOR_READ_TIMEOUT_MS,
|
|
72
|
+
'editor.console.subscribe',
|
|
73
|
+
).catch(() => undefined);
|
|
74
|
+
if (!metadata) {
|
|
75
|
+
throw new OperationError(
|
|
76
|
+
'CONSOLE_METADATA_UNAVAILABLE',
|
|
77
|
+
'The connected editor session did not answer the log-entries read in time.',
|
|
78
|
+
{},
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return metadata;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export function registerConsoleOperations(registry: OperationRegistry): void {
|
|
86
|
+
registry.register(editorConsoleSubscribe);
|
|
87
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `editor.hierarchy.inspect` (B3, §8 B3 "hierarchy inspect").
|
|
3
|
+
*
|
|
4
|
+
* B3-followup: wired to the REAL entity tree. `collectState`
|
|
5
|
+
* (`packages/editor/src/command-listener.ts`) now flattens `EditorStore.entities`
|
|
6
|
+
* — the same nested `SceneEntity[]` data model `GameHierarchy`/`hierarchy-rows.ts`
|
|
7
|
+
* render the hierarchy panel from — into `id`/`name`/`childIds` rows and reports
|
|
8
|
+
* them alongside `entityCount`/`savePath`. `HttpEditorTransport.getHierarchy`
|
|
9
|
+
* reads them off `GET /__editor/state`. `entities` is `null` only in the narrow
|
|
10
|
+
* window before any browser tab has POSTed state at least once (a session IS
|
|
11
|
+
* connected — this is a timing gap, not a fabricated tree).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { OperationError } from '../errors.js';
|
|
16
|
+
import { defineOperation, type OperationRegistry } from '../registry.js';
|
|
17
|
+
import {
|
|
18
|
+
EDITOR_NOT_RUNNING_ERROR,
|
|
19
|
+
EDITOR_READ_TIMEOUT_MS,
|
|
20
|
+
getTransport,
|
|
21
|
+
resolveEditorSession,
|
|
22
|
+
withTimeout,
|
|
23
|
+
} from './transport.js';
|
|
24
|
+
|
|
25
|
+
const HIERARCHY_UNAVAILABLE_ERROR = {
|
|
26
|
+
code: 'HIERARCHY_UNAVAILABLE',
|
|
27
|
+
summary: 'A session is connected but did not answer the hierarchy-state read in time.',
|
|
28
|
+
data: z.object({}),
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
const EditorHierarchyInspectInput = z
|
|
32
|
+
.object({})
|
|
33
|
+
.describe(
|
|
34
|
+
"No input — inspects the hierarchy of the target editor session's currently loaded scene.",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const HierarchyNodeSchema = z.object({
|
|
38
|
+
id: z.string(),
|
|
39
|
+
name: z.string(),
|
|
40
|
+
childIds: z.array(z.string()),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const EditorHierarchyInspectResult = z.object({
|
|
44
|
+
savePath: z
|
|
45
|
+
.string()
|
|
46
|
+
.nullable()
|
|
47
|
+
.describe('Project-relative path of the scene currently loaded in the editor, or null.'),
|
|
48
|
+
entityCount: z.number().describe('Live entity count reported by the editor.'),
|
|
49
|
+
entities: z
|
|
50
|
+
.array(HierarchyNodeSchema)
|
|
51
|
+
.nullable()
|
|
52
|
+
.describe(
|
|
53
|
+
'Full flattened entity list (id/name/childIds), reflecting the real live scene graph. Null ' +
|
|
54
|
+
'only in the narrow window before any browser tab has POSTed state at least once — ' +
|
|
55
|
+
'savePath/entityCount are still real live data either way.',
|
|
56
|
+
),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const editorHierarchyInspect = defineOperation({
|
|
60
|
+
name: 'editor.hierarchy.inspect',
|
|
61
|
+
summary: "Inspect the entity hierarchy of the target editor session's currently loaded scene.",
|
|
62
|
+
description:
|
|
63
|
+
'Resolves a target session then reads its live state. savePath/entityCount/entities are all ' +
|
|
64
|
+
'real, live data — entities is the flattened scene graph (id/name/childIds), null only before ' +
|
|
65
|
+
'any browser tab has reported state at least once.',
|
|
66
|
+
input: EditorHierarchyInspectInput,
|
|
67
|
+
result: EditorHierarchyInspectResult,
|
|
68
|
+
errors: [EDITOR_NOT_RUNNING_ERROR, HIERARCHY_UNAVAILABLE_ERROR],
|
|
69
|
+
requires: { editor: true },
|
|
70
|
+
host: 'editor-browser',
|
|
71
|
+
mutates: false,
|
|
72
|
+
supportsDryRun: false,
|
|
73
|
+
permission: { risk: 'read', summary: 'Reads live editor scene state only.' },
|
|
74
|
+
async impl(_input, ctx) {
|
|
75
|
+
const transport = getTransport(ctx);
|
|
76
|
+
const session = await resolveEditorSession(ctx, transport);
|
|
77
|
+
const hierarchy = await withTimeout(
|
|
78
|
+
transport.getHierarchy(session, EDITOR_READ_TIMEOUT_MS),
|
|
79
|
+
EDITOR_READ_TIMEOUT_MS,
|
|
80
|
+
'editor.hierarchy.inspect',
|
|
81
|
+
).catch(() => undefined);
|
|
82
|
+
if (!hierarchy) {
|
|
83
|
+
throw new OperationError(
|
|
84
|
+
'HIERARCHY_UNAVAILABLE',
|
|
85
|
+
'The connected editor session did not answer the hierarchy-state read in time.',
|
|
86
|
+
{},
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return hierarchy;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export function registerHierarchyOperations(registry: OperationRegistry): void {
|
|
94
|
+
registry.register(editorHierarchyInspect);
|
|
95
|
+
}
|