@genart-dev/mcp-server 0.1.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/LICENSE +21 -0
- package/dist/index.cjs +3879 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3882 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.cjs +3874 -0
- package/dist/lib.cjs.map +1 -0
- package/dist/lib.d.cts +98 -0
- package/dist/lib.d.ts +98 -0
- package/dist/lib.js +3862 -0
- package/dist/lib.js.map +1 -0
- package/package.json +82 -0
package/dist/lib.js
ADDED
|
@@ -0,0 +1,3862 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { z as z2 } from "zod";
|
|
4
|
+
|
|
5
|
+
// src/tools/workspace.ts
|
|
6
|
+
import { readFile, writeFile, stat } from "fs/promises";
|
|
7
|
+
import { basename, dirname } from "path";
|
|
8
|
+
import {
|
|
9
|
+
parseGenart,
|
|
10
|
+
serializeWorkspace
|
|
11
|
+
} from "@genart-dev/core";
|
|
12
|
+
function now() {
|
|
13
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
14
|
+
}
|
|
15
|
+
function kebabify(title) {
|
|
16
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
17
|
+
}
|
|
18
|
+
async function fileExists(path) {
|
|
19
|
+
try {
|
|
20
|
+
await stat(path);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function dirExists(path) {
|
|
27
|
+
try {
|
|
28
|
+
const s = await stat(path);
|
|
29
|
+
return s.isDirectory();
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function arrangePositions(sketches, layout, spacing) {
|
|
35
|
+
if (sketches.length === 0) return [];
|
|
36
|
+
if (layout === "row") {
|
|
37
|
+
let x = 0;
|
|
38
|
+
return sketches.map((s) => {
|
|
39
|
+
const pos = { file: s.file, position: { x, y: 0 } };
|
|
40
|
+
x += s.width + spacing;
|
|
41
|
+
return pos;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (layout === "column") {
|
|
45
|
+
let y = 0;
|
|
46
|
+
return sketches.map((s) => {
|
|
47
|
+
const pos = { file: s.file, position: { x: 0, y } };
|
|
48
|
+
y += s.height + spacing;
|
|
49
|
+
return pos;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const cols = Math.ceil(Math.sqrt(sketches.length));
|
|
53
|
+
const maxW = Math.max(...sketches.map((s) => s.width));
|
|
54
|
+
const maxH = Math.max(...sketches.map((s) => s.height));
|
|
55
|
+
const cellW = maxW + spacing;
|
|
56
|
+
const cellH = maxH + spacing;
|
|
57
|
+
return sketches.map((s, i) => ({
|
|
58
|
+
file: s.file,
|
|
59
|
+
position: {
|
|
60
|
+
x: i % cols * cellW,
|
|
61
|
+
y: Math.floor(i / cols) * cellH
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
function computeViewport(positions) {
|
|
66
|
+
if (positions.length === 0) return { x: 0, y: 0, zoom: 1 };
|
|
67
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
68
|
+
for (const p of positions) {
|
|
69
|
+
const w = p.width ?? 1200;
|
|
70
|
+
const h = p.height ?? 1200;
|
|
71
|
+
if (p.position.x < minX) minX = p.position.x;
|
|
72
|
+
if (p.position.y < minY) minY = p.position.y;
|
|
73
|
+
if (p.position.x + w > maxX) maxX = p.position.x + w;
|
|
74
|
+
if (p.position.y + h > maxY) maxY = p.position.y + h;
|
|
75
|
+
}
|
|
76
|
+
const centerX = (minX + maxX) / 2;
|
|
77
|
+
const centerY = (minY + maxY) / 2;
|
|
78
|
+
const totalW = maxX - minX;
|
|
79
|
+
const totalH = maxY - minY;
|
|
80
|
+
const zoom = Math.min(1, Math.min(1920 / (totalW + 200), 1080 / (totalH + 200)));
|
|
81
|
+
return { x: Math.round(centerX), y: Math.round(centerY), zoom: Math.round(zoom * 100) / 100 };
|
|
82
|
+
}
|
|
83
|
+
async function createWorkspace(state, input) {
|
|
84
|
+
const absPath = state.resolvePath(input.path);
|
|
85
|
+
if (!absPath.endsWith(".genart-workspace")) {
|
|
86
|
+
throw new Error("Path must end with .genart-workspace");
|
|
87
|
+
}
|
|
88
|
+
if (!state.remoteMode) {
|
|
89
|
+
const parentDir = dirname(absPath);
|
|
90
|
+
if (!await dirExists(parentDir)) {
|
|
91
|
+
throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
92
|
+
}
|
|
93
|
+
if (await fileExists(absPath)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Workspace already exists at ${absPath}. Use open_workspace to load it.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const sketchRefs = [];
|
|
100
|
+
const sketchDefs = [];
|
|
101
|
+
if (input.sketches && input.sketches.length > 0) {
|
|
102
|
+
for (const sketchPath of input.sketches) {
|
|
103
|
+
const absSketchPath = state.resolvePath(sketchPath);
|
|
104
|
+
if (!await fileExists(absSketchPath)) {
|
|
105
|
+
throw new Error(`Sketch file not found: ${absSketchPath}`);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const raw = await readFile(absSketchPath, "utf-8");
|
|
109
|
+
const json2 = JSON.parse(raw);
|
|
110
|
+
const def = parseGenart(json2);
|
|
111
|
+
sketchDefs.push({
|
|
112
|
+
file: basename(absSketchPath),
|
|
113
|
+
width: def.canvas.width,
|
|
114
|
+
height: def.canvas.height
|
|
115
|
+
});
|
|
116
|
+
} catch (e) {
|
|
117
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
118
|
+
throw new Error(`Invalid .genart file: ${absSketchPath} \u2014 ${msg}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const layout = input.arrangement ?? "grid";
|
|
122
|
+
const spacing = input.spacing ?? 200;
|
|
123
|
+
const positions = arrangePositions(sketchDefs, layout, spacing);
|
|
124
|
+
for (const p of positions) {
|
|
125
|
+
sketchRefs.push({ file: p.file, position: p.position });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const viewport = computeViewport(
|
|
129
|
+
sketchRefs.map((r) => {
|
|
130
|
+
const def = sketchDefs.find((d) => d.file === r.file);
|
|
131
|
+
return { position: r.position, width: def?.width, height: def?.height };
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
const ts = now();
|
|
135
|
+
const ws = {
|
|
136
|
+
"genart-workspace": "1.0",
|
|
137
|
+
id: kebabify(input.title),
|
|
138
|
+
title: input.title,
|
|
139
|
+
created: ts,
|
|
140
|
+
modified: ts,
|
|
141
|
+
viewport,
|
|
142
|
+
sketches: sketchRefs
|
|
143
|
+
};
|
|
144
|
+
const json = serializeWorkspace(ws);
|
|
145
|
+
if (state.remoteMode) {
|
|
146
|
+
state.workspacePath = absPath;
|
|
147
|
+
state.workspace = ws;
|
|
148
|
+
state.sketches.clear();
|
|
149
|
+
state.selection.clear();
|
|
150
|
+
state.emitMutation("workspace:loaded", { path: absPath, title: ws.title });
|
|
151
|
+
} else {
|
|
152
|
+
await writeFile(absPath, json, "utf-8");
|
|
153
|
+
await state.loadWorkspace(absPath);
|
|
154
|
+
}
|
|
155
|
+
state.emitMutation("workspace:updated", { path: absPath });
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
path: absPath,
|
|
159
|
+
title: input.title,
|
|
160
|
+
sketchCount: sketchRefs.length,
|
|
161
|
+
viewport,
|
|
162
|
+
fileContent: json
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function openWorkspace(state, input) {
|
|
166
|
+
const absPath = state.resolvePath(input.path);
|
|
167
|
+
if (!absPath.endsWith(".genart-workspace")) {
|
|
168
|
+
throw new Error("Path must end with .genart-workspace");
|
|
169
|
+
}
|
|
170
|
+
if (!await fileExists(absPath)) {
|
|
171
|
+
throw new Error(`Workspace not found: ${absPath}`);
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
await state.loadWorkspace(absPath);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
177
|
+
if (msg.includes("not found") || msg.includes("ENOENT")) {
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
180
|
+
throw new Error(`Invalid workspace file: ${absPath} \u2014 ${msg}`);
|
|
181
|
+
}
|
|
182
|
+
const ws = state.requireWorkspace();
|
|
183
|
+
const sketches = ws.sketches.map((ref) => {
|
|
184
|
+
const loaded = state.getSketch(
|
|
185
|
+
// Find by filename match
|
|
186
|
+
[...state.sketches.entries()].find(
|
|
187
|
+
([, v]) => basename(v.path) === ref.file
|
|
188
|
+
)?.[0] ?? ""
|
|
189
|
+
);
|
|
190
|
+
const def = loaded?.definition;
|
|
191
|
+
return {
|
|
192
|
+
file: ref.file,
|
|
193
|
+
position: ref.position,
|
|
194
|
+
label: ref.label,
|
|
195
|
+
id: def?.id,
|
|
196
|
+
title: def?.title,
|
|
197
|
+
renderer: def?.renderer ? { type: def.renderer.type, version: def.renderer.version } : void 0,
|
|
198
|
+
canvas: def?.canvas ? { width: def.canvas.width, height: def.canvas.height } : void 0,
|
|
199
|
+
locked: ref.locked ?? false,
|
|
200
|
+
visible: ref.visible ?? true
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
state.emitMutation("workspace:updated", { path: absPath });
|
|
204
|
+
return {
|
|
205
|
+
success: true,
|
|
206
|
+
path: absPath,
|
|
207
|
+
id: ws.id,
|
|
208
|
+
title: ws.title,
|
|
209
|
+
viewport: ws.viewport,
|
|
210
|
+
sketchCount: ws.sketches.length,
|
|
211
|
+
sketches,
|
|
212
|
+
groups: ws.groups ?? []
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async function addSketchToWorkspace(state, input) {
|
|
216
|
+
const ws = state.requireWorkspace();
|
|
217
|
+
const absSketchPath = state.resolvePath(input.sketchPath);
|
|
218
|
+
if (!await fileExists(absSketchPath)) {
|
|
219
|
+
throw new Error(`Sketch file not found: ${absSketchPath}`);
|
|
220
|
+
}
|
|
221
|
+
const file = basename(absSketchPath);
|
|
222
|
+
if (ws.sketches.some((s) => s.file === file)) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Sketch '${file}' is already in the workspace`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
let def;
|
|
228
|
+
try {
|
|
229
|
+
def = await state.loadSketch(absSketchPath);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
232
|
+
throw new Error(`Invalid .genart file: ${absSketchPath} \u2014 ${msg}`);
|
|
233
|
+
}
|
|
234
|
+
const position = input.position ?? autoPosition(ws, def);
|
|
235
|
+
const newRef = {
|
|
236
|
+
file,
|
|
237
|
+
position,
|
|
238
|
+
...input.label ? { label: input.label } : {}
|
|
239
|
+
};
|
|
240
|
+
state.workspace = {
|
|
241
|
+
...ws,
|
|
242
|
+
modified: now(),
|
|
243
|
+
sketches: [...ws.sketches, newRef]
|
|
244
|
+
};
|
|
245
|
+
await state.saveWorkspace();
|
|
246
|
+
state.emitMutation("workspace:updated", { added: file });
|
|
247
|
+
return {
|
|
248
|
+
success: true,
|
|
249
|
+
file,
|
|
250
|
+
id: def.id,
|
|
251
|
+
title: def.title,
|
|
252
|
+
position,
|
|
253
|
+
sketchCount: state.workspace.sketches.length
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function autoPosition(ws, def) {
|
|
257
|
+
if (ws.sketches.length === 0) return { x: 0, y: 0 };
|
|
258
|
+
let maxRight = -Infinity;
|
|
259
|
+
for (const s of ws.sketches) {
|
|
260
|
+
const right = s.position.x + 1200;
|
|
261
|
+
if (right > maxRight) maxRight = right;
|
|
262
|
+
}
|
|
263
|
+
return { x: maxRight + 200, y: 0 };
|
|
264
|
+
}
|
|
265
|
+
async function removeSketchFromWorkspace(state, input) {
|
|
266
|
+
const ws = state.requireWorkspace();
|
|
267
|
+
const loaded = state.getSketch(input.sketchId);
|
|
268
|
+
if (!loaded) {
|
|
269
|
+
throw new Error(`Sketch not found: '${input.sketchId}'`);
|
|
270
|
+
}
|
|
271
|
+
const file = basename(loaded.path);
|
|
272
|
+
const hadSketch = ws.sketches.some((s) => s.file === file);
|
|
273
|
+
if (!hadSketch) {
|
|
274
|
+
throw new Error(`Sketch '${input.sketchId}' is not in the workspace`);
|
|
275
|
+
}
|
|
276
|
+
const newSketches = ws.sketches.filter((s) => s.file !== file);
|
|
277
|
+
const newGroups = ws.groups?.map((g) => ({
|
|
278
|
+
...g,
|
|
279
|
+
sketchFiles: g.sketchFiles.filter((f) => f !== file)
|
|
280
|
+
})).filter((g) => g.sketchFiles.length > 0);
|
|
281
|
+
state.workspace = {
|
|
282
|
+
...ws,
|
|
283
|
+
modified: now(),
|
|
284
|
+
sketches: newSketches,
|
|
285
|
+
...newGroups && newGroups.length > 0 ? { groups: newGroups } : {}
|
|
286
|
+
};
|
|
287
|
+
state.sketches.delete(input.sketchId);
|
|
288
|
+
state.selection.delete(input.sketchId);
|
|
289
|
+
if (input.deleteFile) {
|
|
290
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
291
|
+
try {
|
|
292
|
+
await unlink2(loaded.path);
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
await state.saveWorkspace();
|
|
297
|
+
state.emitMutation("workspace:updated", { removed: file });
|
|
298
|
+
return {
|
|
299
|
+
success: true,
|
|
300
|
+
removedId: input.sketchId,
|
|
301
|
+
removedFile: file,
|
|
302
|
+
fileDeleted: input.deleteFile ?? false,
|
|
303
|
+
sketchCount: state.workspace.sketches.length
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async function listWorkspaceSketches(state, input) {
|
|
307
|
+
const ws = state.requireWorkspace();
|
|
308
|
+
const sketches = ws.sketches.map((ref) => {
|
|
309
|
+
const loaded = [...state.sketches.values()].find(
|
|
310
|
+
(v) => basename(v.path) === ref.file
|
|
311
|
+
);
|
|
312
|
+
const def = loaded?.definition;
|
|
313
|
+
const entry = {
|
|
314
|
+
file: ref.file,
|
|
315
|
+
position: ref.position,
|
|
316
|
+
label: ref.label,
|
|
317
|
+
id: def?.id,
|
|
318
|
+
title: def?.title,
|
|
319
|
+
renderer: def?.renderer.type,
|
|
320
|
+
canvas: def ? { width: def.canvas.width, height: def.canvas.height } : void 0,
|
|
321
|
+
parameterCount: def?.parameters.length ?? 0,
|
|
322
|
+
colorCount: def?.colors.length ?? 0,
|
|
323
|
+
locked: ref.locked ?? false,
|
|
324
|
+
visible: ref.visible ?? true
|
|
325
|
+
};
|
|
326
|
+
if (input.includeState && def) {
|
|
327
|
+
entry.state = def.state;
|
|
328
|
+
}
|
|
329
|
+
return entry;
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
success: true,
|
|
333
|
+
workspace: {
|
|
334
|
+
id: ws.id,
|
|
335
|
+
title: ws.title,
|
|
336
|
+
path: state.workspacePath
|
|
337
|
+
},
|
|
338
|
+
sketchCount: sketches.length,
|
|
339
|
+
sketches
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/tools/sketch.ts
|
|
344
|
+
import { writeFile as writeFile2, stat as stat2, unlink } from "fs/promises";
|
|
345
|
+
import { basename as basename2, dirname as dirname2, resolve } from "path";
|
|
346
|
+
import {
|
|
347
|
+
createDefaultRegistry,
|
|
348
|
+
resolvePreset,
|
|
349
|
+
serializeGenart,
|
|
350
|
+
serializeWorkspace as serializeWorkspace2
|
|
351
|
+
} from "@genart-dev/core";
|
|
352
|
+
var VALID_RENDERERS = [
|
|
353
|
+
"p5",
|
|
354
|
+
"three",
|
|
355
|
+
"glsl",
|
|
356
|
+
"canvas2d",
|
|
357
|
+
"svg"
|
|
358
|
+
];
|
|
359
|
+
var KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
360
|
+
function now2() {
|
|
361
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
362
|
+
}
|
|
363
|
+
async function fileExists2(path) {
|
|
364
|
+
try {
|
|
365
|
+
await stat2(path);
|
|
366
|
+
return true;
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function validateRendererType(type) {
|
|
372
|
+
if (!VALID_RENDERERS.includes(type)) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
`Unknown renderer type: '${type}'. Valid types: ${VALID_RENDERERS.join(", ")}`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function validateKebabId(id) {
|
|
379
|
+
if (!KEBAB_RE.test(id)) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
"ID must be kebab-case: lowercase letters, numbers, hyphens"
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function validateParameters(parameters) {
|
|
386
|
+
const keys = /* @__PURE__ */ new Set();
|
|
387
|
+
for (const p of parameters) {
|
|
388
|
+
if (keys.has(p.key)) {
|
|
389
|
+
throw new Error(`Duplicate parameter key: '${p.key}'`);
|
|
390
|
+
}
|
|
391
|
+
keys.add(p.key);
|
|
392
|
+
if (p.default < p.min || p.default > p.max) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
`Parameter '${p.key}' default (${p.default}) outside range [${p.min}, ${p.max}]`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function resolveCanvas(canvas) {
|
|
400
|
+
if (!canvas) {
|
|
401
|
+
return resolvePreset("square-1200");
|
|
402
|
+
}
|
|
403
|
+
if (canvas.preset) {
|
|
404
|
+
return resolvePreset(canvas.preset);
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
width: canvas.width ?? 1200,
|
|
408
|
+
height: canvas.height ?? 1200
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function buildState(parameters, colors, seed) {
|
|
412
|
+
const params = {};
|
|
413
|
+
for (const p of parameters) {
|
|
414
|
+
params[p.key] = p.default;
|
|
415
|
+
}
|
|
416
|
+
const colorPalette = colors.map((c) => c.default);
|
|
417
|
+
return { seed, params, colorPalette };
|
|
418
|
+
}
|
|
419
|
+
async function createSketch(state, input) {
|
|
420
|
+
const absPath = state.resolvePath(input.path);
|
|
421
|
+
if (!absPath.endsWith(".genart")) {
|
|
422
|
+
throw new Error("Path must end with .genart");
|
|
423
|
+
}
|
|
424
|
+
validateKebabId(input.id);
|
|
425
|
+
if (!state.remoteMode && await fileExists2(absPath)) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`File already exists at ${absPath}. Use update_sketch or fork_sketch.`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
const rendererType = input.renderer ?? "p5";
|
|
431
|
+
validateRendererType(rendererType);
|
|
432
|
+
const canvasDims = resolveCanvas(input.canvas);
|
|
433
|
+
const parameters = input.parameters ?? [];
|
|
434
|
+
const colors = input.colors ?? [];
|
|
435
|
+
if (parameters.length > 0) {
|
|
436
|
+
validateParameters(parameters);
|
|
437
|
+
}
|
|
438
|
+
let algorithm = input.algorithm;
|
|
439
|
+
if (!algorithm) {
|
|
440
|
+
const registry4 = createDefaultRegistry();
|
|
441
|
+
const adapter = registry4.resolve(rendererType);
|
|
442
|
+
algorithm = adapter.getAlgorithmTemplate();
|
|
443
|
+
}
|
|
444
|
+
const seed = input.seed ?? Math.floor(Math.random() * 1e5);
|
|
445
|
+
const ts = now2();
|
|
446
|
+
const sketch = {
|
|
447
|
+
genart: "1.1",
|
|
448
|
+
id: input.id,
|
|
449
|
+
title: input.title,
|
|
450
|
+
created: ts,
|
|
451
|
+
modified: ts,
|
|
452
|
+
renderer: { type: rendererType, version: "1.x" },
|
|
453
|
+
canvas: canvasDims,
|
|
454
|
+
parameters,
|
|
455
|
+
colors,
|
|
456
|
+
state: buildState(parameters, colors, seed),
|
|
457
|
+
algorithm,
|
|
458
|
+
...input.philosophy ? { philosophy: input.philosophy } : {},
|
|
459
|
+
...input.themes && input.themes.length > 0 ? { themes: input.themes } : {},
|
|
460
|
+
...input.skills && input.skills.length > 0 ? { skills: input.skills } : {},
|
|
461
|
+
...input.agent ? { agent: input.agent } : {},
|
|
462
|
+
...input.model ? { model: input.model } : {}
|
|
463
|
+
};
|
|
464
|
+
const json = serializeGenart(sketch);
|
|
465
|
+
if (!state.remoteMode) {
|
|
466
|
+
await writeFile2(absPath, json, "utf-8");
|
|
467
|
+
}
|
|
468
|
+
state.sketches.set(input.id, { definition: sketch, path: absPath });
|
|
469
|
+
state.emitMutation("sketch:created", { id: input.id, path: absPath });
|
|
470
|
+
if (state.remoteMode && !input.addToWorkspace) {
|
|
471
|
+
const file = basename2(absPath);
|
|
472
|
+
if (!state.workspace) {
|
|
473
|
+
const ts2 = now2();
|
|
474
|
+
state.workspace = {
|
|
475
|
+
"genart-workspace": "1.0",
|
|
476
|
+
id: "session",
|
|
477
|
+
title: "genart.dev",
|
|
478
|
+
created: ts2,
|
|
479
|
+
modified: ts2,
|
|
480
|
+
viewport: { x: 0, y: 0, zoom: 1 },
|
|
481
|
+
sketches: [{ file, position: { x: 0, y: 0 } }]
|
|
482
|
+
};
|
|
483
|
+
state.workspacePath = state.resolvePath("workspace.genart-workspace");
|
|
484
|
+
state.emitMutation("workspace:loaded", { path: state.workspacePath, title: state.workspace.title });
|
|
485
|
+
} else {
|
|
486
|
+
let maxRight = 0;
|
|
487
|
+
for (const s of state.workspace.sketches) {
|
|
488
|
+
const right = s.position.x + 1200;
|
|
489
|
+
if (right > maxRight) maxRight = right;
|
|
490
|
+
}
|
|
491
|
+
state.workspace = {
|
|
492
|
+
...state.workspace,
|
|
493
|
+
modified: now2(),
|
|
494
|
+
sketches: [...state.workspace.sketches, { file, position: { x: maxRight + 200, y: 0 } }]
|
|
495
|
+
};
|
|
496
|
+
state.emitMutation("workspace:updated", { added: file });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
let workspaceContent;
|
|
500
|
+
if (input.addToWorkspace) {
|
|
501
|
+
const wsPath = state.resolvePath(input.addToWorkspace);
|
|
502
|
+
if (state.workspace && state.workspacePath === wsPath) {
|
|
503
|
+
const file = basename2(absPath);
|
|
504
|
+
const ws = state.requireWorkspace();
|
|
505
|
+
let maxRight = 0;
|
|
506
|
+
for (const s of ws.sketches) {
|
|
507
|
+
const right = s.position.x + 1200;
|
|
508
|
+
if (right > maxRight) maxRight = right;
|
|
509
|
+
}
|
|
510
|
+
const position = ws.sketches.length === 0 ? { x: 0, y: 0 } : { x: maxRight + 200, y: 0 };
|
|
511
|
+
state.workspace = {
|
|
512
|
+
...ws,
|
|
513
|
+
modified: now2(),
|
|
514
|
+
sketches: [...ws.sketches, { file, position }]
|
|
515
|
+
};
|
|
516
|
+
workspaceContent = serializeWorkspace2(state.workspace);
|
|
517
|
+
if (!state.remoteMode) {
|
|
518
|
+
await writeFile2(wsPath, workspaceContent, "utf-8");
|
|
519
|
+
}
|
|
520
|
+
state.emitMutation("workspace:updated", { added: file });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
success: true,
|
|
525
|
+
path: absPath,
|
|
526
|
+
id: input.id,
|
|
527
|
+
title: input.title,
|
|
528
|
+
renderer: rendererType,
|
|
529
|
+
canvas: canvasDims,
|
|
530
|
+
seed,
|
|
531
|
+
fileContent: json,
|
|
532
|
+
...workspaceContent ? { workspaceContent } : {}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
async function openSketch(state, input) {
|
|
536
|
+
state.requireWorkspace();
|
|
537
|
+
const loaded = state.requireSketch(input.sketchId);
|
|
538
|
+
const def = loaded.definition;
|
|
539
|
+
state.setSelection([input.sketchId]);
|
|
540
|
+
state.emitMutation("selection:changed", { selected: [input.sketchId] });
|
|
541
|
+
return {
|
|
542
|
+
success: true,
|
|
543
|
+
id: def.id,
|
|
544
|
+
title: def.title,
|
|
545
|
+
renderer: def.renderer.type,
|
|
546
|
+
canvas: { width: def.canvas.width, height: def.canvas.height },
|
|
547
|
+
parameterCount: def.parameters.length,
|
|
548
|
+
colorCount: def.colors.length,
|
|
549
|
+
seed: def.state.seed,
|
|
550
|
+
philosophy: def.philosophy ?? null,
|
|
551
|
+
algorithmLength: def.algorithm.length
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
async function updateSketch(state, input) {
|
|
555
|
+
state.requireWorkspace();
|
|
556
|
+
const loaded = state.requireSketch(input.sketchId);
|
|
557
|
+
const def = loaded.definition;
|
|
558
|
+
const updatableFields = [
|
|
559
|
+
"title",
|
|
560
|
+
"philosophy",
|
|
561
|
+
"canvas",
|
|
562
|
+
"parameters",
|
|
563
|
+
"colors",
|
|
564
|
+
"themes",
|
|
565
|
+
"seed",
|
|
566
|
+
"skills"
|
|
567
|
+
];
|
|
568
|
+
const updated = [];
|
|
569
|
+
for (const field of updatableFields) {
|
|
570
|
+
if (input[field] !== void 0) {
|
|
571
|
+
updated.push(field);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (updated.length === 0) {
|
|
575
|
+
throw new Error(
|
|
576
|
+
"No fields to update. Provide at least one of: title, philosophy, canvas, parameters, colors, themes, seed, skills"
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
if (input.parameters) {
|
|
580
|
+
validateParameters(input.parameters);
|
|
581
|
+
}
|
|
582
|
+
let canvasDims = { width: def.canvas.width, height: def.canvas.height };
|
|
583
|
+
if (input.canvas) {
|
|
584
|
+
canvasDims = resolveCanvas(input.canvas);
|
|
585
|
+
}
|
|
586
|
+
const newParams = input.parameters ?? def.parameters;
|
|
587
|
+
const newColors = input.colors ?? def.colors;
|
|
588
|
+
const newSeed = input.seed ?? def.state.seed;
|
|
589
|
+
const newState = buildState(newParams, newColors, newSeed);
|
|
590
|
+
const newDef = {
|
|
591
|
+
...def,
|
|
592
|
+
modified: now2(),
|
|
593
|
+
canvas: canvasDims,
|
|
594
|
+
parameters: newParams,
|
|
595
|
+
colors: newColors,
|
|
596
|
+
state: newState,
|
|
597
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
598
|
+
...input.philosophy !== void 0 ? { philosophy: input.philosophy } : {},
|
|
599
|
+
...input.themes !== void 0 ? { themes: input.themes } : {},
|
|
600
|
+
...input.seed !== void 0 ? { state: { ...newState, seed: input.seed } } : {},
|
|
601
|
+
...input.skills !== void 0 ? { skills: input.skills } : {},
|
|
602
|
+
...input.agent ? { agent: input.agent } : {},
|
|
603
|
+
...input.model ? { model: input.model } : {}
|
|
604
|
+
};
|
|
605
|
+
state.sketches.set(input.sketchId, {
|
|
606
|
+
definition: newDef,
|
|
607
|
+
path: loaded.path
|
|
608
|
+
});
|
|
609
|
+
const json = serializeGenart(newDef);
|
|
610
|
+
if (!state.remoteMode) {
|
|
611
|
+
await writeFile2(loaded.path, json, "utf-8");
|
|
612
|
+
}
|
|
613
|
+
state.emitMutation("sketch:updated", { id: input.sketchId, updated });
|
|
614
|
+
return {
|
|
615
|
+
success: true,
|
|
616
|
+
sketchId: input.sketchId,
|
|
617
|
+
updated,
|
|
618
|
+
canvas: canvasDims,
|
|
619
|
+
parameterCount: newDef.parameters.length,
|
|
620
|
+
colorCount: newDef.colors.length,
|
|
621
|
+
seed: newDef.state.seed,
|
|
622
|
+
fileContent: json
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
async function updateAlgorithm(state, input) {
|
|
626
|
+
state.requireWorkspace();
|
|
627
|
+
const loaded = state.requireSketch(input.sketchId);
|
|
628
|
+
const def = loaded.definition;
|
|
629
|
+
if (!input.algorithm || input.algorithm.trim() === "") {
|
|
630
|
+
throw new Error("Algorithm cannot be empty");
|
|
631
|
+
}
|
|
632
|
+
const shouldValidate = input.validate !== false;
|
|
633
|
+
let validationPassed = true;
|
|
634
|
+
if (shouldValidate) {
|
|
635
|
+
const registry4 = createDefaultRegistry();
|
|
636
|
+
const adapter = registry4.resolve(def.renderer.type);
|
|
637
|
+
const result = adapter.validate(input.algorithm);
|
|
638
|
+
if (!result.valid) {
|
|
639
|
+
throw new Error(
|
|
640
|
+
`Algorithm validation failed: ${result.errors.join("; ")}`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const newDef = {
|
|
645
|
+
...def,
|
|
646
|
+
modified: now2(),
|
|
647
|
+
algorithm: input.algorithm,
|
|
648
|
+
...input.agent ? { agent: input.agent } : {},
|
|
649
|
+
...input.model ? { model: input.model } : {}
|
|
650
|
+
};
|
|
651
|
+
state.sketches.set(input.sketchId, {
|
|
652
|
+
definition: newDef,
|
|
653
|
+
path: loaded.path
|
|
654
|
+
});
|
|
655
|
+
const json = serializeGenart(newDef);
|
|
656
|
+
if (!state.remoteMode) {
|
|
657
|
+
await writeFile2(loaded.path, json, "utf-8");
|
|
658
|
+
}
|
|
659
|
+
state.emitMutation("sketch:updated", {
|
|
660
|
+
id: input.sketchId,
|
|
661
|
+
updated: ["algorithm"]
|
|
662
|
+
});
|
|
663
|
+
return {
|
|
664
|
+
success: true,
|
|
665
|
+
sketchId: input.sketchId,
|
|
666
|
+
renderer: def.renderer.type,
|
|
667
|
+
algorithmLength: input.algorithm.length,
|
|
668
|
+
validationPassed,
|
|
669
|
+
fileContent: json
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
async function saveSketch(state, input) {
|
|
673
|
+
const loaded = state.requireSketch(input.sketchId);
|
|
674
|
+
const newDef = {
|
|
675
|
+
...loaded.definition,
|
|
676
|
+
modified: now2()
|
|
677
|
+
};
|
|
678
|
+
state.sketches.set(input.sketchId, {
|
|
679
|
+
definition: newDef,
|
|
680
|
+
path: loaded.path
|
|
681
|
+
});
|
|
682
|
+
const json = serializeGenart(newDef);
|
|
683
|
+
if (!state.remoteMode) {
|
|
684
|
+
await writeFile2(loaded.path, json, "utf-8");
|
|
685
|
+
}
|
|
686
|
+
state.emitMutation("sketch:saved", { id: input.sketchId, path: loaded.path });
|
|
687
|
+
return {
|
|
688
|
+
success: true,
|
|
689
|
+
sketchId: input.sketchId,
|
|
690
|
+
path: loaded.path,
|
|
691
|
+
fileContent: json
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
async function forkSketch(state, input) {
|
|
695
|
+
const ws = state.requireWorkspace();
|
|
696
|
+
const source = state.requireSketch(input.sourceId);
|
|
697
|
+
const sourceDef = source.definition;
|
|
698
|
+
validateKebabId(input.newId);
|
|
699
|
+
if (state.getSketch(input.newId)) {
|
|
700
|
+
throw new Error(
|
|
701
|
+
`Sketch with ID '${input.newId}' already exists in workspace`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
const sourceDir = dirname2(source.path);
|
|
705
|
+
const newPath = resolve(sourceDir, `${input.newId}.genart`);
|
|
706
|
+
if (await fileExists2(newPath)) {
|
|
707
|
+
throw new Error(`File already exists at ${newPath}`);
|
|
708
|
+
}
|
|
709
|
+
const mods = input.modifications ?? {};
|
|
710
|
+
let rendererType = sourceDef.renderer.type;
|
|
711
|
+
if (mods.renderer) {
|
|
712
|
+
validateRendererType(mods.renderer);
|
|
713
|
+
rendererType = mods.renderer;
|
|
714
|
+
}
|
|
715
|
+
const canvasDims = mods.canvas ? resolveCanvas(mods.canvas) : { width: sourceDef.canvas.width, height: sourceDef.canvas.height };
|
|
716
|
+
const parameters = mods.parameters ?? [...sourceDef.parameters];
|
|
717
|
+
const colors = mods.colors ?? [...sourceDef.colors];
|
|
718
|
+
const algorithm = mods.algorithm ?? sourceDef.algorithm;
|
|
719
|
+
const philosophy = mods.philosophy ?? sourceDef.philosophy;
|
|
720
|
+
const generateNewSeed = input.newSeed !== false;
|
|
721
|
+
const seed = generateNewSeed ? Math.floor(Math.random() * 1e5) : sourceDef.state.seed;
|
|
722
|
+
const title = input.title ?? `${sourceDef.title} (fork)`;
|
|
723
|
+
const ts = now2();
|
|
724
|
+
const forkedDef = {
|
|
725
|
+
genart: "1.1",
|
|
726
|
+
id: input.newId,
|
|
727
|
+
title,
|
|
728
|
+
created: ts,
|
|
729
|
+
modified: ts,
|
|
730
|
+
renderer: { type: rendererType, version: "1.x" },
|
|
731
|
+
canvas: canvasDims,
|
|
732
|
+
parameters,
|
|
733
|
+
colors,
|
|
734
|
+
state: buildState(parameters, colors, seed),
|
|
735
|
+
algorithm,
|
|
736
|
+
...philosophy ? { philosophy } : {},
|
|
737
|
+
...sourceDef.themes ? { themes: [...sourceDef.themes] } : {},
|
|
738
|
+
...sourceDef.skills ? { skills: [...sourceDef.skills] } : {},
|
|
739
|
+
...input.agent ? { agent: input.agent } : {},
|
|
740
|
+
...input.model ? { model: input.model } : {}
|
|
741
|
+
};
|
|
742
|
+
const json = serializeGenart(forkedDef);
|
|
743
|
+
if (!state.remoteMode) {
|
|
744
|
+
await writeFile2(newPath, json, "utf-8");
|
|
745
|
+
}
|
|
746
|
+
state.sketches.set(input.newId, { definition: forkedDef, path: newPath });
|
|
747
|
+
let position = input.position;
|
|
748
|
+
if (!position) {
|
|
749
|
+
const sourceRef = ws.sketches.find(
|
|
750
|
+
(s) => s.file === basename2(source.path)
|
|
751
|
+
);
|
|
752
|
+
if (sourceRef) {
|
|
753
|
+
position = {
|
|
754
|
+
x: sourceRef.position.x + sourceDef.canvas.width + 200,
|
|
755
|
+
y: sourceRef.position.y
|
|
756
|
+
};
|
|
757
|
+
} else {
|
|
758
|
+
position = { x: 0, y: 0 };
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const file = basename2(newPath);
|
|
762
|
+
state.workspace = {
|
|
763
|
+
...ws,
|
|
764
|
+
modified: now2(),
|
|
765
|
+
sketches: [...ws.sketches, { file, position }]
|
|
766
|
+
};
|
|
767
|
+
const workspaceJson = serializeWorkspace2(state.workspace);
|
|
768
|
+
if (!state.remoteMode) {
|
|
769
|
+
await writeFile2(state.workspacePath, workspaceJson, "utf-8");
|
|
770
|
+
}
|
|
771
|
+
state.emitMutation("sketch:created", { id: input.newId, path: newPath });
|
|
772
|
+
state.emitMutation("workspace:updated", { added: file });
|
|
773
|
+
return {
|
|
774
|
+
success: true,
|
|
775
|
+
sourceId: input.sourceId,
|
|
776
|
+
forkedSketch: {
|
|
777
|
+
id: input.newId,
|
|
778
|
+
title,
|
|
779
|
+
path: newPath,
|
|
780
|
+
renderer: rendererType,
|
|
781
|
+
canvas: canvasDims,
|
|
782
|
+
seed,
|
|
783
|
+
position
|
|
784
|
+
},
|
|
785
|
+
fileContent: json,
|
|
786
|
+
workspaceContent: workspaceJson
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
async function deleteSketch(state, input) {
|
|
790
|
+
const ws = state.requireWorkspace();
|
|
791
|
+
const loaded = state.requireSketch(input.sketchId);
|
|
792
|
+
const file = basename2(loaded.path);
|
|
793
|
+
const newSketches = ws.sketches.filter((s) => s.file !== file);
|
|
794
|
+
const newGroups = ws.groups?.map((g) => ({
|
|
795
|
+
...g,
|
|
796
|
+
sketchFiles: g.sketchFiles.filter((f) => f !== file)
|
|
797
|
+
})).filter((g) => g.sketchFiles.length > 0);
|
|
798
|
+
state.workspace = {
|
|
799
|
+
...ws,
|
|
800
|
+
modified: now2(),
|
|
801
|
+
sketches: newSketches,
|
|
802
|
+
...newGroups && newGroups.length > 0 ? { groups: newGroups } : {}
|
|
803
|
+
};
|
|
804
|
+
state.sketches.delete(input.sketchId);
|
|
805
|
+
state.selection.delete(input.sketchId);
|
|
806
|
+
const shouldDelete = !input.keepFile;
|
|
807
|
+
if (shouldDelete) {
|
|
808
|
+
try {
|
|
809
|
+
await unlink(loaded.path);
|
|
810
|
+
} catch {
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
await state.saveWorkspace();
|
|
814
|
+
state.emitMutation("sketch:deleted", { id: input.sketchId });
|
|
815
|
+
state.emitMutation("workspace:updated", { removed: file });
|
|
816
|
+
return {
|
|
817
|
+
success: true,
|
|
818
|
+
deletedId: input.sketchId,
|
|
819
|
+
path: loaded.path,
|
|
820
|
+
fileDeleted: shouldDelete,
|
|
821
|
+
sketchCount: state.workspace.sketches.length
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/tools/selection.ts
|
|
826
|
+
import { basename as basename3 } from "path";
|
|
827
|
+
async function getSelection(state, input) {
|
|
828
|
+
const ws = state.requireWorkspace();
|
|
829
|
+
const includeAlgorithm = input.includeAlgorithm !== false;
|
|
830
|
+
const includePhilosophy = input.includePhilosophy !== false;
|
|
831
|
+
const includeNeighbors = input.includeNeighbors === true;
|
|
832
|
+
const selected = [];
|
|
833
|
+
for (const id of state.selection) {
|
|
834
|
+
const loaded = state.getSketch(id);
|
|
835
|
+
if (!loaded) continue;
|
|
836
|
+
const def = loaded.definition;
|
|
837
|
+
const ref = ws.sketches.find((s) => s.file === basename3(loaded.path));
|
|
838
|
+
const entry = {
|
|
839
|
+
id: def.id,
|
|
840
|
+
title: def.title,
|
|
841
|
+
path: loaded.path,
|
|
842
|
+
renderer: { type: def.renderer.type, version: def.renderer.version },
|
|
843
|
+
canvas: { preset: void 0, width: def.canvas.width, height: def.canvas.height },
|
|
844
|
+
state: def.state,
|
|
845
|
+
parameters: def.parameters,
|
|
846
|
+
colors: def.colors,
|
|
847
|
+
themes: def.themes ?? [],
|
|
848
|
+
skills: def.skills ?? [],
|
|
849
|
+
position: ref?.position ?? { x: 0, y: 0 },
|
|
850
|
+
snapshotCount: def.snapshots?.length ?? 0
|
|
851
|
+
};
|
|
852
|
+
if (includePhilosophy) {
|
|
853
|
+
entry.philosophy = def.philosophy ?? null;
|
|
854
|
+
}
|
|
855
|
+
if (includeAlgorithm) {
|
|
856
|
+
entry.algorithm = def.algorithm;
|
|
857
|
+
}
|
|
858
|
+
selected.push(entry);
|
|
859
|
+
}
|
|
860
|
+
const neighbors = [];
|
|
861
|
+
if (includeNeighbors && selected.length > 0) {
|
|
862
|
+
const selectedIds = new Set(state.selection);
|
|
863
|
+
for (const ref of ws.sketches) {
|
|
864
|
+
const loaded = [...state.sketches.values()].find(
|
|
865
|
+
(v) => basename3(v.path) === ref.file
|
|
866
|
+
);
|
|
867
|
+
if (!loaded || selectedIds.has(loaded.definition.id)) continue;
|
|
868
|
+
const isNear = selected.some((sel) => {
|
|
869
|
+
const selPos = sel.position;
|
|
870
|
+
const dx = Math.abs(ref.position.x - selPos.x);
|
|
871
|
+
const dy = Math.abs(ref.position.y - selPos.y);
|
|
872
|
+
return dx <= 2e3 && dy <= 2e3;
|
|
873
|
+
});
|
|
874
|
+
if (isNear) {
|
|
875
|
+
neighbors.push({
|
|
876
|
+
id: loaded.definition.id,
|
|
877
|
+
title: loaded.definition.title,
|
|
878
|
+
renderer: loaded.definition.renderer.type,
|
|
879
|
+
position: ref.position
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
selected,
|
|
886
|
+
workspace: {
|
|
887
|
+
id: ws.id,
|
|
888
|
+
title: ws.title,
|
|
889
|
+
sketchCount: ws.sketches.length
|
|
890
|
+
},
|
|
891
|
+
neighbors
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
async function selectSketch(state, input) {
|
|
895
|
+
state.requireWorkspace();
|
|
896
|
+
if (!input.sketchIds || input.sketchIds.length === 0) {
|
|
897
|
+
throw new Error("At least one sketch ID is required");
|
|
898
|
+
}
|
|
899
|
+
for (const id of input.sketchIds) {
|
|
900
|
+
state.requireSketch(id);
|
|
901
|
+
}
|
|
902
|
+
if (input.addToSelection) {
|
|
903
|
+
for (const id of input.sketchIds) {
|
|
904
|
+
state.selection.add(id);
|
|
905
|
+
}
|
|
906
|
+
} else {
|
|
907
|
+
state.setSelection(input.sketchIds);
|
|
908
|
+
}
|
|
909
|
+
const selected = [...state.selection];
|
|
910
|
+
state.emitMutation("selection:changed", { selected });
|
|
911
|
+
return {
|
|
912
|
+
success: true,
|
|
913
|
+
selected,
|
|
914
|
+
selectionCount: selected.length
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
async function getEditorState(state) {
|
|
918
|
+
if (!state.workspace) {
|
|
919
|
+
return {
|
|
920
|
+
hasWorkspace: false,
|
|
921
|
+
workingDirectory: state.basePath,
|
|
922
|
+
workspace: null,
|
|
923
|
+
selection: [],
|
|
924
|
+
sketches: []
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
const ws = state.workspace;
|
|
928
|
+
const sketches = [...state.sketches.values()].map((loaded) => {
|
|
929
|
+
const def = loaded.definition;
|
|
930
|
+
return {
|
|
931
|
+
id: def.id,
|
|
932
|
+
title: def.title,
|
|
933
|
+
renderer: def.renderer.type,
|
|
934
|
+
canvas: { width: def.canvas.width, height: def.canvas.height },
|
|
935
|
+
parameterCount: def.parameters.length,
|
|
936
|
+
colorCount: def.colors.length,
|
|
937
|
+
seed: def.state.seed
|
|
938
|
+
};
|
|
939
|
+
});
|
|
940
|
+
return {
|
|
941
|
+
hasWorkspace: true,
|
|
942
|
+
workingDirectory: state.basePath,
|
|
943
|
+
workspace: {
|
|
944
|
+
id: ws.id,
|
|
945
|
+
title: ws.title,
|
|
946
|
+
path: state.workspacePath,
|
|
947
|
+
sketchCount: ws.sketches.length,
|
|
948
|
+
viewport: ws.viewport,
|
|
949
|
+
groups: ws.groups ?? []
|
|
950
|
+
},
|
|
951
|
+
selection: [...state.selection],
|
|
952
|
+
sketches
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/tools/parameters.ts
|
|
957
|
+
import {
|
|
958
|
+
resolvePreset as resolvePreset2
|
|
959
|
+
} from "@genart-dev/core";
|
|
960
|
+
function now3() {
|
|
961
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
962
|
+
}
|
|
963
|
+
var HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
964
|
+
function updateSketchInState(state, id, newDef) {
|
|
965
|
+
const loaded = state.requireSketch(id);
|
|
966
|
+
state.sketches.set(id, { definition: newDef, path: loaded.path });
|
|
967
|
+
}
|
|
968
|
+
async function setParameters(state, input) {
|
|
969
|
+
state.requireWorkspace();
|
|
970
|
+
const loaded = state.requireSketch(input.sketchId);
|
|
971
|
+
const def = loaded.definition;
|
|
972
|
+
const validKeys = new Set(def.parameters.map((p) => p.key));
|
|
973
|
+
const updated = [];
|
|
974
|
+
for (const [key, value] of Object.entries(input.params)) {
|
|
975
|
+
if (!validKeys.has(key)) {
|
|
976
|
+
throw new Error(
|
|
977
|
+
`Unknown parameter: '${key}'. Valid keys: ${[...validKeys].join(", ")}`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
const paramDef = def.parameters.find((p) => p.key === key);
|
|
981
|
+
if (value < paramDef.min || value > paramDef.max) {
|
|
982
|
+
throw new Error(
|
|
983
|
+
`Parameter '${key}' value ${value} outside range [${paramDef.min}, ${paramDef.max}]`
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
updated.push(key);
|
|
987
|
+
}
|
|
988
|
+
const newParams = { ...def.state.params, ...input.params };
|
|
989
|
+
const newState = { ...def.state, params: newParams };
|
|
990
|
+
const newDef = { ...def, modified: now3(), state: newState };
|
|
991
|
+
updateSketchInState(state, input.sketchId, newDef);
|
|
992
|
+
await state.saveSketch(input.sketchId);
|
|
993
|
+
state.emitMutation("sketch:updated", { id: input.sketchId, updated: ["params"] });
|
|
994
|
+
return {
|
|
995
|
+
success: true,
|
|
996
|
+
sketchId: input.sketchId,
|
|
997
|
+
updated,
|
|
998
|
+
state: newState
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
async function setColors(state, input) {
|
|
1002
|
+
state.requireWorkspace();
|
|
1003
|
+
const loaded = state.requireSketch(input.sketchId);
|
|
1004
|
+
const def = loaded.definition;
|
|
1005
|
+
const colorDefs = def.colors;
|
|
1006
|
+
const validKeys = new Set(colorDefs.map((c) => c.key));
|
|
1007
|
+
const updated = [];
|
|
1008
|
+
for (const [key, value] of Object.entries(input.colors)) {
|
|
1009
|
+
if (!validKeys.has(key)) {
|
|
1010
|
+
throw new Error(
|
|
1011
|
+
`Unknown color: '${key}'. Valid keys: ${[...validKeys].join(", ")}`
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
if (!HEX_RE.test(value)) {
|
|
1015
|
+
throw new Error(`Invalid hex color for '${key}': '${value}'`);
|
|
1016
|
+
}
|
|
1017
|
+
updated.push(key);
|
|
1018
|
+
}
|
|
1019
|
+
const newPalette = colorDefs.map((cDef) => {
|
|
1020
|
+
if (input.colors[cDef.key] !== void 0) {
|
|
1021
|
+
return input.colors[cDef.key];
|
|
1022
|
+
}
|
|
1023
|
+
const idx = colorDefs.findIndex((c) => c.key === cDef.key);
|
|
1024
|
+
return def.state.colorPalette[idx] ?? cDef.default;
|
|
1025
|
+
});
|
|
1026
|
+
const newState = { ...def.state, colorPalette: newPalette };
|
|
1027
|
+
const newDef = { ...def, modified: now3(), state: newState };
|
|
1028
|
+
updateSketchInState(state, input.sketchId, newDef);
|
|
1029
|
+
await state.saveSketch(input.sketchId);
|
|
1030
|
+
state.emitMutation("sketch:updated", { id: input.sketchId, updated: ["colors"] });
|
|
1031
|
+
return {
|
|
1032
|
+
success: true,
|
|
1033
|
+
sketchId: input.sketchId,
|
|
1034
|
+
updated,
|
|
1035
|
+
colorPalette: newPalette
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
async function setSeed(state, input) {
|
|
1039
|
+
state.requireWorkspace();
|
|
1040
|
+
const loaded = state.requireSketch(input.sketchId);
|
|
1041
|
+
const def = loaded.definition;
|
|
1042
|
+
const previousSeed = def.state.seed;
|
|
1043
|
+
const newSeed = input.seed ?? Math.floor(Math.random() * 1e5);
|
|
1044
|
+
const newState = { ...def.state, seed: newSeed };
|
|
1045
|
+
const newDef = { ...def, modified: now3(), state: newState };
|
|
1046
|
+
updateSketchInState(state, input.sketchId, newDef);
|
|
1047
|
+
await state.saveSketch(input.sketchId);
|
|
1048
|
+
state.emitMutation("sketch:updated", { id: input.sketchId, updated: ["seed"] });
|
|
1049
|
+
return {
|
|
1050
|
+
success: true,
|
|
1051
|
+
sketchId: input.sketchId,
|
|
1052
|
+
seed: newSeed,
|
|
1053
|
+
previousSeed
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
async function setCanvasSize(state, input) {
|
|
1057
|
+
state.requireWorkspace();
|
|
1058
|
+
const loaded = state.requireSketch(input.sketchId);
|
|
1059
|
+
const def = loaded.definition;
|
|
1060
|
+
const previousCanvas = { width: def.canvas.width, height: def.canvas.height };
|
|
1061
|
+
let newCanvas;
|
|
1062
|
+
if (input.preset) {
|
|
1063
|
+
newCanvas = resolvePreset2(input.preset);
|
|
1064
|
+
} else if (input.width !== void 0 && input.height !== void 0) {
|
|
1065
|
+
newCanvas = { width: input.width, height: input.height };
|
|
1066
|
+
} else {
|
|
1067
|
+
throw new Error("Provide either a preset or both width and height");
|
|
1068
|
+
}
|
|
1069
|
+
const newDef = { ...def, modified: now3(), canvas: newCanvas };
|
|
1070
|
+
updateSketchInState(state, input.sketchId, newDef);
|
|
1071
|
+
await state.saveSketch(input.sketchId);
|
|
1072
|
+
state.emitMutation("sketch:updated", { id: input.sketchId, updated: ["canvas"] });
|
|
1073
|
+
return {
|
|
1074
|
+
success: true,
|
|
1075
|
+
sketchId: input.sketchId,
|
|
1076
|
+
canvas: newCanvas,
|
|
1077
|
+
previousCanvas
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
async function randomizeParameters(state, input) {
|
|
1081
|
+
state.requireWorkspace();
|
|
1082
|
+
const loaded = state.requireSketch(input.sketchId);
|
|
1083
|
+
const def = loaded.definition;
|
|
1084
|
+
if (def.parameters.length === 0) {
|
|
1085
|
+
throw new Error("Sketch has no parameters to randomize");
|
|
1086
|
+
}
|
|
1087
|
+
let paramsToRandomize = def.parameters;
|
|
1088
|
+
if (input.paramKeys && input.paramKeys.length > 0) {
|
|
1089
|
+
const validKeys = new Set(def.parameters.map((p) => p.key));
|
|
1090
|
+
for (const key of input.paramKeys) {
|
|
1091
|
+
if (!validKeys.has(key)) {
|
|
1092
|
+
throw new Error(
|
|
1093
|
+
`Unknown parameter: '${key}'. Valid keys: ${[...validKeys].join(", ")}`
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const keySet = new Set(input.paramKeys);
|
|
1098
|
+
paramsToRandomize = def.parameters.filter((p) => keySet.has(p.key));
|
|
1099
|
+
}
|
|
1100
|
+
const newParams = { ...def.state.params };
|
|
1101
|
+
const randomized = [];
|
|
1102
|
+
for (const paramDef of paramsToRandomize) {
|
|
1103
|
+
const steps = Math.round((paramDef.max - paramDef.min) / paramDef.step);
|
|
1104
|
+
const randomStep = Math.floor(Math.random() * (steps + 1));
|
|
1105
|
+
newParams[paramDef.key] = paramDef.min + randomStep * paramDef.step;
|
|
1106
|
+
randomized.push(paramDef.key);
|
|
1107
|
+
}
|
|
1108
|
+
const newSeed = input.newSeed ? Math.floor(Math.random() * 1e5) : def.state.seed;
|
|
1109
|
+
const newState = { ...def.state, params: newParams, seed: newSeed };
|
|
1110
|
+
const newDef = { ...def, modified: now3(), state: newState };
|
|
1111
|
+
updateSketchInState(state, input.sketchId, newDef);
|
|
1112
|
+
await state.saveSketch(input.sketchId);
|
|
1113
|
+
state.emitMutation("sketch:updated", { id: input.sketchId, updated: ["params"] });
|
|
1114
|
+
return {
|
|
1115
|
+
success: true,
|
|
1116
|
+
sketchId: input.sketchId,
|
|
1117
|
+
randomized,
|
|
1118
|
+
state: newState
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/tools/arrangement.ts
|
|
1123
|
+
import { basename as basename4 } from "path";
|
|
1124
|
+
function now4() {
|
|
1125
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1126
|
+
}
|
|
1127
|
+
function getSketchDimensions(state, sketchId) {
|
|
1128
|
+
const loaded = state.getSketch(sketchId);
|
|
1129
|
+
if (!loaded) return { width: 1200, height: 1200 };
|
|
1130
|
+
return {
|
|
1131
|
+
width: loaded.definition.canvas.width,
|
|
1132
|
+
height: loaded.definition.canvas.height
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function computeViewport2(positions) {
|
|
1136
|
+
if (positions.length === 0) return { x: 0, y: 0, zoom: 1 };
|
|
1137
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1138
|
+
for (const p of positions) {
|
|
1139
|
+
if (p.position.x < minX) minX = p.position.x;
|
|
1140
|
+
if (p.position.y < minY) minY = p.position.y;
|
|
1141
|
+
if (p.position.x + p.width > maxX) maxX = p.position.x + p.width;
|
|
1142
|
+
if (p.position.y + p.height > maxY) maxY = p.position.y + p.height;
|
|
1143
|
+
}
|
|
1144
|
+
const centerX = (minX + maxX) / 2;
|
|
1145
|
+
const centerY = (minY + maxY) / 2;
|
|
1146
|
+
const totalW = maxX - minX;
|
|
1147
|
+
const totalH = maxY - minY;
|
|
1148
|
+
const zoom = Math.min(
|
|
1149
|
+
1,
|
|
1150
|
+
Math.min(1920 / (totalW + 200), 1080 / (totalH + 200))
|
|
1151
|
+
);
|
|
1152
|
+
return {
|
|
1153
|
+
x: Math.round(centerX),
|
|
1154
|
+
y: Math.round(centerY),
|
|
1155
|
+
zoom: Math.round(zoom * 100) / 100
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
function layoutGrid(items, spacing, origin) {
|
|
1159
|
+
if (items.length === 0) return [];
|
|
1160
|
+
const cols = Math.ceil(Math.sqrt(items.length));
|
|
1161
|
+
const maxW = Math.max(...items.map((s) => s.width));
|
|
1162
|
+
const maxH = Math.max(...items.map((s) => s.height));
|
|
1163
|
+
const cellW = maxW + spacing;
|
|
1164
|
+
const cellH = maxH + spacing;
|
|
1165
|
+
return items.map((s, i) => ({
|
|
1166
|
+
id: s.id,
|
|
1167
|
+
position: {
|
|
1168
|
+
x: origin.x + i % cols * cellW,
|
|
1169
|
+
y: origin.y + Math.floor(i / cols) * cellH
|
|
1170
|
+
}
|
|
1171
|
+
}));
|
|
1172
|
+
}
|
|
1173
|
+
function layoutRow(items, spacing, origin) {
|
|
1174
|
+
let x = origin.x;
|
|
1175
|
+
return items.map((s) => {
|
|
1176
|
+
const pos = { id: s.id, position: { x, y: origin.y } };
|
|
1177
|
+
x += s.width + spacing;
|
|
1178
|
+
return pos;
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
function layoutColumn(items, spacing, origin) {
|
|
1182
|
+
let y = origin.y;
|
|
1183
|
+
return items.map((s) => {
|
|
1184
|
+
const pos = { id: s.id, position: { x: origin.x, y } };
|
|
1185
|
+
y += s.height + spacing;
|
|
1186
|
+
return pos;
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
function layoutMasonry(items, spacing, origin) {
|
|
1190
|
+
if (items.length === 0) return [];
|
|
1191
|
+
const cols = Math.ceil(Math.sqrt(items.length));
|
|
1192
|
+
const maxW = Math.max(...items.map((s) => s.width));
|
|
1193
|
+
const cellW = maxW + spacing;
|
|
1194
|
+
const columnHeights = new Array(cols).fill(0);
|
|
1195
|
+
const result = [];
|
|
1196
|
+
for (const item of items) {
|
|
1197
|
+
let minCol = 0;
|
|
1198
|
+
for (let c = 1; c < cols; c++) {
|
|
1199
|
+
if (columnHeights[c] < columnHeights[minCol]) minCol = c;
|
|
1200
|
+
}
|
|
1201
|
+
result.push({
|
|
1202
|
+
id: item.id,
|
|
1203
|
+
position: {
|
|
1204
|
+
x: origin.x + minCol * cellW,
|
|
1205
|
+
y: origin.y + columnHeights[minCol]
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
columnHeights[minCol] += item.height + spacing;
|
|
1209
|
+
}
|
|
1210
|
+
return result;
|
|
1211
|
+
}
|
|
1212
|
+
var VALID_LAYOUTS = ["grid", "row", "column", "masonry"];
|
|
1213
|
+
function applyLayout(items, layout, spacing, origin) {
|
|
1214
|
+
switch (layout) {
|
|
1215
|
+
case "row":
|
|
1216
|
+
return layoutRow(items, spacing, origin);
|
|
1217
|
+
case "column":
|
|
1218
|
+
return layoutColumn(items, spacing, origin);
|
|
1219
|
+
case "masonry":
|
|
1220
|
+
return layoutMasonry(items, spacing, origin);
|
|
1221
|
+
case "grid":
|
|
1222
|
+
default:
|
|
1223
|
+
return layoutGrid(items, spacing, origin);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
async function arrangeSketches(state, input) {
|
|
1227
|
+
const ws = state.requireWorkspace();
|
|
1228
|
+
if (!input.positions || input.positions.length === 0) {
|
|
1229
|
+
throw new Error("At least one position is required");
|
|
1230
|
+
}
|
|
1231
|
+
for (const pos of input.positions) {
|
|
1232
|
+
state.requireSketch(pos.sketchId);
|
|
1233
|
+
}
|
|
1234
|
+
const idToFile = /* @__PURE__ */ new Map();
|
|
1235
|
+
for (const [id, loaded] of state.sketches) {
|
|
1236
|
+
idToFile.set(id, basename4(loaded.path));
|
|
1237
|
+
}
|
|
1238
|
+
const positionMap = new Map(
|
|
1239
|
+
input.positions.map((p) => [idToFile.get(p.sketchId), { x: p.x, y: p.y }])
|
|
1240
|
+
);
|
|
1241
|
+
const newSketches = ws.sketches.map((ref) => {
|
|
1242
|
+
const newPos = positionMap.get(ref.file);
|
|
1243
|
+
if (newPos) {
|
|
1244
|
+
return { ...ref, position: newPos };
|
|
1245
|
+
}
|
|
1246
|
+
return ref;
|
|
1247
|
+
});
|
|
1248
|
+
const viewportItems = input.positions.map((p) => {
|
|
1249
|
+
const dims = getSketchDimensions(state, p.sketchId);
|
|
1250
|
+
return { position: { x: p.x, y: p.y }, width: dims.width, height: dims.height };
|
|
1251
|
+
});
|
|
1252
|
+
const viewport = computeViewport2(viewportItems);
|
|
1253
|
+
state.workspace = { ...ws, modified: now4(), sketches: newSketches, viewport };
|
|
1254
|
+
await state.saveWorkspace();
|
|
1255
|
+
state.emitMutation("workspace:updated", { arranged: input.positions.length });
|
|
1256
|
+
return {
|
|
1257
|
+
success: true,
|
|
1258
|
+
moved: input.positions.length,
|
|
1259
|
+
positions: input.positions.map((p) => ({
|
|
1260
|
+
id: p.sketchId,
|
|
1261
|
+
position: { x: p.x, y: p.y }
|
|
1262
|
+
})),
|
|
1263
|
+
viewport
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
async function autoArrange(state, input) {
|
|
1267
|
+
const ws = state.requireWorkspace();
|
|
1268
|
+
const layout = input.layout ?? "grid";
|
|
1269
|
+
if (!VALID_LAYOUTS.includes(layout)) {
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
`Unknown layout: '${layout}'. Valid layouts: ${VALID_LAYOUTS.join(", ")}`
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
const spacing = input.spacing ?? 200;
|
|
1275
|
+
const origin = input.origin ?? { x: 0, y: 0 };
|
|
1276
|
+
let sketchIds;
|
|
1277
|
+
if (input.sketchIds && input.sketchIds.length > 0) {
|
|
1278
|
+
for (const id of input.sketchIds) {
|
|
1279
|
+
state.requireSketch(id);
|
|
1280
|
+
}
|
|
1281
|
+
sketchIds = input.sketchIds;
|
|
1282
|
+
} else {
|
|
1283
|
+
sketchIds = [...state.sketches.keys()];
|
|
1284
|
+
}
|
|
1285
|
+
if (sketchIds.length === 0) {
|
|
1286
|
+
throw new Error("No sketches to arrange");
|
|
1287
|
+
}
|
|
1288
|
+
const sortBy = input.sortBy ?? "created";
|
|
1289
|
+
const sortedItems = sketchIds.map((id) => {
|
|
1290
|
+
const loaded = state.requireSketch(id);
|
|
1291
|
+
const def = loaded.definition;
|
|
1292
|
+
return {
|
|
1293
|
+
id,
|
|
1294
|
+
title: def.title,
|
|
1295
|
+
created: def.created,
|
|
1296
|
+
modified: def.modified,
|
|
1297
|
+
renderer: def.renderer.type,
|
|
1298
|
+
width: def.canvas.width,
|
|
1299
|
+
height: def.canvas.height
|
|
1300
|
+
};
|
|
1301
|
+
}).sort((a, b) => {
|
|
1302
|
+
switch (sortBy) {
|
|
1303
|
+
case "title":
|
|
1304
|
+
return a.title.localeCompare(b.title);
|
|
1305
|
+
case "modified":
|
|
1306
|
+
return a.modified.localeCompare(b.modified);
|
|
1307
|
+
case "renderer":
|
|
1308
|
+
return a.renderer.localeCompare(b.renderer);
|
|
1309
|
+
case "created":
|
|
1310
|
+
default:
|
|
1311
|
+
return a.created.localeCompare(b.created);
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
const arranged = applyLayout(sortedItems, layout, spacing, origin);
|
|
1315
|
+
const idToFile = /* @__PURE__ */ new Map();
|
|
1316
|
+
for (const [id, loaded] of state.sketches) {
|
|
1317
|
+
idToFile.set(id, basename4(loaded.path));
|
|
1318
|
+
}
|
|
1319
|
+
const positionMap = new Map(
|
|
1320
|
+
arranged.map((a) => [idToFile.get(a.id), a.position])
|
|
1321
|
+
);
|
|
1322
|
+
const newSketches = ws.sketches.map((ref) => {
|
|
1323
|
+
const newPos = positionMap.get(ref.file);
|
|
1324
|
+
if (newPos) {
|
|
1325
|
+
return { ...ref, position: newPos };
|
|
1326
|
+
}
|
|
1327
|
+
return ref;
|
|
1328
|
+
});
|
|
1329
|
+
const viewportItems = arranged.map((a) => {
|
|
1330
|
+
const item = sortedItems.find((s) => s.id === a.id);
|
|
1331
|
+
return { position: a.position, width: item.width, height: item.height };
|
|
1332
|
+
});
|
|
1333
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1334
|
+
for (const item of viewportItems) {
|
|
1335
|
+
if (item.position.x < minX) minX = item.position.x;
|
|
1336
|
+
if (item.position.y < minY) minY = item.position.y;
|
|
1337
|
+
if (item.position.x + item.width > maxX) maxX = item.position.x + item.width;
|
|
1338
|
+
if (item.position.y + item.height > maxY) maxY = item.position.y + item.height;
|
|
1339
|
+
}
|
|
1340
|
+
const boundingBox = {
|
|
1341
|
+
x: minX,
|
|
1342
|
+
y: minY,
|
|
1343
|
+
width: maxX - minX,
|
|
1344
|
+
height: maxY - minY
|
|
1345
|
+
};
|
|
1346
|
+
const viewport = computeViewport2(viewportItems);
|
|
1347
|
+
state.workspace = { ...ws, modified: now4(), sketches: newSketches, viewport };
|
|
1348
|
+
await state.saveWorkspace();
|
|
1349
|
+
state.emitMutation("workspace:updated", { arranged: arranged.length });
|
|
1350
|
+
return {
|
|
1351
|
+
success: true,
|
|
1352
|
+
layout,
|
|
1353
|
+
arranged: arranged.length,
|
|
1354
|
+
positions: arranged.map((a) => ({
|
|
1355
|
+
id: a.id,
|
|
1356
|
+
position: a.position
|
|
1357
|
+
})),
|
|
1358
|
+
boundingBox,
|
|
1359
|
+
viewport
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
async function groupSketches(state, input) {
|
|
1363
|
+
const ws = state.requireWorkspace();
|
|
1364
|
+
if (!input.sketchIds || input.sketchIds.length === 0) {
|
|
1365
|
+
throw new Error("At least one sketch ID is required");
|
|
1366
|
+
}
|
|
1367
|
+
const sketchFiles = [];
|
|
1368
|
+
for (const id of input.sketchIds) {
|
|
1369
|
+
const loaded = state.requireSketch(id);
|
|
1370
|
+
sketchFiles.push(basename4(loaded.path));
|
|
1371
|
+
}
|
|
1372
|
+
const newGroup = {
|
|
1373
|
+
id: input.groupId,
|
|
1374
|
+
label: input.label,
|
|
1375
|
+
sketchFiles,
|
|
1376
|
+
...input.color ? { color: input.color } : {}
|
|
1377
|
+
};
|
|
1378
|
+
const existingGroups = ws.groups ?? [];
|
|
1379
|
+
const filtered = existingGroups.filter((g) => g.id !== input.groupId);
|
|
1380
|
+
const newGroups = [...filtered, newGroup];
|
|
1381
|
+
state.workspace = { ...ws, modified: now4(), groups: newGroups };
|
|
1382
|
+
await state.saveWorkspace();
|
|
1383
|
+
state.emitMutation("workspace:updated", { groupUpdated: input.groupId });
|
|
1384
|
+
return {
|
|
1385
|
+
success: true,
|
|
1386
|
+
group: newGroup,
|
|
1387
|
+
groupCount: newGroups.length
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// src/tools/gallery.ts
|
|
1392
|
+
import { readdir, readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
1393
|
+
import { dirname as dirname3, join } from "path";
|
|
1394
|
+
import { parseGenart as parseGenart3 } from "@genart-dev/core";
|
|
1395
|
+
async function tryParseGenart(absPath) {
|
|
1396
|
+
try {
|
|
1397
|
+
const raw = await readFile3(absPath, "utf-8");
|
|
1398
|
+
const json = JSON.parse(raw);
|
|
1399
|
+
const def = parseGenart3(json);
|
|
1400
|
+
return {
|
|
1401
|
+
id: def.id,
|
|
1402
|
+
title: def.title,
|
|
1403
|
+
renderer: def.renderer.type,
|
|
1404
|
+
canvas: { width: def.canvas.width, height: def.canvas.height },
|
|
1405
|
+
parameterCount: def.parameters.length,
|
|
1406
|
+
colorCount: def.colors.length,
|
|
1407
|
+
snapshotCount: def.snapshots?.length ?? 0,
|
|
1408
|
+
hasPhilosophy: !!def.philosophy,
|
|
1409
|
+
skills: def.skills ?? [],
|
|
1410
|
+
modified: def.modified,
|
|
1411
|
+
path: absPath
|
|
1412
|
+
};
|
|
1413
|
+
} catch {
|
|
1414
|
+
return null;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
async function scanGenartFiles(dir, recursive) {
|
|
1418
|
+
const results = [];
|
|
1419
|
+
let entries;
|
|
1420
|
+
try {
|
|
1421
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1422
|
+
} catch {
|
|
1423
|
+
return results;
|
|
1424
|
+
}
|
|
1425
|
+
for (const entry of entries) {
|
|
1426
|
+
const fullPath = join(dir, entry.name);
|
|
1427
|
+
if (entry.isFile() && entry.name.endsWith(".genart")) {
|
|
1428
|
+
results.push(fullPath);
|
|
1429
|
+
} else if (recursive && entry.isDirectory()) {
|
|
1430
|
+
const sub = await scanGenartFiles(fullPath, true);
|
|
1431
|
+
results.push(...sub);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return results;
|
|
1435
|
+
}
|
|
1436
|
+
async function listSketches(state, input) {
|
|
1437
|
+
const dir = input.directory ? state.resolvePath(input.directory) : state.workspacePath ? dirname3(state.workspacePath) : null;
|
|
1438
|
+
if (!dir) {
|
|
1439
|
+
throw new Error("No workspace is currently open and no directory specified");
|
|
1440
|
+
}
|
|
1441
|
+
try {
|
|
1442
|
+
const s = await stat3(dir);
|
|
1443
|
+
if (!s.isDirectory()) {
|
|
1444
|
+
throw new Error(`Not a directory: '${dir}'`);
|
|
1445
|
+
}
|
|
1446
|
+
} catch (e) {
|
|
1447
|
+
if (e instanceof Error && e.message.startsWith("Not a directory")) throw e;
|
|
1448
|
+
throw new Error(`Directory does not exist: '${dir}'`);
|
|
1449
|
+
}
|
|
1450
|
+
const recursive = input.recursive ?? false;
|
|
1451
|
+
const includeUnreferenced = input.includeUnreferenced !== false;
|
|
1452
|
+
const wsFiles = /* @__PURE__ */ new Set();
|
|
1453
|
+
if (state.workspace) {
|
|
1454
|
+
for (const ref of state.workspace.sketches) {
|
|
1455
|
+
const absPath = state.resolveSketchPath(ref.file);
|
|
1456
|
+
wsFiles.add(absPath);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
const genartFiles = await scanGenartFiles(dir, recursive);
|
|
1460
|
+
const sketches = [];
|
|
1461
|
+
for (const filePath of genartFiles) {
|
|
1462
|
+
const inWorkspace = wsFiles.has(filePath);
|
|
1463
|
+
if (!includeUnreferenced && !inWorkspace) continue;
|
|
1464
|
+
const info = await tryParseGenart(filePath);
|
|
1465
|
+
if (!info) continue;
|
|
1466
|
+
sketches.push({
|
|
1467
|
+
id: info.id,
|
|
1468
|
+
title: info.title,
|
|
1469
|
+
renderer: info.renderer,
|
|
1470
|
+
canvas: info.canvas,
|
|
1471
|
+
parameterCount: info.parameterCount,
|
|
1472
|
+
colorCount: info.colorCount,
|
|
1473
|
+
path: info.path,
|
|
1474
|
+
inWorkspace,
|
|
1475
|
+
modified: info.modified
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
const inWorkspaceCount = sketches.filter(
|
|
1479
|
+
(s) => s.inWorkspace
|
|
1480
|
+
).length;
|
|
1481
|
+
return {
|
|
1482
|
+
success: true,
|
|
1483
|
+
directory: dir,
|
|
1484
|
+
sketches,
|
|
1485
|
+
total: sketches.length,
|
|
1486
|
+
inWorkspace: inWorkspaceCount,
|
|
1487
|
+
unreferenced: sketches.length - inWorkspaceCount
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
async function searchSketches(state, input) {
|
|
1491
|
+
state.requireWorkspace();
|
|
1492
|
+
const hasFilter = input.query !== void 0 || input.renderer !== void 0 || input.minParameters !== void 0 || input.maxParameters !== void 0 || input.canvasWidth !== void 0 || input.canvasHeight !== void 0 || input.hasPhilosophy !== void 0 || input.skills !== void 0 && input.skills.length > 0;
|
|
1493
|
+
if (!hasFilter) {
|
|
1494
|
+
throw new Error("At least one search filter is required");
|
|
1495
|
+
}
|
|
1496
|
+
const matches = [];
|
|
1497
|
+
const filters = {};
|
|
1498
|
+
if (input.query !== void 0) filters.query = input.query;
|
|
1499
|
+
if (input.renderer !== void 0) filters.renderer = input.renderer;
|
|
1500
|
+
if (input.minParameters !== void 0) filters.minParameters = input.minParameters;
|
|
1501
|
+
if (input.maxParameters !== void 0) filters.maxParameters = input.maxParameters;
|
|
1502
|
+
if (input.canvasWidth !== void 0) filters.canvasWidth = input.canvasWidth;
|
|
1503
|
+
if (input.canvasHeight !== void 0) filters.canvasHeight = input.canvasHeight;
|
|
1504
|
+
if (input.hasPhilosophy !== void 0) filters.hasPhilosophy = input.hasPhilosophy;
|
|
1505
|
+
if (input.skills !== void 0) filters.skills = input.skills;
|
|
1506
|
+
for (const [, loaded] of state.sketches) {
|
|
1507
|
+
const def = loaded.definition;
|
|
1508
|
+
if (input.query !== void 0) {
|
|
1509
|
+
if (!def.title.toLowerCase().includes(input.query.toLowerCase())) continue;
|
|
1510
|
+
}
|
|
1511
|
+
if (input.renderer !== void 0) {
|
|
1512
|
+
if (def.renderer.type !== input.renderer) continue;
|
|
1513
|
+
}
|
|
1514
|
+
if (input.minParameters !== void 0) {
|
|
1515
|
+
if (def.parameters.length < input.minParameters) continue;
|
|
1516
|
+
}
|
|
1517
|
+
if (input.maxParameters !== void 0) {
|
|
1518
|
+
if (def.parameters.length > input.maxParameters) continue;
|
|
1519
|
+
}
|
|
1520
|
+
if (input.canvasWidth !== void 0) {
|
|
1521
|
+
if (def.canvas.width !== input.canvasWidth) continue;
|
|
1522
|
+
}
|
|
1523
|
+
if (input.canvasHeight !== void 0) {
|
|
1524
|
+
if (def.canvas.height !== input.canvasHeight) continue;
|
|
1525
|
+
}
|
|
1526
|
+
if (input.hasPhilosophy !== void 0) {
|
|
1527
|
+
const has = !!def.philosophy;
|
|
1528
|
+
if (has !== input.hasPhilosophy) continue;
|
|
1529
|
+
}
|
|
1530
|
+
if (input.skills !== void 0 && input.skills.length > 0) {
|
|
1531
|
+
const sketchSkills = new Set(def.skills ?? []);
|
|
1532
|
+
const hasAny = input.skills.some((s) => sketchSkills.has(s));
|
|
1533
|
+
if (!hasAny) continue;
|
|
1534
|
+
}
|
|
1535
|
+
matches.push({
|
|
1536
|
+
id: def.id,
|
|
1537
|
+
title: def.title,
|
|
1538
|
+
renderer: def.renderer.type,
|
|
1539
|
+
canvas: { width: def.canvas.width, height: def.canvas.height },
|
|
1540
|
+
parameterCount: def.parameters.length,
|
|
1541
|
+
colorCount: def.colors.length,
|
|
1542
|
+
snapshotCount: def.snapshots?.length ?? 0,
|
|
1543
|
+
hasPhilosophy: !!def.philosophy,
|
|
1544
|
+
skills: def.skills ?? []
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
return {
|
|
1548
|
+
success: true,
|
|
1549
|
+
matches,
|
|
1550
|
+
total: matches.length,
|
|
1551
|
+
filters
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/tools/merge.ts
|
|
1556
|
+
import { dirname as dirname4, join as join2 } from "path";
|
|
1557
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
1558
|
+
import {
|
|
1559
|
+
serializeGenart as serializeGenart2
|
|
1560
|
+
} from "@genart-dev/core";
|
|
1561
|
+
function now5() {
|
|
1562
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1563
|
+
}
|
|
1564
|
+
var VALID_STRATEGIES = ["blend", "layer", "alternate"];
|
|
1565
|
+
function blendParameters(sources) {
|
|
1566
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1567
|
+
const result = [];
|
|
1568
|
+
for (const source of sources) {
|
|
1569
|
+
for (const param of source.parameters) {
|
|
1570
|
+
if (!seen.has(param.key)) {
|
|
1571
|
+
seen.add(param.key);
|
|
1572
|
+
result.push(param);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return result;
|
|
1577
|
+
}
|
|
1578
|
+
function layerParameters(sources) {
|
|
1579
|
+
const result = [];
|
|
1580
|
+
for (let i = 0; i < sources.length; i++) {
|
|
1581
|
+
const source = sources[i];
|
|
1582
|
+
const prefix = `source${i + 1}_`;
|
|
1583
|
+
for (const param of source.parameters) {
|
|
1584
|
+
result.push({
|
|
1585
|
+
...param,
|
|
1586
|
+
key: `${prefix}${param.key}`,
|
|
1587
|
+
label: `[${i + 1}] ${param.label}`
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return result;
|
|
1592
|
+
}
|
|
1593
|
+
function alternateParameters(sources) {
|
|
1594
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1595
|
+
const result = [];
|
|
1596
|
+
for (let i = 0; i < sources.length; i += 2) {
|
|
1597
|
+
const source = sources[i];
|
|
1598
|
+
for (const param of source.parameters) {
|
|
1599
|
+
if (!seen.has(param.key)) {
|
|
1600
|
+
seen.add(param.key);
|
|
1601
|
+
result.push(param);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return result;
|
|
1606
|
+
}
|
|
1607
|
+
function mergeColors(sources) {
|
|
1608
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1609
|
+
const result = [];
|
|
1610
|
+
for (const source of sources) {
|
|
1611
|
+
for (const color of source.colors) {
|
|
1612
|
+
if (!seen.has(color.key)) {
|
|
1613
|
+
seen.add(color.key);
|
|
1614
|
+
result.push(color);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return result;
|
|
1619
|
+
}
|
|
1620
|
+
function alternateColors(sources) {
|
|
1621
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1622
|
+
const result = [];
|
|
1623
|
+
for (let i = 1; i < sources.length; i += 2) {
|
|
1624
|
+
const source = sources[i];
|
|
1625
|
+
for (const color of source.colors) {
|
|
1626
|
+
if (!seen.has(color.key)) {
|
|
1627
|
+
seen.add(color.key);
|
|
1628
|
+
result.push(color);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
if (result.length === 0) {
|
|
1633
|
+
return mergeColors(sources);
|
|
1634
|
+
}
|
|
1635
|
+
return result;
|
|
1636
|
+
}
|
|
1637
|
+
async function mergeSketches(state, input) {
|
|
1638
|
+
const ws = state.requireWorkspace();
|
|
1639
|
+
if (!input.sourceIds || input.sourceIds.length < 2) {
|
|
1640
|
+
throw new Error("At least 2 source sketches are required");
|
|
1641
|
+
}
|
|
1642
|
+
const strategy = input.strategy ?? "blend";
|
|
1643
|
+
if (!VALID_STRATEGIES.includes(strategy)) {
|
|
1644
|
+
throw new Error(
|
|
1645
|
+
`Unknown merge strategy: '${strategy}'. Valid strategies: ${VALID_STRATEGIES.join(", ")}`
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
if (state.getSketch(input.newId)) {
|
|
1649
|
+
throw new Error(`Sketch with ID '${input.newId}' already exists`);
|
|
1650
|
+
}
|
|
1651
|
+
const sources = [];
|
|
1652
|
+
for (const id of input.sourceIds) {
|
|
1653
|
+
const loaded = state.requireSketch(id);
|
|
1654
|
+
sources.push(loaded.definition);
|
|
1655
|
+
}
|
|
1656
|
+
const renderer = input.renderer ?? sources[0].renderer.type;
|
|
1657
|
+
const canvasWidth = input.canvas?.width ?? Math.max(...sources.map((s) => s.canvas.width));
|
|
1658
|
+
const canvasHeight = input.canvas?.height ?? Math.max(...sources.map((s) => s.canvas.height));
|
|
1659
|
+
let parameters;
|
|
1660
|
+
let algorithm;
|
|
1661
|
+
let colors;
|
|
1662
|
+
switch (strategy) {
|
|
1663
|
+
case "layer":
|
|
1664
|
+
parameters = layerParameters(sources);
|
|
1665
|
+
colors = mergeColors(sources);
|
|
1666
|
+
break;
|
|
1667
|
+
case "alternate":
|
|
1668
|
+
parameters = alternateParameters(sources);
|
|
1669
|
+
colors = alternateColors(sources);
|
|
1670
|
+
break;
|
|
1671
|
+
case "blend":
|
|
1672
|
+
default:
|
|
1673
|
+
parameters = blendParameters(sources);
|
|
1674
|
+
colors = mergeColors(sources);
|
|
1675
|
+
break;
|
|
1676
|
+
}
|
|
1677
|
+
algorithm = `// Merged from: ${input.sourceIds.join(", ")}
|
|
1678
|
+
// Strategy: ${strategy}
|
|
1679
|
+
// TODO: Write a unified algorithm combining the source concepts.
|
|
1680
|
+
`;
|
|
1681
|
+
const philosophyParts = sources.filter((s) => s.philosophy).map((s) => `## ${s.title}
|
|
1682
|
+
|
|
1683
|
+
${s.philosophy}`);
|
|
1684
|
+
const philosophy = philosophyParts.length > 0 ? philosophyParts.join("\n\n---\n\n") : void 0;
|
|
1685
|
+
const themes = [];
|
|
1686
|
+
const seenThemeNames = /* @__PURE__ */ new Set();
|
|
1687
|
+
for (const source of sources) {
|
|
1688
|
+
for (const theme of source.themes ?? []) {
|
|
1689
|
+
if (!seenThemeNames.has(theme.name)) {
|
|
1690
|
+
seenThemeNames.add(theme.name);
|
|
1691
|
+
themes.push(theme);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const skills = [...new Set(sources.flatMap((s) => s.skills ?? []))];
|
|
1696
|
+
const params = {};
|
|
1697
|
+
for (const p of parameters) {
|
|
1698
|
+
params[p.key] = p.default;
|
|
1699
|
+
}
|
|
1700
|
+
const colorPalette = colors.map((c) => c.default);
|
|
1701
|
+
const timestamp = now5();
|
|
1702
|
+
const newDef = {
|
|
1703
|
+
genart: "1.1",
|
|
1704
|
+
id: input.newId,
|
|
1705
|
+
title: input.title,
|
|
1706
|
+
created: timestamp,
|
|
1707
|
+
modified: timestamp,
|
|
1708
|
+
...skills.length > 0 ? { skills } : {},
|
|
1709
|
+
renderer: { type: renderer, version: sources[0].renderer.version },
|
|
1710
|
+
canvas: { width: canvasWidth, height: canvasHeight },
|
|
1711
|
+
...philosophy ? { philosophy } : {},
|
|
1712
|
+
parameters,
|
|
1713
|
+
colors,
|
|
1714
|
+
...themes.length > 0 ? { themes } : {},
|
|
1715
|
+
state: {
|
|
1716
|
+
seed: Math.floor(Math.random() * 1e5),
|
|
1717
|
+
params,
|
|
1718
|
+
colorPalette
|
|
1719
|
+
},
|
|
1720
|
+
algorithm
|
|
1721
|
+
};
|
|
1722
|
+
const wsDir = dirname4(state.workspacePath);
|
|
1723
|
+
const fileName = `${input.newId}.genart`;
|
|
1724
|
+
const filePath = join2(wsDir, fileName);
|
|
1725
|
+
const json = serializeGenart2(newDef);
|
|
1726
|
+
await writeFile3(filePath, json, "utf-8");
|
|
1727
|
+
state.sketches.set(input.newId, { definition: newDef, path: filePath });
|
|
1728
|
+
const maxX = ws.sketches.reduce(
|
|
1729
|
+
(max, ref) => Math.max(max, ref.position.x),
|
|
1730
|
+
0
|
|
1731
|
+
);
|
|
1732
|
+
const newRef = {
|
|
1733
|
+
file: fileName,
|
|
1734
|
+
position: { x: maxX + 1400, y: 0 }
|
|
1735
|
+
};
|
|
1736
|
+
state.workspace = {
|
|
1737
|
+
...ws,
|
|
1738
|
+
modified: timestamp,
|
|
1739
|
+
sketches: [...ws.sketches, newRef]
|
|
1740
|
+
};
|
|
1741
|
+
await state.saveWorkspace();
|
|
1742
|
+
state.emitMutation("sketch:created", { id: input.newId });
|
|
1743
|
+
const crossRenderer = sources.some((s) => s.renderer.type !== renderer);
|
|
1744
|
+
return {
|
|
1745
|
+
success: true,
|
|
1746
|
+
sketchId: input.newId,
|
|
1747
|
+
title: input.title,
|
|
1748
|
+
path: filePath,
|
|
1749
|
+
renderer,
|
|
1750
|
+
strategy,
|
|
1751
|
+
sources: input.sourceIds,
|
|
1752
|
+
parameterCount: parameters.length,
|
|
1753
|
+
colorCount: colors.length,
|
|
1754
|
+
...crossRenderer ? {
|
|
1755
|
+
crossRendererNotice: `Source sketches use different renderers. The merged sketch targets '${renderer}' \u2014 the algorithm must be written for this renderer.`
|
|
1756
|
+
} : {}
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/tools/snapshot-layout.ts
|
|
1761
|
+
import { basename as basename7 } from "path";
|
|
1762
|
+
async function snapshotLayout(state, input) {
|
|
1763
|
+
const ws = state.requireWorkspace();
|
|
1764
|
+
const includeGroups = input.includeGroups !== false;
|
|
1765
|
+
const includeState = input.includeState === true;
|
|
1766
|
+
const sketches = [];
|
|
1767
|
+
const rendererCounts = {};
|
|
1768
|
+
for (const ref of ws.sketches) {
|
|
1769
|
+
const loaded = [...state.sketches.values()].find(
|
|
1770
|
+
(v) => basename7(v.path) === ref.file
|
|
1771
|
+
);
|
|
1772
|
+
if (!loaded) continue;
|
|
1773
|
+
const def = loaded.definition;
|
|
1774
|
+
const entry = {
|
|
1775
|
+
id: def.id,
|
|
1776
|
+
title: def.title,
|
|
1777
|
+
renderer: def.renderer.type,
|
|
1778
|
+
position: ref.position,
|
|
1779
|
+
canvas: { width: def.canvas.width, height: def.canvas.height },
|
|
1780
|
+
parameterCount: def.parameters.length,
|
|
1781
|
+
colorCount: def.colors.length,
|
|
1782
|
+
snapshotCount: def.snapshots?.length ?? 0,
|
|
1783
|
+
locked: false,
|
|
1784
|
+
visible: true
|
|
1785
|
+
};
|
|
1786
|
+
if (includeState) {
|
|
1787
|
+
entry.state = {
|
|
1788
|
+
seed: def.state.seed,
|
|
1789
|
+
params: def.state.params,
|
|
1790
|
+
colorPalette: def.state.colorPalette
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
sketches.push(entry);
|
|
1794
|
+
const rt = def.renderer.type;
|
|
1795
|
+
rendererCounts[rt] = (rendererCounts[rt] ?? 0) + 1;
|
|
1796
|
+
}
|
|
1797
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1798
|
+
for (const sketch of sketches) {
|
|
1799
|
+
const pos = sketch.position;
|
|
1800
|
+
const canvas = sketch.canvas;
|
|
1801
|
+
if (pos.x < minX) minX = pos.x;
|
|
1802
|
+
if (pos.y < minY) minY = pos.y;
|
|
1803
|
+
if (pos.x + canvas.width > maxX) maxX = pos.x + canvas.width;
|
|
1804
|
+
if (pos.y + canvas.height > maxY) maxY = pos.y + canvas.height;
|
|
1805
|
+
}
|
|
1806
|
+
const boundingBox = sketches.length > 0 ? { x: minX, y: minY, width: maxX - minX, height: maxY - minY } : { x: 0, y: 0, width: 0, height: 0 };
|
|
1807
|
+
const groups = [];
|
|
1808
|
+
if (includeGroups && ws.groups) {
|
|
1809
|
+
for (const group of ws.groups) {
|
|
1810
|
+
const sketchIds = [];
|
|
1811
|
+
for (const file of group.sketchFiles) {
|
|
1812
|
+
const loaded = [...state.sketches.values()].find(
|
|
1813
|
+
(v) => basename7(v.path) === file
|
|
1814
|
+
);
|
|
1815
|
+
if (loaded) {
|
|
1816
|
+
sketchIds.push(loaded.definition.id);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
groups.push({
|
|
1820
|
+
id: group.id,
|
|
1821
|
+
label: group.label,
|
|
1822
|
+
sketchIds,
|
|
1823
|
+
...group.color ? { color: group.color } : {}
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
const result = {
|
|
1828
|
+
success: true,
|
|
1829
|
+
workspace: {
|
|
1830
|
+
id: ws.id,
|
|
1831
|
+
title: ws.title,
|
|
1832
|
+
viewport: ws.viewport
|
|
1833
|
+
},
|
|
1834
|
+
sketches,
|
|
1835
|
+
boundingBox,
|
|
1836
|
+
totalSketches: sketches.length,
|
|
1837
|
+
rendererBreakdown: rendererCounts
|
|
1838
|
+
};
|
|
1839
|
+
if (includeGroups) {
|
|
1840
|
+
result.groups = groups;
|
|
1841
|
+
}
|
|
1842
|
+
return result;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// src/tools/knowledge.ts
|
|
1846
|
+
import { createDefaultSkillRegistry } from "@genart-dev/core";
|
|
1847
|
+
var registry = createDefaultSkillRegistry();
|
|
1848
|
+
async function listSkills(input) {
|
|
1849
|
+
const skills = registry.list(input.category);
|
|
1850
|
+
return {
|
|
1851
|
+
success: true,
|
|
1852
|
+
skills: skills.map((s) => ({
|
|
1853
|
+
id: s.id,
|
|
1854
|
+
name: s.name,
|
|
1855
|
+
category: s.category,
|
|
1856
|
+
complexity: s.complexity,
|
|
1857
|
+
description: s.description
|
|
1858
|
+
})),
|
|
1859
|
+
total: skills.length
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
async function loadSkill(input) {
|
|
1863
|
+
const skill = registry.get(input.skillId);
|
|
1864
|
+
if (!skill) {
|
|
1865
|
+
return {
|
|
1866
|
+
success: false,
|
|
1867
|
+
error: `Skill not found: '${input.skillId}'`,
|
|
1868
|
+
skill: null
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
const skillData = {
|
|
1872
|
+
id: skill.id,
|
|
1873
|
+
name: skill.name,
|
|
1874
|
+
category: skill.category,
|
|
1875
|
+
complexity: skill.complexity,
|
|
1876
|
+
description: skill.description,
|
|
1877
|
+
theory: skill.theory,
|
|
1878
|
+
principles: skill.principles,
|
|
1879
|
+
references: skill.references,
|
|
1880
|
+
suggestedParameters: skill.suggestedParameters ?? [],
|
|
1881
|
+
suggestedColors: skill.suggestedColors ?? []
|
|
1882
|
+
};
|
|
1883
|
+
if (input.renderer && skill.examples) {
|
|
1884
|
+
const example = skill.examples[input.renderer];
|
|
1885
|
+
if (example) {
|
|
1886
|
+
skillData["example"] = example;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return {
|
|
1890
|
+
success: true,
|
|
1891
|
+
skill: skillData
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
var STATIC_GUIDELINES = {
|
|
1895
|
+
parameters: `## Parameter Design Guidelines
|
|
1896
|
+
|
|
1897
|
+
- Keep parameter count between 3 and 8 for usability
|
|
1898
|
+
- Use intuitive ranges: 0-1 for normalized values, actual units for physical quantities
|
|
1899
|
+
- Set defaults that produce an interesting (not extreme) result
|
|
1900
|
+
- Group related parameters in tabs for complex sketches
|
|
1901
|
+
- Name parameters descriptively: "noiseScale" not "ns"
|
|
1902
|
+
- Step size should match visual sensitivity: fine for color, coarse for counts`,
|
|
1903
|
+
animation: `## Animation Guidelines
|
|
1904
|
+
|
|
1905
|
+
- Use requestAnimationFrame for smooth 60fps animation
|
|
1906
|
+
- Separate initialization from per-frame updates
|
|
1907
|
+
- Provide pause/resume control for resource management
|
|
1908
|
+
- Use time-based animation (not frame-count) for consistent speed
|
|
1909
|
+
- Consider using easing functions for natural motion
|
|
1910
|
+
- Keep draw loops lightweight \u2014 precompute what you can in setup`,
|
|
1911
|
+
performance: `## Performance Guidelines
|
|
1912
|
+
|
|
1913
|
+
- Only the selected artboard should run live; others should pause
|
|
1914
|
+
- Use offscreen rendering for thumbnails
|
|
1915
|
+
- Dispose WebGL contexts and GPU resources when unmounting
|
|
1916
|
+
- Limit particle/element counts with parameters
|
|
1917
|
+
- Use spatial data structures (quadtree, grid) for collision/proximity
|
|
1918
|
+
- Profile with Chrome DevTools before optimizing`
|
|
1919
|
+
};
|
|
1920
|
+
async function getGuidelines(input) {
|
|
1921
|
+
const topic = input.topic;
|
|
1922
|
+
if (STATIC_GUIDELINES[topic]) {
|
|
1923
|
+
return {
|
|
1924
|
+
success: true,
|
|
1925
|
+
topic,
|
|
1926
|
+
guidelines: STATIC_GUIDELINES[topic],
|
|
1927
|
+
relatedSkills: []
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
const categoryMap = {
|
|
1931
|
+
composition: "composition",
|
|
1932
|
+
color: "color",
|
|
1933
|
+
colours: "color",
|
|
1934
|
+
layout: "composition",
|
|
1935
|
+
palette: "color"
|
|
1936
|
+
};
|
|
1937
|
+
const category = categoryMap[topic];
|
|
1938
|
+
if (!category) {
|
|
1939
|
+
return {
|
|
1940
|
+
success: false,
|
|
1941
|
+
topic,
|
|
1942
|
+
error: `No guidelines found for topic: '${topic}'. Available topics: composition, color, parameters, animation, performance`,
|
|
1943
|
+
guidelines: null,
|
|
1944
|
+
relatedSkills: []
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
const skills = registry.list(category);
|
|
1948
|
+
const guidelines = skills.map(
|
|
1949
|
+
(s) => `### ${s.name}
|
|
1950
|
+
|
|
1951
|
+
${s.description}
|
|
1952
|
+
|
|
1953
|
+
**Principles:**
|
|
1954
|
+
${s.principles.map((p) => `- ${p}`).join("\n")}`
|
|
1955
|
+
).join("\n\n---\n\n");
|
|
1956
|
+
return {
|
|
1957
|
+
success: true,
|
|
1958
|
+
topic,
|
|
1959
|
+
guidelines: `## ${category.charAt(0).toUpperCase() + category.slice(1)} Guidelines
|
|
1960
|
+
|
|
1961
|
+
${guidelines}`,
|
|
1962
|
+
relatedSkills: skills.map((s) => ({
|
|
1963
|
+
id: s.id,
|
|
1964
|
+
name: s.name,
|
|
1965
|
+
complexity: s.complexity
|
|
1966
|
+
}))
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/tools/capture.ts
|
|
1971
|
+
import { writeFile as writeFile4 } from "fs/promises";
|
|
1972
|
+
import {
|
|
1973
|
+
createDefaultRegistry as createDefaultRegistry2
|
|
1974
|
+
} from "@genart-dev/core";
|
|
1975
|
+
|
|
1976
|
+
// src/capture/headless.ts
|
|
1977
|
+
var cachedModule = null;
|
|
1978
|
+
async function loadPuppeteer() {
|
|
1979
|
+
if (!cachedModule) {
|
|
1980
|
+
try {
|
|
1981
|
+
const mod = await import("puppeteer");
|
|
1982
|
+
cachedModule = mod.default ?? mod;
|
|
1983
|
+
} catch {
|
|
1984
|
+
throw new Error(
|
|
1985
|
+
"Puppeteer is required for screenshot capture. Install it with: npm install puppeteer"
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
return cachedModule;
|
|
1990
|
+
}
|
|
1991
|
+
var browserInstance = null;
|
|
1992
|
+
async function getBrowser() {
|
|
1993
|
+
if (browserInstance && browserInstance.connected) {
|
|
1994
|
+
return browserInstance;
|
|
1995
|
+
}
|
|
1996
|
+
const puppeteer = await loadPuppeteer();
|
|
1997
|
+
browserInstance = await puppeteer.launch({
|
|
1998
|
+
headless: true,
|
|
1999
|
+
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || void 0,
|
|
2000
|
+
args: [
|
|
2001
|
+
"--no-sandbox",
|
|
2002
|
+
"--disable-setuid-sandbox",
|
|
2003
|
+
"--disable-gpu",
|
|
2004
|
+
"--disable-dev-shm-usage",
|
|
2005
|
+
"--single-process"
|
|
2006
|
+
]
|
|
2007
|
+
});
|
|
2008
|
+
return browserInstance;
|
|
2009
|
+
}
|
|
2010
|
+
async function captureHtml(options) {
|
|
2011
|
+
const { html, width, height, waitMs = 500, imageType = "png", quality } = options;
|
|
2012
|
+
const browser = await getBrowser();
|
|
2013
|
+
const page = await browser.newPage();
|
|
2014
|
+
try {
|
|
2015
|
+
await page.setViewport({ width, height, deviceScaleFactor: 1 });
|
|
2016
|
+
await page.setContent(html, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
2017
|
+
await new Promise((resolve3) => setTimeout(resolve3, waitMs));
|
|
2018
|
+
const buffer = await page.screenshot({
|
|
2019
|
+
type: imageType,
|
|
2020
|
+
clip: { x: 0, y: 0, width, height },
|
|
2021
|
+
...imageType === "jpeg" && quality !== void 0 ? { quality } : {}
|
|
2022
|
+
});
|
|
2023
|
+
const bytes = new Uint8Array(buffer);
|
|
2024
|
+
const mimeType = imageType === "jpeg" ? "image/jpeg" : "image/png";
|
|
2025
|
+
return { bytes, mimeType, width, height };
|
|
2026
|
+
} finally {
|
|
2027
|
+
await page.close();
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
async function captureHtmlMulti(options) {
|
|
2031
|
+
const { html, width, height, waitMs = 500, inlineSize = 400, jpegQuality = 70 } = options;
|
|
2032
|
+
const browser = await getBrowser();
|
|
2033
|
+
const page = await browser.newPage();
|
|
2034
|
+
try {
|
|
2035
|
+
await page.setViewport({ width, height, deviceScaleFactor: 1 });
|
|
2036
|
+
await page.setContent(html, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
2037
|
+
await new Promise((resolve3) => setTimeout(resolve3, waitMs));
|
|
2038
|
+
const pngBuffer = await page.screenshot({
|
|
2039
|
+
type: "png",
|
|
2040
|
+
clip: { x: 0, y: 0, width, height }
|
|
2041
|
+
});
|
|
2042
|
+
const previewPng = new Uint8Array(pngBuffer);
|
|
2043
|
+
const scale = Math.min(inlineSize / width, inlineSize / height, 1);
|
|
2044
|
+
const inlineWidth = Math.round(width * scale);
|
|
2045
|
+
const inlineHeight = Math.round(height * scale);
|
|
2046
|
+
await page.setViewport({ width: inlineWidth, height: inlineHeight, deviceScaleFactor: 1 });
|
|
2047
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
2048
|
+
const jpegBuffer = await page.screenshot({
|
|
2049
|
+
type: "jpeg",
|
|
2050
|
+
quality: jpegQuality,
|
|
2051
|
+
clip: { x: 0, y: 0, width: inlineWidth, height: inlineHeight }
|
|
2052
|
+
});
|
|
2053
|
+
const inlineJpeg = new Uint8Array(jpegBuffer);
|
|
2054
|
+
return {
|
|
2055
|
+
previewPng,
|
|
2056
|
+
previewWidth: width,
|
|
2057
|
+
previewHeight: height,
|
|
2058
|
+
inlineJpeg,
|
|
2059
|
+
inlineWidth,
|
|
2060
|
+
inlineHeight
|
|
2061
|
+
};
|
|
2062
|
+
} finally {
|
|
2063
|
+
await page.close();
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// src/tools/capture.ts
|
|
2068
|
+
var registry2 = createDefaultRegistry2();
|
|
2069
|
+
function applyOverrides(sketch, overrides) {
|
|
2070
|
+
if (overrides.seed === void 0 && overrides.params === void 0) {
|
|
2071
|
+
return sketch;
|
|
2072
|
+
}
|
|
2073
|
+
const newState = {
|
|
2074
|
+
seed: overrides.seed ?? sketch.state.seed,
|
|
2075
|
+
params: overrides.params ? { ...sketch.state.params, ...overrides.params } : sketch.state.params,
|
|
2076
|
+
colorPalette: sketch.state.colorPalette
|
|
2077
|
+
};
|
|
2078
|
+
return { ...sketch, state: newState };
|
|
2079
|
+
}
|
|
2080
|
+
function generateSketchHtml(sketch, opts) {
|
|
2081
|
+
const effective = applyOverrides(sketch, opts);
|
|
2082
|
+
const adapter = registry2.resolve(effective.renderer.type);
|
|
2083
|
+
if (!adapter) {
|
|
2084
|
+
throw new Error(
|
|
2085
|
+
`Unsupported renderer type: '${effective.renderer.type}'`
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
return adapter.generateStandaloneHTML(effective);
|
|
2089
|
+
}
|
|
2090
|
+
function derivePreviewPath(sketchPath) {
|
|
2091
|
+
return sketchPath.replace(/\.genart$/, ".png");
|
|
2092
|
+
}
|
|
2093
|
+
async function captureScreenshot(state, input) {
|
|
2094
|
+
state.requireWorkspace();
|
|
2095
|
+
const target = input.target ?? "selected";
|
|
2096
|
+
let sketchId;
|
|
2097
|
+
if (target === "selected") {
|
|
2098
|
+
if (state.selection.size === 0) {
|
|
2099
|
+
throw new Error("No sketch is currently selected");
|
|
2100
|
+
}
|
|
2101
|
+
sketchId = [...state.selection][0];
|
|
2102
|
+
} else {
|
|
2103
|
+
if (!input.sketchId) {
|
|
2104
|
+
throw new Error("sketchId is required when target is 'sketch'");
|
|
2105
|
+
}
|
|
2106
|
+
sketchId = input.sketchId;
|
|
2107
|
+
}
|
|
2108
|
+
const loaded = state.requireSketch(sketchId);
|
|
2109
|
+
const sketch = loaded.definition;
|
|
2110
|
+
try {
|
|
2111
|
+
const html = generateSketchHtml(sketch, {
|
|
2112
|
+
seed: input.seed,
|
|
2113
|
+
params: input.params
|
|
2114
|
+
});
|
|
2115
|
+
const width = input.width ?? sketch.canvas.width;
|
|
2116
|
+
const height = input.height ?? sketch.canvas.height;
|
|
2117
|
+
const inlineSize = input.previewSize ?? 400;
|
|
2118
|
+
const multi = await captureHtmlMulti({
|
|
2119
|
+
html,
|
|
2120
|
+
width,
|
|
2121
|
+
height,
|
|
2122
|
+
inlineSize
|
|
2123
|
+
});
|
|
2124
|
+
const previewPath = derivePreviewPath(loaded.path);
|
|
2125
|
+
const metadata = await buildScreenshotMetadata(state, multi, {
|
|
2126
|
+
target,
|
|
2127
|
+
sketchId,
|
|
2128
|
+
seed: input.seed ?? sketch.state.seed,
|
|
2129
|
+
previewPath
|
|
2130
|
+
});
|
|
2131
|
+
const previewJpegBase64 = Buffer.from(multi.inlineJpeg).toString("base64");
|
|
2132
|
+
return { metadata, previewJpegBase64 };
|
|
2133
|
+
} catch (e) {
|
|
2134
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2135
|
+
throw new Error(`Renderer error for '${sketchId}': ${msg}`);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
async function buildScreenshotMetadata(state, multi, info) {
|
|
2139
|
+
const metadata = {
|
|
2140
|
+
success: true,
|
|
2141
|
+
target: info.target,
|
|
2142
|
+
sketchId: info.sketchId,
|
|
2143
|
+
width: multi.previewWidth,
|
|
2144
|
+
height: multi.previewHeight,
|
|
2145
|
+
seed: info.seed,
|
|
2146
|
+
previewPath: info.previewPath
|
|
2147
|
+
};
|
|
2148
|
+
if (!state.remoteMode) {
|
|
2149
|
+
await writeFile4(info.previewPath, multi.previewPng);
|
|
2150
|
+
metadata.savedPreviewTo = info.previewPath;
|
|
2151
|
+
}
|
|
2152
|
+
return metadata;
|
|
2153
|
+
}
|
|
2154
|
+
async function captureBatch(state, input) {
|
|
2155
|
+
state.requireWorkspace();
|
|
2156
|
+
const ids = input.sketchIds ?? [...state.sketches.keys()];
|
|
2157
|
+
if (ids.length === 0) {
|
|
2158
|
+
throw new Error("No sketches to capture");
|
|
2159
|
+
}
|
|
2160
|
+
for (const id of ids) {
|
|
2161
|
+
state.requireSketch(id);
|
|
2162
|
+
}
|
|
2163
|
+
const inlineSize = input.previewSize ?? 200;
|
|
2164
|
+
const items = [];
|
|
2165
|
+
const errors = [];
|
|
2166
|
+
const promises = ids.map(async (id) => {
|
|
2167
|
+
const loaded = state.requireSketch(id);
|
|
2168
|
+
const sketch = loaded.definition;
|
|
2169
|
+
try {
|
|
2170
|
+
const html = generateSketchHtml(sketch, { seed: input.seed });
|
|
2171
|
+
const width = input.width ?? sketch.canvas.width;
|
|
2172
|
+
const height = input.height ?? sketch.canvas.height;
|
|
2173
|
+
const multi = await captureHtmlMulti({
|
|
2174
|
+
html,
|
|
2175
|
+
width,
|
|
2176
|
+
height,
|
|
2177
|
+
inlineSize
|
|
2178
|
+
});
|
|
2179
|
+
const previewPath = derivePreviewPath(loaded.path);
|
|
2180
|
+
const itemMetadata = await buildScreenshotMetadata(state, multi, {
|
|
2181
|
+
target: "sketch",
|
|
2182
|
+
sketchId: id,
|
|
2183
|
+
seed: input.seed ?? sketch.state.seed,
|
|
2184
|
+
previewPath
|
|
2185
|
+
});
|
|
2186
|
+
items.push({
|
|
2187
|
+
metadata: itemMetadata,
|
|
2188
|
+
inlineJpegBase64: Buffer.from(multi.inlineJpeg).toString("base64")
|
|
2189
|
+
});
|
|
2190
|
+
} catch (e) {
|
|
2191
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2192
|
+
errors.push({ sketchId: id, error: msg });
|
|
2193
|
+
}
|
|
2194
|
+
});
|
|
2195
|
+
await Promise.all(promises);
|
|
2196
|
+
return {
|
|
2197
|
+
metadata: {
|
|
2198
|
+
success: errors.length === 0,
|
|
2199
|
+
total: ids.length,
|
|
2200
|
+
captured: items.length,
|
|
2201
|
+
failed: errors.length,
|
|
2202
|
+
errors: errors.length > 0 ? errors : void 0
|
|
2203
|
+
},
|
|
2204
|
+
items
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// src/tools/export.ts
|
|
2209
|
+
import { createWriteStream } from "fs";
|
|
2210
|
+
import { stat as stat4, writeFile as writeFile5 } from "fs/promises";
|
|
2211
|
+
import { dirname as dirname5 } from "path";
|
|
2212
|
+
import archiver from "archiver";
|
|
2213
|
+
import {
|
|
2214
|
+
createDefaultRegistry as createDefaultRegistry3,
|
|
2215
|
+
serializeGenart as serializeGenart3
|
|
2216
|
+
} from "@genart-dev/core";
|
|
2217
|
+
var registry3 = createDefaultRegistry3();
|
|
2218
|
+
async function validateOutputPath(outputPath) {
|
|
2219
|
+
const parentDir = dirname5(outputPath);
|
|
2220
|
+
try {
|
|
2221
|
+
const s = await stat4(parentDir);
|
|
2222
|
+
if (!s.isDirectory()) {
|
|
2223
|
+
throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
2224
|
+
}
|
|
2225
|
+
} catch (e) {
|
|
2226
|
+
if (e instanceof Error && e.message.startsWith("Parent directory")) throw e;
|
|
2227
|
+
throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
2228
|
+
}
|
|
2229
|
+
try {
|
|
2230
|
+
await stat4(outputPath);
|
|
2231
|
+
throw new Error(
|
|
2232
|
+
`File already exists at ${outputPath}. Delete it first or use a different path.`
|
|
2233
|
+
);
|
|
2234
|
+
} catch (e) {
|
|
2235
|
+
if (e instanceof Error && e.message.startsWith("File already exists")) throw e;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
function algorithmExtension(rendererType) {
|
|
2239
|
+
return rendererType === "glsl" ? ".glsl" : ".js";
|
|
2240
|
+
}
|
|
2241
|
+
function applyOverrides2(sketch, overrides) {
|
|
2242
|
+
if (overrides.seed === void 0 && overrides.params === void 0) {
|
|
2243
|
+
return sketch;
|
|
2244
|
+
}
|
|
2245
|
+
const newState = {
|
|
2246
|
+
seed: overrides.seed ?? sketch.state.seed,
|
|
2247
|
+
params: overrides.params ? { ...sketch.state.params, ...overrides.params } : sketch.state.params,
|
|
2248
|
+
colorPalette: sketch.state.colorPalette
|
|
2249
|
+
};
|
|
2250
|
+
return { ...sketch, state: newState };
|
|
2251
|
+
}
|
|
2252
|
+
async function exportSketch(state, input) {
|
|
2253
|
+
state.requireWorkspace();
|
|
2254
|
+
const loaded = state.requireSketch(input.sketchId);
|
|
2255
|
+
const sketch = applyOverrides2(loaded.definition, {
|
|
2256
|
+
seed: input.seed,
|
|
2257
|
+
params: input.params
|
|
2258
|
+
});
|
|
2259
|
+
await validateOutputPath(input.outputPath);
|
|
2260
|
+
const adapter = registry3.resolve(sketch.renderer.type);
|
|
2261
|
+
if (!adapter) {
|
|
2262
|
+
throw new Error(
|
|
2263
|
+
`Unsupported renderer type: '${sketch.renderer.type}'`
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
switch (input.format) {
|
|
2267
|
+
case "html":
|
|
2268
|
+
return await exportHtml(sketch, input.outputPath);
|
|
2269
|
+
case "png":
|
|
2270
|
+
return await exportPng(sketch, input);
|
|
2271
|
+
case "svg":
|
|
2272
|
+
return await exportSvg(sketch, input);
|
|
2273
|
+
case "algorithm":
|
|
2274
|
+
return await exportAlgorithm(sketch, input.outputPath);
|
|
2275
|
+
case "zip":
|
|
2276
|
+
return await exportZip(sketch, input);
|
|
2277
|
+
default:
|
|
2278
|
+
throw new Error(`Unsupported export format: '${input.format}'`);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
async function exportHtml(sketch, outputPath) {
|
|
2282
|
+
const adapter = registry3.resolve(sketch.renderer.type);
|
|
2283
|
+
const html = adapter.generateStandaloneHTML(sketch);
|
|
2284
|
+
const content = Buffer.from(html, "utf-8");
|
|
2285
|
+
await writeFile5(outputPath, content);
|
|
2286
|
+
return {
|
|
2287
|
+
success: true,
|
|
2288
|
+
sketchId: sketch.id,
|
|
2289
|
+
format: "html",
|
|
2290
|
+
outputPath,
|
|
2291
|
+
fileSize: content.byteLength,
|
|
2292
|
+
renderer: sketch.renderer.type
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
async function exportPng(sketch, input) {
|
|
2296
|
+
const adapter = registry3.resolve(sketch.renderer.type);
|
|
2297
|
+
const html = adapter.generateStandaloneHTML(sketch);
|
|
2298
|
+
const width = input.width ?? sketch.canvas.width;
|
|
2299
|
+
const height = input.height ?? sketch.canvas.height;
|
|
2300
|
+
const result = await captureHtml({ html, width, height });
|
|
2301
|
+
await writeFile5(input.outputPath, result.bytes);
|
|
2302
|
+
return {
|
|
2303
|
+
success: true,
|
|
2304
|
+
sketchId: sketch.id,
|
|
2305
|
+
format: "png",
|
|
2306
|
+
outputPath: input.outputPath,
|
|
2307
|
+
fileSize: result.bytes.byteLength,
|
|
2308
|
+
renderer: sketch.renderer.type
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
async function exportSvg(sketch, input) {
|
|
2312
|
+
const width = input.width ?? sketch.canvas.width;
|
|
2313
|
+
const height = input.height ?? sketch.canvas.height;
|
|
2314
|
+
if (sketch.renderer.type === "svg") {
|
|
2315
|
+
const content2 = Buffer.from(sketch.algorithm, "utf-8");
|
|
2316
|
+
await writeFile5(input.outputPath, content2);
|
|
2317
|
+
return {
|
|
2318
|
+
success: true,
|
|
2319
|
+
sketchId: sketch.id,
|
|
2320
|
+
format: "svg",
|
|
2321
|
+
outputPath: input.outputPath,
|
|
2322
|
+
fileSize: content2.byteLength,
|
|
2323
|
+
renderer: sketch.renderer.type,
|
|
2324
|
+
notice: null
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
const adapter = registry3.resolve(sketch.renderer.type);
|
|
2328
|
+
const html = adapter.generateStandaloneHTML(sketch);
|
|
2329
|
+
const result = await captureHtml({ html, width, height });
|
|
2330
|
+
const b64 = Buffer.from(result.bytes).toString("base64");
|
|
2331
|
+
const svg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2332
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
2333
|
+
width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
2334
|
+
<image width="${width}" height="${height}"
|
|
2335
|
+
href="data:image/png;base64,${b64}"/>
|
|
2336
|
+
</svg>`;
|
|
2337
|
+
const content = Buffer.from(svg, "utf-8");
|
|
2338
|
+
await writeFile5(input.outputPath, content);
|
|
2339
|
+
return {
|
|
2340
|
+
success: true,
|
|
2341
|
+
sketchId: sketch.id,
|
|
2342
|
+
format: "svg",
|
|
2343
|
+
outputPath: input.outputPath,
|
|
2344
|
+
fileSize: content.byteLength,
|
|
2345
|
+
renderer: sketch.renderer.type,
|
|
2346
|
+
notice: "Non-SVG renderer \u2014 rasterized PNG embedded in SVG container"
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
async function exportAlgorithm(sketch, outputPath) {
|
|
2350
|
+
const content = Buffer.from(sketch.algorithm, "utf-8");
|
|
2351
|
+
await writeFile5(outputPath, content);
|
|
2352
|
+
return {
|
|
2353
|
+
success: true,
|
|
2354
|
+
sketchId: sketch.id,
|
|
2355
|
+
format: "algorithm",
|
|
2356
|
+
outputPath,
|
|
2357
|
+
fileSize: content.byteLength,
|
|
2358
|
+
renderer: sketch.renderer.type
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
async function exportZip(sketch, input) {
|
|
2362
|
+
const adapter = registry3.resolve(sketch.renderer.type);
|
|
2363
|
+
const width = input.width ?? sketch.canvas.width;
|
|
2364
|
+
const height = input.height ?? sketch.canvas.height;
|
|
2365
|
+
const html = adapter.generateStandaloneHTML(sketch);
|
|
2366
|
+
const genartJson = serializeGenart3(sketch);
|
|
2367
|
+
const algorithm = sketch.algorithm;
|
|
2368
|
+
const algExt = algorithmExtension(sketch.renderer.type);
|
|
2369
|
+
const captureResult = await captureHtml({ html, width, height });
|
|
2370
|
+
const output = createWriteStream(input.outputPath);
|
|
2371
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
2372
|
+
const finished = new Promise((resolve3, reject) => {
|
|
2373
|
+
output.on("close", resolve3);
|
|
2374
|
+
archive.on("error", reject);
|
|
2375
|
+
});
|
|
2376
|
+
archive.pipe(output);
|
|
2377
|
+
archive.append(html, { name: `${sketch.id}.html` });
|
|
2378
|
+
archive.append(Buffer.from(captureResult.bytes), { name: `${sketch.id}.png` });
|
|
2379
|
+
archive.append(algorithm, { name: `${sketch.id}${algExt}` });
|
|
2380
|
+
archive.append(genartJson, { name: `${sketch.id}.genart` });
|
|
2381
|
+
await archive.finalize();
|
|
2382
|
+
await finished;
|
|
2383
|
+
const s = await stat4(input.outputPath);
|
|
2384
|
+
return {
|
|
2385
|
+
success: true,
|
|
2386
|
+
sketchId: sketch.id,
|
|
2387
|
+
format: "zip",
|
|
2388
|
+
outputPath: input.outputPath,
|
|
2389
|
+
fileSize: s.size,
|
|
2390
|
+
renderer: sketch.renderer.type,
|
|
2391
|
+
contents: [
|
|
2392
|
+
`${sketch.id}.html`,
|
|
2393
|
+
`${sketch.id}.png`,
|
|
2394
|
+
`${sketch.id}${algExt}`,
|
|
2395
|
+
`${sketch.id}.genart`
|
|
2396
|
+
]
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
// src/resources/index.ts
|
|
2401
|
+
import {
|
|
2402
|
+
CANVAS_PRESETS,
|
|
2403
|
+
createDefaultRegistry as createDefaultRegistry4,
|
|
2404
|
+
createDefaultSkillRegistry as createDefaultSkillRegistry2
|
|
2405
|
+
} from "@genart-dev/core";
|
|
2406
|
+
function registerResources(server, state) {
|
|
2407
|
+
registerSkillsResource(server);
|
|
2408
|
+
registerCanvasPresetsResource(server);
|
|
2409
|
+
registerGalleryResource(server, state);
|
|
2410
|
+
registerRenderersResource(server);
|
|
2411
|
+
}
|
|
2412
|
+
function registerSkillsResource(server) {
|
|
2413
|
+
const skillRegistry = createDefaultSkillRegistry2();
|
|
2414
|
+
server.resource(
|
|
2415
|
+
"skills",
|
|
2416
|
+
"genart://skills",
|
|
2417
|
+
{
|
|
2418
|
+
description: "List available design knowledge skills with id, name, category, complexity, and description.",
|
|
2419
|
+
mimeType: "application/json"
|
|
2420
|
+
},
|
|
2421
|
+
async () => {
|
|
2422
|
+
const skills = skillRegistry.list().map((s) => ({
|
|
2423
|
+
id: s.id,
|
|
2424
|
+
name: s.name,
|
|
2425
|
+
category: s.category,
|
|
2426
|
+
complexity: s.complexity,
|
|
2427
|
+
description: s.description
|
|
2428
|
+
}));
|
|
2429
|
+
return {
|
|
2430
|
+
contents: [
|
|
2431
|
+
{
|
|
2432
|
+
uri: "genart://skills",
|
|
2433
|
+
mimeType: "application/json",
|
|
2434
|
+
text: JSON.stringify(
|
|
2435
|
+
{
|
|
2436
|
+
skills,
|
|
2437
|
+
total: skills.length,
|
|
2438
|
+
categories: skillRegistry.categories()
|
|
2439
|
+
},
|
|
2440
|
+
null,
|
|
2441
|
+
2
|
|
2442
|
+
)
|
|
2443
|
+
}
|
|
2444
|
+
]
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
function registerCanvasPresetsResource(server) {
|
|
2450
|
+
server.resource(
|
|
2451
|
+
"canvas-presets",
|
|
2452
|
+
"genart://presets/canvas",
|
|
2453
|
+
{
|
|
2454
|
+
description: "List all built-in canvas dimension presets with id, label, category, width, and height.",
|
|
2455
|
+
mimeType: "application/json"
|
|
2456
|
+
},
|
|
2457
|
+
async () => ({
|
|
2458
|
+
contents: [
|
|
2459
|
+
{
|
|
2460
|
+
uri: "genart://presets/canvas",
|
|
2461
|
+
mimeType: "application/json",
|
|
2462
|
+
text: JSON.stringify(
|
|
2463
|
+
{
|
|
2464
|
+
presets: CANVAS_PRESETS.map((p) => ({
|
|
2465
|
+
id: p.id,
|
|
2466
|
+
label: p.label,
|
|
2467
|
+
category: p.category,
|
|
2468
|
+
width: p.width,
|
|
2469
|
+
height: p.height
|
|
2470
|
+
}))
|
|
2471
|
+
},
|
|
2472
|
+
null,
|
|
2473
|
+
2
|
|
2474
|
+
)
|
|
2475
|
+
}
|
|
2476
|
+
]
|
|
2477
|
+
})
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
function registerGalleryResource(server, state) {
|
|
2481
|
+
server.resource(
|
|
2482
|
+
"gallery",
|
|
2483
|
+
"genart://gallery",
|
|
2484
|
+
{
|
|
2485
|
+
description: "List all loaded sketches in the active workspace with metadata summaries.",
|
|
2486
|
+
mimeType: "application/json"
|
|
2487
|
+
},
|
|
2488
|
+
async () => {
|
|
2489
|
+
const sketches = [...state.sketches.values()].map(({ definition, path }) => ({
|
|
2490
|
+
id: definition.id,
|
|
2491
|
+
title: definition.title,
|
|
2492
|
+
renderer: definition.renderer,
|
|
2493
|
+
canvas: definition.canvas,
|
|
2494
|
+
parameterCount: definition.parameters?.length ?? 0,
|
|
2495
|
+
colorCount: definition.colors?.length ?? 0,
|
|
2496
|
+
hasPhilosophy: !!definition.philosophy,
|
|
2497
|
+
seed: definition.seed,
|
|
2498
|
+
path
|
|
2499
|
+
}));
|
|
2500
|
+
return {
|
|
2501
|
+
contents: [
|
|
2502
|
+
{
|
|
2503
|
+
uri: "genart://gallery",
|
|
2504
|
+
mimeType: "application/json",
|
|
2505
|
+
text: JSON.stringify(
|
|
2506
|
+
{
|
|
2507
|
+
workspacePath: state.workspacePath,
|
|
2508
|
+
sketchCount: sketches.length,
|
|
2509
|
+
sketches
|
|
2510
|
+
},
|
|
2511
|
+
null,
|
|
2512
|
+
2
|
|
2513
|
+
)
|
|
2514
|
+
}
|
|
2515
|
+
]
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
function registerRenderersResource(server) {
|
|
2521
|
+
const registry4 = createDefaultRegistry4();
|
|
2522
|
+
server.resource(
|
|
2523
|
+
"renderers",
|
|
2524
|
+
"genart://renderers",
|
|
2525
|
+
{
|
|
2526
|
+
description: "List all available renderer types with display name, algorithm language, and runtime dependencies.",
|
|
2527
|
+
mimeType: "application/json"
|
|
2528
|
+
},
|
|
2529
|
+
async () => {
|
|
2530
|
+
const types = registry4.list();
|
|
2531
|
+
const renderers = types.map((type) => {
|
|
2532
|
+
const adapter = registry4.resolve(type);
|
|
2533
|
+
return {
|
|
2534
|
+
type: adapter.type,
|
|
2535
|
+
displayName: adapter.displayName,
|
|
2536
|
+
algorithmLanguage: adapter.algorithmLanguage,
|
|
2537
|
+
dependencies: adapter.getRuntimeDependencies().map((dep) => ({
|
|
2538
|
+
name: dep.name,
|
|
2539
|
+
version: dep.version,
|
|
2540
|
+
cdnUrl: dep.cdnUrl
|
|
2541
|
+
}))
|
|
2542
|
+
};
|
|
2543
|
+
});
|
|
2544
|
+
const defaultAdapter = registry4.getDefault();
|
|
2545
|
+
return {
|
|
2546
|
+
contents: [
|
|
2547
|
+
{
|
|
2548
|
+
uri: "genart://renderers",
|
|
2549
|
+
mimeType: "application/json",
|
|
2550
|
+
text: JSON.stringify(
|
|
2551
|
+
{
|
|
2552
|
+
defaultRenderer: defaultAdapter.type,
|
|
2553
|
+
renderers
|
|
2554
|
+
},
|
|
2555
|
+
null,
|
|
2556
|
+
2
|
|
2557
|
+
)
|
|
2558
|
+
}
|
|
2559
|
+
]
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
// src/prompts/index.ts
|
|
2566
|
+
import { z } from "zod";
|
|
2567
|
+
function registerPrompts(server, state) {
|
|
2568
|
+
registerCreateGenerativeArt(server);
|
|
2569
|
+
registerExploreVariations(server, state);
|
|
2570
|
+
registerApplyDesignTheory(server, state);
|
|
2571
|
+
}
|
|
2572
|
+
function registerCreateGenerativeArt(server) {
|
|
2573
|
+
server.prompt(
|
|
2574
|
+
"create-generative-art",
|
|
2575
|
+
"Create a new piece of generative art with structured guidance",
|
|
2576
|
+
{
|
|
2577
|
+
concept: z.string().describe("The artistic concept or visual idea to explore"),
|
|
2578
|
+
renderer: z.enum(["p5", "three", "glsl", "canvas2d", "svg"]).optional().describe("Renderer to use (default: p5)"),
|
|
2579
|
+
complexity: z.enum(["simple", "moderate", "complex"]).optional().describe("Desired complexity level (default: moderate)"),
|
|
2580
|
+
canvas: z.string().optional().describe("Canvas preset name or WxH dimensions (default: square-1200)")
|
|
2581
|
+
},
|
|
2582
|
+
async (args) => {
|
|
2583
|
+
const renderer = args.renderer ?? "p5";
|
|
2584
|
+
const complexity = args.complexity ?? "moderate";
|
|
2585
|
+
const canvas = args.canvas ?? "square-1200";
|
|
2586
|
+
return {
|
|
2587
|
+
messages: [
|
|
2588
|
+
{
|
|
2589
|
+
role: "user",
|
|
2590
|
+
content: {
|
|
2591
|
+
type: "text",
|
|
2592
|
+
text: [
|
|
2593
|
+
`Create a generative art sketch with the following specifications:`,
|
|
2594
|
+
``,
|
|
2595
|
+
`## Concept`,
|
|
2596
|
+
`${args.concept}`,
|
|
2597
|
+
``,
|
|
2598
|
+
`## Technical Specifications`,
|
|
2599
|
+
`- **Renderer:** ${renderer}`,
|
|
2600
|
+
`- **Complexity:** ${complexity}`,
|
|
2601
|
+
`- **Canvas:** ${canvas}`,
|
|
2602
|
+
``,
|
|
2603
|
+
`## Steps`,
|
|
2604
|
+
`1. Use \`create_sketch\` to create a new .genart file with a relative path (e.g. \`my-sketch.genart\`)`,
|
|
2605
|
+
`2. Design 3\u20136 parameters that control visual variation (range sliders)`,
|
|
2606
|
+
`3. Define 2\u20134 color parameters for palette control`,
|
|
2607
|
+
`4. Write the algorithm that implements the concept`,
|
|
2608
|
+
`5. Use \`update_algorithm\` to set the algorithm with validation`,
|
|
2609
|
+
`6. Use \`capture_screenshot\` to verify the visual output`,
|
|
2610
|
+
`7. Iterate on parameters and algorithm until the result matches the concept`,
|
|
2611
|
+
``,
|
|
2612
|
+
`## Guidelines`,
|
|
2613
|
+
`- Parameters should have meaningful ranges that produce visually distinct results`,
|
|
2614
|
+
`- The algorithm should be deterministic given the same seed and parameters`,
|
|
2615
|
+
`- Include a philosophy field describing the artistic intent`,
|
|
2616
|
+
`- Use design principles: balance, contrast, rhythm, harmony`,
|
|
2617
|
+
`- Always pass your \`agent\` name and \`model\` identifier when calling tools that create or modify sketches`,
|
|
2618
|
+
complexity === "simple" ? `- Keep the algorithm under 50 lines with 2\u20133 parameters` : complexity === "complex" ? `- The algorithm can be extensive; use 5+ parameters and consider animation` : `- Aim for 30\u201380 lines with 3\u20135 parameters`
|
|
2619
|
+
].join("\n")
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
]
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2627
|
+
function registerExploreVariations(server, state) {
|
|
2628
|
+
server.prompt(
|
|
2629
|
+
"explore-variations",
|
|
2630
|
+
"Explore parameter and seed variations of an existing sketch",
|
|
2631
|
+
{
|
|
2632
|
+
sketchId: z.string().describe("ID of the sketch to explore variations of"),
|
|
2633
|
+
strategy: z.enum(["seeds", "params", "both", "extremes"]).optional().describe("Variation strategy (default: both)"),
|
|
2634
|
+
count: z.string().optional().describe("Number of variations to explore (default: 6)")
|
|
2635
|
+
},
|
|
2636
|
+
async (args) => {
|
|
2637
|
+
const strategy = args.strategy ?? "both";
|
|
2638
|
+
const count = args.count ?? "6";
|
|
2639
|
+
const sketch = state.getSketch(args.sketchId);
|
|
2640
|
+
let sketchContext = "";
|
|
2641
|
+
if (sketch) {
|
|
2642
|
+
const def = sketch.definition;
|
|
2643
|
+
const params = def.parameters?.map(
|
|
2644
|
+
(p) => ` - ${p.key} (${p.label}): ${p.min}\u2013${p.max}, step ${p.step}, default ${p.default}`
|
|
2645
|
+
).join("\n");
|
|
2646
|
+
const colors = def.colors?.map((c) => ` - ${c.key} (${c.label}): ${c.default}`).join("\n");
|
|
2647
|
+
sketchContext = [
|
|
2648
|
+
`## Current Sketch: "${def.title}"`,
|
|
2649
|
+
`- **ID:** ${def.id}`,
|
|
2650
|
+
`- **Renderer:** ${def.renderer}`,
|
|
2651
|
+
`- **Canvas:** ${def.canvas.width}\xD7${def.canvas.height}`,
|
|
2652
|
+
`- **Seed:** ${def.seed}`,
|
|
2653
|
+
params ? `- **Parameters:**
|
|
2654
|
+
${params}` : `- **Parameters:** none`,
|
|
2655
|
+
colors ? `- **Colors:**
|
|
2656
|
+
${colors}` : `- **Colors:** none`
|
|
2657
|
+
].join("\n");
|
|
2658
|
+
} else {
|
|
2659
|
+
sketchContext = `## Sketch: ${args.sketchId}
|
|
2660
|
+
*(Not currently loaded \u2014 use open_sketch or get_selection to load it first)*`;
|
|
2661
|
+
}
|
|
2662
|
+
const strategyInstructions = {
|
|
2663
|
+
seeds: [
|
|
2664
|
+
`## Strategy: Seed Exploration`,
|
|
2665
|
+
`Generate ${count} variations by changing only the seed value.`,
|
|
2666
|
+
`1. Use \`fork_sketch\` to create each variation with \`newSeed: true\``,
|
|
2667
|
+
`2. Use \`capture_batch\` to capture all variations`,
|
|
2668
|
+
`3. Compare the results and identify which seeds produce the most interesting outputs`
|
|
2669
|
+
].join("\n"),
|
|
2670
|
+
params: [
|
|
2671
|
+
`## Strategy: Parameter Exploration`,
|
|
2672
|
+
`Generate ${count} variations by systematically varying parameters.`,
|
|
2673
|
+
`1. Use \`fork_sketch\` for each variation`,
|
|
2674
|
+
`2. For each fork, use \`set_parameters\` to explore different regions of the parameter space`,
|
|
2675
|
+
`3. Try: minimum values, maximum values, center values, and random combinations`,
|
|
2676
|
+
`4. Use \`capture_batch\` to capture all variations`
|
|
2677
|
+
].join("\n"),
|
|
2678
|
+
both: [
|
|
2679
|
+
`## Strategy: Combined Exploration`,
|
|
2680
|
+
`Generate ${count} variations by varying both seeds and parameters.`,
|
|
2681
|
+
`1. Create ${count} forks with \`fork_sketch\` (newSeed: true)`,
|
|
2682
|
+
`2. For half the forks, also adjust parameters to explore different visual territories`,
|
|
2683
|
+
`3. Use \`capture_batch\` to capture all variations`,
|
|
2684
|
+
`4. Identify the most visually interesting combinations`
|
|
2685
|
+
].join("\n"),
|
|
2686
|
+
extremes: [
|
|
2687
|
+
`## Strategy: Extreme Parameter Exploration`,
|
|
2688
|
+
`Generate ${count} variations by pushing parameters to their limits.`,
|
|
2689
|
+
`1. Create forks with all parameters at minimum, all at maximum, and diagonal extremes`,
|
|
2690
|
+
`2. Also create forks with each parameter individually at its min and max`,
|
|
2691
|
+
`3. Use \`capture_batch\` to capture all variations`,
|
|
2692
|
+
`4. This reveals the full range of the parameter space`
|
|
2693
|
+
].join("\n")
|
|
2694
|
+
};
|
|
2695
|
+
return {
|
|
2696
|
+
messages: [
|
|
2697
|
+
{
|
|
2698
|
+
role: "user",
|
|
2699
|
+
content: {
|
|
2700
|
+
type: "text",
|
|
2701
|
+
text: [
|
|
2702
|
+
`Explore variations of an existing generative art sketch.`,
|
|
2703
|
+
``,
|
|
2704
|
+
sketchContext,
|
|
2705
|
+
``,
|
|
2706
|
+
strategyInstructions[strategy],
|
|
2707
|
+
``,
|
|
2708
|
+
`## After Exploration`,
|
|
2709
|
+
`- Use \`auto_arrange\` to lay out all variations in a grid`,
|
|
2710
|
+
`- Use \`snapshot_layout\` to capture the arrangement`,
|
|
2711
|
+
`- Summarize which variations are most interesting and why`,
|
|
2712
|
+
``,
|
|
2713
|
+
`**Attribution:** Always pass your \`agent\` name and \`model\` identifier when calling \`fork_sketch\` and other tools that create or modify sketches.`
|
|
2714
|
+
].join("\n")
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
]
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
function registerApplyDesignTheory(server, state) {
|
|
2723
|
+
server.prompt(
|
|
2724
|
+
"apply-design-theory",
|
|
2725
|
+
"Apply design theory concepts to improve or evolve a generative art sketch",
|
|
2726
|
+
{
|
|
2727
|
+
sketchId: z.string().describe("ID of the sketch to apply design theory to"),
|
|
2728
|
+
theory: z.enum([
|
|
2729
|
+
"gestalt",
|
|
2730
|
+
"color-theory",
|
|
2731
|
+
"composition",
|
|
2732
|
+
"rhythm-repetition",
|
|
2733
|
+
"negative-space",
|
|
2734
|
+
"contrast"
|
|
2735
|
+
]).describe("Design theory to apply")
|
|
2736
|
+
},
|
|
2737
|
+
async (args) => {
|
|
2738
|
+
const sketch = state.getSketch(args.sketchId);
|
|
2739
|
+
let sketchContext = "";
|
|
2740
|
+
if (sketch) {
|
|
2741
|
+
const def = sketch.definition;
|
|
2742
|
+
sketchContext = [
|
|
2743
|
+
`## Current Sketch: "${def.title}"`,
|
|
2744
|
+
`- **Renderer:** ${def.renderer}`,
|
|
2745
|
+
`- **Canvas:** ${def.canvas.width}\xD7${def.canvas.height}`,
|
|
2746
|
+
`- **Parameters:** ${def.parameters?.length ?? 0} defined`,
|
|
2747
|
+
`- **Colors:** ${def.colors?.length ?? 0} defined`,
|
|
2748
|
+
def.philosophy ? `- **Philosophy:** ${def.philosophy}` : `- **Philosophy:** not set`
|
|
2749
|
+
].join("\n");
|
|
2750
|
+
} else {
|
|
2751
|
+
sketchContext = `## Sketch: ${args.sketchId}
|
|
2752
|
+
*(Not currently loaded \u2014 use open_sketch first)*`;
|
|
2753
|
+
}
|
|
2754
|
+
const theoryGuides = {
|
|
2755
|
+
gestalt: [
|
|
2756
|
+
`## Theory: Gestalt Principles`,
|
|
2757
|
+
`Apply principles of visual perception to the sketch:`,
|
|
2758
|
+
`- **Proximity:** Group related elements closer together`,
|
|
2759
|
+
`- **Similarity:** Make related elements share visual properties (size, color, shape)`,
|
|
2760
|
+
`- **Continuity:** Align elements to create implied lines and flow`,
|
|
2761
|
+
`- **Closure:** Allow the viewer's mind to complete partial shapes`,
|
|
2762
|
+
`- **Figure-Ground:** Establish clear foreground/background relationships`,
|
|
2763
|
+
``,
|
|
2764
|
+
`### Implementation`,
|
|
2765
|
+
`1. Analyze the current algorithm for element placement patterns`,
|
|
2766
|
+
`2. Add or modify parameters that control grouping, spacing, and alignment`,
|
|
2767
|
+
`3. Ensure the seed produces consistent perceptual grouping`
|
|
2768
|
+
].join("\n"),
|
|
2769
|
+
"color-theory": [
|
|
2770
|
+
`## Theory: Color Theory`,
|
|
2771
|
+
`Apply color harmony and contrast principles:`,
|
|
2772
|
+
`- **Complementary:** Use colors opposite on the color wheel for high contrast`,
|
|
2773
|
+
`- **Analogous:** Use adjacent colors for harmony`,
|
|
2774
|
+
`- **Triadic:** Use three evenly-spaced colors for vibrancy`,
|
|
2775
|
+
`- **Value contrast:** Ensure sufficient light/dark variation`,
|
|
2776
|
+
`- **Saturation:** Control intensity for emphasis and depth`,
|
|
2777
|
+
``,
|
|
2778
|
+
`### Implementation`,
|
|
2779
|
+
`1. Review the current color definitions and themes`,
|
|
2780
|
+
`2. Add color parameters that follow a chosen harmony scheme`,
|
|
2781
|
+
`3. Create 2\u20133 theme presets demonstrating different harmonies`,
|
|
2782
|
+
`4. Update the algorithm to use colors intentionally for depth and emphasis`
|
|
2783
|
+
].join("\n"),
|
|
2784
|
+
composition: [
|
|
2785
|
+
`## Theory: Composition`,
|
|
2786
|
+
`Apply compositional rules for visual impact:`,
|
|
2787
|
+
`- **Rule of thirds:** Place key elements at intersection points`,
|
|
2788
|
+
`- **Golden ratio:** Use \u03C6 (1.618) for proportional divisions`,
|
|
2789
|
+
`- **Visual weight:** Balance heavy elements with negative space`,
|
|
2790
|
+
`- **Leading lines:** Direct the eye through the composition`,
|
|
2791
|
+
`- **Focal point:** Establish a clear center of interest`,
|
|
2792
|
+
``,
|
|
2793
|
+
`### Implementation`,
|
|
2794
|
+
`1. Analyze element distribution in the current algorithm`,
|
|
2795
|
+
`2. Add parameters for compositional control (focal point position, density distribution)`,
|
|
2796
|
+
`3. Use mathematical ratios (thirds, golden) for element placement`,
|
|
2797
|
+
`4. Test with \`capture_screenshot\` and verify visual balance`
|
|
2798
|
+
].join("\n"),
|
|
2799
|
+
"rhythm-repetition": [
|
|
2800
|
+
`## Theory: Rhythm & Repetition`,
|
|
2801
|
+
`Apply rhythmic patterns and controlled repetition:`,
|
|
2802
|
+
`- **Regular rhythm:** Equal spacing between repeated elements`,
|
|
2803
|
+
`- **Alternating rhythm:** Two or more elements in sequence`,
|
|
2804
|
+
`- **Progressive rhythm:** Gradual size, color, or spacing changes`,
|
|
2805
|
+
`- **Random rhythm:** Organic, noise-driven variation within structure`,
|
|
2806
|
+
`- **Fractal repetition:** Self-similar patterns at different scales`,
|
|
2807
|
+
``,
|
|
2808
|
+
`### Implementation`,
|
|
2809
|
+
`1. Identify repeating elements in the current algorithm`,
|
|
2810
|
+
`2. Add parameters for rhythm type, frequency, and amplitude`,
|
|
2811
|
+
`3. Layer multiple rhythmic patterns for visual complexity`,
|
|
2812
|
+
`4. Use the seed to introduce controlled randomness within the rhythm`
|
|
2813
|
+
].join("\n"),
|
|
2814
|
+
"negative-space": [
|
|
2815
|
+
`## Theory: Negative Space`,
|
|
2816
|
+
`Use emptiness as a compositional element:`,
|
|
2817
|
+
`- **Active negative space:** Intentional empty areas that form shapes`,
|
|
2818
|
+
`- **Breathing room:** Prevent visual overcrowding`,
|
|
2819
|
+
`- **Figure-ground reversal:** Make negative space as interesting as positive`,
|
|
2820
|
+
`- **Density gradients:** Transition from dense to sparse areas`,
|
|
2821
|
+
``,
|
|
2822
|
+
`### Implementation`,
|
|
2823
|
+
`1. Add a density/coverage parameter to control fill percentage`,
|
|
2824
|
+
`2. Create regions of intentional emptiness in the algorithm`,
|
|
2825
|
+
`3. Use the canvas edges and margins as compositional anchors`,
|
|
2826
|
+
`4. Test at different density levels with \`capture_screenshot\``
|
|
2827
|
+
].join("\n"),
|
|
2828
|
+
contrast: [
|
|
2829
|
+
`## Theory: Contrast`,
|
|
2830
|
+
`Apply contrast principles for visual interest:`,
|
|
2831
|
+
`- **Size contrast:** Juxtapose large and small elements`,
|
|
2832
|
+
`- **Color contrast:** Use complementary or value differences`,
|
|
2833
|
+
`- **Shape contrast:** Mix geometric and organic forms`,
|
|
2834
|
+
`- **Texture contrast:** Smooth vs. rough, dense vs. sparse`,
|
|
2835
|
+
`- **Movement contrast:** Static vs. dynamic, fast vs. slow`,
|
|
2836
|
+
``,
|
|
2837
|
+
`### Implementation`,
|
|
2838
|
+
`1. Identify the dominant visual quality in the current algorithm`,
|
|
2839
|
+
`2. Introduce its opposite as a secondary element`,
|
|
2840
|
+
`3. Add parameters that control the degree of contrast`,
|
|
2841
|
+
`4. Use \`fork_sketch\` to compare low-contrast and high-contrast versions`
|
|
2842
|
+
].join("\n")
|
|
2843
|
+
};
|
|
2844
|
+
return {
|
|
2845
|
+
messages: [
|
|
2846
|
+
{
|
|
2847
|
+
role: "user",
|
|
2848
|
+
content: {
|
|
2849
|
+
type: "text",
|
|
2850
|
+
text: [
|
|
2851
|
+
`Apply design theory to improve a generative art sketch.`,
|
|
2852
|
+
``,
|
|
2853
|
+
sketchContext,
|
|
2854
|
+
``,
|
|
2855
|
+
theoryGuides[args.theory],
|
|
2856
|
+
``,
|
|
2857
|
+
`## Workflow`,
|
|
2858
|
+
`1. Use \`get_selection\` or \`open_sketch\` to examine the current sketch`,
|
|
2859
|
+
`2. Analyze how the theory applies to the existing algorithm`,
|
|
2860
|
+
`3. Use \`fork_sketch\` to create a theory-applied variant`,
|
|
2861
|
+
`4. Modify the fork's algorithm and parameters using the theory principles above`,
|
|
2862
|
+
`5. Use \`capture_screenshot\` to compare before and after`,
|
|
2863
|
+
`6. Update the philosophy field to document the design rationale`,
|
|
2864
|
+
``,
|
|
2865
|
+
`**Attribution:** Always pass your \`agent\` name and \`model\` identifier when calling tools that create or modify sketches.`
|
|
2866
|
+
].join("\n")
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
]
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
// src/server.ts
|
|
2876
|
+
function jsonResult(data) {
|
|
2877
|
+
return {
|
|
2878
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
function toolError(message) {
|
|
2882
|
+
return {
|
|
2883
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
2884
|
+
isError: true
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
function createServer(state) {
|
|
2888
|
+
const server = new McpServer(
|
|
2889
|
+
{
|
|
2890
|
+
name: "@genart/mcp-server",
|
|
2891
|
+
version: "0.0.1"
|
|
2892
|
+
},
|
|
2893
|
+
{
|
|
2894
|
+
capabilities: {
|
|
2895
|
+
tools: {},
|
|
2896
|
+
resources: {},
|
|
2897
|
+
prompts: {}
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
);
|
|
2901
|
+
registerWorkspaceTools(server, state);
|
|
2902
|
+
registerSketchTools(server, state);
|
|
2903
|
+
registerSelectionTools(server, state);
|
|
2904
|
+
registerParameterTools(server, state);
|
|
2905
|
+
registerArrangementTools(server, state);
|
|
2906
|
+
registerGalleryTools(server, state);
|
|
2907
|
+
registerMergeTools(server, state);
|
|
2908
|
+
registerSnapshotTools(server, state);
|
|
2909
|
+
registerKnowledgeTools(server, state);
|
|
2910
|
+
registerCaptureTools(server, state);
|
|
2911
|
+
registerExportTools(server, state);
|
|
2912
|
+
registerResources(server, state);
|
|
2913
|
+
registerPrompts(server, state);
|
|
2914
|
+
return server;
|
|
2915
|
+
}
|
|
2916
|
+
function registerWorkspaceTools(server, state) {
|
|
2917
|
+
server.tool(
|
|
2918
|
+
"create_workspace",
|
|
2919
|
+
"Create a new .genart-workspace file with optional initial sketches",
|
|
2920
|
+
{
|
|
2921
|
+
title: z2.string().describe("Workspace title"),
|
|
2922
|
+
path: z2.string().describe("File path (must end in .genart-workspace)"),
|
|
2923
|
+
sketches: z2.array(z2.string()).optional().describe("Initial sketch file paths to include"),
|
|
2924
|
+
arrangement: z2.enum(["grid", "row", "column"]).optional().describe("Auto-arrange initial sketches (default: grid)"),
|
|
2925
|
+
spacing: z2.number().optional().describe("Spacing between arranged sketches in pixels (default: 200)")
|
|
2926
|
+
},
|
|
2927
|
+
async (args) => {
|
|
2928
|
+
try {
|
|
2929
|
+
const result = await createWorkspace(state, args);
|
|
2930
|
+
return jsonResult(result);
|
|
2931
|
+
} catch (e) {
|
|
2932
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
);
|
|
2936
|
+
server.tool(
|
|
2937
|
+
"open_workspace",
|
|
2938
|
+
"Open an existing .genart-workspace file and load all referenced sketches",
|
|
2939
|
+
{
|
|
2940
|
+
path: z2.string().describe("File path to .genart-workspace file")
|
|
2941
|
+
},
|
|
2942
|
+
async (args) => {
|
|
2943
|
+
try {
|
|
2944
|
+
const result = await openWorkspace(state, args);
|
|
2945
|
+
return jsonResult(result);
|
|
2946
|
+
} catch (e) {
|
|
2947
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
);
|
|
2951
|
+
server.tool(
|
|
2952
|
+
"add_sketch_to_workspace",
|
|
2953
|
+
"Add an existing .genart sketch file to the active workspace",
|
|
2954
|
+
{
|
|
2955
|
+
sketchPath: z2.string().describe("Path to the .genart file to add"),
|
|
2956
|
+
position: z2.object({
|
|
2957
|
+
x: z2.number().describe("X position on canvas"),
|
|
2958
|
+
y: z2.number().describe("Y position on canvas")
|
|
2959
|
+
}).optional().describe("Canvas position (default: auto-placed to the right)"),
|
|
2960
|
+
label: z2.string().optional().describe("Display label override")
|
|
2961
|
+
},
|
|
2962
|
+
async (args) => {
|
|
2963
|
+
try {
|
|
2964
|
+
const result = await addSketchToWorkspace(state, args);
|
|
2965
|
+
return jsonResult(result);
|
|
2966
|
+
} catch (e) {
|
|
2967
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
);
|
|
2971
|
+
server.tool(
|
|
2972
|
+
"remove_sketch_from_workspace",
|
|
2973
|
+
"Remove a sketch from the active workspace (optionally delete the file)",
|
|
2974
|
+
{
|
|
2975
|
+
sketchId: z2.string().describe("ID of the sketch to remove"),
|
|
2976
|
+
deleteFile: z2.boolean().optional().describe("Also delete the .genart file from disk (default: false)")
|
|
2977
|
+
},
|
|
2978
|
+
async (args) => {
|
|
2979
|
+
try {
|
|
2980
|
+
const result = await removeSketchFromWorkspace(state, args);
|
|
2981
|
+
return jsonResult(result);
|
|
2982
|
+
} catch (e) {
|
|
2983
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
);
|
|
2987
|
+
server.tool(
|
|
2988
|
+
"list_workspace_sketches",
|
|
2989
|
+
"List all sketches in the active workspace with metadata",
|
|
2990
|
+
{
|
|
2991
|
+
includeState: z2.boolean().optional().describe("Include current seed and param values (default: false)")
|
|
2992
|
+
},
|
|
2993
|
+
async (args) => {
|
|
2994
|
+
try {
|
|
2995
|
+
const result = await listWorkspaceSketches(state, args);
|
|
2996
|
+
return jsonResult(result);
|
|
2997
|
+
} catch (e) {
|
|
2998
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
);
|
|
3002
|
+
}
|
|
3003
|
+
function registerSketchTools(server, state) {
|
|
3004
|
+
server.tool(
|
|
3005
|
+
"create_sketch",
|
|
3006
|
+
"Create a new .genart sketch file from metadata, parameters, and algorithm",
|
|
3007
|
+
{
|
|
3008
|
+
id: z2.string().describe("URL-safe kebab-case identifier"),
|
|
3009
|
+
title: z2.string().describe("Human-readable title"),
|
|
3010
|
+
path: z2.string().describe("Relative file path (must end in .genart, e.g. 'my-sketch.genart')"),
|
|
3011
|
+
renderer: z2.enum(["p5", "three", "glsl", "canvas2d", "svg"]).optional().describe("Renderer type (default: p5)"),
|
|
3012
|
+
canvas: z2.object({
|
|
3013
|
+
preset: z2.string().optional().describe("Canvas preset name"),
|
|
3014
|
+
width: z2.number().optional().describe("Width in pixels"),
|
|
3015
|
+
height: z2.number().optional().describe("Height in pixels")
|
|
3016
|
+
}).optional().describe("Canvas dimensions (default: square-1200)"),
|
|
3017
|
+
philosophy: z2.string().optional().describe("Markdown design philosophy"),
|
|
3018
|
+
parameters: z2.array(
|
|
3019
|
+
z2.object({
|
|
3020
|
+
key: z2.string(),
|
|
3021
|
+
label: z2.string(),
|
|
3022
|
+
min: z2.number(),
|
|
3023
|
+
max: z2.number(),
|
|
3024
|
+
step: z2.number(),
|
|
3025
|
+
default: z2.number()
|
|
3026
|
+
})
|
|
3027
|
+
).optional().describe("Parameter definitions"),
|
|
3028
|
+
colors: z2.array(
|
|
3029
|
+
z2.object({
|
|
3030
|
+
key: z2.string(),
|
|
3031
|
+
label: z2.string(),
|
|
3032
|
+
default: z2.string()
|
|
3033
|
+
})
|
|
3034
|
+
).optional().describe("Color definitions"),
|
|
3035
|
+
themes: z2.array(
|
|
3036
|
+
z2.object({
|
|
3037
|
+
name: z2.string(),
|
|
3038
|
+
colors: z2.array(z2.string())
|
|
3039
|
+
})
|
|
3040
|
+
).optional().describe("Theme presets"),
|
|
3041
|
+
algorithm: z2.string().optional().describe("Algorithm source code (default: renderer template). For p5: must be `function sketch(p, state) { ... }` in instance mode. State provides: state.WIDTH, state.HEIGHT, state.SEED (number), state.PARAMS (keyed by param key), state.COLORS (keyed by color key, hex strings). Use p5 instance methods (p.createCanvas, p.background, etc)."),
|
|
3042
|
+
seed: z2.number().optional().describe("Initial random seed (default: random)"),
|
|
3043
|
+
skills: z2.array(z2.string()).optional().describe("Design skill references"),
|
|
3044
|
+
addToWorkspace: z2.string().optional().describe("Path to workspace to add sketch to after creation"),
|
|
3045
|
+
agent: z2.string().optional().describe("Your CLI agent name (e.g. 'claude-code', 'codex-cli', 'gemini-cli', 'opencode', 'kiro')"),
|
|
3046
|
+
model: z2.string().optional().describe("Your AI model identifier (e.g. 'claude-opus-4-6', 'gpt-4o', 'gemini-2.5-pro')")
|
|
3047
|
+
},
|
|
3048
|
+
async (args) => {
|
|
3049
|
+
try {
|
|
3050
|
+
const result = await createSketch(state, args);
|
|
3051
|
+
return jsonResult(result);
|
|
3052
|
+
} catch (e) {
|
|
3053
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
);
|
|
3057
|
+
server.tool(
|
|
3058
|
+
"open_sketch",
|
|
3059
|
+
"Open a sketch by ID to view and edit it (sets selection)",
|
|
3060
|
+
{
|
|
3061
|
+
sketchId: z2.string().describe("ID of the sketch to open")
|
|
3062
|
+
},
|
|
3063
|
+
async (args) => {
|
|
3064
|
+
try {
|
|
3065
|
+
const result = await openSketch(state, args);
|
|
3066
|
+
return jsonResult(result);
|
|
3067
|
+
} catch (e) {
|
|
3068
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
);
|
|
3072
|
+
server.tool(
|
|
3073
|
+
"update_sketch",
|
|
3074
|
+
"Update metadata, parameters, colors, or canvas of an existing sketch",
|
|
3075
|
+
{
|
|
3076
|
+
sketchId: z2.string().describe("ID of the sketch to update"),
|
|
3077
|
+
title: z2.string().optional().describe("New title"),
|
|
3078
|
+
philosophy: z2.string().optional().describe("New philosophy text (markdown)"),
|
|
3079
|
+
canvas: z2.object({
|
|
3080
|
+
preset: z2.string().optional().describe("Canvas preset name"),
|
|
3081
|
+
width: z2.number().optional().describe("Width in pixels"),
|
|
3082
|
+
height: z2.number().optional().describe("Height in pixels")
|
|
3083
|
+
}).optional().describe("New canvas dimensions"),
|
|
3084
|
+
parameters: z2.array(
|
|
3085
|
+
z2.object({
|
|
3086
|
+
key: z2.string(),
|
|
3087
|
+
label: z2.string(),
|
|
3088
|
+
min: z2.number(),
|
|
3089
|
+
max: z2.number(),
|
|
3090
|
+
step: z2.number(),
|
|
3091
|
+
default: z2.number()
|
|
3092
|
+
})
|
|
3093
|
+
).optional().describe("Replace parameter definitions"),
|
|
3094
|
+
colors: z2.array(
|
|
3095
|
+
z2.object({
|
|
3096
|
+
key: z2.string(),
|
|
3097
|
+
label: z2.string(),
|
|
3098
|
+
default: z2.string()
|
|
3099
|
+
})
|
|
3100
|
+
).optional().describe("Replace color definitions"),
|
|
3101
|
+
themes: z2.array(
|
|
3102
|
+
z2.object({
|
|
3103
|
+
name: z2.string(),
|
|
3104
|
+
colors: z2.array(z2.string())
|
|
3105
|
+
})
|
|
3106
|
+
).optional().describe("Replace theme presets"),
|
|
3107
|
+
seed: z2.number().optional().describe("New random seed"),
|
|
3108
|
+
skills: z2.array(z2.string()).optional().describe("Replace design skill references"),
|
|
3109
|
+
agent: z2.string().optional().describe("Your CLI agent name (e.g. 'claude-code', 'codex-cli', 'gemini-cli', 'opencode', 'kiro')"),
|
|
3110
|
+
model: z2.string().optional().describe("Your AI model identifier (e.g. 'claude-opus-4-6', 'gpt-4o', 'gemini-2.5-pro')")
|
|
3111
|
+
},
|
|
3112
|
+
async (args) => {
|
|
3113
|
+
try {
|
|
3114
|
+
const result = await updateSketch(state, args);
|
|
3115
|
+
return jsonResult(result);
|
|
3116
|
+
} catch (e) {
|
|
3117
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
);
|
|
3121
|
+
server.tool(
|
|
3122
|
+
"update_algorithm",
|
|
3123
|
+
"Replace the algorithm source code of a sketch",
|
|
3124
|
+
{
|
|
3125
|
+
sketchId: z2.string().describe("ID of the sketch to update"),
|
|
3126
|
+
algorithm: z2.string().describe("New algorithm source code. For p5: must be `function sketch(p, state) { ... }` in instance mode. State provides: state.WIDTH, state.HEIGHT, state.SEED, state.PARAMS (keyed by param key), state.COLORS (keyed by color key)."),
|
|
3127
|
+
validate: z2.boolean().optional().describe("Run renderer-specific validation before saving (default: true)"),
|
|
3128
|
+
agent: z2.string().optional().describe("Your CLI agent name (e.g. 'claude-code', 'codex-cli', 'gemini-cli', 'opencode', 'kiro')"),
|
|
3129
|
+
model: z2.string().optional().describe("Your AI model identifier (e.g. 'claude-opus-4-6', 'gpt-4o', 'gemini-2.5-pro')")
|
|
3130
|
+
},
|
|
3131
|
+
async (args) => {
|
|
3132
|
+
try {
|
|
3133
|
+
const result = await updateAlgorithm(state, args);
|
|
3134
|
+
return jsonResult(result);
|
|
3135
|
+
} catch (e) {
|
|
3136
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
);
|
|
3140
|
+
server.tool(
|
|
3141
|
+
"save_sketch",
|
|
3142
|
+
"Persist the current in-memory state of a sketch to disk",
|
|
3143
|
+
{
|
|
3144
|
+
sketchId: z2.string().describe("ID of the sketch to save")
|
|
3145
|
+
},
|
|
3146
|
+
async (args) => {
|
|
3147
|
+
try {
|
|
3148
|
+
const result = await saveSketch(state, args);
|
|
3149
|
+
return jsonResult(result);
|
|
3150
|
+
} catch (e) {
|
|
3151
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
);
|
|
3155
|
+
server.tool(
|
|
3156
|
+
"fork_sketch",
|
|
3157
|
+
"Create a variant of an existing sketch with a new ID and optional modifications",
|
|
3158
|
+
{
|
|
3159
|
+
sourceId: z2.string().describe("ID of the sketch to fork"),
|
|
3160
|
+
newId: z2.string().describe("URL-safe kebab-case ID for the forked sketch"),
|
|
3161
|
+
title: z2.string().optional().describe("Title for the fork (default: '[source title] (fork)')"),
|
|
3162
|
+
position: z2.object({
|
|
3163
|
+
x: z2.number().describe("X position on canvas"),
|
|
3164
|
+
y: z2.number().describe("Y position on canvas")
|
|
3165
|
+
}).optional().describe("Canvas position (default: auto-placed to the right of source)"),
|
|
3166
|
+
modifications: z2.object({
|
|
3167
|
+
renderer: z2.enum(["p5", "three", "glsl", "canvas2d", "svg"]).optional(),
|
|
3168
|
+
canvas: z2.object({
|
|
3169
|
+
preset: z2.string().optional(),
|
|
3170
|
+
width: z2.number().optional(),
|
|
3171
|
+
height: z2.number().optional()
|
|
3172
|
+
}).optional(),
|
|
3173
|
+
parameters: z2.array(
|
|
3174
|
+
z2.object({
|
|
3175
|
+
key: z2.string(),
|
|
3176
|
+
label: z2.string(),
|
|
3177
|
+
min: z2.number(),
|
|
3178
|
+
max: z2.number(),
|
|
3179
|
+
step: z2.number(),
|
|
3180
|
+
default: z2.number()
|
|
3181
|
+
})
|
|
3182
|
+
).optional(),
|
|
3183
|
+
colors: z2.array(
|
|
3184
|
+
z2.object({
|
|
3185
|
+
key: z2.string(),
|
|
3186
|
+
label: z2.string(),
|
|
3187
|
+
default: z2.string()
|
|
3188
|
+
})
|
|
3189
|
+
).optional(),
|
|
3190
|
+
algorithm: z2.string().optional(),
|
|
3191
|
+
philosophy: z2.string().optional()
|
|
3192
|
+
}).optional().describe("Fields to override in the fork"),
|
|
3193
|
+
newSeed: z2.boolean().optional().describe("Generate a new random seed for the fork (default: true)"),
|
|
3194
|
+
agent: z2.string().optional().describe("Your CLI agent name (e.g. 'claude-code', 'codex-cli', 'gemini-cli', 'opencode', 'kiro')"),
|
|
3195
|
+
model: z2.string().optional().describe("Your AI model identifier (e.g. 'claude-opus-4-6', 'gpt-4o', 'gemini-2.5-pro')")
|
|
3196
|
+
},
|
|
3197
|
+
async (args) => {
|
|
3198
|
+
try {
|
|
3199
|
+
const result = await forkSketch(state, args);
|
|
3200
|
+
return jsonResult(result);
|
|
3201
|
+
} catch (e) {
|
|
3202
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
);
|
|
3206
|
+
server.tool(
|
|
3207
|
+
"delete_sketch",
|
|
3208
|
+
"Delete a sketch file from disk and remove it from the workspace",
|
|
3209
|
+
{
|
|
3210
|
+
sketchId: z2.string().describe("ID of the sketch to delete"),
|
|
3211
|
+
keepFile: z2.boolean().optional().describe("Keep the .genart file on disk (default: false)")
|
|
3212
|
+
},
|
|
3213
|
+
async (args) => {
|
|
3214
|
+
try {
|
|
3215
|
+
const result = await deleteSketch(state, args);
|
|
3216
|
+
return jsonResult(result);
|
|
3217
|
+
} catch (e) {
|
|
3218
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
);
|
|
3222
|
+
}
|
|
3223
|
+
function registerSelectionTools(server, state) {
|
|
3224
|
+
server.tool(
|
|
3225
|
+
"get_selection",
|
|
3226
|
+
"Return full context for the currently selected sketch(es) on the canvas",
|
|
3227
|
+
{
|
|
3228
|
+
includeAlgorithm: z2.boolean().optional().describe("Include full algorithm source (default: true)"),
|
|
3229
|
+
includePhilosophy: z2.boolean().optional().describe("Include philosophy markdown (default: true)"),
|
|
3230
|
+
includeNeighbors: z2.boolean().optional().describe("Include summaries of adjacent sketches (default: false)")
|
|
3231
|
+
},
|
|
3232
|
+
async (args) => {
|
|
3233
|
+
try {
|
|
3234
|
+
const result = await getSelection(state, args);
|
|
3235
|
+
return jsonResult(result);
|
|
3236
|
+
} catch (e) {
|
|
3237
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
);
|
|
3241
|
+
server.tool(
|
|
3242
|
+
"select_sketch",
|
|
3243
|
+
"Set the canvas selection to one or more sketches by ID",
|
|
3244
|
+
{
|
|
3245
|
+
sketchIds: z2.array(z2.string()).describe("IDs of sketches to select"),
|
|
3246
|
+
addToSelection: z2.boolean().optional().describe("Add to existing selection instead of replacing (default: false)")
|
|
3247
|
+
},
|
|
3248
|
+
async (args) => {
|
|
3249
|
+
try {
|
|
3250
|
+
const result = await selectSketch(state, args);
|
|
3251
|
+
return jsonResult(result);
|
|
3252
|
+
} catch (e) {
|
|
3253
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
);
|
|
3257
|
+
server.tool(
|
|
3258
|
+
"get_editor_state",
|
|
3259
|
+
"Return a full snapshot of the MCP server's current state",
|
|
3260
|
+
{},
|
|
3261
|
+
async () => {
|
|
3262
|
+
try {
|
|
3263
|
+
const result = await getEditorState(state);
|
|
3264
|
+
return jsonResult(result);
|
|
3265
|
+
} catch (e) {
|
|
3266
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
);
|
|
3270
|
+
server.tool(
|
|
3271
|
+
"set_working_directory",
|
|
3272
|
+
"Set the working directory for file operations. All paths are resolved relative to this directory.",
|
|
3273
|
+
{
|
|
3274
|
+
path: z2.string().describe("Absolute path to use as the working directory")
|
|
3275
|
+
},
|
|
3276
|
+
async (args) => {
|
|
3277
|
+
try {
|
|
3278
|
+
const dir = args.path;
|
|
3279
|
+
if (!dir.startsWith("/")) {
|
|
3280
|
+
return toolError("Path must be absolute");
|
|
3281
|
+
}
|
|
3282
|
+
state.setBasePath(dir);
|
|
3283
|
+
return jsonResult({ success: true, workingDirectory: dir });
|
|
3284
|
+
} catch (e) {
|
|
3285
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
);
|
|
3289
|
+
}
|
|
3290
|
+
function registerParameterTools(server, state) {
|
|
3291
|
+
server.tool(
|
|
3292
|
+
"set_parameters",
|
|
3293
|
+
"Update the runtime parameter values of a sketch's current state",
|
|
3294
|
+
{
|
|
3295
|
+
sketchId: z2.string().describe("ID of the sketch to update"),
|
|
3296
|
+
params: z2.record(z2.number()).describe("Parameter key-value pairs to set")
|
|
3297
|
+
},
|
|
3298
|
+
async (args) => {
|
|
3299
|
+
try {
|
|
3300
|
+
const result = await setParameters(state, args);
|
|
3301
|
+
return jsonResult(result);
|
|
3302
|
+
} catch (e) {
|
|
3303
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
);
|
|
3307
|
+
server.tool(
|
|
3308
|
+
"set_colors",
|
|
3309
|
+
"Update the runtime color palette values of a sketch's current state",
|
|
3310
|
+
{
|
|
3311
|
+
sketchId: z2.string().describe("ID of the sketch to update"),
|
|
3312
|
+
colors: z2.record(z2.string()).describe("Color key-value pairs to set (hex strings)")
|
|
3313
|
+
},
|
|
3314
|
+
async (args) => {
|
|
3315
|
+
try {
|
|
3316
|
+
const result = await setColors(state, args);
|
|
3317
|
+
return jsonResult(result);
|
|
3318
|
+
} catch (e) {
|
|
3319
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
);
|
|
3323
|
+
server.tool(
|
|
3324
|
+
"set_seed",
|
|
3325
|
+
"Set the random seed of a sketch, optionally generating a random value",
|
|
3326
|
+
{
|
|
3327
|
+
sketchId: z2.string().describe("ID of the sketch to update"),
|
|
3328
|
+
seed: z2.number().optional().describe("Explicit seed value (default: generate random 0\u201399999)")
|
|
3329
|
+
},
|
|
3330
|
+
async (args) => {
|
|
3331
|
+
try {
|
|
3332
|
+
const result = await setSeed(state, args);
|
|
3333
|
+
return jsonResult(result);
|
|
3334
|
+
} catch (e) {
|
|
3335
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
);
|
|
3339
|
+
server.tool(
|
|
3340
|
+
"set_canvas_size",
|
|
3341
|
+
"Change the canvas dimensions of a sketch using a preset or explicit width/height",
|
|
3342
|
+
{
|
|
3343
|
+
sketchId: z2.string().describe("ID of the sketch to update"),
|
|
3344
|
+
preset: z2.string().optional().describe("Canvas preset name"),
|
|
3345
|
+
width: z2.number().optional().describe("Explicit width in pixels"),
|
|
3346
|
+
height: z2.number().optional().describe("Explicit height in pixels")
|
|
3347
|
+
},
|
|
3348
|
+
async (args) => {
|
|
3349
|
+
try {
|
|
3350
|
+
const result = await setCanvasSize(state, args);
|
|
3351
|
+
return jsonResult(result);
|
|
3352
|
+
} catch (e) {
|
|
3353
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
);
|
|
3357
|
+
server.tool(
|
|
3358
|
+
"randomize_parameters",
|
|
3359
|
+
"Generate random values for all or specific parameters within their defined ranges",
|
|
3360
|
+
{
|
|
3361
|
+
sketchId: z2.string().describe("ID of the sketch to randomize"),
|
|
3362
|
+
paramKeys: z2.array(z2.string()).optional().describe("Specific parameter keys to randomize (default: all)"),
|
|
3363
|
+
newSeed: z2.boolean().optional().describe("Also randomize the seed (default: false)")
|
|
3364
|
+
},
|
|
3365
|
+
async (args) => {
|
|
3366
|
+
try {
|
|
3367
|
+
const result = await randomizeParameters(state, args);
|
|
3368
|
+
return jsonResult(result);
|
|
3369
|
+
} catch (e) {
|
|
3370
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
function registerArrangementTools(server, state) {
|
|
3376
|
+
server.tool(
|
|
3377
|
+
"arrange_sketches",
|
|
3378
|
+
"Move specific sketches to explicit positions on the canvas",
|
|
3379
|
+
{
|
|
3380
|
+
positions: z2.array(
|
|
3381
|
+
z2.object({
|
|
3382
|
+
sketchId: z2.string().describe("ID of the sketch to move"),
|
|
3383
|
+
x: z2.number().describe("X position on canvas"),
|
|
3384
|
+
y: z2.number().describe("Y position on canvas")
|
|
3385
|
+
})
|
|
3386
|
+
).describe("Explicit position assignments")
|
|
3387
|
+
},
|
|
3388
|
+
async (args) => {
|
|
3389
|
+
try {
|
|
3390
|
+
const result = await arrangeSketches(state, args);
|
|
3391
|
+
return jsonResult(result);
|
|
3392
|
+
} catch (e) {
|
|
3393
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
);
|
|
3397
|
+
server.tool(
|
|
3398
|
+
"auto_arrange",
|
|
3399
|
+
"Automatically lay out all or selected sketches using a configurable layout algorithm",
|
|
3400
|
+
{
|
|
3401
|
+
layout: z2.enum(["grid", "row", "column", "masonry"]).optional().describe("Layout algorithm (default: grid)"),
|
|
3402
|
+
sketchIds: z2.array(z2.string()).optional().describe("Specific sketches to arrange (default: all)"),
|
|
3403
|
+
spacing: z2.number().optional().describe("Gap between sketches in pixels (default: 200)"),
|
|
3404
|
+
sortBy: z2.enum(["title", "created", "modified", "renderer"]).optional().describe("Sort order before arranging (default: created)"),
|
|
3405
|
+
origin: z2.object({
|
|
3406
|
+
x: z2.number().describe("X origin"),
|
|
3407
|
+
y: z2.number().describe("Y origin")
|
|
3408
|
+
}).optional().describe("Top-left origin for the arrangement (default: {x:0, y:0})")
|
|
3409
|
+
},
|
|
3410
|
+
async (args) => {
|
|
3411
|
+
try {
|
|
3412
|
+
const result = await autoArrange(state, args);
|
|
3413
|
+
return jsonResult(result);
|
|
3414
|
+
} catch (e) {
|
|
3415
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
);
|
|
3419
|
+
server.tool(
|
|
3420
|
+
"group_sketches",
|
|
3421
|
+
"Create or update a named group of sketches in the workspace",
|
|
3422
|
+
{
|
|
3423
|
+
groupId: z2.string().describe("Unique group identifier"),
|
|
3424
|
+
label: z2.string().describe("Display label for the group"),
|
|
3425
|
+
sketchIds: z2.array(z2.string()).describe("IDs of sketches to include in the group"),
|
|
3426
|
+
color: z2.string().optional().describe("Optional group color (hex string)")
|
|
3427
|
+
},
|
|
3428
|
+
async (args) => {
|
|
3429
|
+
try {
|
|
3430
|
+
const result = await groupSketches(state, args);
|
|
3431
|
+
return jsonResult(result);
|
|
3432
|
+
} catch (e) {
|
|
3433
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
);
|
|
3437
|
+
}
|
|
3438
|
+
function registerGalleryTools(server, state) {
|
|
3439
|
+
server.tool(
|
|
3440
|
+
"list_sketches",
|
|
3441
|
+
"Scan the workspace directory for all .genart files with metadata summaries",
|
|
3442
|
+
{
|
|
3443
|
+
directory: z2.string().optional().describe("Directory to scan (default: workspace directory)"),
|
|
3444
|
+
recursive: z2.boolean().optional().describe("Scan subdirectories (default: false)"),
|
|
3445
|
+
includeUnreferenced: z2.boolean().optional().describe(
|
|
3446
|
+
"Include files not in the active workspace (default: true)"
|
|
3447
|
+
)
|
|
3448
|
+
},
|
|
3449
|
+
async (args) => {
|
|
3450
|
+
try {
|
|
3451
|
+
const result = await listSketches(state, args);
|
|
3452
|
+
return jsonResult(result);
|
|
3453
|
+
} catch (e) {
|
|
3454
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
);
|
|
3458
|
+
server.tool(
|
|
3459
|
+
"search_sketches",
|
|
3460
|
+
"Search loaded sketches by title, renderer, parameters, canvas size, or skills",
|
|
3461
|
+
{
|
|
3462
|
+
query: z2.string().optional().describe("Substring match against sketch title (case-insensitive)"),
|
|
3463
|
+
renderer: z2.enum(["p5", "three", "glsl", "canvas2d", "svg"]).optional().describe("Filter by renderer type"),
|
|
3464
|
+
minParameters: z2.number().optional().describe("Minimum number of parameters"),
|
|
3465
|
+
maxParameters: z2.number().optional().describe("Maximum number of parameters"),
|
|
3466
|
+
canvasWidth: z2.number().optional().describe("Exact canvas width match"),
|
|
3467
|
+
canvasHeight: z2.number().optional().describe("Exact canvas height match"),
|
|
3468
|
+
hasPhilosophy: z2.boolean().optional().describe("Filter by presence of philosophy text"),
|
|
3469
|
+
skills: z2.array(z2.string()).optional().describe("Filter by sketches that use any of these skills")
|
|
3470
|
+
},
|
|
3471
|
+
async (args) => {
|
|
3472
|
+
try {
|
|
3473
|
+
const result = await searchSketches(state, args);
|
|
3474
|
+
return jsonResult(result);
|
|
3475
|
+
} catch (e) {
|
|
3476
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
);
|
|
3480
|
+
}
|
|
3481
|
+
function registerMergeTools(server, state) {
|
|
3482
|
+
server.tool(
|
|
3483
|
+
"merge_sketches",
|
|
3484
|
+
"Combine parameters, colors, and algorithm from 2+ source sketches into a new sketch",
|
|
3485
|
+
{
|
|
3486
|
+
sourceIds: z2.array(z2.string()).describe("IDs of 2+ source sketches to merge"),
|
|
3487
|
+
newId: z2.string().describe("URL-safe kebab-case ID for the merged sketch"),
|
|
3488
|
+
title: z2.string().describe("Title for the merged sketch"),
|
|
3489
|
+
strategy: z2.enum(["blend", "layer", "alternate"]).optional().describe("Merge strategy (default: blend)"),
|
|
3490
|
+
renderer: z2.enum(["p5", "three", "glsl", "canvas2d", "svg"]).optional().describe("Renderer for merged sketch (default: first source's renderer)"),
|
|
3491
|
+
canvas: z2.object({
|
|
3492
|
+
width: z2.number().optional().describe("Canvas width"),
|
|
3493
|
+
height: z2.number().optional().describe("Canvas height")
|
|
3494
|
+
}).optional().describe("Canvas size (default: largest source dimensions)")
|
|
3495
|
+
},
|
|
3496
|
+
async (args) => {
|
|
3497
|
+
try {
|
|
3498
|
+
const result = await mergeSketches(state, args);
|
|
3499
|
+
return jsonResult(result);
|
|
3500
|
+
} catch (e) {
|
|
3501
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
);
|
|
3505
|
+
}
|
|
3506
|
+
function registerSnapshotTools(server, state) {
|
|
3507
|
+
server.tool(
|
|
3508
|
+
"snapshot_layout",
|
|
3509
|
+
"Return a structural summary of the workspace layout for AI spatial reasoning",
|
|
3510
|
+
{
|
|
3511
|
+
includeGroups: z2.boolean().optional().describe("Include group information (default: true)"),
|
|
3512
|
+
includeState: z2.boolean().optional().describe("Include current seed and param values (default: false)")
|
|
3513
|
+
},
|
|
3514
|
+
async (args) => {
|
|
3515
|
+
try {
|
|
3516
|
+
const result = await snapshotLayout(state, args);
|
|
3517
|
+
return jsonResult(result);
|
|
3518
|
+
} catch (e) {
|
|
3519
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
function registerCaptureTools(server, state) {
|
|
3525
|
+
server.tool(
|
|
3526
|
+
"capture_screenshot",
|
|
3527
|
+
"Capture a screenshot of a sketch. Returns metadata as text + a small inline JPEG image for visual review. In remote mode, metadata includes previewFileContent (base64 PNG) to Write locally.",
|
|
3528
|
+
{
|
|
3529
|
+
target: z2.enum(["selected", "sketch"]).optional().describe("What to capture (default: selected)"),
|
|
3530
|
+
sketchId: z2.string().optional().describe("Required when target is 'sketch'"),
|
|
3531
|
+
width: z2.number().optional().describe("Output width in pixels (default: sketch canvas width)"),
|
|
3532
|
+
height: z2.number().optional().describe("Output height in pixels (default: sketch canvas height)"),
|
|
3533
|
+
seed: z2.number().optional().describe("Override seed for this capture only"),
|
|
3534
|
+
params: z2.record(z2.number()).optional().describe("Override params for this capture only"),
|
|
3535
|
+
previewSize: z2.number().optional().describe("Max dimension for inline preview JPEG (default: 400)")
|
|
3536
|
+
},
|
|
3537
|
+
async (args) => {
|
|
3538
|
+
try {
|
|
3539
|
+
const result = await captureScreenshot(state, args);
|
|
3540
|
+
return {
|
|
3541
|
+
content: [
|
|
3542
|
+
{ type: "text", text: JSON.stringify(result.metadata, null, 2) },
|
|
3543
|
+
{ type: "image", data: result.previewJpegBase64, mimeType: "image/jpeg" }
|
|
3544
|
+
]
|
|
3545
|
+
};
|
|
3546
|
+
} catch (e) {
|
|
3547
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
);
|
|
3551
|
+
server.tool(
|
|
3552
|
+
"capture_batch",
|
|
3553
|
+
"Capture screenshots of multiple sketches in parallel. Returns metadata as text + inline JPEG images for visual review.",
|
|
3554
|
+
{
|
|
3555
|
+
sketchIds: z2.array(z2.string()).optional().describe("IDs of sketches to capture (default: all)"),
|
|
3556
|
+
width: z2.number().optional().describe("Override width for all captures"),
|
|
3557
|
+
height: z2.number().optional().describe("Override height for all captures"),
|
|
3558
|
+
seed: z2.number().optional().describe("Override seed for all captures"),
|
|
3559
|
+
previewSize: z2.number().optional().describe("Max dimension for inline preview JPEGs (default: 200 for batch)")
|
|
3560
|
+
},
|
|
3561
|
+
async (args) => {
|
|
3562
|
+
try {
|
|
3563
|
+
const result = await captureBatch(state, args);
|
|
3564
|
+
const content = [
|
|
3565
|
+
{ type: "text", text: JSON.stringify(result.metadata, null, 2) }
|
|
3566
|
+
];
|
|
3567
|
+
for (const item of result.items) {
|
|
3568
|
+
content.push({
|
|
3569
|
+
type: "text",
|
|
3570
|
+
text: JSON.stringify(item.metadata, null, 2)
|
|
3571
|
+
});
|
|
3572
|
+
content.push({
|
|
3573
|
+
type: "image",
|
|
3574
|
+
data: item.inlineJpegBase64,
|
|
3575
|
+
mimeType: "image/jpeg"
|
|
3576
|
+
});
|
|
3577
|
+
}
|
|
3578
|
+
return { content };
|
|
3579
|
+
} catch (e) {
|
|
3580
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
function registerExportTools(server, state) {
|
|
3586
|
+
server.tool(
|
|
3587
|
+
"export_sketch",
|
|
3588
|
+
"Export a sketch as standalone HTML, PNG, SVG, raw algorithm, or bundled ZIP",
|
|
3589
|
+
{
|
|
3590
|
+
sketchId: z2.string().describe("ID of the sketch to export"),
|
|
3591
|
+
format: z2.enum(["html", "png", "svg", "algorithm", "zip"]).describe("Export format"),
|
|
3592
|
+
outputPath: z2.string().describe("File path to write the export"),
|
|
3593
|
+
width: z2.number().optional().describe("Override width for PNG/SVG export"),
|
|
3594
|
+
height: z2.number().optional().describe("Override height for PNG/SVG export"),
|
|
3595
|
+
seed: z2.number().optional().describe("Override seed for this export"),
|
|
3596
|
+
params: z2.record(z2.number()).optional().describe("Override params for this export")
|
|
3597
|
+
},
|
|
3598
|
+
async (args) => {
|
|
3599
|
+
try {
|
|
3600
|
+
const result = await exportSketch(state, args);
|
|
3601
|
+
return jsonResult(result);
|
|
3602
|
+
} catch (e) {
|
|
3603
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
);
|
|
3607
|
+
}
|
|
3608
|
+
function registerKnowledgeTools(server, _state) {
|
|
3609
|
+
server.tool(
|
|
3610
|
+
"list_skills",
|
|
3611
|
+
"List all available design knowledge skills (Phase 5)",
|
|
3612
|
+
{
|
|
3613
|
+
category: z2.string().optional().describe("Filter by skill category")
|
|
3614
|
+
},
|
|
3615
|
+
async (args) => {
|
|
3616
|
+
try {
|
|
3617
|
+
const result = await listSkills(args);
|
|
3618
|
+
return jsonResult(result);
|
|
3619
|
+
} catch (e) {
|
|
3620
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
);
|
|
3624
|
+
server.tool(
|
|
3625
|
+
"load_skill",
|
|
3626
|
+
"Load a specific design knowledge skill with full content (Phase 5)",
|
|
3627
|
+
{
|
|
3628
|
+
skillId: z2.string().describe("ID of the skill to load"),
|
|
3629
|
+
renderer: z2.enum(["p5", "three", "glsl", "canvas2d", "svg"]).optional().describe("Renderer-specific examples (default: p5)")
|
|
3630
|
+
},
|
|
3631
|
+
async (args) => {
|
|
3632
|
+
try {
|
|
3633
|
+
const result = await loadSkill(args);
|
|
3634
|
+
return jsonResult(result);
|
|
3635
|
+
} catch (e) {
|
|
3636
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
);
|
|
3640
|
+
server.tool(
|
|
3641
|
+
"get_guidelines",
|
|
3642
|
+
"Return design guidelines and best practices for a topic (Phase 5)",
|
|
3643
|
+
{
|
|
3644
|
+
topic: z2.enum(["composition", "color", "parameters", "animation", "performance"]).describe("Guideline topic"),
|
|
3645
|
+
renderer: z2.enum(["p5", "three", "glsl", "canvas2d", "svg"]).optional().describe("Renderer-specific guidance")
|
|
3646
|
+
},
|
|
3647
|
+
async (args) => {
|
|
3648
|
+
try {
|
|
3649
|
+
const result = await getGuidelines(args);
|
|
3650
|
+
return jsonResult(result);
|
|
3651
|
+
} catch (e) {
|
|
3652
|
+
return toolError(e instanceof Error ? e.message : String(e));
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
// src/state.ts
|
|
3659
|
+
import { EventEmitter } from "events";
|
|
3660
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
3661
|
+
import { dirname as dirname6, isAbsolute, resolve as resolve2 } from "path";
|
|
3662
|
+
|
|
3663
|
+
// src/sidecar.ts
|
|
3664
|
+
function isSidecarMode() {
|
|
3665
|
+
const modeIdx = process.argv.indexOf("--mode");
|
|
3666
|
+
if (modeIdx !== -1 && process.argv[modeIdx + 1] === "sidecar") return true;
|
|
3667
|
+
return process.env.GENART_SIDECAR === "1";
|
|
3668
|
+
}
|
|
3669
|
+
function notifyMutation(type, payload) {
|
|
3670
|
+
if (isSidecarMode() && typeof process.send === "function") {
|
|
3671
|
+
process.send({ type, payload });
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
// src/state.ts
|
|
3676
|
+
import {
|
|
3677
|
+
parseGenart as parseGenart4,
|
|
3678
|
+
parseWorkspace as parseWorkspace2,
|
|
3679
|
+
serializeGenart as serializeGenart4,
|
|
3680
|
+
serializeWorkspace as serializeWorkspace3
|
|
3681
|
+
} from "@genart-dev/core";
|
|
3682
|
+
import { writeFile as writeFile6 } from "fs/promises";
|
|
3683
|
+
var EditorState = class extends EventEmitter {
|
|
3684
|
+
/** Absolute path to the active .genart-workspace file, or null. */
|
|
3685
|
+
workspacePath = null;
|
|
3686
|
+
/** Parsed workspace definition, or null if no workspace is open. */
|
|
3687
|
+
workspace = null;
|
|
3688
|
+
/** Loaded sketches keyed by sketch ID. */
|
|
3689
|
+
sketches = /* @__PURE__ */ new Map();
|
|
3690
|
+
/** Currently selected sketch IDs. */
|
|
3691
|
+
selection = /* @__PURE__ */ new Set();
|
|
3692
|
+
/**
|
|
3693
|
+
* Base directory for all file operations. When set, all paths are
|
|
3694
|
+
* resolved relative to this directory and constrained within it.
|
|
3695
|
+
* Used by mcp-host for per-session sandboxing.
|
|
3696
|
+
*/
|
|
3697
|
+
basePath = null;
|
|
3698
|
+
/**
|
|
3699
|
+
* When true, the server is running remotely and cannot access the
|
|
3700
|
+
* user's local filesystem. Tools return file content in responses
|
|
3701
|
+
* instead of writing to disk. Set by mcp-host for HTTP-based sessions.
|
|
3702
|
+
*/
|
|
3703
|
+
remoteMode = false;
|
|
3704
|
+
constructor(options) {
|
|
3705
|
+
super();
|
|
3706
|
+
if (options?.basePath) {
|
|
3707
|
+
this.basePath = options.basePath;
|
|
3708
|
+
}
|
|
3709
|
+
if (options?.remoteMode) {
|
|
3710
|
+
this.remoteMode = true;
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
/** Update the working directory / sandbox base path. */
|
|
3714
|
+
setBasePath(dir) {
|
|
3715
|
+
this.basePath = dir;
|
|
3716
|
+
}
|
|
3717
|
+
/** Resolve a file path, respecting the sandbox basePath when set. */
|
|
3718
|
+
resolvePath(file) {
|
|
3719
|
+
if (isAbsolute(file)) {
|
|
3720
|
+
if (this.basePath && !file.startsWith(this.basePath)) {
|
|
3721
|
+
throw new Error(`Path escapes sandbox: ${file}`);
|
|
3722
|
+
}
|
|
3723
|
+
return file;
|
|
3724
|
+
}
|
|
3725
|
+
if ((file.startsWith("~/") || file === "~") && !this.basePath) {
|
|
3726
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
3727
|
+
return resolve2(home, file.slice(2));
|
|
3728
|
+
}
|
|
3729
|
+
const base = this.basePath ?? process.cwd();
|
|
3730
|
+
const resolved = resolve2(base, file);
|
|
3731
|
+
if (this.basePath && !resolved.startsWith(this.basePath)) {
|
|
3732
|
+
throw new Error(`Path escapes sandbox: ${resolved}`);
|
|
3733
|
+
}
|
|
3734
|
+
return resolved;
|
|
3735
|
+
}
|
|
3736
|
+
/** Resolve a sketch file reference (relative to workspace dir) to an absolute path. */
|
|
3737
|
+
resolveSketchPath(file) {
|
|
3738
|
+
if (isAbsolute(file)) {
|
|
3739
|
+
if (this.basePath && !file.startsWith(this.basePath)) {
|
|
3740
|
+
throw new Error(`Path escapes sandbox: ${file}`);
|
|
3741
|
+
}
|
|
3742
|
+
return file;
|
|
3743
|
+
}
|
|
3744
|
+
if (!this.workspacePath) {
|
|
3745
|
+
if (this.basePath) {
|
|
3746
|
+
return resolve2(this.basePath, file);
|
|
3747
|
+
}
|
|
3748
|
+
throw new Error("No workspace is currently open");
|
|
3749
|
+
}
|
|
3750
|
+
const resolved = resolve2(dirname6(this.workspacePath), file);
|
|
3751
|
+
if (this.basePath && !resolved.startsWith(this.basePath)) {
|
|
3752
|
+
throw new Error(`Path escapes sandbox: ${resolved}`);
|
|
3753
|
+
}
|
|
3754
|
+
return resolved;
|
|
3755
|
+
}
|
|
3756
|
+
/** Load a workspace from disk and all its referenced sketches. */
|
|
3757
|
+
async loadWorkspace(absPath) {
|
|
3758
|
+
if (this.basePath && !absPath.startsWith(this.basePath)) {
|
|
3759
|
+
throw new Error(`Path escapes sandbox: ${absPath}`);
|
|
3760
|
+
}
|
|
3761
|
+
const raw = await readFile4(absPath, "utf-8");
|
|
3762
|
+
const json = JSON.parse(raw);
|
|
3763
|
+
const ws = parseWorkspace2(json);
|
|
3764
|
+
this.workspacePath = absPath;
|
|
3765
|
+
this.workspace = ws;
|
|
3766
|
+
this.sketches.clear();
|
|
3767
|
+
this.selection.clear();
|
|
3768
|
+
for (const ref of ws.sketches) {
|
|
3769
|
+
const sketchPath = this.resolveSketchPath(ref.file);
|
|
3770
|
+
await this.loadSketch(sketchPath);
|
|
3771
|
+
}
|
|
3772
|
+
this.emitMutation("workspace:loaded", { path: absPath, title: ws.title });
|
|
3773
|
+
}
|
|
3774
|
+
/** Load a single sketch from disk and add it to the cache. */
|
|
3775
|
+
async loadSketch(absPath) {
|
|
3776
|
+
if (this.basePath && !absPath.startsWith(this.basePath)) {
|
|
3777
|
+
throw new Error(`Path escapes sandbox: ${absPath}`);
|
|
3778
|
+
}
|
|
3779
|
+
const raw = await readFile4(absPath, "utf-8");
|
|
3780
|
+
const json = JSON.parse(raw);
|
|
3781
|
+
const definition = parseGenart4(json);
|
|
3782
|
+
this.sketches.set(definition.id, { definition, path: absPath });
|
|
3783
|
+
this.emitMutation("sketch:loaded", { id: definition.id, path: absPath });
|
|
3784
|
+
return definition;
|
|
3785
|
+
}
|
|
3786
|
+
/** Get a loaded sketch by ID. */
|
|
3787
|
+
getSketch(id) {
|
|
3788
|
+
return this.sketches.get(id);
|
|
3789
|
+
}
|
|
3790
|
+
/** Require a loaded sketch by ID, throwing if not found. */
|
|
3791
|
+
requireSketch(id) {
|
|
3792
|
+
const sketch = this.sketches.get(id);
|
|
3793
|
+
if (!sketch) {
|
|
3794
|
+
throw new Error(`Sketch not found: '${id}'`);
|
|
3795
|
+
}
|
|
3796
|
+
return sketch;
|
|
3797
|
+
}
|
|
3798
|
+
/** Require an open workspace, throwing if none is open. */
|
|
3799
|
+
requireWorkspace() {
|
|
3800
|
+
if (!this.workspace) {
|
|
3801
|
+
throw new Error("No workspace is currently open");
|
|
3802
|
+
}
|
|
3803
|
+
return this.workspace;
|
|
3804
|
+
}
|
|
3805
|
+
/** Remove a sketch from the in-memory cache. */
|
|
3806
|
+
removeSketch(id) {
|
|
3807
|
+
this.sketches.delete(id);
|
|
3808
|
+
this.selection.delete(id);
|
|
3809
|
+
this.emitMutation("sketch:removed", { id });
|
|
3810
|
+
}
|
|
3811
|
+
/** Save the active workspace to disk. */
|
|
3812
|
+
async saveWorkspace() {
|
|
3813
|
+
if (!this.workspace || !this.workspacePath) {
|
|
3814
|
+
throw new Error("No workspace is currently open");
|
|
3815
|
+
}
|
|
3816
|
+
const json = serializeWorkspace3(this.workspace);
|
|
3817
|
+
if (!this.remoteMode) {
|
|
3818
|
+
await writeFile6(this.workspacePath, json, "utf-8");
|
|
3819
|
+
}
|
|
3820
|
+
this.emitMutation("workspace:saved", { path: this.workspacePath });
|
|
3821
|
+
}
|
|
3822
|
+
/** Save a sketch to disk. */
|
|
3823
|
+
async saveSketch(id) {
|
|
3824
|
+
const loaded = this.requireSketch(id);
|
|
3825
|
+
const json = serializeGenart4(loaded.definition);
|
|
3826
|
+
if (!this.remoteMode) {
|
|
3827
|
+
await writeFile6(loaded.path, json, "utf-8");
|
|
3828
|
+
}
|
|
3829
|
+
this.emitMutation("sketch:saved", { id, path: loaded.path });
|
|
3830
|
+
}
|
|
3831
|
+
/** Update the selection and return the new set. */
|
|
3832
|
+
setSelection(ids) {
|
|
3833
|
+
this.selection.clear();
|
|
3834
|
+
for (const id of ids) {
|
|
3835
|
+
this.selection.add(id);
|
|
3836
|
+
}
|
|
3837
|
+
this.emitMutation("selection:changed", { ids });
|
|
3838
|
+
}
|
|
3839
|
+
/** Get a serializable snapshot of the full editor state. */
|
|
3840
|
+
getSnapshot() {
|
|
3841
|
+
const sketches = [];
|
|
3842
|
+
for (const [id, loaded] of this.sketches) {
|
|
3843
|
+
sketches.push({ id, definition: loaded.definition, path: loaded.path });
|
|
3844
|
+
}
|
|
3845
|
+
return {
|
|
3846
|
+
workspacePath: this.workspacePath,
|
|
3847
|
+
workspace: this.workspace,
|
|
3848
|
+
sketches,
|
|
3849
|
+
selection: Array.from(this.selection)
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
/** Emit a mutation event for external listeners (WebSocket broadcast, sidecar IPC). */
|
|
3853
|
+
emitMutation(type, payload) {
|
|
3854
|
+
this.emit("mutation", { type, payload });
|
|
3855
|
+
notifyMutation(type, payload);
|
|
3856
|
+
}
|
|
3857
|
+
};
|
|
3858
|
+
export {
|
|
3859
|
+
EditorState,
|
|
3860
|
+
createServer
|
|
3861
|
+
};
|
|
3862
|
+
//# sourceMappingURL=lib.js.map
|