@compilr-dev/sdk 0.29.8 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canvas/index.d.ts +2 -0
- package/dist/canvas/index.js +7 -0
- package/dist/canvas/scene-ops.d.ts +122 -0
- package/dist/canvas/scene-ops.js +420 -0
- package/dist/canvas/scene.d.ts +277 -0
- package/dist/canvas/scene.js +963 -0
- package/dist/canvas/types.d.ts +9 -1
- package/dist/canvas/types.js +1 -0
- package/dist/capabilities/packs.d.ts +1 -1
- package/dist/capabilities/packs.js +36 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/platform/context.d.ts +18 -1
- package/dist/platform/index.d.ts +4 -2
- package/dist/platform/index.js +4 -1
- package/dist/platform/scene-writer.d.ts +95 -0
- package/dist/platform/scene-writer.js +184 -0
- package/dist/platform/services.d.ts +20 -2
- package/dist/platform/tools/canvas-tools.js +101 -5
- package/dist/platform/tools/index.d.ts +1 -0
- package/dist/platform/tools/index.js +18 -2
- package/dist/platform/tools/scene-tools.d.ts +65 -0
- package/dist/platform/tools/scene-tools.js +511 -0
- package/dist/team/tool-config.js +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene Tools — agents build 3D scenes as validated JSON (3d-canvas-spec §6).
|
|
3
|
+
*
|
|
4
|
+
* 7 tools: scene_create, scene_get, scene_add_object, scene_update_object,
|
|
5
|
+
* scene_remove_object, scene_set_camera, scene_screenshot.
|
|
6
|
+
*
|
|
7
|
+
* Every edit goes through the scene writer (`platform/scene-writer.ts`) — the same instance the
|
|
8
|
+
* host's inspector uses — so user and agent edits serialise and the agent is TOLD about the
|
|
9
|
+
* user's changes: a successful result starts with "The user edited this scene since your last
|
|
10
|
+
* call: …" when there are any.
|
|
11
|
+
*
|
|
12
|
+
* Names carry the `scene_` prefix: generic names (`add_object`) collide with MCP servers, and
|
|
13
|
+
* hosts refresh views on tool-name prefixes.
|
|
14
|
+
*/
|
|
15
|
+
import { defineTool, createErrorResult } from '@compilr-dev/agents';
|
|
16
|
+
import { createSceneWriter } from '../scene-writer.js';
|
|
17
|
+
import { SCENE_MAX_BATCH, SCENE_OBJECT_TYPES, describeScene, serializeScene, } from '../../canvas/scene.js';
|
|
18
|
+
import { formatUserEditNotice, } from '../../canvas/scene-ops.js';
|
|
19
|
+
export const SCENE_TOOL_NAMES = [
|
|
20
|
+
'scene_create',
|
|
21
|
+
'scene_get',
|
|
22
|
+
'scene_add_object',
|
|
23
|
+
'scene_update_object',
|
|
24
|
+
'scene_remove_object',
|
|
25
|
+
'scene_set_camera',
|
|
26
|
+
'scene_screenshot',
|
|
27
|
+
];
|
|
28
|
+
const DEFAULT_SHOT = { width: 1280, height: 800 };
|
|
29
|
+
const SHOT_MIN = 320;
|
|
30
|
+
const SHOT_MAX = 2048;
|
|
31
|
+
/** "sofa · box · Sofa" rows, indented under their group parent. */
|
|
32
|
+
export function sceneOutline(scene) {
|
|
33
|
+
if (scene.objects.length === 0)
|
|
34
|
+
return '(no objects yet)';
|
|
35
|
+
const byParent = new Map();
|
|
36
|
+
const ids = new Set(scene.objects.map((o) => o.id));
|
|
37
|
+
for (const o of scene.objects) {
|
|
38
|
+
const key = o.group !== undefined && ids.has(o.group) ? o.group : undefined;
|
|
39
|
+
const list = byParent.get(key) ?? [];
|
|
40
|
+
list.push(o);
|
|
41
|
+
byParent.set(key, list);
|
|
42
|
+
}
|
|
43
|
+
/** The type column, with what changes its shape: "group (3 inside)", "cone (4 sides)", … */
|
|
44
|
+
const typeOf = (o) => {
|
|
45
|
+
switch (o.type) {
|
|
46
|
+
case 'group': {
|
|
47
|
+
const n = byParent.get(o.id)?.length ?? 0;
|
|
48
|
+
return n === 0 ? 'group (empty)' : `group (${String(n)} inside)`;
|
|
49
|
+
}
|
|
50
|
+
case 'cone':
|
|
51
|
+
case 'cylinder':
|
|
52
|
+
return o.segments !== undefined ? `${o.type} (${String(o.segments)} sides)` : o.type;
|
|
53
|
+
case 'extrude':
|
|
54
|
+
return o.axis !== undefined && o.axis !== 'y' ? `extrude (along ${o.axis})` : o.type;
|
|
55
|
+
case 'wedge':
|
|
56
|
+
return o.ridge !== undefined && o.ridge !== 0.5
|
|
57
|
+
? `wedge (ridge ${String(o.ridge)})`
|
|
58
|
+
: o.type;
|
|
59
|
+
default:
|
|
60
|
+
return o.type;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const rows = [];
|
|
64
|
+
const walk = (parent, depth) => {
|
|
65
|
+
for (const o of byParent.get(parent) ?? []) {
|
|
66
|
+
const lock = o.locked ? ' · locked' : '';
|
|
67
|
+
rows.push(`${' '.repeat(depth)}${o.id} · ${typeOf(o)}${o.name ? ` · ${o.name}` : ''}${lock}`);
|
|
68
|
+
if (depth < 8)
|
|
69
|
+
walk(o.id, depth + 1);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
walk(undefined, 0);
|
|
73
|
+
return rows.join('\n');
|
|
74
|
+
}
|
|
75
|
+
function clampShot(v, fallback) {
|
|
76
|
+
if (typeof v !== 'number' || !Number.isFinite(v))
|
|
77
|
+
return fallback;
|
|
78
|
+
return Math.round(Math.max(SHOT_MIN, Math.min(SHOT_MAX, v)));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Render a scene through the host's `renderSceneToImage` — shared by scene_screenshot and
|
|
82
|
+
* canvas_screenshot (either tool works on a scene).
|
|
83
|
+
*/
|
|
84
|
+
export async function screenshotScene(ctx, args) {
|
|
85
|
+
const render = ctx.canvasRenderer?.renderSceneToImage?.bind(ctx.canvasRenderer);
|
|
86
|
+
if (!render) {
|
|
87
|
+
return {
|
|
88
|
+
available: false,
|
|
89
|
+
text: '3D scene rendering is not available in this environment — open the scene in compilr.dev ' +
|
|
90
|
+
`Desktop to see it. Rely on scene_get instead: "${args.title}" has ${describeScene(args.scene)}.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const width = clampShot(args.width, DEFAULT_SHOT.width);
|
|
94
|
+
const height = clampShot(args.height, DEFAULT_SHOT.height);
|
|
95
|
+
const img = await render({ scene: args.scene, width, height });
|
|
96
|
+
const view = args.scene.camera ? 'its camera' : 'the fitted view';
|
|
97
|
+
return {
|
|
98
|
+
available: true,
|
|
99
|
+
text: `Rendered scene "${args.title}" (${describeScene(args.scene)}) at ${String(width)}×${String(height)} from ${view}. ` +
|
|
100
|
+
'Check proportions, gaps, overlaps and floating objects (position.y is the BOTTOM); fix with ' +
|
|
101
|
+
'scene_update_object and screenshot again.',
|
|
102
|
+
image: { data: img.data, mediaType: img.mediaType, width: img.width, height: img.height },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const VEC3 = { type: 'array', items: { type: 'number' }, minItems: 3, maxItems: 3 };
|
|
106
|
+
/** The per-object fields, shared by the single-object form and each entry of `objects`. */
|
|
107
|
+
const OBJECT_PROPERTIES = {
|
|
108
|
+
type: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
enum: SCENE_OBJECT_TYPES,
|
|
111
|
+
description: 'box (size [w,h,d]) · sphere (radius) · cylinder / cone (radius, height, optional segments) · torus (radius, tube; lies flat) · ' +
|
|
112
|
+
'extrude (points + height, optional axis) for walls, floors and profiles · wedge (size [w,h,d], optional ridge) — a ' +
|
|
113
|
+
'triangular prism for roofs · group — a container with no geometry: give it your own id and put parts in it with ' +
|
|
114
|
+
'group: "<id>". There is no "wall" type — use extrude.',
|
|
115
|
+
},
|
|
116
|
+
position: {
|
|
117
|
+
...VEC3,
|
|
118
|
+
description: "Metres [x, y, z]. y is the object's BOTTOM: y = 0 sits on the floor.",
|
|
119
|
+
},
|
|
120
|
+
id: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description: 'Your own id (lowercase letters, digits, - or _; ≤ 48), e.g. "house", "roof-left". Choose ids for ' +
|
|
123
|
+
'groups so later objects — even in the same batch — can say group: "<id>". Must be new: an id already ' +
|
|
124
|
+
'in the scene is an error (never renamed). Omit to generate one from name.',
|
|
125
|
+
},
|
|
126
|
+
name: { type: 'string', description: 'Display name (≤ 80 chars), e.g. "Sofa".' },
|
|
127
|
+
size: {
|
|
128
|
+
...VEC3,
|
|
129
|
+
description: 'box / wedge: [width (x), height (y), depth (z)] in metres (0.01–500).',
|
|
130
|
+
},
|
|
131
|
+
radius: {
|
|
132
|
+
type: 'number',
|
|
133
|
+
description: 'sphere/cylinder/cone/torus: radius in metres. With segments it is the corner (circum-) radius.',
|
|
134
|
+
},
|
|
135
|
+
height: {
|
|
136
|
+
type: 'number',
|
|
137
|
+
description: 'cylinder/cone: height in metres. extrude: how far it extends along its axis.',
|
|
138
|
+
},
|
|
139
|
+
tube: { type: 'number', description: 'torus only: tube radius (≤ radius).' },
|
|
140
|
+
segments: {
|
|
141
|
+
type: 'integer',
|
|
142
|
+
minimum: 3,
|
|
143
|
+
maximum: 64,
|
|
144
|
+
description: 'cone/cylinder only: number of flat sides (3–64); omit for smooth. cone + 4 = a pyramid (hip roof) — ' +
|
|
145
|
+
'its corners point along ±x/±z, so add rotation [0, 45, 0] for sides parallel to the axes (side = radius·√2).',
|
|
146
|
+
},
|
|
147
|
+
points: {
|
|
148
|
+
type: 'array',
|
|
149
|
+
items: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 },
|
|
150
|
+
description: 'extrude only: outline [[a, b], …] local to position, 3–256 points, not self-intersecting. ' +
|
|
151
|
+
'axis "y" (default): [x, z] plan points, rising 0 → height. axis "z": [x, y] side profile ' +
|
|
152
|
+
'(b is up), pushed 0 → height along +z. axis "x": [z, y] side profile (b is up), pushed 0 → height along +x. ' +
|
|
153
|
+
"The profile's lowest point sits at position.y.",
|
|
154
|
+
},
|
|
155
|
+
axis: {
|
|
156
|
+
type: 'string',
|
|
157
|
+
enum: ['x', 'y', 'z'],
|
|
158
|
+
description: 'extrude only: the direction it extends. "y" (default) = a plan outline rising vertically (walls, floors); ' +
|
|
159
|
+
'"x" / "z" = a side profile (a gable, a roof section) extended horizontally — see points.',
|
|
160
|
+
},
|
|
161
|
+
ridge: {
|
|
162
|
+
type: 'number',
|
|
163
|
+
description: 'wedge only: where the apex sits across the width, 0–1 (default 0.5 = centred; 0 or 1 = a lean-to). ' +
|
|
164
|
+
'The triangle is in the x–y plane; the ridge runs along z (the depth). Rotate [0, 90, 0] for a ridge along x.',
|
|
165
|
+
},
|
|
166
|
+
rotation: {
|
|
167
|
+
...VEC3,
|
|
168
|
+
description: 'Degrees [x, y, z]. Rotate about Y; x/z tilt breaks the floor rule.',
|
|
169
|
+
},
|
|
170
|
+
color: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
description: '"#RRGGBB". Omit for the neutral palette — keep neutral colours unless asked. Not on a group.',
|
|
173
|
+
},
|
|
174
|
+
group: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
description: 'Parent id — normally a type "group" container, created earlier or in the same batch. position/rotation ' +
|
|
177
|
+
"become relative to the parent (y = 0 is the group's y); moving the group moves its children.",
|
|
178
|
+
},
|
|
179
|
+
material: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
description: '{ roughness 0–1, metalness 0–1, opacity 0.05–1 }. Not on a group.',
|
|
182
|
+
properties: {
|
|
183
|
+
roughness: { type: 'number' },
|
|
184
|
+
metalness: { type: 'number' },
|
|
185
|
+
opacity: { type: 'number' },
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
locked: {
|
|
189
|
+
type: 'boolean',
|
|
190
|
+
description: 'Lock against USER edits in the inspector (agents can still edit it).',
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
const OBJECT_FIELD_NAMES = Object.keys(OBJECT_PROPERTIES);
|
|
194
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
195
|
+
export function createSceneTools(config, options) {
|
|
196
|
+
const ctx = config.context;
|
|
197
|
+
const canvases = ctx.canvases;
|
|
198
|
+
if (!canvases)
|
|
199
|
+
throw new Error('canvases repository required');
|
|
200
|
+
const writer = options?.writer ?? ctx.sceneWriter ?? createSceneWriter(canvases);
|
|
201
|
+
/** The calling agent (keys the "told about user edits" mark per agent). */
|
|
202
|
+
const agentKey = () => config.hooks?.resolveOwner?.('self') ?? '';
|
|
203
|
+
const agentSource = () => ({ kind: 'agent', agentId: agentKey() || undefined });
|
|
204
|
+
/** Prepend the user-edit notice (successful results only) and mark it delivered. */
|
|
205
|
+
const withNotice = (canvasId, body) => {
|
|
206
|
+
const notice = formatUserEditNotice(writer.consumeUserEdits(canvasId, agentKey()));
|
|
207
|
+
return notice ? `${notice}\n\n${body}` : body;
|
|
208
|
+
};
|
|
209
|
+
const errorMessage = (verb, error) => `Failed to ${verb}: ${error instanceof Error ? error.message : String(error)}`;
|
|
210
|
+
/** Apply ops as the agent and shape the tool result. */
|
|
211
|
+
const applyAsAgent = async (canvasId, ops, idLine) => {
|
|
212
|
+
const r = await writer.apply(canvasId, ops, agentSource());
|
|
213
|
+
if (!r.ok)
|
|
214
|
+
return createErrorResult(r.error);
|
|
215
|
+
const lines = [...r.summaries];
|
|
216
|
+
const ids = idLine(r.ids);
|
|
217
|
+
if (ids)
|
|
218
|
+
lines.push(ids);
|
|
219
|
+
lines.push(`Scene "${r.title}": ${describeScene(r.scene)} · rev ${String(r.rev)}`);
|
|
220
|
+
if (r.warnings.length)
|
|
221
|
+
lines.push('', ...r.warnings);
|
|
222
|
+
return {
|
|
223
|
+
success: true,
|
|
224
|
+
result: withNotice(canvasId, lines.join('\n')),
|
|
225
|
+
// Structured sibling for hosts (view refresh / P2 grow-in) — prose stays for the model.
|
|
226
|
+
scene: { canvasId, rev: r.rev, ids: r.ids, action: ops[0]?.op },
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
const sceneCreateTool = defineTool({
|
|
231
|
+
name: 'scene_create',
|
|
232
|
+
description: 'Create a 3D SCENE canvas — objects in metres the user can orbit, select, edit and export. ' +
|
|
233
|
+
'Scenes are JSON data, not HTML/code (never use canvas_write for 3D). Pass only a title for an ' +
|
|
234
|
+
'empty scene, then build it with scene_add_object (up to 50 objects per call), or pass a whole ' +
|
|
235
|
+
'scene { objects: [...], camera?, lights? }. Returns the canvas ID used by every scene_* tool.',
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {
|
|
239
|
+
title: { type: 'string', description: 'Scene title (shown in the library and tab).' },
|
|
240
|
+
scene: {
|
|
241
|
+
type: 'object',
|
|
242
|
+
description: 'Optional initial scene: { objects: [...], camera?: { position, target }, lights?: [...] }. ' +
|
|
243
|
+
'version/units/name default from the title. Each object is { id, type, position, … } as in scene_add_object.',
|
|
244
|
+
},
|
|
245
|
+
project_id: {
|
|
246
|
+
type: 'number',
|
|
247
|
+
description: 'Optional project id. Defaults to the current project.',
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
required: ['title'],
|
|
251
|
+
},
|
|
252
|
+
execute: async (input) => {
|
|
253
|
+
try {
|
|
254
|
+
const projectId = input.project_id ?? ctx.currentProjectId;
|
|
255
|
+
if (!projectId) {
|
|
256
|
+
return createErrorResult('No active project. Provide project_id or select a project.');
|
|
257
|
+
}
|
|
258
|
+
const r = await writer.create({ projectId, title: input.title, scene: input.scene });
|
|
259
|
+
if (!r.ok)
|
|
260
|
+
return createErrorResult(r.error);
|
|
261
|
+
// Mark first contact so later user edits are announced to this agent.
|
|
262
|
+
writer.consumeUserEdits(r.canvas.id, agentKey());
|
|
263
|
+
/*
|
|
264
|
+
⚠️ Same prose + `canvas` sibling as canvas_write: Desktop reveals/auto-splits new
|
|
265
|
+
canvases from this exact shape (`renderer/src/lib/canvas-authored.ts`).
|
|
266
|
+
*/
|
|
267
|
+
return {
|
|
268
|
+
success: true,
|
|
269
|
+
result: `Created canvas "${r.canvas.title}" (scene)\nID: ${String(r.canvas.id)}\n` +
|
|
270
|
+
`${describeScene(r.scene)}. Add objects with scene_add_object; check it with scene_screenshot.`,
|
|
271
|
+
canvas: {
|
|
272
|
+
id: r.canvas.id,
|
|
273
|
+
title: r.canvas.title,
|
|
274
|
+
type: r.canvas.type,
|
|
275
|
+
action: 'created',
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
return createErrorResult(errorMessage('create scene', error));
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
const sceneGetTool = defineTool({
|
|
286
|
+
name: 'scene_get',
|
|
287
|
+
description: 'Read a 3D scene: the full JSON (pretty-printed) plus a one-line summary. Always read before ' +
|
|
288
|
+
'editing — the user edits scenes too, and object ids come from here.',
|
|
289
|
+
inputSchema: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
properties: {
|
|
292
|
+
canvas_id: { type: 'number', description: 'The scene canvas id.' },
|
|
293
|
+
},
|
|
294
|
+
required: ['canvas_id'],
|
|
295
|
+
},
|
|
296
|
+
execute: async (input) => {
|
|
297
|
+
try {
|
|
298
|
+
const r = await writer.read(input.canvas_id);
|
|
299
|
+
if (!r.ok)
|
|
300
|
+
return createErrorResult(r.error);
|
|
301
|
+
const body = `Scene "${r.title}" (canvas ${String(r.canvasId)}): ${describeScene(r.scene)} · rev ${String(r.rev)}\n\n` +
|
|
302
|
+
serializeScene(r.scene);
|
|
303
|
+
return { success: true, result: withNotice(r.canvasId, body) };
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
return createErrorResult(errorMessage('read scene', error));
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
const sceneAddObjectTool = defineTool({
|
|
312
|
+
name: 'scene_add_object',
|
|
313
|
+
description: 'Add one object to a 3D scene, or up to 50 at once with objects: [...] (atomic: all or none). ' +
|
|
314
|
+
"Units are metres; position.y is the object's BOTTOM (0 = on the floor); rotation in degrees about Y. " +
|
|
315
|
+
'Build hierarchically: a type "group" with your own id (e.g. "house", then "walls", "roof" inside it), ' +
|
|
316
|
+
'and its parts with group: "<id>" — a batch may name a group it creates itself. ' +
|
|
317
|
+
'Returns the new id(s) — yours, or generated from name when id is omitted.',
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: 'object',
|
|
320
|
+
properties: {
|
|
321
|
+
canvas_id: { type: 'number', description: 'The scene canvas id.' },
|
|
322
|
+
...OBJECT_PROPERTIES,
|
|
323
|
+
objects: {
|
|
324
|
+
type: 'array',
|
|
325
|
+
maxItems: SCENE_MAX_BATCH,
|
|
326
|
+
description: `Batch form: up to ${String(SCENE_MAX_BATCH)} objects, each with the same fields as the single form (type, position, …). Use instead of the top-level fields.`,
|
|
327
|
+
items: { type: 'object', properties: OBJECT_PROPERTIES, required: ['type', 'position'] },
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
required: ['canvas_id'],
|
|
331
|
+
},
|
|
332
|
+
execute: async (input) => {
|
|
333
|
+
try {
|
|
334
|
+
const single = {};
|
|
335
|
+
for (const k of OBJECT_FIELD_NAMES)
|
|
336
|
+
if (input[k] !== undefined)
|
|
337
|
+
single[k] = input[k];
|
|
338
|
+
const hasSingle = Object.keys(single).length > 0;
|
|
339
|
+
let op;
|
|
340
|
+
if (input.objects !== undefined) {
|
|
341
|
+
if (hasSingle) {
|
|
342
|
+
return createErrorResult("Pass either one object's fields (type, position, …) or objects: [...], not both.");
|
|
343
|
+
}
|
|
344
|
+
if (!Array.isArray(input.objects)) {
|
|
345
|
+
return createErrorResult('objects must be an array of objects.');
|
|
346
|
+
}
|
|
347
|
+
op = { op: 'add', objects: input.objects };
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
if (!hasSingle) {
|
|
351
|
+
return createErrorResult('Nothing to add. Pass type + position (+ size/radius/height/…), or objects: [...].');
|
|
352
|
+
}
|
|
353
|
+
op = { op: 'add', object: single };
|
|
354
|
+
}
|
|
355
|
+
return await applyAsAgent(input.canvas_id, [op], (ids) => ids.length === 1 ? `ID: ${ids[0]}` : `IDs: ${ids.join(', ')}`);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
return createErrorResult(errorMessage('add object', error));
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
const sceneUpdateObjectTool = defineTool({
|
|
364
|
+
name: 'scene_update_object',
|
|
365
|
+
description: 'Change fields of one object in a 3D scene. patch is a SHALLOW merge — arrays (position, size, ' +
|
|
366
|
+
'points, rotation) and material are replaced whole; null clears an optional field (e.g. group). ' +
|
|
367
|
+
'id and type cannot change: remove the object and add it again. Read ids with scene_get first.',
|
|
368
|
+
inputSchema: {
|
|
369
|
+
type: 'object',
|
|
370
|
+
properties: {
|
|
371
|
+
canvas_id: { type: 'number', description: 'The scene canvas id.' },
|
|
372
|
+
id: { type: 'string', description: 'The object id to change.' },
|
|
373
|
+
patch: {
|
|
374
|
+
type: 'object',
|
|
375
|
+
description: 'Fields to set, e.g. { "position": [1.2, 0, -0.4], "color": "#8FA3B0" }. Same fields as scene_add_object: ' +
|
|
376
|
+
'size, radius, height, tube, points, segments (cone/cylinder), axis (extrude), ridge (wedge), group ' +
|
|
377
|
+
'(move it into a group; null = back to the root), name, rotation, color/material (not on a group), locked.',
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
required: ['canvas_id', 'id', 'patch'],
|
|
381
|
+
},
|
|
382
|
+
execute: async (input) => {
|
|
383
|
+
try {
|
|
384
|
+
return await applyAsAgent(input.canvas_id, [{ op: 'update', id: input.id, patch: input.patch }], () => null);
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
return createErrorResult(errorMessage('update object', error));
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
const sceneRemoveObjectTool = defineTool({
|
|
393
|
+
name: 'scene_remove_object',
|
|
394
|
+
description: 'Remove an object from a 3D scene. Objects grouped under it (its children) are removed too, ' +
|
|
395
|
+
'and the result lists them.',
|
|
396
|
+
inputSchema: {
|
|
397
|
+
type: 'object',
|
|
398
|
+
properties: {
|
|
399
|
+
canvas_id: { type: 'number', description: 'The scene canvas id.' },
|
|
400
|
+
id: { type: 'string', description: 'The object id to remove.' },
|
|
401
|
+
},
|
|
402
|
+
required: ['canvas_id', 'id'],
|
|
403
|
+
},
|
|
404
|
+
execute: async (input) => {
|
|
405
|
+
try {
|
|
406
|
+
return await applyAsAgent(input.canvas_id, [{ op: 'remove', id: input.id }], () => null);
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
return createErrorResult(errorMessage('remove object', error));
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
const sceneSetCameraTool = defineTool({
|
|
415
|
+
name: 'scene_set_camera',
|
|
416
|
+
description: 'Set the default view of a 3D scene: position + target in metres, or fit: true to clear it ' +
|
|
417
|
+
"(the viewer then frames all objects). The user's own orbiting never changes this.",
|
|
418
|
+
inputSchema: {
|
|
419
|
+
type: 'object',
|
|
420
|
+
properties: {
|
|
421
|
+
canvas_id: { type: 'number', description: 'The scene canvas id.' },
|
|
422
|
+
position: { ...VEC3, description: 'Camera position [x, y, z] in metres.' },
|
|
423
|
+
target: { ...VEC3, description: 'The point the camera looks at [x, y, z].' },
|
|
424
|
+
fit: {
|
|
425
|
+
type: 'boolean',
|
|
426
|
+
description: 'true = remove the stored camera and frame all objects.',
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
required: ['canvas_id'],
|
|
430
|
+
},
|
|
431
|
+
execute: async (input) => {
|
|
432
|
+
try {
|
|
433
|
+
let op;
|
|
434
|
+
if (input.fit === true) {
|
|
435
|
+
if (input.position !== undefined || input.target !== undefined) {
|
|
436
|
+
return createErrorResult('Pass position + target, or fit: true — not both.');
|
|
437
|
+
}
|
|
438
|
+
op = { op: 'camera', camera: { fit: true } };
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
if (input.position === undefined || input.target === undefined) {
|
|
442
|
+
return createErrorResult('Pass both position and target ([x, y, z] in metres), or fit: true to frame all objects.');
|
|
443
|
+
}
|
|
444
|
+
op = {
|
|
445
|
+
op: 'camera',
|
|
446
|
+
camera: { position: input.position, target: input.target },
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return await applyAsAgent(input.canvas_id, [op], () => null);
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
return createErrorResult(errorMessage('set camera', error));
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
const sceneScreenshotTool = defineTool({
|
|
458
|
+
name: 'scene_screenshot',
|
|
459
|
+
description: 'RENDER a 3D scene and SEE it — check proportions, overlaps, gaps and floating objects after ' +
|
|
460
|
+
'building. Uses the scene camera, or frames all objects when it has none. Only useful on a ' +
|
|
461
|
+
'vision-capable model; unavailable in hosts without a 3D viewer (e.g. the terminal).',
|
|
462
|
+
inputSchema: {
|
|
463
|
+
type: 'object',
|
|
464
|
+
properties: {
|
|
465
|
+
canvas_id: { type: 'number', description: 'The scene canvas id.' },
|
|
466
|
+
width: {
|
|
467
|
+
type: 'number',
|
|
468
|
+
description: `Image width in px (default ${String(DEFAULT_SHOT.width)}).`,
|
|
469
|
+
},
|
|
470
|
+
height: {
|
|
471
|
+
type: 'number',
|
|
472
|
+
description: `Image height in px (default ${String(DEFAULT_SHOT.height)}).`,
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
required: ['canvas_id'],
|
|
476
|
+
},
|
|
477
|
+
execute: async (input) => {
|
|
478
|
+
try {
|
|
479
|
+
const r = await writer.read(input.canvas_id);
|
|
480
|
+
if (!r.ok)
|
|
481
|
+
return createErrorResult(r.error);
|
|
482
|
+
const shot = await screenshotScene(ctx, {
|
|
483
|
+
canvasId: r.canvasId,
|
|
484
|
+
title: r.title,
|
|
485
|
+
scene: r.scene,
|
|
486
|
+
width: input.width,
|
|
487
|
+
height: input.height,
|
|
488
|
+
});
|
|
489
|
+
if (!shot.available)
|
|
490
|
+
return { success: true, result: withNotice(r.canvasId, shot.text) };
|
|
491
|
+
return {
|
|
492
|
+
success: true,
|
|
493
|
+
result: withNotice(r.canvasId, shot.text),
|
|
494
|
+
imageBlocks: [{ ...shot.image, filename: `scene-${String(r.canvasId)}.png` }],
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
return createErrorResult(errorMessage('screenshot scene', error));
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
return [
|
|
503
|
+
sceneCreateTool,
|
|
504
|
+
sceneGetTool,
|
|
505
|
+
sceneAddObjectTool,
|
|
506
|
+
sceneUpdateObjectTool,
|
|
507
|
+
sceneRemoveObjectTool,
|
|
508
|
+
sceneSetCameraTool,
|
|
509
|
+
sceneScreenshotTool,
|
|
510
|
+
];
|
|
511
|
+
}
|
package/dist/team/tool-config.js
CHANGED
|
@@ -304,6 +304,21 @@ export const TOOL_GROUPS = {
|
|
|
304
304
|
readOnly: false,
|
|
305
305
|
tier: 'meta',
|
|
306
306
|
},
|
|
307
|
+
scene: {
|
|
308
|
+
label: '3D Scene',
|
|
309
|
+
// Defined in @compilr-dev/sdk (createSceneTools) — literal names, like canvas.
|
|
310
|
+
tools: [
|
|
311
|
+
'scene_create',
|
|
312
|
+
'scene_get',
|
|
313
|
+
'scene_add_object',
|
|
314
|
+
'scene_update_object',
|
|
315
|
+
'scene_remove_object',
|
|
316
|
+
'scene_set_camera',
|
|
317
|
+
'scene_screenshot',
|
|
318
|
+
],
|
|
319
|
+
readOnly: false,
|
|
320
|
+
tier: 'meta',
|
|
321
|
+
},
|
|
307
322
|
plans: {
|
|
308
323
|
label: 'Planning',
|
|
309
324
|
tools: [
|
|
@@ -535,6 +550,7 @@ export const TOOL_PROFILES = {
|
|
|
535
550
|
'backlog_write',
|
|
536
551
|
'documents',
|
|
537
552
|
'canvas',
|
|
553
|
+
'scene',
|
|
538
554
|
'plans',
|
|
539
555
|
'artifacts',
|
|
540
556
|
'anchors',
|
|
@@ -575,6 +591,7 @@ export const TOOL_PROFILES = {
|
|
|
575
591
|
'search',
|
|
576
592
|
'documents',
|
|
577
593
|
'canvas',
|
|
594
|
+
'scene',
|
|
578
595
|
'web',
|
|
579
596
|
'factory_models',
|
|
580
597
|
],
|
|
@@ -631,6 +648,7 @@ export const TOOL_PROFILES = {
|
|
|
631
648
|
'backlog_read',
|
|
632
649
|
'documents',
|
|
633
650
|
'canvas',
|
|
651
|
+
'scene',
|
|
634
652
|
'plans',
|
|
635
653
|
'artifacts',
|
|
636
654
|
'anchors',
|
|
@@ -655,6 +673,7 @@ export const TOOL_PROFILES = {
|
|
|
655
673
|
'backlog_write',
|
|
656
674
|
'documents',
|
|
657
675
|
'canvas',
|
|
676
|
+
'scene',
|
|
658
677
|
'plans',
|
|
659
678
|
'artifacts',
|
|
660
679
|
'anchors',
|
|
@@ -677,6 +696,7 @@ export const TOOL_PROFILES = {
|
|
|
677
696
|
'backlog_write',
|
|
678
697
|
'documents',
|
|
679
698
|
'canvas',
|
|
699
|
+
'scene',
|
|
680
700
|
'artifacts',
|
|
681
701
|
'anchors',
|
|
682
702
|
'web',
|
|
@@ -696,6 +716,7 @@ export const TOOL_PROFILES = {
|
|
|
696
716
|
'search',
|
|
697
717
|
'documents',
|
|
698
718
|
'canvas',
|
|
719
|
+
'scene',
|
|
699
720
|
'artifacts',
|
|
700
721
|
'anchors',
|
|
701
722
|
'web',
|