@volter/editor-sdk 0.5.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +19 -0
- package/package.json +90 -0
- package/src/account.ts +210 -0
- package/src/chrome.ts +83 -0
- package/src/client.ts +1547 -0
- package/src/commands.ts +66 -0
- package/src/contributions.ts +985 -0
- package/src/document-probe.ts +237 -0
- package/src/editor-view.ts +220 -0
- package/src/extension.ts +40 -0
- package/src/generations.ts +178 -0
- package/src/host.ts +1167 -0
- package/src/http-transport.browser.ts +14 -0
- package/src/http-transport.node.ts +19 -0
- package/src/index.ts +128 -0
- package/src/layout-arrangements.ts +5 -0
- package/src/layouts.tsx +108 -0
- package/src/looks.ts +14 -0
- package/src/project/output-roots.ts +73 -0
- package/src/project/tab-census.ts +149 -0
- package/src/project-tool-catalog.ts +96 -0
- package/src/selection.tsx +108 -0
- package/src/services.ts +18 -0
- package/src/session/build-report.ts +19 -0
- package/src/session/collaboration-types.ts +262 -0
- package/src/session/command-table.ts +333 -0
- package/src/session/discovery.ts +90 -0
- package/src/session/editor-brand.ts +73 -0
- package/src/session/editor-compatibility.ts +248 -0
- package/src/session/editor-control-lifecycle.ts +68 -0
- package/src/session/editor-control-protocol.ts +5 -0
- package/src/session/entrypoint-selection-readers.ts +66 -0
- package/src/session/entrypoint-selection-source.ts +120 -0
- package/src/session/game-css-scope.ts +30 -0
- package/src/session/product-create.ts +24 -0
- package/src/session/product-locator.ts +389 -0
- package/src/session/project-module-url.ts +245 -0
- package/src/session/registry-format.ts +203 -0
- package/src/session/relative-path-guard.ts +56 -0
- package/src/session/source-glob.ts +15 -0
- package/src/session/tool-contribution-convention.ts +116 -0
- package/src/session/workbench-locator.ts +650 -0
- package/src/session.ts +41 -0
- package/src/share.ts +160 -0
- package/src/tools/errors.ts +91 -0
- package/src/tools/provider-execution.ts +70 -0
- package/src/tools/registry.ts +341 -0
- package/src/tools/types.ts +159 -0
- package/src/transport.ts +97 -0
- package/src/types.ts +1581 -0
- package/src/views.ts +164 -0
- package/src/widgets/design-system.ts +93 -0
- package/src/widgets/editor-appearance.ts +149 -0
- package/src/widgets/editor-material.ts +83 -0
- package/src/widgets/icon-set-registry.ts +105 -0
- package/src/widgets/index.ts +71 -0
- package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
- package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
- package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
- package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
- package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
- package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
- package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
- package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
- package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
- package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
- package/src/widgets/inspector-widgets/color-utils.ts +201 -0
- package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
- package/src/widgets/inspector-widgets/index.ts +24 -0
- package/src/widgets/inspector-widgets/shared.tsx +140 -0
- package/src/widgets/interactive-edit-scope.ts +33 -0
- package/src/widgets/patterns/Dialog.tsx +129 -0
- package/src/widgets/patterns/Fields.tsx +44 -0
- package/src/widgets/patterns/List.tsx +25 -0
- package/src/widgets/patterns/StateSurface.tsx +40 -0
- package/src/widgets/patterns/Surfaces.tsx +122 -0
- package/src/widgets/patterns/Tabs.tsx +80 -0
- package/src/widgets/patterns/Toolbar.tsx +72 -0
- package/src/widgets/patterns/Tree.tsx +72 -0
- package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
- package/src/widgets/primitives/Button.tsx +62 -0
- package/src/widgets/primitives/ColorInput.tsx +78 -0
- package/src/widgets/primitives/DraftTextInput.tsx +63 -0
- package/src/widgets/primitives/EditorIcon.tsx +157 -0
- package/src/widgets/primitives/FormControls.tsx +88 -0
- package/src/widgets/primitives/HoverPreview.tsx +96 -0
- package/src/widgets/primitives/JsonInput.tsx +113 -0
- package/src/widgets/primitives/Layout.tsx +100 -0
- package/src/widgets/primitives/Menu.tsx +140 -0
- package/src/widgets/primitives/NumberInput.tsx +169 -0
- package/src/widgets/primitives/Panel.tsx +80 -0
- package/src/widgets/primitives/SectionHeader.tsx +77 -0
- package/src/widgets/primitives/Text.tsx +54 -0
- package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
- package/src/widgets/primitives/Tooltip.tsx +204 -0
- package/src/widgets/primitives/Vec3Input.tsx +70 -0
- package/src/widgets/primitives/banner-tones.ts +32 -0
- package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
- package/src/widgets/primitives/editor-icons.ts +245 -0
- package/src/widgets/primitives/panel-header-styles.ts +42 -0
- package/src/widgets/theme.ts +2633 -0
- package/src/widgets/z-index.ts +25 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,1581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two viewport tabs — the editor's two MODES, named for what the user is
|
|
3
|
+
* doing rather than for what happens to be mounted (ARCHITECTURE-CORE
|
|
4
|
+
* §Vocabulary, decided 2026-08-02). Deliberately not `'scene' | 'game'`:
|
|
5
|
+
* `'scene'` names a document format (a three root is TSX, and
|
|
6
|
+
* asset/story/tool documents live on that tab too), and `'game'`
|
|
7
|
+
* names the mounted artifact rather than the mode. Not to be confused with the
|
|
8
|
+
* PLAY TRANSPORT (the five stable controls) — this is which tab is showing.
|
|
9
|
+
*/
|
|
10
|
+
export type ViewportTab = 'edit' | 'play';
|
|
11
|
+
|
|
12
|
+
export type AssetKind =
|
|
13
|
+
| 'model'
|
|
14
|
+
| 'image'
|
|
15
|
+
| 'video'
|
|
16
|
+
| 'audio'
|
|
17
|
+
| 'animation'
|
|
18
|
+
| 'json'
|
|
19
|
+
| 'prefab'
|
|
20
|
+
| 'source';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The editor's NAMED WORKSPACES — task-named layout memories over the one
|
|
24
|
+
* physical dock (ARCHITECTURE-CORE §Editor chrome). Named for the TASK, each
|
|
25
|
+
* borrowing the arrangement of the tool that does it best; `game` is the
|
|
26
|
+
* default and is the editor's standing arrangement. Restated here (rather than
|
|
27
|
+
* imported from the editor) for the same reason every other id union in this
|
|
28
|
+
* file is: the SDK is a wire client and must not depend on the editor bundle.
|
|
29
|
+
*/
|
|
30
|
+
/** One entry of the resolved document table, as `EditorState['adapter']['scenes']`
|
|
31
|
+
* and `documentTable()` carry it over the wire. */
|
|
32
|
+
export interface DocumentTableEntryProjection {
|
|
33
|
+
id: string;
|
|
34
|
+
label: string;
|
|
35
|
+
/** OPEN: `scene`, `prefab`, `page`, `model`, `shot`, `take`, … */
|
|
36
|
+
kind: string;
|
|
37
|
+
region: string | null;
|
|
38
|
+
authorable: boolean;
|
|
39
|
+
reach: { kind: string; [field: string]: unknown };
|
|
40
|
+
source?: { path: string; export?: string };
|
|
41
|
+
finder?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DocumentTableProjection {
|
|
45
|
+
default: string | null;
|
|
46
|
+
entries: DocumentTableEntryProjection[];
|
|
47
|
+
/** True while a declared finder has no registration yet — a contribution's
|
|
48
|
+
* finder before the contribution pass — so the table is NOT final. */
|
|
49
|
+
pending: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A registered workspace's id: one of the editor's own (`look`, `animate`,
|
|
53
|
+
* `design`) or one a declared package contributes (`game`, `model`,
|
|
54
|
+
* `sculpt`, `texture`, `stage`, …). Validated by the editor against its
|
|
55
|
+
* live registry, the way a utility id is — not a closed set here. */
|
|
56
|
+
export type EditorWorkspaceName = string;
|
|
57
|
+
|
|
58
|
+
export interface HelperVisibility {
|
|
59
|
+
bounds: boolean;
|
|
60
|
+
lights: boolean;
|
|
61
|
+
cameras: boolean;
|
|
62
|
+
colliders: boolean;
|
|
63
|
+
joints: boolean;
|
|
64
|
+
particles: boolean;
|
|
65
|
+
lod: boolean;
|
|
66
|
+
audio: boolean;
|
|
67
|
+
splines: boolean;
|
|
68
|
+
navmesh: boolean;
|
|
69
|
+
constraints: boolean;
|
|
70
|
+
reflectionProbes: boolean;
|
|
71
|
+
triggerVolumes: boolean;
|
|
72
|
+
skeletons: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The WEIGHT display — a mesh coloured by its active vertex group
|
|
75
|
+
* (`@vgai/blender`'s `blender-runtime-weights.ts`). Added 2026-09-19 (I4)
|
|
76
|
+
* because nothing in this set stood for it: `skeletons` is the bones, and
|
|
77
|
+
* Blender's own viewport overlay has a Bones checkbox but reaches weight
|
|
78
|
+
* colours through Weight Paint MODE, which an inspection surface has no
|
|
79
|
+
* brushes to enter. OFF by default, the way Blender shows no weights until
|
|
80
|
+
* you ask for them.
|
|
81
|
+
*/
|
|
82
|
+
weights: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface Vec3Value {
|
|
86
|
+
x: number;
|
|
87
|
+
y: number;
|
|
88
|
+
z: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface EditorCameraState {
|
|
92
|
+
position: Vec3Value;
|
|
93
|
+
target: Vec3Value;
|
|
94
|
+
fov?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface EditorEntitySummary {
|
|
98
|
+
id: string;
|
|
99
|
+
name: string;
|
|
100
|
+
childIds: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ViewportCapture {
|
|
104
|
+
base64: string;
|
|
105
|
+
mimeType: 'image/png';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* How an animated look move on the open Object3D document ended. Never an
|
|
110
|
+
* error: the camera is SHARED with the person watching, so "they grabbed it
|
|
111
|
+
* mid-orbit" is an outcome to read, not a failure to handle.
|
|
112
|
+
*/
|
|
113
|
+
export interface DocumentLookOutcome {
|
|
114
|
+
/** True only when the whole move was drawn. */
|
|
115
|
+
completed: boolean;
|
|
116
|
+
/** Why it stopped early: a human drag, a later look verb, or a closed document. */
|
|
117
|
+
cancelledBy?: 'human' | 'superseded' | 'closed';
|
|
118
|
+
/** Where the camera ended up, radians around the framed subject. */
|
|
119
|
+
azimuth: number;
|
|
120
|
+
/** Radians above the subject's horizon. */
|
|
121
|
+
elevation: number;
|
|
122
|
+
/** Seconds of the move that were actually drawn. */
|
|
123
|
+
seconds: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Where the open document's camera is standing and what it is aimed at. */
|
|
127
|
+
export interface DocumentCameraPose {
|
|
128
|
+
position: [number, number, number];
|
|
129
|
+
target: [number, number, number];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Unit 4 (live-front-door wave) — the RUNNING GAME's pixels, as captured by
|
|
134
|
+
* the `bridge-screenshot` relay op (`command-listener.ts`'s
|
|
135
|
+
* `handleBridgeScreenshot`): the full play-mode game stack — canvas(es) PLUS
|
|
136
|
+
* the DOM adapter layers (for example React roots) composited by
|
|
137
|
+
* `capturePlayComposite`. Distinct from {@link ViewportCapture}, which is the
|
|
138
|
+
* EDITOR viewport's own canvas (`capture-viewport`) and knows nothing about
|
|
139
|
+
* play mode or the HUD.
|
|
140
|
+
*
|
|
141
|
+
* `composite` reports honestly whether the DOM-layer composite leg actually
|
|
142
|
+
* ran, or whether the capture degraded to a canvas-only `toDataURL` frame (no
|
|
143
|
+
* container, no DOM `Image`/`XMLSerializer`, or a rasterization failure) —
|
|
144
|
+
* never a silently HUD-less image passed off as the whole game. `layers` is
|
|
145
|
+
* only present on the composite leg.
|
|
146
|
+
*
|
|
147
|
+
* `flatness` and `loopRecoveryFrame` are the same honesty contract applied to
|
|
148
|
+
* the PIXELS: how much of the frame is one flat surface (a near-blank capture
|
|
149
|
+
* carries its own "weak evidence" warning), and whether the frame exists only
|
|
150
|
+
* because capture recovered a starved host loop with one deterministic tick.
|
|
151
|
+
*/
|
|
152
|
+
export interface GameCapture {
|
|
153
|
+
base64: string;
|
|
154
|
+
mimeType: 'image/png';
|
|
155
|
+
composite: boolean;
|
|
156
|
+
layers?: { canvases: number; domOverlays: number };
|
|
157
|
+
/** Degeneracy measure — `packages/editor/src/composite-screenshot.ts`'s
|
|
158
|
+
* `measureFlatness`. Absent when pixel readback was unavailable. */
|
|
159
|
+
flatness?: {
|
|
160
|
+
dominantFraction: number;
|
|
161
|
+
dominantColor: string;
|
|
162
|
+
distinctRegions: number;
|
|
163
|
+
degenerate: boolean;
|
|
164
|
+
/** Present iff `degenerate`; the sentence to show the caller verbatim. */
|
|
165
|
+
warning?: string;
|
|
166
|
+
};
|
|
167
|
+
/** True when the host loop was starved and the runtime rendered one
|
|
168
|
+
* deterministic tick to produce this frame. */
|
|
169
|
+
loopRecoveryFrame?: boolean;
|
|
170
|
+
/** Present when this frame came out of a RECORDED run (every `vgai play`
|
|
171
|
+
* records). The still is delivered either way; `notice` is the sentence
|
|
172
|
+
* naming the clip, its offset-0 wall clock, and what a still cannot answer —
|
|
173
|
+
* shown verbatim, never re-derived by the caller. */
|
|
174
|
+
recording?: {
|
|
175
|
+
path: string;
|
|
176
|
+
startedAt: string;
|
|
177
|
+
notice: string;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Options for recording the clean running-game composite. */
|
|
182
|
+
export interface GameplayRecordingOptions {
|
|
183
|
+
/** Requested real-time capture cadence. Defaults to 30; range 1–60. */
|
|
184
|
+
fps?: number;
|
|
185
|
+
/** `composite-webm` is the compatible one-file artifact. `canvas-dom`
|
|
186
|
+
* records the world canvas directly and writes the HUD to a synchronized
|
|
187
|
+
* replay sidecar, avoiding live DOM rasterization. */
|
|
188
|
+
format?: 'composite-webm' | 'canvas-dom';
|
|
189
|
+
/** Names the file under `.vgai/recordings/`. During Play, absence names the
|
|
190
|
+
* clip after its durable Gameplay Session. */
|
|
191
|
+
name?: string | null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Facts fixed when a gameplay recording starts. */
|
|
195
|
+
export interface GameplayRecordingStarted {
|
|
196
|
+
format: 'composite-webm' | 'canvas-dom';
|
|
197
|
+
startedAt: string;
|
|
198
|
+
mimeType: string;
|
|
199
|
+
width: number;
|
|
200
|
+
height: number;
|
|
201
|
+
fps: number;
|
|
202
|
+
audio: boolean;
|
|
203
|
+
layers: { canvases: number; domOverlays: number };
|
|
204
|
+
/** Where the bytes are landing — known at START, so a caller can say where
|
|
205
|
+
* the evidence for a run still in progress will be. */
|
|
206
|
+
path: string;
|
|
207
|
+
/** Replay sidecar directory for `canvas-dom`; null for a composite. */
|
|
208
|
+
replayPath: string | null;
|
|
209
|
+
/** True only for the out-of-session rotating fallback. */
|
|
210
|
+
rotates: boolean;
|
|
211
|
+
/** This run's `logs/play-*.jsonl`, whose event timestamps map to clip offsets. */
|
|
212
|
+
logFile: string | null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Final browser recording. Media chunks stream directly to the project while
|
|
216
|
+
* recording, so the command result stays small even for a long playthrough. */
|
|
217
|
+
export interface GameplayRecordingCapture extends GameplayRecordingStarted {
|
|
218
|
+
durationMs: number;
|
|
219
|
+
droppedFrames: number;
|
|
220
|
+
frameErrors: number;
|
|
221
|
+
/** The cadence ACHIEVED (frames over wall duration), which diverges from the
|
|
222
|
+
* requested `fps` on a throttled hidden tab. Read this before reasoning
|
|
223
|
+
* about a clip's timing. */
|
|
224
|
+
effectiveFps: number;
|
|
225
|
+
/** True when the tab was hidden for any part of the recording. */
|
|
226
|
+
hidden: boolean;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** A position on the active recorder's authoritative monotonic timeline.
|
|
230
|
+
* `startedAt` identifies the recording; `elapsedMs` shares the exact origin
|
|
231
|
+
* used to calculate the finalized capture's `durationMs`. */
|
|
232
|
+
export interface GameplayRecordingTimeline {
|
|
233
|
+
startedAt: string;
|
|
234
|
+
elapsedMs: number;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** A PNG reconstructed on demand from canvas video plus its DOM timeline. */
|
|
238
|
+
export interface GameplayReplayCapture {
|
|
239
|
+
replayPath: string;
|
|
240
|
+
positionMs: number;
|
|
241
|
+
width: number;
|
|
242
|
+
height: number;
|
|
243
|
+
base64: string;
|
|
244
|
+
mimeType: 'image/png';
|
|
245
|
+
layers: { canvases: number; domOverlays: number };
|
|
246
|
+
flatness?: GameCapture['flatness'];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** What Play can report while its recording is still open. Geometry, layers, media cadence, and
|
|
250
|
+
* final health are deliberately absent: those facts are only authoritative after Stop finalizes
|
|
251
|
+
* the capture. */
|
|
252
|
+
export interface PlayRecordingStatus {
|
|
253
|
+
path: string;
|
|
254
|
+
format: 'composite-webm' | 'canvas-dom';
|
|
255
|
+
replayPath: string | null;
|
|
256
|
+
startedAt: string;
|
|
257
|
+
rotates: boolean;
|
|
258
|
+
logFile: string | null;
|
|
259
|
+
idleAutoStopMs: number;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** What `play` reports back once the run is up and recording. */
|
|
263
|
+
export interface PlayStarted {
|
|
264
|
+
/** Absent only when the recorder could not start; play is up either way and
|
|
265
|
+
* the editor console carries the reason. */
|
|
266
|
+
recording?: PlayRecordingStatus;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export type AssetPreviewView = 'front' | 'right' | 'top' | 'perspective';
|
|
270
|
+
export type AssetPreviewBackground = 'neutral' | 'transparent';
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* What the Asset Lab is being asked to photograph: a same-origin project
|
|
274
|
+
* model path, a live scene entity, or RAW GLB BYTES that travel with the
|
|
275
|
+
* command.
|
|
276
|
+
*
|
|
277
|
+
* The bytes form exists because rasterization only happens where there is a
|
|
278
|
+
* GPU. A Node host that built a model in memory — the module-look lane's
|
|
279
|
+
* `project.bake.preview`, which compiles a project TS module's `Object3D` and
|
|
280
|
+
* exports it to an in-memory GLB without writing anything — has pixels
|
|
281
|
+
* nowhere until the live editor session renders them. It is four-view,
|
|
282
|
+
* lab-stage only: bytes stand nowhere, so `stage: 'scene'`, the shot-set /
|
|
283
|
+
* source-review modes and compare are all refused by name at the relay.
|
|
284
|
+
*/
|
|
285
|
+
export type AssetPreviewSource =
|
|
286
|
+
| { assetPath: string; entityId?: never; glbBase64?: never }
|
|
287
|
+
| { entityId: string; assetPath?: never; glbBase64?: never }
|
|
288
|
+
| { glbBase64: string; assetPath?: never; entityId?: never };
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Where an ENTITY capture is staged.
|
|
292
|
+
*
|
|
293
|
+
* `'lab'` (the default on every surface) is the editor's neutral Asset Lab
|
|
294
|
+
* stage: an isolated, yaw-normalized snapshot under fixed studio lighting,
|
|
295
|
+
* identical whatever the entity's surroundings are. `'scene'` photographs the
|
|
296
|
+
* entity where it stands in the live scene, under the scene's own lighting,
|
|
297
|
+
* with the editor's own grid/gizmos/helpers excluded — see
|
|
298
|
+
* `packages/editor/src/asset-preview.ts`'s `captureSceneStageAssetPreview`.
|
|
299
|
+
*
|
|
300
|
+
* `'scene'` is an ENTITY-only, four-view option: the relay refuses it by name
|
|
301
|
+
* for an `assetPath` source (a model loaded from disk stands nowhere) and for
|
|
302
|
+
* the shot-set / source-review / compare modes (each stages its own subject).
|
|
303
|
+
*/
|
|
304
|
+
export type AssetPreviewStage = 'lab' | 'scene';
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* A free capture camera for the Asset Lab legs (`vgai screenshot`'s
|
|
308
|
+
* `--azimuth/--elevation/--distance`): ONE view from a chosen angle instead
|
|
309
|
+
* of the fixed four. Angles are relative to the subject's AUTHORED front —
|
|
310
|
+
* azimuth 0 photographs the declared front, 90 walks toward the side the
|
|
311
|
+
* turntable's yaw-90 shot shows; elevation raises the camera (degrees above
|
|
312
|
+
* level); `distance` is meters from the framing center, auto-fit when
|
|
313
|
+
* omitted.
|
|
314
|
+
*/
|
|
315
|
+
export interface AssetPreviewCameraChoice {
|
|
316
|
+
azimuthDegrees: number;
|
|
317
|
+
elevationDegrees: number;
|
|
318
|
+
distance?: number;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Pose an animated subject before capturing (`vgai screenshot`'s
|
|
323
|
+
* `--clip <name> --time <t>`): the named clip is sampled at `timeSeconds`
|
|
324
|
+
* on the capture's disposable snapshot — the source is never mutated. The
|
|
325
|
+
* capture fails loudly (naming the clips that DO exist) when the subject
|
|
326
|
+
* carries no clip by this name.
|
|
327
|
+
*/
|
|
328
|
+
export interface AssetPreviewPose {
|
|
329
|
+
clip: string;
|
|
330
|
+
timeSeconds: number;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface AssetPreviewOptions {
|
|
334
|
+
width?: number;
|
|
335
|
+
height?: number;
|
|
336
|
+
background?: AssetPreviewBackground;
|
|
337
|
+
stage?: AssetPreviewStage;
|
|
338
|
+
camera?: AssetPreviewCameraChoice;
|
|
339
|
+
pose?: AssetPreviewPose;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* How the captured subject was oriented relative to its AUTHORED coordinates.
|
|
344
|
+
* The editor yaw-normalizes a subject so the front camera photographs its
|
|
345
|
+
* declared front; a 180-degree yaw maps authored +X to screen-LEFT in the
|
|
346
|
+
* front view. The yaw is reported here (and stamped onto the images' own
|
|
347
|
+
* pixels as `+X>` / `<+X` markers) so it is never applied silently.
|
|
348
|
+
*/
|
|
349
|
+
export interface AssetPreviewOrientation {
|
|
350
|
+
/** The subject's declared forward, `[0,0,1]` when it declares none. */
|
|
351
|
+
forward: [number, number, number];
|
|
352
|
+
/** Yaw applied to face the front camera; 0 means authored axes = world axes. */
|
|
353
|
+
yawDegrees: number;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export interface AssetPreviewCapture {
|
|
357
|
+
width: number;
|
|
358
|
+
height: number;
|
|
359
|
+
/** Absent from editors that predate orientation reporting. */
|
|
360
|
+
orientation?: AssetPreviewOrientation;
|
|
361
|
+
views: Array<ViewportCapture & { view: AssetPreviewView }>;
|
|
362
|
+
contactSheet: ViewportCapture & { width: number; height: number };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The STORY lane (`vgai screenshot <file>.stories.tsx`): a project CSF file's
|
|
367
|
+
* exports rendered in the live session's DOM and captured through the same
|
|
368
|
+
* composite leg the game lane uses, as ONE variant sheet per file. `story`
|
|
369
|
+
* narrows to a single export. See
|
|
370
|
+
* `packages/editor/src/stories/story-capture.ts`.
|
|
371
|
+
*/
|
|
372
|
+
export interface StoryCaptureOptions {
|
|
373
|
+
/** Narrow the sheet to one CSF export name (`--story <export>`). */
|
|
374
|
+
story?: string;
|
|
375
|
+
/** Per-variant cell size in CSS pixels; defaults to a 960x540 rectangle,
|
|
376
|
+
* because a UI story is not the Asset Lab's square 3D view. */
|
|
377
|
+
width?: number;
|
|
378
|
+
height?: number;
|
|
379
|
+
/** Free capture camera for THREE stories (same contract as the Asset Lab's
|
|
380
|
+
* {@link AssetPreviewCameraChoice}). A selected story that renders on the
|
|
381
|
+
* DOM leg refuses these BY NAME rather than silently ignoring them. */
|
|
382
|
+
camera?: AssetPreviewCameraChoice;
|
|
383
|
+
/** Clip pose for THREE stories (same contract as the Asset Lab's
|
|
384
|
+
* {@link AssetPreviewPose}); DOM-leg stories refuse it by name. */
|
|
385
|
+
pose?: AssetPreviewPose;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export interface StoryVariantImage extends ViewportCapture {
|
|
389
|
+
/** The CSF export name — the file name each variant PNG is written under. */
|
|
390
|
+
name: string;
|
|
391
|
+
/** Storybook's human-facing story name. */
|
|
392
|
+
label: string;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export interface StoryVariantCapture {
|
|
396
|
+
/** Project-relative path of the CSF module that was photographed. */
|
|
397
|
+
modulePath: string;
|
|
398
|
+
width: number;
|
|
399
|
+
height: number;
|
|
400
|
+
variants: StoryVariantImage[];
|
|
401
|
+
contactSheet: ViewportCapture & { width: number; height: number };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* B8.4 — the Asset Lab compare mode (`vgai screenshot <model.glb>
|
|
406
|
+
* --compare <ref.glb>`): the asset and a caller-supplied reference GLB rendered with
|
|
407
|
+
* matched orthographic front + side framing (equal-height bounding-box
|
|
408
|
+
* normalization, both yaw-normalized to face the camera), scored by
|
|
409
|
+
* silhouette IoU with per-view overlay evidence (orange asset / cyan
|
|
410
|
+
* reference / near-white agreement). See
|
|
411
|
+
* `packages/editor/src/asset-compare.ts`.
|
|
412
|
+
*/
|
|
413
|
+
export type AssetCompareView = 'front' | 'side';
|
|
414
|
+
|
|
415
|
+
export interface AssetCompareOptions {
|
|
416
|
+
width?: number;
|
|
417
|
+
height?: number;
|
|
418
|
+
/** Override the reference GLB's ground-plane forward vector; defaults to
|
|
419
|
+
* its `userData.forward` extras when present, else glTF's +Z. */
|
|
420
|
+
refForward?: [number, number, number];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export interface AssetCompareCapture {
|
|
424
|
+
width: number;
|
|
425
|
+
height: number;
|
|
426
|
+
views: Array<{
|
|
427
|
+
view: AssetCompareView;
|
|
428
|
+
/** Silhouette intersection-over-union in [0, 1]. */
|
|
429
|
+
iou: number;
|
|
430
|
+
overlay: ViewportCapture;
|
|
431
|
+
asset: ViewportCapture;
|
|
432
|
+
ref: ViewportCapture;
|
|
433
|
+
}>;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* THE shot-set contract. This block is the ONE declaration of it.
|
|
438
|
+
*
|
|
439
|
+
* A project-defined labeled shot set (`vgai screenshot <target> --shots <set>`).
|
|
440
|
+
* The DEFINITION is project data: a registered project tool named
|
|
441
|
+
* `project.<set>.previewShots` returns it (installed capabilities register
|
|
442
|
+
* theirs — the bird, humanoid and walking-castle capabilities each contribute
|
|
443
|
+
* their canonical verify set), and the editor's generic capture engine renders
|
|
444
|
+
* it — turntable yaw angles and skeleton-anchored zoom crops, optionally under
|
|
445
|
+
* a named pose applied to a disposable snapshot. Every shot is labeled so a
|
|
446
|
+
* flat directory of PNGs is self-describing without a manifest file.
|
|
447
|
+
*
|
|
448
|
+
* The contract lives HERE, in the SDK, because it crosses the editor relay:
|
|
449
|
+
* the capability tool that authors a set, the CLI that ships it across, and
|
|
450
|
+
* `packages/editor/src/asset-preview.ts`'s capture engine that renders it are
|
|
451
|
+
* three different programs. Each of those used to declare its own copy — five
|
|
452
|
+
* declarations in total — and the copies had already drifted on what a pose
|
|
453
|
+
* step's `radians` is measured FROM. Every side now type-checks against this
|
|
454
|
+
* one: the capture engine annotates its parser's return with
|
|
455
|
+
* `AssetPreviewShotSetDefinition`, and each capability tool annotates its
|
|
456
|
+
* exported set with it, so a Zod schema that stops matching this shape is a
|
|
457
|
+
* compile error rather than a runtime surprise at the relay.
|
|
458
|
+
*/
|
|
459
|
+
export interface ShotSetPoseRotation {
|
|
460
|
+
bone: string;
|
|
461
|
+
axis: 'x' | 'y' | 'z';
|
|
462
|
+
/**
|
|
463
|
+
* A DELTA about the bone's own local axis, in radians, relative to the
|
|
464
|
+
* loaded GLB's baked rest pose — NOT an absolute local rotation.
|
|
465
|
+
*
|
|
466
|
+
* That is what the capture engine does with it: `bone.rotateX/Y/Z(radians)`,
|
|
467
|
+
* which composes onto the bone's existing local quaternion. The consequence
|
|
468
|
+
* to watch for is that "rest pose" means whatever the GLB baked, not
|
|
469
|
+
* identity — a rig baked mid-gait needs `pose(t) - pose(0)` here, while a
|
|
470
|
+
* rig baked at identity can pass its absolute angle unchanged. Getting that
|
|
471
|
+
* backwards silently double-applies the baked pose.
|
|
472
|
+
*/
|
|
473
|
+
radians: number;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** The other half of a pose: a named morph target driven to an influence.
|
|
477
|
+
* Morphs are how a rig expresses what a joint cannot — an eyelid sliding over
|
|
478
|
+
* an eyeball, a brow band tilting — so a set that poses a FACE needs both
|
|
479
|
+
* halves or it can only ever show the jaw. A rig whose meshes carry no such
|
|
480
|
+
* morph is posed by the rotations alone: the same degrade-don't-throw rule
|
|
481
|
+
* the rotations follow for a missing joint. */
|
|
482
|
+
export interface ShotSetPoseMorph {
|
|
483
|
+
morph: string;
|
|
484
|
+
influence: number;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** The TRANSLATION channel of a pose: a joint displaced along its own local
|
|
488
|
+
* axis. Rotations alone cannot state a gait's vertical truth — a crouch, a
|
|
489
|
+
* jump apex, the hip dip that makes a walk read as weighted — because those
|
|
490
|
+
* are the root/hips MOVING, not a joint bending (round-4 finding: shot sets
|
|
491
|
+
* could not photograph a gait). Same delta semantics as the rotation: the
|
|
492
|
+
* capture engine applies `bone.translateX/Y/Z(meters)`, composing onto
|
|
493
|
+
* whatever local position the GLB baked. */
|
|
494
|
+
export interface ShotSetPoseTranslation {
|
|
495
|
+
bone: string;
|
|
496
|
+
axis: 'x' | 'y' | 'z';
|
|
497
|
+
/** A DELTA along the bone's own local axis, in meters, relative to the
|
|
498
|
+
* loaded GLB's baked rest position. */
|
|
499
|
+
meters: number;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** One step of a named pose. A flat union rather than parallel lists: a
|
|
503
|
+
* single expression is normally one rotation AND one morph (a jaw ROTATION
|
|
504
|
+
* plus a brow MORPH), so keeping them in one ordered list means a definition
|
|
505
|
+
* never has to zip them. Discriminated by field name: `radians` is a
|
|
506
|
+
* rotation, `meters` a translation, `influence` a morph. */
|
|
507
|
+
export type ShotSetPoseStep = ShotSetPoseRotation | ShotSetPoseMorph | ShotSetPoseTranslation;
|
|
508
|
+
|
|
509
|
+
export type ShotSetShot =
|
|
510
|
+
| { label: string; view: 'turntable'; yaw: number; pose?: string | undefined }
|
|
511
|
+
| {
|
|
512
|
+
label: string;
|
|
513
|
+
view: 'bone-zoom';
|
|
514
|
+
bones: string[];
|
|
515
|
+
spanFraction: number;
|
|
516
|
+
/**
|
|
517
|
+
* The angle the crop is taken FROM, in the same convention and units as
|
|
518
|
+
* a turntable shot's `yaw` (radians; 0 is the front camera, -PI/2 the
|
|
519
|
+
* subject's left, +PI/2 its right). Omitted means 0 — the front-camera
|
|
520
|
+
* framing every bone-zoom shot had before this field existed, so an
|
|
521
|
+
* existing shot set renders unchanged.
|
|
522
|
+
*
|
|
523
|
+
* It exists because the front camera is not a general answer for a long
|
|
524
|
+
* subject: on an 8 m quadruped a crop anchored on the tail root
|
|
525
|
+
* photographs the hind legs standing between the camera and the tail. A
|
|
526
|
+
* junction whose axis runs down the body's length is inspectable only
|
|
527
|
+
* from the side.
|
|
528
|
+
*/
|
|
529
|
+
yaw?: number | undefined;
|
|
530
|
+
pose?: string | undefined;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
export interface AssetPreviewShotSetDefinition {
|
|
534
|
+
/** The set's name (the CLI's `--shots <name>`), echoed in error messages. */
|
|
535
|
+
name: string;
|
|
536
|
+
/** Joints that must exist on the loaded GLB's OWN skeleton — zoom anchors
|
|
537
|
+
* plus a loud failure naming the missing joints (never a silent
|
|
538
|
+
* bounding-box fallback for a set that promised skeleton anchoring). */
|
|
539
|
+
requiredBones?: string[];
|
|
540
|
+
/** Optional clause appended to rig-requirement errors,
|
|
541
|
+
* e.g. "a Mixamo-named humanoid skeleton". */
|
|
542
|
+
rigRequirementHint?: string;
|
|
543
|
+
/** Named poses (bone rotations and morph influences applied to a disposable
|
|
544
|
+
* snapshot only); shots opt in via their `pose` field. */
|
|
545
|
+
poses?: Record<string, ShotSetPoseStep[]>;
|
|
546
|
+
shots: ShotSetShot[];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* A shot the capture engine rendered but does not vouch for.
|
|
551
|
+
*
|
|
552
|
+
* `'empty-frame'` — the shot's frame contained no renderable geometry, so
|
|
553
|
+
* the PNG is background only. It is reported rather than thrown because one
|
|
554
|
+
* mis-aimed crop must not kill a 20-shot render; it is reported LOUDLY
|
|
555
|
+
* because a background tile on a contact sheet otherwise reads as coverage.
|
|
556
|
+
*/
|
|
557
|
+
export interface AssetPreviewShotWarning {
|
|
558
|
+
label: string;
|
|
559
|
+
reason: 'empty-frame';
|
|
560
|
+
/** Already names the shot, its anchor joints and its pose — surfaces print
|
|
561
|
+
* this string rather than re-composing one. */
|
|
562
|
+
message: string;
|
|
563
|
+
bones?: string[];
|
|
564
|
+
pose?: string;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export interface LabeledShotSetCapture {
|
|
568
|
+
width: number;
|
|
569
|
+
height: number;
|
|
570
|
+
shots: Array<ViewportCapture & { label: string }>;
|
|
571
|
+
/** Empty when every shot framed geometry. Absent against an editor that
|
|
572
|
+
* predates the empty-frame guard. */
|
|
573
|
+
warnings: AssetPreviewShotWarning[];
|
|
574
|
+
contactSheet: ViewportCapture & { width: number; height: number };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export interface EditorState {
|
|
578
|
+
/**
|
|
579
|
+
* The Code-OSS workbench this session is running, or `null` when it is
|
|
580
|
+
* running none. The session's children are the session's to report, exactly
|
|
581
|
+
* as its run configurations are — so this is SERVER-computed on every read,
|
|
582
|
+
* never part of the browser-POSTed snapshot.
|
|
583
|
+
*
|
|
584
|
+
* `kind` is how the bytes were obtained: a `release` is an extracted
|
|
585
|
+
* `vscode-reh-web-*` package (its `BUILD.json` carries the commit), `sources`
|
|
586
|
+
* is a fork checkout (`git rev-parse HEAD` is the commit). `dir` is what the
|
|
587
|
+
* project's `.vgai/workbench.json` — or `vgai edit --workbench` — named.
|
|
588
|
+
* `product` is the product whose workbench half is overlaid on those bytes
|
|
589
|
+
* (P3): a workbench is built for ONE product, and the session refuses one
|
|
590
|
+
* built for another than this project's before it spawns.
|
|
591
|
+
* Absent against an older server that predates the field.
|
|
592
|
+
*/
|
|
593
|
+
workbench?: {
|
|
594
|
+
kind: 'release' | 'sources';
|
|
595
|
+
dir: string;
|
|
596
|
+
commit: string;
|
|
597
|
+
product: string;
|
|
598
|
+
} | null;
|
|
599
|
+
/**
|
|
600
|
+
* The PRODUCT this session is serving — `@vgai/game-editor` or
|
|
601
|
+
* `@vgai/model-editor` — or `null` when it is serving none. SERVER-computed
|
|
602
|
+
* on every read, beside {@link workbench}, for the same reason: what a
|
|
603
|
+
* session is running is its own fact, not something the page reports about
|
|
604
|
+
* itself.
|
|
605
|
+
*
|
|
606
|
+
* `id` is the package name, `dir` where it resolved from, `version` what it
|
|
607
|
+
* is. Which product runs is never a switch (ARCHITECTURE-CORE §The target
|
|
608
|
+
* shape, rule 4) — it is what the project's dependencies resolved to — so
|
|
609
|
+
* this is a REPORT and never a thing to branch on. Absent against an older
|
|
610
|
+
* server that predates the field.
|
|
611
|
+
*/
|
|
612
|
+
product?: { id: string; dir: string; version: string } | null;
|
|
613
|
+
playState: 'stopped' | 'playing' | 'paused';
|
|
614
|
+
/**
|
|
615
|
+
* Issue #175 — the REAL engine `GameLoop.liveness` behind the current play
|
|
616
|
+
* session, distinct from `playState` above (editor UI state — a store
|
|
617
|
+
* flag that never reflected whether the loop was actually ticking).
|
|
618
|
+
* `'loop-starved'` means the host loop has observed no recent rAF progress:
|
|
619
|
+
* either the current `document.hidden` gate deliberately parked it, or an
|
|
620
|
+
* armed visible-page callback has not arrived within the starvation
|
|
621
|
+
* interval. It is explicitly NOT a conclusion that the tab is hidden.
|
|
622
|
+
* `null` while not in play mode (no loop to report on) or against an older
|
|
623
|
+
* server that predates this field.
|
|
624
|
+
*/
|
|
625
|
+
loopLiveness?: 'running' | 'loop-starved' | 'stopped' | null;
|
|
626
|
+
/**
|
|
627
|
+
* The connected editor tab's OWN reported visibility/focus
|
|
628
|
+
* (`command-listener.ts`'s `collectPresence`, straight off
|
|
629
|
+
* `document.visibilityState`/`document.hasFocus()`). This is the only field
|
|
630
|
+
* that distinguishes a usable session from a merely attached one:
|
|
631
|
+
* `connected` answers "is an SSE client holding the session open", which a
|
|
632
|
+
* BACKGROUNDED tab satisfies perfectly while the engine hidden-pauses its
|
|
633
|
+
* loop underneath. `null` when the page has no `document` at all; absent
|
|
634
|
+
* against an older server that predates the field.
|
|
635
|
+
*
|
|
636
|
+
* P21 — `reportedAt` is `Date.now()` in the TAB at the moment those two
|
|
637
|
+
* values were read. It is what makes this snapshot readable as a
|
|
638
|
+
* measurement rather than a fact: the tab re-POSTs state on
|
|
639
|
+
* visibilitychange/focus/blur, after commands and on store changes, so
|
|
640
|
+
* between those moments this ages, and a reader with no age has no way to
|
|
641
|
+
* tell a 40ms-old reading from a 40s-old one. Absent against an editor
|
|
642
|
+
* page that predates the field — never fabricated from the read time.
|
|
643
|
+
*/
|
|
644
|
+
presence?: {
|
|
645
|
+
visibility: 'visible' | 'hidden' | 'prerender';
|
|
646
|
+
focused: boolean;
|
|
647
|
+
reportedAt?: number;
|
|
648
|
+
} | null;
|
|
649
|
+
/**
|
|
650
|
+
* The pending-restart reason when source changed while the game was
|
|
651
|
+
* RUNNING and the running session is now stale (e.g. an R3F entry-file
|
|
652
|
+
* write-back during play, a registry.ts edit). The editor's Restart button
|
|
653
|
+
* surfaces the same reason; one restart (`vgai play`, or the button)
|
|
654
|
+
* remounts every root from fresh source and clears it. `null` when the
|
|
655
|
+
* running session is fresh; absent against an older server that predates
|
|
656
|
+
* the field.
|
|
657
|
+
*/
|
|
658
|
+
restartRequired?: string | null;
|
|
659
|
+
selectedEntityId: string | null;
|
|
660
|
+
selectedEntityIds: string[];
|
|
661
|
+
activeViewportTab: ViewportTab;
|
|
662
|
+
/** The actual active center document, including tool/source documents. */
|
|
663
|
+
activeDocumentId?: string | null;
|
|
664
|
+
/** The visible bottom utility, or null when that drawer is collapsed. */
|
|
665
|
+
activeUtilityId?: string | null;
|
|
666
|
+
/** The shared Analytics rail's current durable-session selection. */
|
|
667
|
+
gameplaySession?: {
|
|
668
|
+
selectedSessionId: string | null;
|
|
669
|
+
latestSessionId: string | null;
|
|
670
|
+
selectedStatus: 'live' | 'completed' | null;
|
|
671
|
+
cursorMs: number;
|
|
672
|
+
liveEdgeMs: number;
|
|
673
|
+
};
|
|
674
|
+
/** Every center document the editor currently has open, by stable registry id. */
|
|
675
|
+
openDocumentIds?: string[];
|
|
676
|
+
/** Live editor-owned WebGL renderers, split by resource owner. */
|
|
677
|
+
rendererResources?: {
|
|
678
|
+
hostLive: number;
|
|
679
|
+
interactive: { active: number; idle: number };
|
|
680
|
+
inspectorPreview: { active: number; idle: number };
|
|
681
|
+
};
|
|
682
|
+
activeTabKey: string;
|
|
683
|
+
showGrid: boolean;
|
|
684
|
+
showHelpers: boolean;
|
|
685
|
+
showStats: boolean;
|
|
686
|
+
shadingMode: ShadingMode;
|
|
687
|
+
helperVisibility: HelperVisibility;
|
|
688
|
+
transformMode: 'translate' | 'rotate' | 'scale';
|
|
689
|
+
transformSpace: 'world' | 'local';
|
|
690
|
+
snapEnabled: boolean;
|
|
691
|
+
entityCount: number;
|
|
692
|
+
savePath: string | null;
|
|
693
|
+
/** Live editor persistence state. Wait for `saved` before an external scene-file write. */
|
|
694
|
+
saveState: 'saved' | 'unsaved' | 'failed';
|
|
695
|
+
/** Current editor camera pose when the viewport has bound a camera. */
|
|
696
|
+
camera?: EditorCameraState;
|
|
697
|
+
/** Flattened live authoring hierarchy, useful for agent entity discovery. */
|
|
698
|
+
entities?: EditorEntitySummary[];
|
|
699
|
+
/**
|
|
700
|
+
* How many browser tabs are PRESENT for this session right now, read from
|
|
701
|
+
* the server's tab table (`server/tab-presence.ts`) rather than from a
|
|
702
|
+
* socket count — so a tab mid-reload still counts (it is beating), and a
|
|
703
|
+
* socket with no tab behind it does not.
|
|
704
|
+
*
|
|
705
|
+
* The rest of this object is the last snapshot a browser POSTed and
|
|
706
|
+
* persists even after every tab goes — so a `0` here means the other fields
|
|
707
|
+
* are stale cache and commands (`play`, `scene`, …) will refuse with the
|
|
708
|
+
* table's own reason. Added by the server on every `/__editor/state` read.
|
|
709
|
+
*/
|
|
710
|
+
editorsConnected?: number;
|
|
711
|
+
/** Convenience: `editorsConnected > 0`. */
|
|
712
|
+
connected?: boolean;
|
|
713
|
+
/**
|
|
714
|
+
* ONE ROW PER PRESENT TAB — the whole table, because the owner's rule for
|
|
715
|
+
* this seam is "if they DO get disconnected, make it clear that it
|
|
716
|
+
* happened" and a single boolean can never say that.
|
|
717
|
+
*
|
|
718
|
+
* `lastBeatAgo` is the heartbeat age (null for a tab that cannot beat, e.g.
|
|
719
|
+
* one bridged through the share tunnel); a number climbing past a second or
|
|
720
|
+
* two is a gap in progress. `epochCount` counts page-loads, so a number
|
|
721
|
+
* that keeps rising is a reload loop. `channel: 'down'` with a fresh beat
|
|
722
|
+
* is a tab mid-reload — present, and briefly unable to take a command.
|
|
723
|
+
* `unresponsive` is the zombie: beating, but its page has never opened a
|
|
724
|
+
* command channel this page-load. Absent against an older server.
|
|
725
|
+
*
|
|
726
|
+
* `commandListener` is the standing verdict on the DOCUMENT, and it is a
|
|
727
|
+
* different question from all of the above: the control channel is opened by
|
|
728
|
+
* the tiny pre-React entry before any module loads, so a page that dies
|
|
729
|
+
* during boot beats, holds a channel, gets blessed, and executes nothing. It
|
|
730
|
+
* reads `'not attached'` for that page, `'silent since <t>'` for one whose
|
|
731
|
+
* listener stopped acknowledging relayed commands, and `'ready'` otherwise —
|
|
732
|
+
* each from a timestamp the server already stamps (the listener's own
|
|
733
|
+
* attach/detach report, and the command receipts). Absent against an older
|
|
734
|
+
* server, and absent for a tab the server cannot measure.
|
|
735
|
+
*
|
|
736
|
+
* `pageErrors` is the OTHER half of that verdict — WHY. Uncaught errors and
|
|
737
|
+
* unhandled rejections captured by the page shell's inline bootstrap, which
|
|
738
|
+
* runs before the module graph, so the boot failure that leaves no listener
|
|
739
|
+
* is exactly the one they explain. They come back on a plain GET and need no
|
|
740
|
+
* cooperation from the page beyond the handler itself. `[]` means the page
|
|
741
|
+
* reported none; absent means the server cannot measure this tab.
|
|
742
|
+
*
|
|
743
|
+
* `census` is the tab's RESOURCE PROFILE, sampled by the page every five
|
|
744
|
+
* seconds and carried on the heartbeat: what a browser-level renderer death
|
|
745
|
+
* would otherwise leave unexplained. It is `@volter/editor-sdk/tab-census`'s
|
|
746
|
+
* {@link RecordedTabCensus} — the census MINUS the `mountEpochs` guarantee,
|
|
747
|
+
* because this is a response and the server that answered it may be older
|
|
748
|
+
* than that field; every other absence (`heapUsedMB` off Chromium, renderer
|
|
749
|
+
* counts with no mounted adapter) is documented on the type itself.
|
|
750
|
+
* `censusAgeMs` says how stale the profile is — a hidden tab is not sampled.
|
|
751
|
+
* The census's `blender` block is the OTHER question it carries: how long the
|
|
752
|
+
* tab's Blender worker has been holding a call and how long this thread has
|
|
753
|
+
* stalled, absent in a tab with no Blender session.
|
|
754
|
+
*/
|
|
755
|
+
tabs?: Array<{
|
|
756
|
+
/**
|
|
757
|
+
* WHAT IS TRUE OF THIS TAB, in one word — `ended`, `closed`, `reloading`,
|
|
758
|
+
* `crashed`, `suspended`, `hung`, `busy` or `present` — derived
|
|
759
|
+
* server-side by ONE function over the fields below
|
|
760
|
+
* (`server/tab-presence.ts`'s `tabState`), never re-derived by a reader.
|
|
761
|
+
*
|
|
762
|
+
* It exists because every other field on this row answers a NARROWER
|
|
763
|
+
* question than the one that gets asked. Measured 2026-09-17: one symptom
|
|
764
|
+
* ("the battery stopped") had four causes in one night — a renderer killed
|
|
765
|
+
* by a dev-server reload, a worker that never finished booting, a main
|
|
766
|
+
* thread wedged inside a 46 MB encode, and a twin call genuinely running
|
|
767
|
+
* for sixteen minutes — and each of them is a different instruction to
|
|
768
|
+
* whoever is reading. `stateMs` is THE number that goes with the word, and
|
|
769
|
+
* it is a different measurement per state (time since the goodbye, beat
|
|
770
|
+
* age, the gap that was resumed, census age, the outstanding call's age);
|
|
771
|
+
* `stateBecause` is the evidence in a sentence, so a reader never has to
|
|
772
|
+
* know which field the verdict came from. Absent against an older server.
|
|
773
|
+
*/
|
|
774
|
+
state?:
|
|
775
|
+
| 'ended'
|
|
776
|
+
| 'closed'
|
|
777
|
+
| 'reloading'
|
|
778
|
+
| 'crashed'
|
|
779
|
+
| 'suspended'
|
|
780
|
+
| 'hung'
|
|
781
|
+
| 'busy'
|
|
782
|
+
| 'present';
|
|
783
|
+
stateMs?: number | null;
|
|
784
|
+
stateBecause?: string;
|
|
785
|
+
tabId8: string;
|
|
786
|
+
presentFor: number;
|
|
787
|
+
lastBeatAgo: number | null;
|
|
788
|
+
epochCount: number;
|
|
789
|
+
/** Age of the DOCUMENT running in this tab (this page-load), as opposed
|
|
790
|
+
* to `presentFor`, which is the age of the tab and survives its
|
|
791
|
+
* reloads. Absent against an older server. */
|
|
792
|
+
epochAgeMs?: number;
|
|
793
|
+
visibility: 'visible' | 'hidden';
|
|
794
|
+
route: 'project' | 'no-project' | 'unknown';
|
|
795
|
+
/**
|
|
796
|
+
* WHAT KIND OF PAGE this tab is: the editor's own page, or a Code-OSS
|
|
797
|
+
* workbench window running it through the frame (docs/CODE-OSS.md §Boot,
|
|
798
|
+
* DESKTOP). A reader prints it because "a VS Code window that beats" and
|
|
799
|
+
* "a browser tab that beats" are the same health and different places to
|
|
800
|
+
* look when they are not. Absent against an older server.
|
|
801
|
+
*/
|
|
802
|
+
surface?: 'editor' | 'vscode';
|
|
803
|
+
blessed: boolean;
|
|
804
|
+
channel: 'open' | 'down';
|
|
805
|
+
unresponsive: boolean;
|
|
806
|
+
commandListener?: 'ready' | 'not attached' | (string & {});
|
|
807
|
+
pageErrors?: string[];
|
|
808
|
+
/**
|
|
809
|
+
* The control-plane generation handshake for each page load currently
|
|
810
|
+
* associated with this physical tab. Anything other than `aligned` is a
|
|
811
|
+
* lifecycle contradiction or a handshake still in progress.
|
|
812
|
+
*/
|
|
813
|
+
controlLifecycles?: Array<{
|
|
814
|
+
status: 'aligned' | 'awaiting-heartbeat' | 'unconfirmed' | 'mismatch';
|
|
815
|
+
serverGeneration8: string;
|
|
816
|
+
connectionGeneration8: string;
|
|
817
|
+
pageGeneration8: string;
|
|
818
|
+
clientId8: string;
|
|
819
|
+
}>;
|
|
820
|
+
census?: RecordedTabCensus | null;
|
|
821
|
+
censusAgeMs?: number | null;
|
|
822
|
+
}>;
|
|
823
|
+
/**
|
|
824
|
+
* TABS THAT ARE GONE — the tab table's short departure memory, same row
|
|
825
|
+
* shape as {@link tabs} above.
|
|
826
|
+
*
|
|
827
|
+
* `ended`, `closed` and `crashed` are verdicts about a tab that is no longer present,
|
|
828
|
+
* so this is the only array they can appear in, and they are exactly the two
|
|
829
|
+
* answers the product could not give before: a tab whose beats stopped left
|
|
830
|
+
* the table and took its explanation with it. Kept separate from `tabs`
|
|
831
|
+
* because `editorsConnected` counts that one — a dead row inside it would
|
|
832
|
+
* make a crashed tab read as a connected one. Bounded by age and count
|
|
833
|
+
* (a memory, not a log; the session journal is the archive). Absent against
|
|
834
|
+
* an older server.
|
|
835
|
+
*/
|
|
836
|
+
departedTabs?: EditorState['tabs'];
|
|
837
|
+
/**
|
|
838
|
+
* The auto-open runaway guard. `stopped` means this session opened
|
|
839
|
+
* `attempts` tabs, none of them ever appeared in the table, and it has
|
|
840
|
+
* stopped trying — the browser, not the editor, is what to check.
|
|
841
|
+
*/
|
|
842
|
+
tabAutoOpen?: { attempts: number; stopped: boolean };
|
|
843
|
+
/**
|
|
844
|
+
* Epoch ms of the last genuine HTML page load this server served — i.e.
|
|
845
|
+
* when the editor tab last did a FULL document load (first open, reload,
|
|
846
|
+
* self-heal). Server-observed (`editor-server.ts`'s response-finish
|
|
847
|
+
* middleware), never browser-reported. `null` until the first page load.
|
|
848
|
+
*
|
|
849
|
+
* P20 reads this against {@link publicAssets} to answer "did bytes under
|
|
850
|
+
* `public/` change since the running document loaded", which is when
|
|
851
|
+
* module-scope loaders and the page-lifetime asset caches (Pixi `Assets`,
|
|
852
|
+
* three's loader caches) can still be serving the OLD bytes.
|
|
853
|
+
*/
|
|
854
|
+
lastIndexRequestAt?: number | null;
|
|
855
|
+
/**
|
|
856
|
+
* P20 — what the server has OBSERVED land under this project's `public/`
|
|
857
|
+
* during this server lifetime, from the same chokidar watcher that
|
|
858
|
+
* broadcasts `assets-changed`. `lastChangedAt` is `null` when nothing has
|
|
859
|
+
* changed since the server started. Absent against an older server.
|
|
860
|
+
*
|
|
861
|
+
* This is a divergence signal, not a cache verdict: nothing here knows
|
|
862
|
+
* whether the running page actually holds a stale copy of those bytes,
|
|
863
|
+
* only that they changed after it loaded.
|
|
864
|
+
*/
|
|
865
|
+
publicAssets?: {
|
|
866
|
+
lastChangedAt: number | null;
|
|
867
|
+
lastPath: string | null;
|
|
868
|
+
changedCount: number;
|
|
869
|
+
};
|
|
870
|
+
/**
|
|
871
|
+
* Epoch ms when the server last received a state POST from a browser tab —
|
|
872
|
+
* i.e. the age of the cached snapshot above. Omitted if no tab has ever
|
|
873
|
+
* posted state this server lifetime (or since the last project switch,
|
|
874
|
+
* which clears the cache). Present regardless of `connected`, but only
|
|
875
|
+
* meaningful for interpreting staleness when `connected` is `false`.
|
|
876
|
+
*/
|
|
877
|
+
stateUpdatedAt?: number;
|
|
878
|
+
/**
|
|
879
|
+
* Validate-on-change (#103): per-file validation status for every
|
|
880
|
+
* Project `src/**` source / `vgai.project.json` the dev
|
|
881
|
+
* server has seen
|
|
882
|
+
* change since it booted (or since the last project switch). Server-
|
|
883
|
+
* computed — unlike the rest of `EditorState`, it is NOT part of the
|
|
884
|
+
* browser-POSTed snapshot, so it is always current. A file appears here
|
|
885
|
+
* ONLY while it is currently failing; a clean write removes its entry
|
|
886
|
+
* (absence means "not known to be invalid", not "never checked"). Always
|
|
887
|
+
* present (`{}` when nothing is failing) so `vgai status` consumers can
|
|
888
|
+
* read it unconditionally.
|
|
889
|
+
*/
|
|
890
|
+
projectValidation?: Record<string, { errors: string[]; at: number }>;
|
|
891
|
+
/**
|
|
892
|
+
* PD-13: the WARNING half of the same server-computed validation pass —
|
|
893
|
+
* authoring-convention findings (the R3F00x codes, OID surface conflicts)
|
|
894
|
+
* that do not make a file invalid but do make it unauthorable. Same shape,
|
|
895
|
+
* same lifecycle and same freshness guarantee as `projectValidation` above:
|
|
896
|
+
* whole-project (not just the open document), keyed by project-relative
|
|
897
|
+
* path, present only while the file currently warns, `{}` when clean.
|
|
898
|
+
*
|
|
899
|
+
* The server has sent this since the R3F authoring diagnostics landed; it
|
|
900
|
+
* was missing from this interface, so every typed consumer — `vgai status`
|
|
901
|
+
* included — could only reach it through a cast. Declared here so a caller
|
|
902
|
+
* that wants to react to authoring warnings can see they exist.
|
|
903
|
+
*/
|
|
904
|
+
projectWarnings?: Record<string, { warnings: string[]; at: number }>;
|
|
905
|
+
/**
|
|
906
|
+
* PD-14: whether the SOURCE half of the validation pass above is running at
|
|
907
|
+
* all. `'active'` is the normal state; `'awaiting-src'` means the project
|
|
908
|
+
* has no `src/` directory yet, so nothing under `src/` is being validated —
|
|
909
|
+
* an empty `projectValidation`/`projectWarnings` says nothing about source
|
|
910
|
+
* files while this reads `'awaiting-src'`. It is not terminal: the watcher
|
|
911
|
+
* is armed on the not-yet-existing path and flips to `'active'` (running
|
|
912
|
+
* the boot-equivalent scan) the moment `src/` appears, with no restart.
|
|
913
|
+
* `'no-project'` when no project is open. Server-computed, like its
|
|
914
|
+
* neighbors above.
|
|
915
|
+
*/
|
|
916
|
+
sourceValidation?: 'active' | 'awaiting-src' | 'no-project';
|
|
917
|
+
/**
|
|
918
|
+
* The compatibility verdict between this editor and the project it serves —
|
|
919
|
+
* `null` when they agree, otherwise the refusal the browser renders when it
|
|
920
|
+
* declines to activate the project, with its recovery guidance.
|
|
921
|
+
*
|
|
922
|
+
* Server-computed per read like its neighbors above. It is here because the
|
|
923
|
+
* gate was previously reported ONLY in the browser: an editor started on an
|
|
924
|
+
* incompatible project serves happily (it activates nothing), so the tab
|
|
925
|
+
* showed "This project is pinned to @volter/editor-project X, but this editor is
|
|
926
|
+
* running Y" while `vgai status` reported a connected session with empty
|
|
927
|
+
* validation and `vgai play` timed out into a retry message about the tab
|
|
928
|
+
* reloading. An agent drives this editor through the CLI, so a gate visible
|
|
929
|
+
* only in pixels is invisible by construction.
|
|
930
|
+
*/
|
|
931
|
+
projectCompatibility?: {
|
|
932
|
+
error: string;
|
|
933
|
+
recovery?: { kind: string; title: string; guidance: string; command?: string };
|
|
934
|
+
} | null;
|
|
935
|
+
/**
|
|
936
|
+
* #124: the absolute path of the project this editor server currently has
|
|
937
|
+
* open — server-computed (never part of the browser-POSTed snapshot,
|
|
938
|
+
* exactly like `projectValidation` above), so it is always current. Added
|
|
939
|
+
* so a watcher/relay holding only a port number (e.g. an agent that
|
|
940
|
+
* printed a `vgai edit` URL earlier and lost track of which project it
|
|
941
|
+
* belongs to) can identify which project that port serves without also
|
|
942
|
+
* reading the `~/.vgai/editor-sessions.json` registry file. `null` when no
|
|
943
|
+
* project is open (the in-repo "no project selected" default server
|
|
944
|
+
* state — mirrors `/__editor/project`'s own `{ project: null }` shape).
|
|
945
|
+
*/
|
|
946
|
+
projectRoot?: string | null;
|
|
947
|
+
/**
|
|
948
|
+
* #124: the open project's declared name, alongside `projectRoot` above
|
|
949
|
+
* (`vgai.project.json`'s `name`). `null` when no project is open, or the open
|
|
950
|
+
* project has no readable manifest name.
|
|
951
|
+
*/
|
|
952
|
+
projectName?: string | null;
|
|
953
|
+
/**
|
|
954
|
+
* The open project's ADAPTER, resolved (ARCHITECTURE-CORE §The editor
|
|
955
|
+
* protocol). `source` names WHOSE declaration is running: `'project'` = the
|
|
956
|
+
* project's own `vgai.adapter.ts` supplied the binding table (and it always
|
|
957
|
+
* outranks the registry); `'registry'` = the HOST's in-tree ingest registry
|
|
958
|
+
* supplied it, matched on this project's ingest root id, with `modulePath`
|
|
959
|
+
* naming the repo file — a binding the project did not ship, stated rather
|
|
960
|
+
* than inferred; `'native'` = it declared none and got `nativeAdapter()` —
|
|
961
|
+
* the declared native default, not a silent fallback.
|
|
962
|
+
*
|
|
963
|
+
* `null`/absent means NOBODY HAS LOOKED YET (no project open, or the load
|
|
964
|
+
* has not finished), which is deliberately distinct from a loaded adapter
|
|
965
|
+
* whose `scenes.entries` is empty — that is a real, gradable answer. A
|
|
966
|
+
* non-null `error` means the project's own module did NOT load and the
|
|
967
|
+
* table below is the native default standing in, with the failure named.
|
|
968
|
+
*
|
|
969
|
+
* Structurally declared here rather than imported from
|
|
970
|
+
* `@volter/editor-project/adapter/adapter-module` because this interface is the WIRE
|
|
971
|
+
* contract: everything in it has already been through JSON.
|
|
972
|
+
*/
|
|
973
|
+
adapter?: {
|
|
974
|
+
source: 'project' | 'registry' | 'native';
|
|
975
|
+
modulePath: string | null;
|
|
976
|
+
regions: {
|
|
977
|
+
id: string;
|
|
978
|
+
surface: string;
|
|
979
|
+
projector: string;
|
|
980
|
+
dialect: string | null;
|
|
981
|
+
anchors: string[];
|
|
982
|
+
}[];
|
|
983
|
+
scenes: {
|
|
984
|
+
default: string | null;
|
|
985
|
+
entries: {
|
|
986
|
+
id: string;
|
|
987
|
+
label: string;
|
|
988
|
+
/** OPEN (ARCHITECTURE-CORE §The project model, "Documents, not
|
|
989
|
+
* scenes"): `scene` and `prefab` are the first two kinds; a page,
|
|
990
|
+
* a model, a shot, a take are kinds the same way. */
|
|
991
|
+
kind: string;
|
|
992
|
+
region: string | null;
|
|
993
|
+
authorable: boolean;
|
|
994
|
+
reach: { kind: string; [field: string]: unknown };
|
|
995
|
+
source?: { path: string; export?: string };
|
|
996
|
+
finder?: string;
|
|
997
|
+
}[];
|
|
998
|
+
};
|
|
999
|
+
notes: string[];
|
|
1000
|
+
error: string | null;
|
|
1001
|
+
} | null;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
export type ViewPreset = 'top' | 'front' | 'right' | 'perspective';
|
|
1005
|
+
export type ShadingMode =
|
|
1006
|
+
| 'solid'
|
|
1007
|
+
| 'clay'
|
|
1008
|
+
| 'unlit'
|
|
1009
|
+
| 'wireframe'
|
|
1010
|
+
| 'matcap'
|
|
1011
|
+
| 'normals'
|
|
1012
|
+
| 'overdraw';
|
|
1013
|
+
|
|
1014
|
+
export type TransformMode = 'translate' | 'rotate' | 'scale';
|
|
1015
|
+
export type TransformSpace = 'world' | 'local';
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Stable editor-owned workspace documents that may appear in a shareable
|
|
1019
|
+
* view. One runtime list owns both URL parsing and the public id type.
|
|
1020
|
+
*
|
|
1021
|
+
* This SDK cannot import the editor — the dependency runs the other way — so
|
|
1022
|
+
* nothing links this list to the documents the editor actually registers, and
|
|
1023
|
+
* an id added there is silently unaddressable here (a view carrying it
|
|
1024
|
+
* round-trips to nothing). The link is asserted from the side that CAN see
|
|
1025
|
+
* both: `packages/editor/test/editor-view-address-space.test.ts`. Adding a
|
|
1026
|
+
* document id means adding it here too.
|
|
1027
|
+
*/
|
|
1028
|
+
export const EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS = [
|
|
1029
|
+
'workspace:scene',
|
|
1030
|
+
'workspace:canvas-scene',
|
|
1031
|
+
'workspace:game',
|
|
1032
|
+
'workspace:3d-components',
|
|
1033
|
+
'workspace:2d-components',
|
|
1034
|
+
'workspace:ui-components',
|
|
1035
|
+
'account',
|
|
1036
|
+
'project-tools',
|
|
1037
|
+
] as const;
|
|
1038
|
+
|
|
1039
|
+
export type EditorViewWorkspaceDocumentId = (typeof EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS)[number];
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* The editor's OWN bottom-drawer instruments, as named in a shareable view.
|
|
1043
|
+
* One runtime list owns both URL parsing and the public id type — the same
|
|
1044
|
+
* discipline as {@link EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS}, and for the same
|
|
1045
|
+
* reason: a separate hand-written union and parser allowlist drift silently
|
|
1046
|
+
* (`behavior` was in the union and missing from the allowlist, so a view
|
|
1047
|
+
* carrying it round-tripped to nothing).
|
|
1048
|
+
*
|
|
1049
|
+
* This must equal the editor's live `BUILT_IN_WORKSPACE_UTILITIES`
|
|
1050
|
+
* (`packages/editor/src/workspace-core-utilities.ts`). It had drifted to five
|
|
1051
|
+
* of thirteen — every id from `generations` onward was unaddressable in a
|
|
1052
|
+
* shared view. As above, the SDK cannot import the editor to derive this, so
|
|
1053
|
+
* `packages/editor/test/editor-view-address-space.test.ts` asserts the
|
|
1054
|
+
* equality from the side that sees both.
|
|
1055
|
+
*/
|
|
1056
|
+
export const EDITOR_VIEW_BUILT_IN_UTILITY_IDS = [
|
|
1057
|
+
'animation',
|
|
1058
|
+
'behavior',
|
|
1059
|
+
'generations',
|
|
1060
|
+
'console',
|
|
1061
|
+
'light-explorer',
|
|
1062
|
+
'story-actions',
|
|
1063
|
+
'story-interactions',
|
|
1064
|
+
'story-accessibility',
|
|
1065
|
+
] as const;
|
|
1066
|
+
|
|
1067
|
+
export type EditorViewBuiltInUtilityId = (typeof EDITOR_VIEW_BUILT_IN_UTILITY_IDS)[number];
|
|
1068
|
+
|
|
1069
|
+
/** The prefix a PROJECT-contributed utility's id carries. The editor's tool
|
|
1070
|
+
* loader namespaces every `workspace.utility` contribution as
|
|
1071
|
+
* `tool:<contribution-id>`, exactly as a contributed center document is
|
|
1072
|
+
* addressed by `{ kind: 'tool', id }`. */
|
|
1073
|
+
export const EDITOR_VIEW_TOOL_UTILITY_PREFIX = 'tool:';
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Which bottom-drawer utility a view reveals: one of the editor's own
|
|
1077
|
+
* instruments, or a utility the OPEN PROJECT contributes.
|
|
1078
|
+
*
|
|
1079
|
+
* The project half cannot be an enum — which tabs exist depends entirely on
|
|
1080
|
+
* the game that is open — so the address space admits the namespace and the
|
|
1081
|
+
* EDITOR validates the id against its live utility registry when the view is
|
|
1082
|
+
* presented, refusing an unregistered one by naming what IS registered.
|
|
1083
|
+
*/
|
|
1084
|
+
export type EditorViewUtility =
|
|
1085
|
+
| EditorViewBuiltInUtilityId
|
|
1086
|
+
| `${typeof EDITOR_VIEW_TOOL_UTILITY_PREFIX}${string}`;
|
|
1087
|
+
|
|
1088
|
+
/** Whether `value` is addressable as a view's utility. Shape only — existence
|
|
1089
|
+
* is the editor's answer at present-time, not the URL's. */
|
|
1090
|
+
export function isEditorViewUtility(value: string): value is EditorViewUtility {
|
|
1091
|
+
return (
|
|
1092
|
+
(EDITOR_VIEW_BUILT_IN_UTILITY_IDS as readonly string[]).includes(value) ||
|
|
1093
|
+
(value.startsWith(EDITOR_VIEW_TOOL_UTILITY_PREFIX) &&
|
|
1094
|
+
value.length > EDITOR_VIEW_TOOL_UTILITY_PREFIX.length)
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* A durable, intentionally small projection of what an editor is presenting.
|
|
1100
|
+
* This is not workspace persistence: panel sizes, transient tool state, and
|
|
1101
|
+
* authored document contents remain outside the URL.
|
|
1102
|
+
*/
|
|
1103
|
+
export type EditorViewDocument =
|
|
1104
|
+
| { kind: 'scene'; path: string }
|
|
1105
|
+
| { kind: 'asset'; path: string; entityId?: never; assetKind?: AssetKind }
|
|
1106
|
+
| { kind: 'asset'; entityId: string; path?: never; assetKind?: 'model' }
|
|
1107
|
+
| { kind: 'tool'; id: string }
|
|
1108
|
+
/** A document the adapter's table lists (a model, a page) — `id` is the
|
|
1109
|
+
* table entry's id — opened in the editor registered for its kind. */
|
|
1110
|
+
| { kind: 'document'; id: string }
|
|
1111
|
+
| { kind: 'world'; id: string }
|
|
1112
|
+
| { kind: 'story'; modulePath: string; storyName: string; mode?: 'preview' | 'docs' }
|
|
1113
|
+
| { kind: 'project-tool'; name: string }
|
|
1114
|
+
| { kind: 'generation'; id: string }
|
|
1115
|
+
| {
|
|
1116
|
+
kind: 'workspace';
|
|
1117
|
+
id: EditorViewWorkspaceDocumentId;
|
|
1118
|
+
/**
|
|
1119
|
+
* For a COMPONENT BOARD document: the portable story frame to open on
|
|
1120
|
+
* — a story id, or a unique CSF export name or label (an ambiguous or
|
|
1121
|
+
* unknown value refuses loudly, listing candidates). Emitted back by
|
|
1122
|
+
* the board's own presentation so a captured view round-trips. This is
|
|
1123
|
+
* the design ledger's "board story selector": without it only the
|
|
1124
|
+
* derived default story could ever be addressed.
|
|
1125
|
+
*/
|
|
1126
|
+
story?: string;
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* The document ADDRESS KINDS an `EditorView` can carry — the same published,
|
|
1131
|
+
* finite vocabulary `EDITOR_VIEW_KEYS` is for the view's own keys, and for the
|
|
1132
|
+
* same reason: a kind this list does not hold must refuse by name rather than
|
|
1133
|
+
* fall off the end of a switch. `{kind: 'model', path}` used to present `ok`,
|
|
1134
|
+
* move nothing, and echo the caller's own mistake back in `view.doc=undefined`.
|
|
1135
|
+
*
|
|
1136
|
+
* A document KIND (`model`, `page`) is not one of these: it is the kind of a
|
|
1137
|
+
* row in the project's own document table, and its address is
|
|
1138
|
+
* `{kind: 'document', id}` — "a document opens in the editor registered for its
|
|
1139
|
+
* KIND" (ARCHITECTURE-CORE). The refusal says exactly that, reading the kinds
|
|
1140
|
+
* from the project's resolved table rather than from a list written here.
|
|
1141
|
+
*/
|
|
1142
|
+
export const EDITOR_VIEW_DOCUMENT_KINDS = [
|
|
1143
|
+
'scene',
|
|
1144
|
+
'asset',
|
|
1145
|
+
'tool',
|
|
1146
|
+
'document',
|
|
1147
|
+
'world',
|
|
1148
|
+
'story',
|
|
1149
|
+
'project-tool',
|
|
1150
|
+
'generation',
|
|
1151
|
+
'workspace',
|
|
1152
|
+
] as const satisfies ReadonlyArray<EditorViewDocument['kind']>;
|
|
1153
|
+
|
|
1154
|
+
/** Narrow an arbitrary string to one of {@link EDITOR_VIEW_DOCUMENT_KINDS}. */
|
|
1155
|
+
export function isEditorViewDocumentKind(value: string): value is EditorViewDocument['kind'] {
|
|
1156
|
+
return (EDITOR_VIEW_DOCUMENT_KINDS as readonly string[]).includes(value);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
export interface EditorView {
|
|
1160
|
+
version: 1;
|
|
1161
|
+
/** The workspace (a layout: `model`, `sculpt`, `game`, …) the view is in —
|
|
1162
|
+
* the host's own or a package's `workspace.layout` contribution. Presenting
|
|
1163
|
+
* one the open project does not offer REFUSES and names the vocabulary, the
|
|
1164
|
+
* same answer as `set-workspace`; `currentView` always reports it. */
|
|
1165
|
+
workspace?: string;
|
|
1166
|
+
/** The style bundle (`classic`, `glass`, `blender`, …) the chrome wears —
|
|
1167
|
+
* the host's own or a package's `workspace.style` contribution. Presenting
|
|
1168
|
+
* one the open project does not offer REFUSES and names the vocabulary,
|
|
1169
|
+
* the same answer as `set-style`; `currentView` reports it when the four
|
|
1170
|
+
* appearance axes match a bundle, and omits it for a custom mix. */
|
|
1171
|
+
style?: string;
|
|
1172
|
+
/**
|
|
1173
|
+
* The keymap (`vgai`, `blender`, …) whose bindings the chrome is printing
|
|
1174
|
+
* and dispatching — the editor's own or a package's `workspace.keymap`
|
|
1175
|
+
* contribution, as the project's adapter declares it or its settings
|
|
1176
|
+
* override it. REPORTED, never presented: `currentView` always answers it,
|
|
1177
|
+
* and `present-view` WARNS on a keymap it was handed rather than switching,
|
|
1178
|
+
* because which bindings a project uses is that project's declaration and a
|
|
1179
|
+
* person's preference, not a property of a shared link.
|
|
1180
|
+
*/
|
|
1181
|
+
keymap?: string;
|
|
1182
|
+
/**
|
|
1183
|
+
* The static PANEL the dock is focused on — `hierarchy`, `asset-library`,
|
|
1184
|
+
* `inspector`, and whatever else the editor's panel registry holds. Shape
|
|
1185
|
+
* only here, exactly like `workspace`: the vocabulary is the open editor's
|
|
1186
|
+
* registry, so presenting a key it does not hold REFUSES and names the ones
|
|
1187
|
+
* it does. `currentView` reports it while a panel (rather than a document or
|
|
1188
|
+
* a drawer utility) holds the dock's focus.
|
|
1189
|
+
*/
|
|
1190
|
+
panel?: string;
|
|
1191
|
+
document?: EditorViewDocument;
|
|
1192
|
+
selection?: { ids: string[]; focus?: boolean };
|
|
1193
|
+
viewport?: {
|
|
1194
|
+
camera?: ViewPreset | 'isometric' | EditorCameraState;
|
|
1195
|
+
diagnostic?: ShadingMode | 'uv' | 'vertex-colors' | 'bounds' | 'skeleton';
|
|
1196
|
+
frame?: 'document' | 'selection';
|
|
1197
|
+
grid?: boolean;
|
|
1198
|
+
};
|
|
1199
|
+
utility?: EditorViewUtility;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* THE ADDRESS SPACE, AS DATA — every key {@link EditorView} carries, so the
|
|
1204
|
+
* presenter can REFUSE one it does not (`editor-view-presentation.ts`).
|
|
1205
|
+
*
|
|
1206
|
+
* It lives beside the interface because that is the only placement where a
|
|
1207
|
+
* drift is visible in one screen: adding a field above without adding its key
|
|
1208
|
+
* here is the whole failure mode, and the row order is the interface's.
|
|
1209
|
+
*
|
|
1210
|
+
* WHY IT EXISTS (found live, 2026-09-19, unit 17's proof run): a view with the
|
|
1211
|
+
* document's `kind` spelled at the TOP level — `{version: 1, kind: 'story',
|
|
1212
|
+
* modulePath, storyName}` instead of `{version: 1, document: {kind: 'story',
|
|
1213
|
+
* …}}` — presented `ok`, changed nothing, and ECHOED THE BOGUS KEY BACK in
|
|
1214
|
+
* `PresentedEditorView.view`, so the caller read its own mistake as
|
|
1215
|
+
* confirmation. Three `present` proofs were recorded against it before
|
|
1216
|
+
* `currentView()` showed the document had never moved. Every NAMED field here
|
|
1217
|
+
* already refuses by name (`style`, `workspace`, `panel`); an unnamed one was
|
|
1218
|
+
* the silent hole, which is the case CLAUDE.md's "unknown input must REJECT
|
|
1219
|
+
* LOUDLY rather than be partially read" is about.
|
|
1220
|
+
*/
|
|
1221
|
+
export const EDITOR_VIEW_KEYS = [
|
|
1222
|
+
'version',
|
|
1223
|
+
'workspace',
|
|
1224
|
+
'style',
|
|
1225
|
+
'keymap',
|
|
1226
|
+
'panel',
|
|
1227
|
+
'document',
|
|
1228
|
+
'selection',
|
|
1229
|
+
'viewport',
|
|
1230
|
+
'utility',
|
|
1231
|
+
] as const satisfies ReadonlyArray<keyof EditorView>;
|
|
1232
|
+
|
|
1233
|
+
export interface PresentedEditorView {
|
|
1234
|
+
view: EditorView;
|
|
1235
|
+
/** Shareable URL for the durable projection that was applied. */
|
|
1236
|
+
url: string;
|
|
1237
|
+
/** Honest degradations; an unsupported requested view is never silent. */
|
|
1238
|
+
warnings: string[];
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* How big a capture comes back.
|
|
1243
|
+
*
|
|
1244
|
+
* A NUMBER is a square of that size, and square stays the default — an
|
|
1245
|
+
* unstaged look at a model is a square question. `{width, height}` is for the
|
|
1246
|
+
* shaped answer: a video-aspect frame that needs no crop afterwards, which is
|
|
1247
|
+
* what the model/module lanes' looks are actually for.
|
|
1248
|
+
*
|
|
1249
|
+
* Both are bounded by the EDITOR's own ceiling — 64..1024 per side, plus a
|
|
1250
|
+
* total no larger than a 1024 square. That is the relay budget, not a taste:
|
|
1251
|
+
* the pixels cross the editor relay as base64 JSON, and 1024 is where even
|
|
1252
|
+
* incompressible RGBA still fits its 50 MB request limit (`asset-preview.ts`'s
|
|
1253
|
+
* `MIN_SIZE`/`MAX_SIZE`). For more picture, take more views, not bigger ones.
|
|
1254
|
+
*/
|
|
1255
|
+
export type CaptureDimensions = number | { readonly width: number; readonly height: number };
|
|
1256
|
+
|
|
1257
|
+
/** The pixels of the active center document, with enough provenance for an
|
|
1258
|
+
* agent to prove which user-visible subject it captured. Editor chrome is
|
|
1259
|
+
* deliberately excluded. */
|
|
1260
|
+
/** A photograph of the editor PAGE — every panel as the person sees it
|
|
1261
|
+
* (`capture-editor-chrome`; the door that lets a skin, a workspace or a
|
|
1262
|
+
* contributed panel be judged sighted through the product). */
|
|
1263
|
+
export interface EditorChromeCapture extends ViewportCapture {
|
|
1264
|
+
view: EditorView;
|
|
1265
|
+
/** The frame's own size, in OUTPUT pixels. */
|
|
1266
|
+
size: { width: number; height: number };
|
|
1267
|
+
/** Output pixels per CSS pixel — what one pixel of {@link size} is. */
|
|
1268
|
+
scale: number;
|
|
1269
|
+
layers: { canvases: number; domOverlays: number };
|
|
1270
|
+
flatness?: { degenerate: boolean; warning?: string };
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/** How many output pixels one CSS pixel of the editor page becomes; defaults
|
|
1274
|
+
* to the page's own `devicePixelRatio`, at most 4. */
|
|
1275
|
+
export interface EditorChromeCaptureOptions {
|
|
1276
|
+
readonly scale?: number;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
export interface ActiveDocumentCapture extends ViewportCapture {
|
|
1280
|
+
document: {
|
|
1281
|
+
id: string;
|
|
1282
|
+
title: string;
|
|
1283
|
+
kind: string;
|
|
1284
|
+
sourcePath?: string;
|
|
1285
|
+
rootId?: string;
|
|
1286
|
+
};
|
|
1287
|
+
view: EditorView;
|
|
1288
|
+
source: 'scene-viewport' | 'game-composite' | 'object3d-document' | 'document-composite';
|
|
1289
|
+
layers?: { canvases: number; domOverlays: number };
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/** Template ids accepted by the editor's create-project endpoint. */
|
|
1293
|
+
export type ProjectTemplate = 'default' | '2d' | 'react' | 'example';
|
|
1294
|
+
|
|
1295
|
+
export interface ProjectInfo {
|
|
1296
|
+
path: string;
|
|
1297
|
+
config: { name: string; [key: string]: unknown };
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
export interface RecentProject {
|
|
1301
|
+
name: string;
|
|
1302
|
+
path: string;
|
|
1303
|
+
lastOpened: string;
|
|
1304
|
+
thumbnail?: string;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
export type ProjectToolOutcome =
|
|
1308
|
+
| { ok: true; data: unknown; generation?: GenerationJob; generationWarning?: string }
|
|
1309
|
+
| {
|
|
1310
|
+
ok: false;
|
|
1311
|
+
error: {
|
|
1312
|
+
code: string;
|
|
1313
|
+
message: string;
|
|
1314
|
+
data?: unknown;
|
|
1315
|
+
issues?: Array<{ path?: PropertyKey[]; message?: string; [key: string]: unknown }>;
|
|
1316
|
+
};
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
import type { GenerationJob } from '@volter/editor-sdk/generations';
|
|
1320
|
+
import type { RecordedTabCensus } from '@volter/editor-sdk/tab-census';
|
|
1321
|
+
|
|
1322
|
+
export type {
|
|
1323
|
+
ProjectToolCatalog,
|
|
1324
|
+
ProjectToolCatalogEntry,
|
|
1325
|
+
ProjectToolContribution,
|
|
1326
|
+
ToolContributionPoint,
|
|
1327
|
+
} from '@volter/editor-sdk/project-tool-catalog';
|
|
1328
|
+
|
|
1329
|
+
// ---------------------------------------------------------------------------
|
|
1330
|
+
// Inspection — the serialized inspection subject (`EditorClient.inspect`)
|
|
1331
|
+
// ---------------------------------------------------------------------------
|
|
1332
|
+
//
|
|
1333
|
+
// The wire mirror of the editor's own `SerializedInspectionSubject`
|
|
1334
|
+
// (`packages/editor/src/inspection/serialize.ts`, which owns the contract and
|
|
1335
|
+
// carries the reasoning). `command-listener.ts` annotates its `inspect`
|
|
1336
|
+
// payload with this type, so `tsc` checks the two sides against each other on
|
|
1337
|
+
// every build rather than letting them drift silently.
|
|
1338
|
+
|
|
1339
|
+
/** Where the inspector's subject lives; `asset-lab` is an open asset
|
|
1340
|
+
* document — the three paradigm scoped to a subtree, inspected in the same
|
|
1341
|
+
* box as the scene. */
|
|
1342
|
+
export type InspectionSurface = 'three' | 'canvas' | 'dom' | 'asset-lab';
|
|
1343
|
+
|
|
1344
|
+
/** Which LAYOUT the one inspector box is in: the compact box over the
|
|
1345
|
+
* viewport, the same sections stacked in the dock column, or that column
|
|
1346
|
+
* with the sections tabbed behind a vertical rail (`properties`). */
|
|
1347
|
+
export type InspectionPresentationKind = 'card' | 'column' | 'properties';
|
|
1348
|
+
|
|
1349
|
+
/** One inspected field: a stable scriptable `path` and the value at it. */
|
|
1350
|
+
export interface InspectedField {
|
|
1351
|
+
path: string;
|
|
1352
|
+
label: string;
|
|
1353
|
+
type: 'string' | 'number' | 'boolean' | 'vec3' | 'color' | 'enum' | 'asset' | 'json';
|
|
1354
|
+
/** Absent when nothing is at that address, or when `mixed` is set. */
|
|
1355
|
+
value?: unknown;
|
|
1356
|
+
/** The inspected subjects disagree about this field. */
|
|
1357
|
+
mixed?: true;
|
|
1358
|
+
/** The value shown is the declared default — the document does not carry it. */
|
|
1359
|
+
defaulted?: boolean;
|
|
1360
|
+
readonly?: boolean;
|
|
1361
|
+
/** The same reason shown by the Inspector and returned by a refused write. */
|
|
1362
|
+
readonlyReason?: string;
|
|
1363
|
+
resettable?: boolean;
|
|
1364
|
+
revertsTo?: string;
|
|
1365
|
+
group?: string;
|
|
1366
|
+
options?: readonly unknown[];
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/** A section's content. Custom RENDERING remains opaque — the wire never
|
|
1370
|
+
* introspects React — while any ordinary descriptor channel that chrome owns
|
|
1371
|
+
* IS a `fields` body here, including its write-refusal reasons: nothing is
|
|
1372
|
+
* rendered on this wire, so naming chrome a reader cannot see while hiding
|
|
1373
|
+
* the fields it can use is the wrong half. (Measured: the whole react/DOM
|
|
1374
|
+
* lane draws its own widgets over the style descriptors, so every one of its
|
|
1375
|
+
* sections reported `custom` and `inspect()` enumerated zero fields for a DOM
|
|
1376
|
+
* element.) The two opaque kinds are distinguished because "this subject has
|
|
1377
|
+
* a live preview" is a real fact about it: `custom` is a contributed block
|
|
1378
|
+
* with no descriptor channel of its own, `preview` is the subject's own
|
|
1379
|
+
* square view of itself.
|
|
1380
|
+
*
|
|
1381
|
+
* A custom body carries `data` when it can say what it DISPLAYS — the keys
|
|
1382
|
+
* are the section's own vocabulary, not a shared schema. The shipped case is
|
|
1383
|
+
* `transform`: `{position, rotation, scale}`, three numbers each, with
|
|
1384
|
+
* rotation in Euler XYZ DEGREES exactly as the rotation inputs show it (the
|
|
1385
|
+
* quaternion behind them is not on this wire). */
|
|
1386
|
+
export type InspectedSectionBody =
|
|
1387
|
+
| { kind: 'fields'; fields: readonly InspectedField[] }
|
|
1388
|
+
| {
|
|
1389
|
+
kind: 'custom';
|
|
1390
|
+
id: string;
|
|
1391
|
+
title: string;
|
|
1392
|
+
data?: Record<string, unknown>;
|
|
1393
|
+
}
|
|
1394
|
+
| { kind: 'preview'; id: string; title: string };
|
|
1395
|
+
|
|
1396
|
+
export interface InspectedSection {
|
|
1397
|
+
id: string;
|
|
1398
|
+
title: string;
|
|
1399
|
+
order: number;
|
|
1400
|
+
description?: string;
|
|
1401
|
+
body: InspectedSectionBody;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** A verb on the subject (the visibility eye, the Asset Editor jump). */
|
|
1405
|
+
export interface InspectedAction {
|
|
1406
|
+
id: string;
|
|
1407
|
+
title: string;
|
|
1408
|
+
label?: string;
|
|
1409
|
+
/** Toggle state, for verbs that have one — how visibility is read. */
|
|
1410
|
+
pressed?: boolean;
|
|
1411
|
+
disabled?: boolean;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
export interface InspectedSubjectLink {
|
|
1415
|
+
id: string;
|
|
1416
|
+
title: string;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/** The whole inspection subject, as data — what a human sees in the
|
|
1420
|
+
* inspector, for an agent (`vgai eval 'editor.inspect()'`). */
|
|
1421
|
+
export interface InspectedSubject {
|
|
1422
|
+
id: string;
|
|
1423
|
+
title: string;
|
|
1424
|
+
kindLabel?: string;
|
|
1425
|
+
/** The quiet line a subject with nothing to edit explains itself with. */
|
|
1426
|
+
hint?: string;
|
|
1427
|
+
presentation: {
|
|
1428
|
+
preferred: InspectionPresentationKind;
|
|
1429
|
+
resolved?: InspectionPresentationKind;
|
|
1430
|
+
surface?: InspectionSurface;
|
|
1431
|
+
};
|
|
1432
|
+
quickActions: readonly InspectedAction[];
|
|
1433
|
+
/** Agent-visible counterparts of the inspector's related-document buttons. */
|
|
1434
|
+
related: readonly InspectedSubjectLink[];
|
|
1435
|
+
/** Already in display order. */
|
|
1436
|
+
sections: readonly InspectedSection[];
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/** NOTHING is being inspected: the inspector is unmounted, so the honest
|
|
1440
|
+
* answer is not an empty subject but the absence of one. Distinct from a
|
|
1441
|
+
* missing reply, which means nobody answered
|
|
1442
|
+
* (`editor.inspection.get`'s `INSPECTION_UNAVAILABLE`). */
|
|
1443
|
+
export interface InspectedNothing {
|
|
1444
|
+
none: true;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/** What `editor.inspect()` answers: the subject showing, or nothing at all.
|
|
1448
|
+
* Narrow with `'none' in result`. */
|
|
1449
|
+
export type InspectedInspection = InspectedSubject | InspectedNothing;
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* WHERE THIS WRITE WENT — carried by every `editor.setField()` ack.
|
|
1453
|
+
*
|
|
1454
|
+
* A write with no persistence route still succeeds: it lands on the live
|
|
1455
|
+
* object and journals live-only, exactly as designed. Without this the ack was
|
|
1456
|
+
* indistinguishable from one that reached a file, so a caller could only find
|
|
1457
|
+
* out by diffing the tree — and a healthy consent-off session read as a silent
|
|
1458
|
+
* no-op. `persisted: false` with `destination: "live-only (not saved)"` is the
|
|
1459
|
+
* honest floor: never silence, and never a fabricated file name.
|
|
1460
|
+
*
|
|
1461
|
+
* IT IS PER-EDIT, produced by the component that performed the write and
|
|
1462
|
+
* returned through the editor's persistence pipe — never a property of the
|
|
1463
|
+
* session, the surface or the adapter. A composite holding a live-only three
|
|
1464
|
+
* root beside a source-backed DOM root has no single true answer, and the
|
|
1465
|
+
* adapter-wide one it used to give was the DOM root's (measured on the
|
|
1466
|
+
* vendored racing game: a three-root edit acked `persisted: true` against a
|
|
1467
|
+
* file it never touched). The ack is also AWAITED: it resolves after the bytes
|
|
1468
|
+
* have landed, so a caller holding it can diff the tree immediately.
|
|
1469
|
+
*/
|
|
1470
|
+
export interface InspectedWriteDestination {
|
|
1471
|
+
/** Where THIS edit's bytes landed, in the writer's own words — a source
|
|
1472
|
+
* file, the game's own JSX, or a named non-target like
|
|
1473
|
+
* `"live-only (not saved)"`. */
|
|
1474
|
+
destination: string;
|
|
1475
|
+
/** Whether a byte actually moved for THIS edit. */
|
|
1476
|
+
persisted: boolean;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/** What `editor.setField()` answers: the subject after the write, plus where
|
|
1480
|
+
* the write went. */
|
|
1481
|
+
export interface InspectedFieldWrite {
|
|
1482
|
+
subject: InspectedInspection;
|
|
1483
|
+
write: InspectedWriteDestination;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* The STRUCTURE verbs — the hierarchy context menu's own ops, addressable.
|
|
1488
|
+
*
|
|
1489
|
+
* The names are the menu's, not the provider's, because the menu is the
|
|
1490
|
+
* surface a human uses and an agent is doing the same thing through a
|
|
1491
|
+
* different door (`delete` covers the provider's `remove`/`removeMany`: a
|
|
1492
|
+
* multi-id delete is ONE undoable op when the adapter can batch it).
|
|
1493
|
+
*/
|
|
1494
|
+
export type StructureOp =
|
|
1495
|
+
| 'create'
|
|
1496
|
+
| 'delete'
|
|
1497
|
+
| 'duplicate'
|
|
1498
|
+
| 'reparent'
|
|
1499
|
+
| 'reorder'
|
|
1500
|
+
| 'wrap'
|
|
1501
|
+
| 'unwrap'
|
|
1502
|
+
| 'group'
|
|
1503
|
+
| 'ungroup'
|
|
1504
|
+
| 'copy'
|
|
1505
|
+
| 'cut'
|
|
1506
|
+
| 'paste';
|
|
1507
|
+
|
|
1508
|
+
/** Arguments for one {@link StructureOp}. Everything is optional: `id`/`ids`
|
|
1509
|
+
* default to the current selection, the menu's own subject. */
|
|
1510
|
+
export interface StructureOpOptions {
|
|
1511
|
+
id?: string;
|
|
1512
|
+
ids?: readonly string[];
|
|
1513
|
+
/** `create`: which creatable kind (see the adapter's `creatableKinds`). */
|
|
1514
|
+
kind?: string;
|
|
1515
|
+
/** `create`/`reparent`/`paste`: the destination; `null`/absent = document root. */
|
|
1516
|
+
parentId?: string;
|
|
1517
|
+
/** `reorder`: move immediately before this sibling; absent = to the end. */
|
|
1518
|
+
beforeSiblingId?: string;
|
|
1519
|
+
/** `wrap`: the wrapper tag; absent = the adapter's own default. */
|
|
1520
|
+
tag?: string;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/** What one structure op answers. `write` is the same per-edit ack
|
|
1524
|
+
* `editor.setField()` carries — `persisted: false` means the tree moved and
|
|
1525
|
+
* no byte did. `id`/`ids` name what the op produced, when it produces one. */
|
|
1526
|
+
export interface StructureOpResult {
|
|
1527
|
+
id?: string | null;
|
|
1528
|
+
ids?: readonly string[];
|
|
1529
|
+
write?: InspectedWriteDestination;
|
|
1530
|
+
/** `copy` only: whether the clipboard actually took the payload. */
|
|
1531
|
+
copied?: boolean;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// ---------------------------------------------------------------- hierarchy
|
|
1535
|
+
//
|
|
1536
|
+
// The wire mirror of the editor's own `SerializedHierarchyPanel`
|
|
1537
|
+
// (`packages/editor/src/hierarchy-panel-view.ts`, which owns the contract and
|
|
1538
|
+
// carries the reasoning). `command-listener.ts` annotates its `hierarchy`
|
|
1539
|
+
// payload with this type, so `tsc` checks the two sides against each other on
|
|
1540
|
+
// every build.
|
|
1541
|
+
//
|
|
1542
|
+
// This is NOT `EditorState.entities`: that facet is the raw adapter tree, with
|
|
1543
|
+
// no marks, no internals folding and no document promotion. This one is what
|
|
1544
|
+
// the hierarchy PANEL rendered — the rows a human is looking at.
|
|
1545
|
+
|
|
1546
|
+
/** One row of the hierarchy panel, as data. */
|
|
1547
|
+
export interface InspectedHierarchyRow {
|
|
1548
|
+
id: string;
|
|
1549
|
+
label: string;
|
|
1550
|
+
/** The dim type suffix the row prints (`Coin1 ·Coin`). */
|
|
1551
|
+
typeLabel?: string;
|
|
1552
|
+
role?: string;
|
|
1553
|
+
depth: number;
|
|
1554
|
+
/** Children the row's view has — what opening the caret reveals. Folded
|
|
1555
|
+
* implementation children are NOT counted here. */
|
|
1556
|
+
childCount: number;
|
|
1557
|
+
/** Children folded away as implementation, behind "Reveal Internals". */
|
|
1558
|
+
internalChildCount: number;
|
|
1559
|
+
/** Whether the panel renders a disclosure control. A row with children of
|
|
1560
|
+
* any kind and `expandable: false` is a subtree the UI cannot reach. */
|
|
1561
|
+
expandable: boolean;
|
|
1562
|
+
expanded?: boolean;
|
|
1563
|
+
internal?: true;
|
|
1564
|
+
componentRoot?: true;
|
|
1565
|
+
/** A synthetic "… N more" cap stub rather than a real node. */
|
|
1566
|
+
more?: { hidden: number };
|
|
1567
|
+
children?: readonly InspectedHierarchyRow[];
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/** What `editor.hierarchy()` answers. */
|
|
1571
|
+
export interface InspectedHierarchy {
|
|
1572
|
+
rowCount: number;
|
|
1573
|
+
/** The slice in the DOM; a smaller span than `rowCount` means the rest is
|
|
1574
|
+
* scrolled out, not absent. */
|
|
1575
|
+
window: { start: number; end: number };
|
|
1576
|
+
search?: string;
|
|
1577
|
+
scopeId?: string;
|
|
1578
|
+
playState: string;
|
|
1579
|
+
activeViewportTab: string;
|
|
1580
|
+
roots: readonly InspectedHierarchyRow[];
|
|
1581
|
+
}
|