@vgai/sdk 0.5.2 → 0.5.3
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 +3 -3
- package/src/account.ts +20 -0
- package/src/cinematic/gsap-operations.ts +3 -3
- package/src/cinematic/index.ts +3 -4
- package/src/cinematic/theatre-operations.ts +1 -1
- package/src/editor/index.ts +11 -4
- package/src/editor/inspection-operations.ts +189 -0
- package/src/editor/open-operations.ts +4 -44
- package/src/editor/transport.ts +94 -10
- package/src/operations.ts +10 -5
- package/src/perf/perf-run.ts +4 -4
- package/src/play/control-operations.ts +19 -19
- package/src/play/debug-command-operations.ts +2 -3
- package/src/play/index.ts +4 -5
- package/src/play/input-operations.ts +12 -12
- package/src/play/lifecycle-operations.ts +3 -3
- package/src/play/run-ticks-operations.ts +4 -4
- package/src/play/state-operations.ts +4 -4
- package/src/play/status-operations.ts +13 -13
- package/src/play/transport.ts +29 -38
- package/src/project/index.ts +15 -19
- package/src/project/inspection-node.ts +6 -5
- package/src/project/inspection-operation.ts +1 -1
- package/src/project/inspection.ts +6 -6
- package/src/project/manifest-operations.ts +12 -17
- package/src/project/shared.ts +4 -39
- package/src/registry.ts +9 -3
- package/src/render/index.ts +3 -4
- package/src/render/render-cinematic.ts +27 -28
- package/src/types.ts +7 -3
- package/src/project/component-operations.ts +0 -337
- package/src/project/entity-operations.ts +0 -366
- package/src/project/scene-operations.ts +0 -426
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `project.scene.component.*` (B2, §8 B2 "component list/add/update/remove").
|
|
3
|
-
*
|
|
4
|
-
* A component's identity within an entity is its NAME (the key in
|
|
5
|
-
* `entity.components`, a `Record<string, Record<string, ComponentValue>>` —
|
|
6
|
-
* see `packages/engine/src/scene/schema/entity.ts`'s `ComponentsSchema`) — so
|
|
7
|
-
* "id-aware" here means addressed by that name, never by e.g. iteration
|
|
8
|
-
* order. Every mutation reuses the exact same path as
|
|
9
|
-
* `project.scene.entity.update`: read the entity (by stable id), compute a
|
|
10
|
-
* new `components` record, hand the FULL merged entity to applyDiff's
|
|
11
|
-
* `update` op. No separate component-patching logic exists.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { SceneEntity } from '@vgai/engine/scene/scene-types';
|
|
15
|
-
import { z } from 'zod';
|
|
16
|
-
import { ToolError } from '../errors.js';
|
|
17
|
-
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
18
|
-
import { finalizeSceneMutation } from './entity-operations.js';
|
|
19
|
-
import {
|
|
20
|
-
APPLY_DIFF_FAILED_ERROR,
|
|
21
|
-
applyDiffOrThrow,
|
|
22
|
-
readAndParseScene,
|
|
23
|
-
SCENE_INVALID_ERROR,
|
|
24
|
-
} from './scene-operations.js';
|
|
25
|
-
import {
|
|
26
|
-
BaseHashField,
|
|
27
|
-
CONFLICT_ERROR,
|
|
28
|
-
checkNotStale,
|
|
29
|
-
DryRunField,
|
|
30
|
-
ENTITY_NOT_FOUND_ERROR,
|
|
31
|
-
FILE_NOT_FOUND_ERROR,
|
|
32
|
-
mutationResultSchema,
|
|
33
|
-
NO_PROJECT_ROOT_ERROR,
|
|
34
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
35
|
-
requireEntityById,
|
|
36
|
-
requireProjectRoot,
|
|
37
|
-
} from './shared.js';
|
|
38
|
-
|
|
39
|
-
const ComponentDataValueSchema = z.union([
|
|
40
|
-
z.number(),
|
|
41
|
-
z.string(),
|
|
42
|
-
z.boolean(),
|
|
43
|
-
z.array(z.number()),
|
|
44
|
-
z.array(z.string()),
|
|
45
|
-
z.array(z.boolean()),
|
|
46
|
-
]);
|
|
47
|
-
const ComponentDataSchema = z.record(z.string(), ComponentDataValueSchema);
|
|
48
|
-
|
|
49
|
-
export const COMPONENT_NOT_FOUND_ERROR = {
|
|
50
|
-
code: 'COMPONENT_NOT_FOUND',
|
|
51
|
-
summary: 'No component with the given name is attached to this entity.',
|
|
52
|
-
data: z.object({ entityId: z.string(), componentName: z.string() }),
|
|
53
|
-
} as const;
|
|
54
|
-
|
|
55
|
-
export const COMPONENT_ALREADY_EXISTS_ERROR = {
|
|
56
|
-
code: 'COMPONENT_ALREADY_EXISTS',
|
|
57
|
-
summary:
|
|
58
|
-
'A component with this name is already attached to the entity — use component.update instead.',
|
|
59
|
-
data: z.object({ entityId: z.string(), componentName: z.string() }),
|
|
60
|
-
} as const;
|
|
61
|
-
|
|
62
|
-
/** Build the full entity payload applyDiff's `update` op needs, with `components` replaced by `newComponents`. */
|
|
63
|
-
function entityWithComponents(
|
|
64
|
-
entity: SceneEntity,
|
|
65
|
-
newComponents: Record<string, Record<string, unknown>>,
|
|
66
|
-
): SceneEntity {
|
|
67
|
-
const { id: _id, children: _children, ...fields } = entity;
|
|
68
|
-
return { ...fields, components: newComponents } as SceneEntity;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// project.scene.component.list
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
const ProjectSceneComponentListInput = z.object({
|
|
76
|
-
scenePath: z.string().describe('Project-relative path to the .vscn.json scene file.'),
|
|
77
|
-
entityId: z.string().describe('Entity to list components for.'),
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
export const projectSceneComponentList = defineTool({
|
|
81
|
-
name: 'project.scene.component.list',
|
|
82
|
-
summary: 'List every component (by name) attached to an entity.',
|
|
83
|
-
description: 'Read-only. Resolves the entity by stable id.',
|
|
84
|
-
input: ProjectSceneComponentListInput,
|
|
85
|
-
result: z.object({
|
|
86
|
-
entityId: z.string(),
|
|
87
|
-
components: z
|
|
88
|
-
.record(z.string(), ComponentDataSchema)
|
|
89
|
-
.describe('Component name -> its data record.'),
|
|
90
|
-
}),
|
|
91
|
-
errors: [
|
|
92
|
-
NO_PROJECT_ROOT_ERROR,
|
|
93
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
94
|
-
FILE_NOT_FOUND_ERROR,
|
|
95
|
-
SCENE_INVALID_ERROR,
|
|
96
|
-
ENTITY_NOT_FOUND_ERROR,
|
|
97
|
-
],
|
|
98
|
-
requires: { project: true },
|
|
99
|
-
host: 'node',
|
|
100
|
-
mutates: false,
|
|
101
|
-
supportsDryRun: false,
|
|
102
|
-
permission: { risk: 'read', summary: 'Reads a scene file; no writes.' },
|
|
103
|
-
async impl(input, ctx) {
|
|
104
|
-
const projectRoot = requireProjectRoot(ctx);
|
|
105
|
-
const { scene } = readAndParseScene(projectRoot, input.scenePath);
|
|
106
|
-
const entity = requireEntityById(scene.entities, input.entityId);
|
|
107
|
-
return { entityId: input.entityId, components: entity.components ?? {} };
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// project.scene.component.add
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
|
|
115
|
-
const ProjectSceneComponentAddInput = z.object({
|
|
116
|
-
scenePath: z.string().describe('Project-relative path to the .vscn.json scene file.'),
|
|
117
|
-
entityId: z.string().describe('Entity to attach the component to.'),
|
|
118
|
-
componentName: z.string().describe('Component name (record key).'),
|
|
119
|
-
data: ComponentDataSchema.describe('Initial component data.'),
|
|
120
|
-
dryRun: DryRunField,
|
|
121
|
-
baseHash: BaseHashField,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const ComponentAfterSchema = z.object({
|
|
125
|
-
entityId: z.string(),
|
|
126
|
-
componentName: z.string(),
|
|
127
|
-
data: z.unknown(),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
export const projectSceneComponentAdd = defineTool({
|
|
131
|
-
name: 'project.scene.component.add',
|
|
132
|
-
summary: 'Attach a new named component to an entity.',
|
|
133
|
-
description:
|
|
134
|
-
'Delegates to applyDiff’s `update` op with a merged components record — same reused write path as project.scene.entity.update.',
|
|
135
|
-
input: ProjectSceneComponentAddInput,
|
|
136
|
-
result: mutationResultSchema(ComponentAfterSchema),
|
|
137
|
-
errors: [
|
|
138
|
-
NO_PROJECT_ROOT_ERROR,
|
|
139
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
140
|
-
FILE_NOT_FOUND_ERROR,
|
|
141
|
-
SCENE_INVALID_ERROR,
|
|
142
|
-
APPLY_DIFF_FAILED_ERROR,
|
|
143
|
-
CONFLICT_ERROR,
|
|
144
|
-
ENTITY_NOT_FOUND_ERROR,
|
|
145
|
-
COMPONENT_ALREADY_EXISTS_ERROR,
|
|
146
|
-
],
|
|
147
|
-
requires: { project: true },
|
|
148
|
-
host: 'node',
|
|
149
|
-
mutates: true,
|
|
150
|
-
supportsDryRun: true,
|
|
151
|
-
permission: { risk: 'write', summary: 'Writes the target scene file.' },
|
|
152
|
-
async impl(input, ctx) {
|
|
153
|
-
const projectRoot = requireProjectRoot(ctx);
|
|
154
|
-
const { absPath, hash, scene } = readAndParseScene(projectRoot, input.scenePath);
|
|
155
|
-
checkNotStale(absPath, hash, input.baseHash);
|
|
156
|
-
|
|
157
|
-
const entity = requireEntityById(scene.entities, input.entityId);
|
|
158
|
-
const existing = entity.components ?? {};
|
|
159
|
-
if (input.componentName in existing) {
|
|
160
|
-
throw new ToolError(
|
|
161
|
-
'COMPONENT_ALREADY_EXISTS',
|
|
162
|
-
`Entity "${input.entityId}" already has a component named "${input.componentName}" — use project.scene.component.update instead.`,
|
|
163
|
-
{ entityId: input.entityId, componentName: input.componentName },
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
const newComponents = { ...existing, [input.componentName]: input.data };
|
|
167
|
-
|
|
168
|
-
const result = applyDiffOrThrow(scene, {
|
|
169
|
-
ops: [
|
|
170
|
-
{ type: 'update', id: input.entityId, entity: entityWithComponents(entity, newComponents) },
|
|
171
|
-
],
|
|
172
|
-
environmentChanged: false,
|
|
173
|
-
metaChanged: false,
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
return finalizeSceneMutation(absPath, input.scenePath, result, input.dryRun ?? false, null, {
|
|
177
|
-
entityId: input.entityId,
|
|
178
|
-
componentName: input.componentName,
|
|
179
|
-
data: input.data,
|
|
180
|
-
});
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
// project.scene.component.update — merge new fields onto the existing component's data.
|
|
186
|
-
// ---------------------------------------------------------------------------
|
|
187
|
-
|
|
188
|
-
const ProjectSceneComponentUpdateInput = z.object({
|
|
189
|
-
scenePath: z.string().describe('Project-relative path to the .vscn.json scene file.'),
|
|
190
|
-
entityId: z.string().describe('Entity the component is attached to.'),
|
|
191
|
-
componentName: z.string().describe('Component name (record key) to update.'),
|
|
192
|
-
data: ComponentDataSchema.describe(
|
|
193
|
-
'Fields to overlay onto the component’s current data (a merge).',
|
|
194
|
-
),
|
|
195
|
-
dryRun: DryRunField,
|
|
196
|
-
baseHash: BaseHashField,
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
export const projectSceneComponentUpdate = defineTool({
|
|
200
|
-
name: 'project.scene.component.update',
|
|
201
|
-
summary: 'Merge field changes onto an existing named component.',
|
|
202
|
-
description: 'Delegates to applyDiff’s `update` op with a merged components record.',
|
|
203
|
-
input: ProjectSceneComponentUpdateInput,
|
|
204
|
-
result: mutationResultSchema(ComponentAfterSchema),
|
|
205
|
-
errors: [
|
|
206
|
-
NO_PROJECT_ROOT_ERROR,
|
|
207
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
208
|
-
FILE_NOT_FOUND_ERROR,
|
|
209
|
-
SCENE_INVALID_ERROR,
|
|
210
|
-
APPLY_DIFF_FAILED_ERROR,
|
|
211
|
-
CONFLICT_ERROR,
|
|
212
|
-
ENTITY_NOT_FOUND_ERROR,
|
|
213
|
-
COMPONENT_NOT_FOUND_ERROR,
|
|
214
|
-
],
|
|
215
|
-
requires: { project: true },
|
|
216
|
-
host: 'node',
|
|
217
|
-
mutates: true,
|
|
218
|
-
supportsDryRun: true,
|
|
219
|
-
permission: { risk: 'write', summary: 'Writes the target scene file.' },
|
|
220
|
-
async impl(input, ctx) {
|
|
221
|
-
const projectRoot = requireProjectRoot(ctx);
|
|
222
|
-
const { absPath, hash, scene } = readAndParseScene(projectRoot, input.scenePath);
|
|
223
|
-
checkNotStale(absPath, hash, input.baseHash);
|
|
224
|
-
|
|
225
|
-
const entity = requireEntityById(scene.entities, input.entityId);
|
|
226
|
-
const existing = entity.components ?? {};
|
|
227
|
-
const currentData = existing[input.componentName];
|
|
228
|
-
if (currentData === undefined) {
|
|
229
|
-
throw new ToolError(
|
|
230
|
-
'COMPONENT_NOT_FOUND',
|
|
231
|
-
`Entity "${input.entityId}" has no component named "${input.componentName}".`,
|
|
232
|
-
{ entityId: input.entityId, componentName: input.componentName },
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
const mergedData = { ...currentData, ...input.data };
|
|
236
|
-
const newComponents = { ...existing, [input.componentName]: mergedData };
|
|
237
|
-
|
|
238
|
-
const result = applyDiffOrThrow(scene, {
|
|
239
|
-
ops: [
|
|
240
|
-
{ type: 'update', id: input.entityId, entity: entityWithComponents(entity, newComponents) },
|
|
241
|
-
],
|
|
242
|
-
environmentChanged: false,
|
|
243
|
-
metaChanged: false,
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
return finalizeSceneMutation(
|
|
247
|
-
absPath,
|
|
248
|
-
input.scenePath,
|
|
249
|
-
result,
|
|
250
|
-
input.dryRun ?? false,
|
|
251
|
-
{ entityId: input.entityId, componentName: input.componentName, data: currentData },
|
|
252
|
-
{ entityId: input.entityId, componentName: input.componentName, data: mergedData },
|
|
253
|
-
);
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
// project.scene.component.remove
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
const ProjectSceneComponentRemoveInput = z.object({
|
|
262
|
-
scenePath: z.string().describe('Project-relative path to the .vscn.json scene file.'),
|
|
263
|
-
entityId: z.string().describe('Entity the component is attached to.'),
|
|
264
|
-
componentName: z.string().describe('Component name (record key) to remove.'),
|
|
265
|
-
dryRun: DryRunField,
|
|
266
|
-
baseHash: BaseHashField,
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
export const projectSceneComponentRemove = defineTool({
|
|
270
|
-
name: 'project.scene.component.remove',
|
|
271
|
-
summary: 'Detach a named component from an entity.',
|
|
272
|
-
description:
|
|
273
|
-
'Delegates to applyDiff’s `update` op with the component key deleted from the components record.',
|
|
274
|
-
input: ProjectSceneComponentRemoveInput,
|
|
275
|
-
result: mutationResultSchema(ComponentAfterSchema),
|
|
276
|
-
errors: [
|
|
277
|
-
NO_PROJECT_ROOT_ERROR,
|
|
278
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
279
|
-
FILE_NOT_FOUND_ERROR,
|
|
280
|
-
SCENE_INVALID_ERROR,
|
|
281
|
-
APPLY_DIFF_FAILED_ERROR,
|
|
282
|
-
CONFLICT_ERROR,
|
|
283
|
-
ENTITY_NOT_FOUND_ERROR,
|
|
284
|
-
COMPONENT_NOT_FOUND_ERROR,
|
|
285
|
-
],
|
|
286
|
-
requires: { project: true },
|
|
287
|
-
host: 'node',
|
|
288
|
-
mutates: true,
|
|
289
|
-
supportsDryRun: true,
|
|
290
|
-
permission: {
|
|
291
|
-
risk: 'destructive',
|
|
292
|
-
summary: 'Removes a component from an entity in the target scene file.',
|
|
293
|
-
},
|
|
294
|
-
async impl(input, ctx) {
|
|
295
|
-
const projectRoot = requireProjectRoot(ctx);
|
|
296
|
-
const { absPath, hash, scene } = readAndParseScene(projectRoot, input.scenePath);
|
|
297
|
-
checkNotStale(absPath, hash, input.baseHash);
|
|
298
|
-
|
|
299
|
-
const entity = requireEntityById(scene.entities, input.entityId);
|
|
300
|
-
const existing = entity.components ?? {};
|
|
301
|
-
const currentData = existing[input.componentName];
|
|
302
|
-
if (currentData === undefined) {
|
|
303
|
-
throw new ToolError(
|
|
304
|
-
'COMPONENT_NOT_FOUND',
|
|
305
|
-
`Entity "${input.entityId}" has no component named "${input.componentName}".`,
|
|
306
|
-
{ entityId: input.entityId, componentName: input.componentName },
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
const newComponents = { ...existing };
|
|
310
|
-
delete newComponents[input.componentName];
|
|
311
|
-
|
|
312
|
-
const result = applyDiffOrThrow(scene, {
|
|
313
|
-
ops: [
|
|
314
|
-
{ type: 'update', id: input.entityId, entity: entityWithComponents(entity, newComponents) },
|
|
315
|
-
],
|
|
316
|
-
environmentChanged: false,
|
|
317
|
-
metaChanged: false,
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
return finalizeSceneMutation(
|
|
321
|
-
absPath,
|
|
322
|
-
input.scenePath,
|
|
323
|
-
result,
|
|
324
|
-
input.dryRun ?? false,
|
|
325
|
-
{ entityId: input.entityId, componentName: input.componentName, data: currentData },
|
|
326
|
-
{ entityId: input.entityId, componentName: input.componentName, data: null },
|
|
327
|
-
);
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
/** Register every B2 `project.scene.component.*` operation onto `registry`. */
|
|
332
|
-
export function registerComponentOperations(registry: ToolRegistry): void {
|
|
333
|
-
registry.register(projectSceneComponentList);
|
|
334
|
-
registry.register(projectSceneComponentAdd);
|
|
335
|
-
registry.register(projectSceneComponentUpdate);
|
|
336
|
-
registry.register(projectSceneComponentRemove);
|
|
337
|
-
}
|
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `project.scene.entity.*` (B2, §8 B2 "entity add/update/move/remove").
|
|
3
|
-
*
|
|
4
|
-
* ID-AWARE BY CONSTRUCTION: every op here targets an entity by its stable
|
|
5
|
-
* `id` (never an array index) and builds exactly one `DiffOp`
|
|
6
|
-
* (`@vgai/engine/scene/scene-diff-types`) for `applyDiff`
|
|
7
|
-
* (`@vgai/engine/scene/scene-apply`) to execute — the SAME op-ordering/
|
|
8
|
-
* placement machinery `project.scene.apply` and `vgai apply-diff` use. This
|
|
9
|
-
* module only assembles the single-op `SceneDiff`; it never walks/splices the
|
|
10
|
-
* entity tree itself.
|
|
11
|
-
*
|
|
12
|
-
* `entity.update` is a convenience MERGE over applyDiff's `update` op (which
|
|
13
|
-
* is itself a full field replace — see scene-apply.ts's jsdoc): the impl
|
|
14
|
-
* reads the entity's current fields, overlays the caller's `fields` on top,
|
|
15
|
-
* and hands the FULL merged object to applyDiff as the update payload — so a
|
|
16
|
-
* caller can patch just `{ transform: { position: [...] } }` without
|
|
17
|
-
* re-stating every other field, while applyDiff still only ever sees (and
|
|
18
|
-
* only ever needs to implement) a full-replace update.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { parseSceneFile, SceneParseError } from '@vgai/engine/scene/parse';
|
|
22
|
-
import type { applyDiff } from '@vgai/engine/scene/scene-apply';
|
|
23
|
-
import type { SceneEntity } from '@vgai/engine/scene/scene-types';
|
|
24
|
-
import { SceneEntitySchema } from '@vgai/engine/scene/schema/entity';
|
|
25
|
-
import { z } from 'zod';
|
|
26
|
-
import { ToolError } from '../errors.js';
|
|
27
|
-
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
28
|
-
import {
|
|
29
|
-
APPLY_DIFF_FAILED_ERROR,
|
|
30
|
-
applyDiffOrThrow,
|
|
31
|
-
readAndParseScene,
|
|
32
|
-
SCENE_INVALID_ERROR,
|
|
33
|
-
} from './scene-operations.js';
|
|
34
|
-
import {
|
|
35
|
-
BaseHashField,
|
|
36
|
-
CONFLICT_ERROR,
|
|
37
|
-
checkNotStale,
|
|
38
|
-
DryRunField,
|
|
39
|
-
ENTITY_NOT_FOUND_ERROR,
|
|
40
|
-
FILE_NOT_FOUND_ERROR,
|
|
41
|
-
findEntityById,
|
|
42
|
-
mutationResultSchema,
|
|
43
|
-
NO_PROJECT_ROOT_ERROR,
|
|
44
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
45
|
-
requireEntityById,
|
|
46
|
-
requireProjectRoot,
|
|
47
|
-
writeFileAtomic,
|
|
48
|
-
} from './shared.js';
|
|
49
|
-
|
|
50
|
-
const EntityAfterSchema = z.object({
|
|
51
|
-
entity: z
|
|
52
|
-
.unknown()
|
|
53
|
-
.describe('The affected entity after the change (or that WOULD result), by id.'),
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Shared write tail: re-validate the whole document, honor dryRun, write via
|
|
58
|
-
* checkNotStale-guarded atomic write, and shape the mutation envelope.
|
|
59
|
-
* `before`/`after` are passed through AS GIVEN (not wrapped) — callers shape
|
|
60
|
-
* them to match their own op's declared `after` result schema (entity ops
|
|
61
|
-
* wrap as `{ entity }`; component-operations.ts's callers pass their own
|
|
62
|
-
* `{ entityId, componentName, data }` shape directly).
|
|
63
|
-
*/
|
|
64
|
-
function finalizeSceneMutation<TBefore, TAfter>(
|
|
65
|
-
absPath: string,
|
|
66
|
-
scenePath: string,
|
|
67
|
-
result: SceneFileLike,
|
|
68
|
-
dryRun: boolean,
|
|
69
|
-
before: TBefore,
|
|
70
|
-
after: TAfter,
|
|
71
|
-
) {
|
|
72
|
-
try {
|
|
73
|
-
parseSceneFile(result, scenePath);
|
|
74
|
-
} catch (err) {
|
|
75
|
-
if (err instanceof SceneParseError) {
|
|
76
|
-
throw new ToolError('SCENE_INVALID', `Result fails validation: ${err.message}`, {
|
|
77
|
-
issues: err.issues.map((i) => `${i.path.join('.')}: ${i.message}`),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
throw err;
|
|
81
|
-
}
|
|
82
|
-
if (!dryRun) {
|
|
83
|
-
writeFileAtomic(absPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
84
|
-
}
|
|
85
|
-
return { dryRun, written: !dryRun, filesChanged: [absPath], before, after };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// SceneFile is only used structurally here (applyDiff's return type) — avoid
|
|
89
|
-
// a second import cycle by aliasing via the already-imported applyDiff's own
|
|
90
|
-
// parameter type through a small local type alias.
|
|
91
|
-
type SceneFileLike = ReturnType<typeof applyDiff>;
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
// project.scene.entity.add
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
const ProjectSceneEntityAddInput = z.object({
|
|
98
|
-
scenePath: z.string().describe('Project-relative path to the .vscn.json scene file.'),
|
|
99
|
-
parentId: z.string().nullable().describe('Parent entity id, or null for a top-level entity.'),
|
|
100
|
-
index: z.number().int().min(0).describe('Target position among the (new) parent’s children.'),
|
|
101
|
-
entity: SceneEntitySchema.describe('The entity to insert (id auto-generated if omitted).'),
|
|
102
|
-
dryRun: DryRunField,
|
|
103
|
-
baseHash: BaseHashField,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
export const projectSceneEntityAdd = defineTool({
|
|
107
|
-
name: 'project.scene.entity.add',
|
|
108
|
-
summary: 'Insert a new entity into a scene, by parent id + index.',
|
|
109
|
-
description:
|
|
110
|
-
'Builds a single `add` DiffOp and delegates to applyDiff — the same insertion/placement logic project.scene.apply uses.',
|
|
111
|
-
input: ProjectSceneEntityAddInput,
|
|
112
|
-
result: mutationResultSchema(EntityAfterSchema),
|
|
113
|
-
errors: [
|
|
114
|
-
NO_PROJECT_ROOT_ERROR,
|
|
115
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
116
|
-
FILE_NOT_FOUND_ERROR,
|
|
117
|
-
SCENE_INVALID_ERROR,
|
|
118
|
-
APPLY_DIFF_FAILED_ERROR,
|
|
119
|
-
CONFLICT_ERROR,
|
|
120
|
-
],
|
|
121
|
-
requires: { project: true },
|
|
122
|
-
host: 'node',
|
|
123
|
-
mutates: true,
|
|
124
|
-
supportsDryRun: true,
|
|
125
|
-
permission: { risk: 'write', summary: 'Writes the target scene file.' },
|
|
126
|
-
async impl(input, ctx) {
|
|
127
|
-
const projectRoot = requireProjectRoot(ctx);
|
|
128
|
-
const { absPath, hash, scene } = readAndParseScene(projectRoot, input.scenePath);
|
|
129
|
-
checkNotStale(absPath, hash, input.baseHash);
|
|
130
|
-
|
|
131
|
-
const result = applyDiffOrThrow(scene, {
|
|
132
|
-
ops: [
|
|
133
|
-
{
|
|
134
|
-
type: 'add',
|
|
135
|
-
entity: input.entity as SceneEntity,
|
|
136
|
-
parentId: input.parentId,
|
|
137
|
-
index: input.index,
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
environmentChanged: false,
|
|
141
|
-
metaChanged: false,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Report the entity as it landed (id may have been auto-generated).
|
|
145
|
-
const after = input.entity.id
|
|
146
|
-
? findEntityById(result.entities, input.entity.id)
|
|
147
|
-
: findEntityById(result.entities, findNewestSiblingId(result, input.parentId, input.index));
|
|
148
|
-
|
|
149
|
-
return finalizeSceneMutation(
|
|
150
|
-
absPath,
|
|
151
|
-
input.scenePath,
|
|
152
|
-
result,
|
|
153
|
-
input.dryRun ?? false,
|
|
154
|
-
{ entity: null },
|
|
155
|
-
{ entity: after ?? null },
|
|
156
|
-
);
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
/** Best-effort: when the caller omitted `id`, look up whatever landed at the target position after the add. */
|
|
161
|
-
function findNewestSiblingId(scene: SceneFileLike, parentId: string | null, index: number): string {
|
|
162
|
-
const siblings =
|
|
163
|
-
parentId === null ? scene.entities : (findEntityById(scene.entities, parentId)?.children ?? []);
|
|
164
|
-
const clamped = Math.min(index, siblings.length - 1);
|
|
165
|
-
return siblings[clamped]?.id ?? '';
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
// project.scene.entity.update — merge-onto-full-replace convenience.
|
|
170
|
-
// ---------------------------------------------------------------------------
|
|
171
|
-
|
|
172
|
-
const ProjectSceneEntityUpdateInput = z.object({
|
|
173
|
-
scenePath: z.string().describe('Project-relative path to the .vscn.json scene file.'),
|
|
174
|
-
id: z.string().describe('Id of the entity to update (never an array index).'),
|
|
175
|
-
fields: z
|
|
176
|
-
.record(z.string(), z.unknown())
|
|
177
|
-
.describe(
|
|
178
|
-
'Fields to overlay onto the entity’s CURRENT fields (a merge, not a full replace — ' +
|
|
179
|
-
'unlike the underlying applyDiff `update` op, which this is built on top of). `id` and ' +
|
|
180
|
-
'`children` are ignored if present.',
|
|
181
|
-
),
|
|
182
|
-
dryRun: DryRunField,
|
|
183
|
-
baseHash: BaseHashField,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
export const projectSceneEntityUpdate = defineTool({
|
|
187
|
-
name: 'project.scene.entity.update',
|
|
188
|
-
summary: 'Merge field changes onto an existing entity, addressed by stable id.',
|
|
189
|
-
description:
|
|
190
|
-
'ID-aware — resolves the target by `id` regardless of its position/reordering in the ' +
|
|
191
|
-
'entities array. Reads the entity’s current fields, overlays `fields`, and hands the merged ' +
|
|
192
|
-
'whole object to applyDiff’s `update` op (a full replace under the hood) so nothing the ' +
|
|
193
|
-
'caller did not mention is lost.',
|
|
194
|
-
input: ProjectSceneEntityUpdateInput,
|
|
195
|
-
result: mutationResultSchema(EntityAfterSchema),
|
|
196
|
-
errors: [
|
|
197
|
-
NO_PROJECT_ROOT_ERROR,
|
|
198
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
199
|
-
FILE_NOT_FOUND_ERROR,
|
|
200
|
-
SCENE_INVALID_ERROR,
|
|
201
|
-
APPLY_DIFF_FAILED_ERROR,
|
|
202
|
-
CONFLICT_ERROR,
|
|
203
|
-
ENTITY_NOT_FOUND_ERROR,
|
|
204
|
-
],
|
|
205
|
-
requires: { project: true },
|
|
206
|
-
host: 'node',
|
|
207
|
-
mutates: true,
|
|
208
|
-
supportsDryRun: true,
|
|
209
|
-
permission: { risk: 'write', summary: 'Writes the target scene file.' },
|
|
210
|
-
async impl(input, ctx) {
|
|
211
|
-
const projectRoot = requireProjectRoot(ctx);
|
|
212
|
-
const { absPath, hash, scene } = readAndParseScene(projectRoot, input.scenePath);
|
|
213
|
-
checkNotStale(absPath, hash, input.baseHash);
|
|
214
|
-
|
|
215
|
-
const current = requireEntityById(scene.entities, input.id);
|
|
216
|
-
const { id: _id, children: _children, ...currentFields } = current;
|
|
217
|
-
const {
|
|
218
|
-
id: _fid,
|
|
219
|
-
children: _fchildren,
|
|
220
|
-
...overlayFields
|
|
221
|
-
} = input.fields as Record<string, unknown>;
|
|
222
|
-
const merged = { ...currentFields, ...overlayFields };
|
|
223
|
-
|
|
224
|
-
const result = applyDiffOrThrow(scene, {
|
|
225
|
-
ops: [{ type: 'update', id: input.id, entity: merged as SceneEntity }],
|
|
226
|
-
environmentChanged: false,
|
|
227
|
-
metaChanged: false,
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
const after = findEntityById(result.entities, input.id) ?? null;
|
|
231
|
-
return finalizeSceneMutation(
|
|
232
|
-
absPath,
|
|
233
|
-
input.scenePath,
|
|
234
|
-
result,
|
|
235
|
-
input.dryRun ?? false,
|
|
236
|
-
{ entity: current },
|
|
237
|
-
{ entity: after },
|
|
238
|
-
);
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// ---------------------------------------------------------------------------
|
|
243
|
-
// project.scene.entity.move
|
|
244
|
-
// ---------------------------------------------------------------------------
|
|
245
|
-
|
|
246
|
-
const ProjectSceneEntityMoveInput = z.object({
|
|
247
|
-
scenePath: z.string().describe('Project-relative path to the .vscn.json scene file.'),
|
|
248
|
-
id: z.string().describe('Id of the entity to move.'),
|
|
249
|
-
parentId: z.string().nullable().describe('New parent entity id, or null for top-level.'),
|
|
250
|
-
index: z.number().int().min(0).describe('Target position among the new parent’s children.'),
|
|
251
|
-
dryRun: DryRunField,
|
|
252
|
-
baseHash: BaseHashField,
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
export const projectSceneEntityMove = defineTool({
|
|
256
|
-
name: 'project.scene.entity.move',
|
|
257
|
-
summary: 'Reparent and/or reorder an existing entity, addressed by stable id.',
|
|
258
|
-
description: 'Builds a single `move` DiffOp and delegates to applyDiff.',
|
|
259
|
-
input: ProjectSceneEntityMoveInput,
|
|
260
|
-
result: mutationResultSchema(EntityAfterSchema),
|
|
261
|
-
errors: [
|
|
262
|
-
NO_PROJECT_ROOT_ERROR,
|
|
263
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
264
|
-
FILE_NOT_FOUND_ERROR,
|
|
265
|
-
SCENE_INVALID_ERROR,
|
|
266
|
-
APPLY_DIFF_FAILED_ERROR,
|
|
267
|
-
CONFLICT_ERROR,
|
|
268
|
-
ENTITY_NOT_FOUND_ERROR,
|
|
269
|
-
],
|
|
270
|
-
requires: { project: true },
|
|
271
|
-
host: 'node',
|
|
272
|
-
mutates: true,
|
|
273
|
-
supportsDryRun: true,
|
|
274
|
-
permission: { risk: 'write', summary: 'Writes the target scene file.' },
|
|
275
|
-
async impl(input, ctx) {
|
|
276
|
-
const projectRoot = requireProjectRoot(ctx);
|
|
277
|
-
const { absPath, hash, scene } = readAndParseScene(projectRoot, input.scenePath);
|
|
278
|
-
checkNotStale(absPath, hash, input.baseHash);
|
|
279
|
-
|
|
280
|
-
const before = requireEntityById(scene.entities, input.id);
|
|
281
|
-
const result = applyDiffOrThrow(scene, {
|
|
282
|
-
ops: [{ type: 'move', id: input.id, parentId: input.parentId, index: input.index }],
|
|
283
|
-
environmentChanged: false,
|
|
284
|
-
metaChanged: false,
|
|
285
|
-
});
|
|
286
|
-
const after = findEntityById(result.entities, input.id) ?? null;
|
|
287
|
-
|
|
288
|
-
return finalizeSceneMutation(
|
|
289
|
-
absPath,
|
|
290
|
-
input.scenePath,
|
|
291
|
-
result,
|
|
292
|
-
input.dryRun ?? false,
|
|
293
|
-
{ entity: before },
|
|
294
|
-
{ entity: after },
|
|
295
|
-
);
|
|
296
|
-
},
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// ---------------------------------------------------------------------------
|
|
300
|
-
// project.scene.entity.remove
|
|
301
|
-
// ---------------------------------------------------------------------------
|
|
302
|
-
|
|
303
|
-
const ProjectSceneEntityRemoveInput = z.object({
|
|
304
|
-
scenePath: z.string().describe('Project-relative path to the .vscn.json scene file.'),
|
|
305
|
-
id: z.string().describe('Id of the entity (and its subtree) to remove.'),
|
|
306
|
-
dryRun: DryRunField,
|
|
307
|
-
baseHash: BaseHashField,
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
export const projectSceneEntityRemove = defineTool({
|
|
311
|
-
name: 'project.scene.entity.remove',
|
|
312
|
-
summary: 'Delete an entity (and its subtree), addressed by stable id.',
|
|
313
|
-
description: 'Builds a single `remove` DiffOp and delegates to applyDiff.',
|
|
314
|
-
input: ProjectSceneEntityRemoveInput,
|
|
315
|
-
result: mutationResultSchema(EntityAfterSchema),
|
|
316
|
-
errors: [
|
|
317
|
-
NO_PROJECT_ROOT_ERROR,
|
|
318
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
319
|
-
FILE_NOT_FOUND_ERROR,
|
|
320
|
-
SCENE_INVALID_ERROR,
|
|
321
|
-
APPLY_DIFF_FAILED_ERROR,
|
|
322
|
-
CONFLICT_ERROR,
|
|
323
|
-
ENTITY_NOT_FOUND_ERROR,
|
|
324
|
-
],
|
|
325
|
-
requires: { project: true },
|
|
326
|
-
host: 'node',
|
|
327
|
-
mutates: true,
|
|
328
|
-
supportsDryRun: true,
|
|
329
|
-
permission: {
|
|
330
|
-
risk: 'destructive',
|
|
331
|
-
summary: 'Deletes an entity subtree from the target scene file.',
|
|
332
|
-
},
|
|
333
|
-
async impl(input, ctx) {
|
|
334
|
-
const projectRoot = requireProjectRoot(ctx);
|
|
335
|
-
const { absPath, hash, scene } = readAndParseScene(projectRoot, input.scenePath);
|
|
336
|
-
checkNotStale(absPath, hash, input.baseHash);
|
|
337
|
-
|
|
338
|
-
const before = requireEntityById(scene.entities, input.id);
|
|
339
|
-
const result = applyDiffOrThrow(scene, {
|
|
340
|
-
ops: [{ type: 'remove', id: input.id }],
|
|
341
|
-
environmentChanged: false,
|
|
342
|
-
metaChanged: false,
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
return finalizeSceneMutation(
|
|
346
|
-
absPath,
|
|
347
|
-
input.scenePath,
|
|
348
|
-
result,
|
|
349
|
-
input.dryRun ?? false,
|
|
350
|
-
{ entity: before },
|
|
351
|
-
{ entity: null },
|
|
352
|
-
);
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
/** Register every B2 `project.scene.entity.*` operation onto `registry`. */
|
|
357
|
-
export function registerEntityOperations(registry: ToolRegistry): void {
|
|
358
|
-
registry.register(projectSceneEntityAdd);
|
|
359
|
-
registry.register(projectSceneEntityUpdate);
|
|
360
|
-
registry.register(projectSceneEntityMove);
|
|
361
|
-
registry.register(projectSceneEntityRemove);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Re-exported for component-operations.ts (avoids a second copy of the
|
|
365
|
-
// "merge fields -> applyDiff update -> finalize" plumbing).
|
|
366
|
-
export { finalizeSceneMutation };
|