@synthetic-ai/illustrator-mcp 0.3.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/README.md +57 -0
- package/i18n/i18n-core.cjs +224 -0
- package/i18n/locales/de.json +230 -0
- package/i18n/locales/en.json +230 -0
- package/i18n/locales/es.json +230 -0
- package/i18n/locales/fr.json +230 -0
- package/i18n/locales/hashes/de.json +230 -0
- package/i18n/locales/hashes/es.json +230 -0
- package/i18n/locales/hashes/fr.json +230 -0
- package/i18n/locales/hashes/ja.json +230 -0
- package/i18n/locales/hashes/pt-BR.json +230 -0
- package/i18n/locales/ja.json +230 -0
- package/i18n/locales/pt-BR.json +230 -0
- package/i18n/locales.js +3 -0
- package/i18n/node.cjs +124 -0
- package/i18n/panel.cjs +178 -0
- package/package.json +50 -0
- package/server.mjs +726 -0
package/server.mjs
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Illustrator MCP — Synthetic
|
|
4
|
+
* Puente WebSocket ↔ plugin CEP «Synthetic Illustrator MCP». El plugin corre dentro de
|
|
5
|
+
* Illustrator y ejecuta ExtendScript; este server expone las tools.
|
|
6
|
+
*
|
|
7
|
+
* Arquitectura calcada de photoshop-mcp (el más limpio de los productos Synthetic:
|
|
8
|
+
* WebSocket, cero código específico de sistema operativo, no lanza ningún .exe) --
|
|
9
|
+
* mismo motivo por el que Illustrator MCP arranca ya multiplataforma desde v0.1,
|
|
10
|
+
* sin el problema que tuvo AE MCP (detección de rutas de instalación de Windows).
|
|
11
|
+
*
|
|
12
|
+
* Convención (estilo Synthetic):
|
|
13
|
+
* - Diagnóstico/app: il_healthcheck, il_info
|
|
14
|
+
* - Escape hatches: run_jsx, run_jsx_json
|
|
15
|
+
* - Dominio: verbo_sustantivo en snake_case, sin prefijo
|
|
16
|
+
* (list_layers, get_document_info, create_document…)
|
|
17
|
+
*
|
|
18
|
+
* ESTADO: v0.3 -- 136 tools (88 más vía apply_named_effect, ver abajo).
|
|
19
|
+
* Todo lo de v0.2 (conexión/diagnóstico, documentos/capas, items, paths,
|
|
20
|
+
* formas, texto, color, artboards, export) más: gradientes, símbolos,
|
|
21
|
+
* máscaras de recorte, brushes, alinear/distribuir (geometría propia, sin
|
|
22
|
+
* depender de comandos de menú), paths/transformaciones avanzadas (flip,
|
|
23
|
+
* shear), guías, grupos de color/spot colors, trazo avanzado (dashes/cap/
|
|
24
|
+
* join/miter), texto avanzado (find & replace, leading, estilos de
|
|
25
|
+
* párrafo/carácter), items vinculados/embebidos, capas avanzadas (color,
|
|
26
|
+
* duplicar, reordenar), selección avanzada (misma fill, invertir),
|
|
27
|
+
* patrones, 4 efectos en vivo con parámetros reales (drop shadow, feather,
|
|
28
|
+
* round corners, offset path) + apply_named_effect (88 efectos más
|
|
29
|
+
* confirmados en vivo con parámetros por defecto -- Illustrator Effects:
|
|
30
|
+
* glows, distorsión, 3D, todos los Warp, convertir a forma; Photoshop
|
|
31
|
+
* Effects: blur, artístico, trazo de pincel, boceto, textura, pixelar --
|
|
32
|
+
* más apply_live_effect_raw como escape hatch de XML crudo para los casos
|
|
33
|
+
* que necesiten parámetros no probados), y reproducir acciones existentes.
|
|
34
|
+
* Todo probado en vivo contra Illustrator real; 5 bugs reales encontrados y
|
|
35
|
+
* corregidos en el camino (Layer no tiene duplicate(), GradientColor.angle/
|
|
36
|
+
* length no se aplican via script en esta versión de Illustrator, JSON no
|
|
37
|
+
* existe en el motor de ExtendScript, characterAttributes.leading se
|
|
38
|
+
* ignora mientras autoLeading esté activo, y el XML del efecto Drop Shadow
|
|
39
|
+
* solo acepta offset/blur/opacidad -- sus parámetros de color/blend mode
|
|
40
|
+
* tiraban error 'PARM' con cualquier valor probado).
|
|
41
|
+
* Pendiente (necesita los IDs internos reales de executeMenuCommand, que
|
|
42
|
+
* Illustrator no expone por introspección ni tiene documentados en ningún
|
|
43
|
+
* lado -- probados 7 candidatos entre strings extraídos del binario real
|
|
44
|
+
* de PathfinderUI.aip y nombres razonables, todos fallaron limpio o
|
|
45
|
+
* colgaron la app sin lograr nada; abandonado): pathfinder booleano
|
|
46
|
+
* (unite/minus/intersect/exclude/divide/trim/merge/crop), blends,
|
|
47
|
+
* distorsión de envolvente, malla degradada. Los efectos 3D SÍ están
|
|
48
|
+
* cubiertos (vía apply_named_effect, no vía menú).
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
52
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
53
|
+
import WebSocket from "ws";
|
|
54
|
+
import { z } from "zod";
|
|
55
|
+
import https from "https";
|
|
56
|
+
import os from "os";
|
|
57
|
+
import fs from "fs";
|
|
58
|
+
import path from "path";
|
|
59
|
+
import crypto from "crypto";
|
|
60
|
+
import { createRequire } from "module";
|
|
61
|
+
import { fileURLToPath } from "url";
|
|
62
|
+
|
|
63
|
+
const PORT = Number(process.env.IL_MCP_PORT || 41220);
|
|
64
|
+
const WS_URL = "ws://127.0.0.1:" + PORT;
|
|
65
|
+
|
|
66
|
+
// ── Language (i18n) ─────────────────────────────────────────────────────
|
|
67
|
+
// Dictionaries come from synthetic-idiomas (vendored into ./i18n by
|
|
68
|
+
// scripts/sync-i18n.mjs — don't edit those files here). The language is
|
|
69
|
+
// picked ONCE at startup: IL_MCP_LANG → SYNTHETIC_LANG → the settings.json the
|
|
70
|
+
// panel's ⚙ language selector writes (next to its license.json in CEP's
|
|
71
|
+
// USER_DATA) → English. MCP clients only read tool descriptions at session
|
|
72
|
+
// start, so a language change applies after restarting this server.
|
|
73
|
+
const require = createRequire(import.meta.url);
|
|
74
|
+
const { createServerI18n, patchMcpServerToolsList } = require("./i18n/node.cjs");
|
|
75
|
+
const i18n = createServerI18n({
|
|
76
|
+
localesDir: path.join(path.dirname(fileURLToPath(import.meta.url)), "i18n", "locales"),
|
|
77
|
+
envVar: "IL_MCP_LANG",
|
|
78
|
+
settingsFolder: "com.synthetic.illustrator-mcp.bridge",
|
|
79
|
+
});
|
|
80
|
+
process.stderr.write(`[illustrator-mcp] language: ${i18n.language} (source: ${i18n.languageSource})\n`);
|
|
81
|
+
|
|
82
|
+
// ── Device identity (para el límite de 2 dispositivos por licencia) ────────
|
|
83
|
+
// Mismo archivo compartido que ae-mcp/premiere-mcp/photoshop-mcp a propósito:
|
|
84
|
+
// una máquina física cuenta como UN dispositivo contra el límite, no uno por
|
|
85
|
+
// producto instalado en ella.
|
|
86
|
+
function getMachineId() {
|
|
87
|
+
try {
|
|
88
|
+
const dir = path.join(os.homedir(), ".synthetic");
|
|
89
|
+
const file = path.join(dir, "device-id");
|
|
90
|
+
if (fs.existsSync(file)) {
|
|
91
|
+
const id = fs.readFileSync(file, "utf8").trim();
|
|
92
|
+
if (id) return id;
|
|
93
|
+
}
|
|
94
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
95
|
+
const id = crypto.randomUUID();
|
|
96
|
+
fs.writeFileSync(file, id, "utf8");
|
|
97
|
+
return id;
|
|
98
|
+
} catch {
|
|
99
|
+
return "volatile-" + crypto.randomUUID();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function getDeviceLabel() {
|
|
103
|
+
try { return `${os.hostname()} (${process.platform})`; } catch { return null; }
|
|
104
|
+
}
|
|
105
|
+
const MACHINE_ID = getMachineId();
|
|
106
|
+
const DEVICE_LABEL = getDeviceLabel();
|
|
107
|
+
|
|
108
|
+
// ── License validation ──────────────────────────────────────────────────
|
|
109
|
+
const LICENSE_KEY = process.env.IL_MCP_LICENSE;
|
|
110
|
+
const LICENSE_VALIDATION_URL = "https://synthetic.com.ar/api/v1/license/validate";
|
|
111
|
+
const LICENSE_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hora
|
|
112
|
+
let _licenseCache = null;
|
|
113
|
+
|
|
114
|
+
function _httpsPost(url, body) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const parsed = new URL(url);
|
|
117
|
+
const data = JSON.stringify(body);
|
|
118
|
+
const req = https.request(
|
|
119
|
+
{ hostname: parsed.hostname, path: parsed.pathname, method: "POST",
|
|
120
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data), "User-Agent": "illustrator-mcp/0.1.0" } },
|
|
121
|
+
(res) => {
|
|
122
|
+
let raw = "";
|
|
123
|
+
res.on("data", (c) => { raw += c; });
|
|
124
|
+
res.on("end", () => { try { resolve(JSON.parse(raw)); } catch { resolve(null); } });
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
req.on("error", reject);
|
|
128
|
+
req.setTimeout(5000, () => { req.destroy(new Error("timeout")); });
|
|
129
|
+
req.write(data);
|
|
130
|
+
req.end();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function checkLicense() {
|
|
135
|
+
if (_licenseCache && (Date.now() - _licenseCache.checkedAt) < LICENSE_CACHE_TTL_MS) {
|
|
136
|
+
return _licenseCache;
|
|
137
|
+
}
|
|
138
|
+
if (!LICENSE_KEY) {
|
|
139
|
+
_licenseCache = { valid: false, plan: null, message: i18n.t("msg.license.noKey"), checkedAt: Date.now() };
|
|
140
|
+
return _licenseCache;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const result = await _httpsPost(LICENSE_VALIDATION_URL, {
|
|
144
|
+
key: LICENSE_KEY,
|
|
145
|
+
product: "illustrator-mcp",
|
|
146
|
+
machineId: MACHINE_ID,
|
|
147
|
+
deviceLabel: DEVICE_LABEL,
|
|
148
|
+
});
|
|
149
|
+
_licenseCache = { ...(result || {}), checkedAt: Date.now() };
|
|
150
|
+
if (!result) _licenseCache = { valid: false, message: i18n.t("msg.license.serverUnreachableRetry"), checkedAt: Date.now() };
|
|
151
|
+
} catch {
|
|
152
|
+
if (_licenseCache && _licenseCache.valid) {
|
|
153
|
+
_licenseCache.checkedAt = Date.now();
|
|
154
|
+
} else {
|
|
155
|
+
_licenseCache = { valid: false, message: i18n.t("msg.license.serverUnreachable"), checkedAt: Date.now() };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return _licenseCache;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function licenseErrorResponse(toolName) {
|
|
162
|
+
const msg = (_licenseCache && _licenseCache.message) || i18n.t("msg.license.invalidOrMissing");
|
|
163
|
+
return text({ error: i18n.t("msg.license.required"), detail: msg, tool: toolName, info: i18n.t("msg.license.getLicense") });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Tools que funcionan sin licencia (diagnóstico)
|
|
167
|
+
const FREE_TOOLS = new Set(["il_healthcheck", "il_info"]);
|
|
168
|
+
|
|
169
|
+
/* ---------------- WebSocket: cliente del bridge del panel ----------------
|
|
170
|
+
El bridge (WebSocket server) lo hostea el panel dentro de Illustrator.
|
|
171
|
+
Este server MCP se conecta como cliente y reintenta si el panel no está. */
|
|
172
|
+
let sock = null, nextId = 1;
|
|
173
|
+
const pending = new Map();
|
|
174
|
+
function connectBridge() {
|
|
175
|
+
let s;
|
|
176
|
+
try { s = new WebSocket(WS_URL); } catch (e) { setTimeout(connectBridge, 2000); return; }
|
|
177
|
+
s.on("open", () => { sock = s; });
|
|
178
|
+
s.on("message", (raw) => { let m; try { m = JSON.parse(raw.toString()); } catch { return; } const p = pending.get(m.id); if (p) { pending.delete(m.id); clearTimeout(p.to); p.resolve(m); } });
|
|
179
|
+
s.on("close", () => { if (sock === s) sock = null; setTimeout(connectBridge, 2000); });
|
|
180
|
+
s.on("error", () => {});
|
|
181
|
+
}
|
|
182
|
+
connectBridge();
|
|
183
|
+
|
|
184
|
+
function rpc(cmd, params = {}, timeoutMs = 120000) {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
if (!sock || sock.readyState !== 1) { resolve({ ok: false, error: i18n.t("msg.bridge.notConnected", { port: PORT }) }); return; }
|
|
187
|
+
const id = nextId++;
|
|
188
|
+
const to = setTimeout(() => { pending.delete(id); resolve({ ok: false, error: i18n.t("msg.bridge.timeout", { ms: timeoutMs }) }); }, timeoutMs);
|
|
189
|
+
pending.set(id, { resolve, to });
|
|
190
|
+
try { sock.send(JSON.stringify({ id, cmd, params })); } catch (e) { pending.delete(id); clearTimeout(to); resolve({ ok: false, error: "send: " + e.message }); }
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// host.jsx (ExtendScript) answers in English on purpose — no non-ASCII text
|
|
194
|
+
// inside evalScript. Its known dispatcher errors (thrown by the HELPERS
|
|
195
|
+
// prelude below, or by ilmcp()'s own "unknown cmd") are translated here, on
|
|
196
|
+
// the way out to the AI, in the server language.
|
|
197
|
+
const HOST_ERRORS = [
|
|
198
|
+
[/^No document open$/, "msg.host.noDocument", []],
|
|
199
|
+
[/^Layer not found: ([\s\S]*)$/, "msg.host.layerNotFound", ["name"]],
|
|
200
|
+
[/^Item not found: ([\s\S]*)$/, "msg.host.itemNotFound", ["name"]],
|
|
201
|
+
[/^Not a group: ([\s\S]*)$/, "msg.host.notAGroup", ["name"]],
|
|
202
|
+
[/^Not a path: ([\s\S]*)$/, "msg.host.notAPath", ["name"]],
|
|
203
|
+
[/^Not a compound path: ([\s\S]*)$/, "msg.host.notACompoundPath", ["name"]],
|
|
204
|
+
[/^unknown cmd: ([\s\S]*)$/, "msg.host.unknownCommand", ["cmd"]],
|
|
205
|
+
[/^([\s\S]+) \(line (\d+)\)$/, "msg.host.errorAtLine", ["message", "line"]],
|
|
206
|
+
];
|
|
207
|
+
function localizeHostError(o) {
|
|
208
|
+
if (i18n.language === "en" || !o || typeof o !== "object" || typeof o.error !== "string") return o;
|
|
209
|
+
for (const [re, key, names] of HOST_ERRORS) {
|
|
210
|
+
const m = re.exec(o.error);
|
|
211
|
+
if (m) return { ...o, error: i18n.t(key, Object.fromEntries(names.map((n, i) => [n, m[i + 1]]))) };
|
|
212
|
+
}
|
|
213
|
+
return o;
|
|
214
|
+
}
|
|
215
|
+
const text = (o) => ({ content: [{ type: "text", text: typeof o === "string" ? o : JSON.stringify(localizeHostError(o), null, 2) }] });
|
|
216
|
+
|
|
217
|
+
/* ---------------- prelude ExtendScript (helpers Illustrator) ---------------- */
|
|
218
|
+
const HELPERS = [
|
|
219
|
+
"function _doc(){if(!app.documents.length)throw 'No document open';return app.activeDocument;}",
|
|
220
|
+
// Top edge (native AI coords, Y-up) of the active artboard. Illustrator's
|
|
221
|
+
// ruler origin defaults to the artboard's BOTTOM-left for a fresh document
|
|
222
|
+
// (confirmed live: artboardRect top/bottom for a 400x400 doc is [0,400,400,0]),
|
|
223
|
+
// not the top-left some callers might assume -- so a plain '-A.top' style
|
|
224
|
+
// conversion places shapes outside the artboard. Every tool that accepts a
|
|
225
|
+
// 'top/y, Y-down, document coordinates' argument must convert through this,
|
|
226
|
+
// never through a bare negation.
|
|
227
|
+
"function _abTop(){var doc=_doc();var ab=doc.artboards[doc.artboards.getActiveArtboardIndex()];return ab.artboardRect[1];}",
|
|
228
|
+
"function _find(n){function r(ls){for(var i=0;i<ls.length;i++){var L=ls[i];if(L.name===n)return L;if(L.layers&&L.layers.length){var x=r(L.layers);if(x)return x;}}return null;}return r(app.activeDocument.layers);}",
|
|
229
|
+
"function _need(n){var L=_find(n);if(!L)throw 'Layer not found: '+n;return L;}",
|
|
230
|
+
"function _act(n){var L=_need(n);app.activeDocument.activeLayer=L;return L;}",
|
|
231
|
+
// Item lookup by name. Document.pageItems is a flat, recursive listing of
|
|
232
|
+
// every item in the document (including inside groups), so no manual
|
|
233
|
+
// recursion is needed here unlike _find() for layers above.
|
|
234
|
+
"function _itemByName(n){var items=_doc().pageItems;for(var i=0;i<items.length;i++){if(items[i].name===n)return items[i];}return null;}",
|
|
235
|
+
"function _needItem(n){var it=_itemByName(n);if(!it)throw 'Item not found: '+n;return it;}",
|
|
236
|
+
// Color spec -> Illustrator Color object. {r,g,b} (0-255) | {c,m,y,k} (0-100) | {gray} (0-100).
|
|
237
|
+
"function _color(spec){if(!spec)return null;if(spec.gray!==undefined){var c=new GrayColor();c.gray=spec.gray;return c;}if(spec.c!==undefined){var c=new CMYKColor();c.cyan=spec.c;c.magenta=spec.m;c.yellow=spec.y;c.black=spec.k;return c;}if(spec.r!==undefined){var c=new RGBColor();c.red=spec.r;c.green=spec.g;c.blue=spec.b;return c;}throw 'Invalid color spec (expected {r,g,b} or {c,m,y,k} or {gray})';}",
|
|
238
|
+
// Color object -> plain spec, for reading back fill/stroke colors.
|
|
239
|
+
"function _colorOut(c){if(!c)return null;var t=String(c.typename);if(t==='RGBColor')return{type:'rgb',r:c.red,g:c.green,b:c.blue};if(t==='CMYKColor')return{type:'cmyk',c:c.cyan,m:c.magenta,y:c.yellow,k:c.black};if(t==='GrayColor')return{type:'gray',gray:c.gray};if(t==='SpotColor')return{type:'spot',name:c.spot.name};if(t==='NoColor')return{type:'none'};return{type:t};}",
|
|
240
|
+
"function _bounds(it){var b=it.geometricBounds;return{left:__pt2(b[0]),top:__pt2(b[1]),right:__pt2(b[2]),bottom:__pt2(b[3]),width:__pt2(b[2]-b[0]),height:__pt2(b[1]-b[3])};}",
|
|
241
|
+
"function __pt2(v){return Math.round(v*100)/100;}",
|
|
242
|
+
"function _itemsByNames(names){var out=[];for(var i=0;i<names.length;i++)out.push(_needItem(names[i]));return out;}",
|
|
243
|
+
"function _needSymbol(n){try{return _doc().symbols.getByName(n);}catch(e){throw 'Symbol not found: '+n;}}",
|
|
244
|
+
"function _needBrush(n){try{return _doc().brushes.getByName(n);}catch(e){throw 'Brush not found: '+n;}}",
|
|
245
|
+
"function _needSwatch(n){try{return _doc().swatches.getByName(n);}catch(e){throw 'Swatch not found: '+n;}}",
|
|
246
|
+
].join("\n");
|
|
247
|
+
|
|
248
|
+
const server = new McpServer(
|
|
249
|
+
{ name: "illustrator", version: "0.3.0" },
|
|
250
|
+
{ instructions: i18n.t("msg.server.instructions") }
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
function jsxTool(name, description, schema, build, timeout = 120000) {
|
|
254
|
+
server.tool(name, description, schema, async (a) => {
|
|
255
|
+
if (!FREE_TOOLS.has(name)) {
|
|
256
|
+
const lic = await checkLicense();
|
|
257
|
+
if (!lic.valid) return licenseErrorResponse(name);
|
|
258
|
+
}
|
|
259
|
+
return text(await rpc("eval", { code: HELPERS + "\nvar A=" + JSON.stringify(a || {}) + ";\n" + build }, timeout));
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const S = z;
|
|
263
|
+
|
|
264
|
+
/* ====================== DIAGNOSTICS / APP ====================== */
|
|
265
|
+
server.tool("il_healthcheck", "Checks the connection to Illustrator: version and active document.", {}, async () => {
|
|
266
|
+
if (!sock || sock.readyState !== 1) return text({ connected: false, port: PORT, hint: i18n.t("msg.healthcheck.hint") });
|
|
267
|
+
return text({ connected: true, port: PORT, ...(await rpc("health", {}, 15000)) });
|
|
268
|
+
});
|
|
269
|
+
jsxTool("il_info", "App info: version, open documents.", {},
|
|
270
|
+
"return {version:app.version, openDocuments:app.documents.length};");
|
|
271
|
+
server.tool("run_jsx", "Runs arbitrary ExtendScript in Illustrator. The code can use 'return'. Returns {ok,data}.", { code: S.string() },
|
|
272
|
+
async ({ code }) => {
|
|
273
|
+
const lic = await checkLicense();
|
|
274
|
+
if (!lic.valid) return licenseErrorResponse("run_jsx");
|
|
275
|
+
return text(await rpc("eval", { code }, 600000));
|
|
276
|
+
});
|
|
277
|
+
server.tool("run_jsx_json", "Runs ExtendScript that returns a serializable value (JSON). Same as run_jsx but meant for data.", { code: S.string() },
|
|
278
|
+
async ({ code }) => {
|
|
279
|
+
const lic = await checkLicense();
|
|
280
|
+
if (!lic.valid) return licenseErrorResponse("run_jsx_json");
|
|
281
|
+
return text(await rpc("eval", { code }, 600000));
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
/* ====================== DOCUMENTS / LAYERS (first batch) ====================== */
|
|
285
|
+
server.tool("list_documents", "Lists the open documents.", {}, async () => {
|
|
286
|
+
const lic = await checkLicense();
|
|
287
|
+
if (!lic.valid) return licenseErrorResponse("list_documents");
|
|
288
|
+
return text(await rpc("eval", { code: "var a=[];for(var i=0;i<app.documents.length;i++)a.push(app.documents[i].name);return a;" }));
|
|
289
|
+
});
|
|
290
|
+
server.tool("get_document_info", "Info about the active document: active artboard, artboard count, color space, units, layers.", {}, async () => {
|
|
291
|
+
const lic = await checkLicense();
|
|
292
|
+
if (!lic.valid) return licenseErrorResponse("get_document_info");
|
|
293
|
+
return text(await rpc("documentInfo", {}));
|
|
294
|
+
});
|
|
295
|
+
server.tool("list_layers", "Lists the layer tree of the active document (name, visible, locked, item count by type).", {}, async () => {
|
|
296
|
+
const lic = await checkLicense();
|
|
297
|
+
if (!lic.valid) return licenseErrorResponse("list_layers");
|
|
298
|
+
return text(await rpc("listLayers", {}));
|
|
299
|
+
});
|
|
300
|
+
server.tool("get_layer", "Info about a layer by name.", { name: S.string() }, async ({ name }) => {
|
|
301
|
+
const lic = await checkLicense();
|
|
302
|
+
if (!lic.valid) return licenseErrorResponse("get_layer");
|
|
303
|
+
return text(await rpc("getLayer", { name }));
|
|
304
|
+
});
|
|
305
|
+
jsxTool("create_document", "Creates a new document.", { width: S.number(), height: S.number(), name: S.string().optional() },
|
|
306
|
+
"var preset=new DocumentPreset(); preset.width=A.width; preset.height=A.height; preset.units=RulerUnits.Points; preset.colorMode=DocumentColorSpace.RGB; var d=app.documents.addDocument(DocumentColorSpace.RGB, preset); if(A.name)d.name=A.name; return 'created: '+d.name;");
|
|
307
|
+
jsxTool("open_document", "Opens a file.", { path: S.string() },
|
|
308
|
+
"app.open(new File(A.path));return 'opened: '+app.activeDocument.name;");
|
|
309
|
+
jsxTool("save_document", "Saves the active document.", {}, "_doc().save();return 'saved';");
|
|
310
|
+
jsxTool("close_document", "Closes the active document (save=true to save first).", { save: S.boolean().optional() },
|
|
311
|
+
"_doc().close(A.save?SaveOptions.SAVECHANGES:SaveOptions.DONOTSAVECHANGES);return 'closed';");
|
|
312
|
+
jsxTool("add_layer", "Adds a new layer to the active document.", { name: S.string() },
|
|
313
|
+
"var L=_doc().layers.add(); L.name=A.name; return 'created: '+L.name;");
|
|
314
|
+
jsxTool("set_layer_visible", "Shows or hides a layer.", { name: S.string(), visible: S.boolean() },
|
|
315
|
+
"_need(A.name).visible=A.visible; return 'ok';");
|
|
316
|
+
jsxTool("set_layer_locked", "Locks or unlocks a layer.", { name: S.string(), locked: S.boolean() },
|
|
317
|
+
"_need(A.name).locked=A.locked; return 'ok';");
|
|
318
|
+
jsxTool("delete_layer", "Deletes a layer and everything on it.", { name: S.string() },
|
|
319
|
+
"_need(A.name).remove(); return 'ok';");
|
|
320
|
+
jsxTool("rename_layer", "Renames a layer.", { name: S.string(), newName: S.string() },
|
|
321
|
+
"_need(A.name).name=A.newName; return 'ok';");
|
|
322
|
+
|
|
323
|
+
/* ====================== ITEMS (general) ====================== */
|
|
324
|
+
jsxTool("list_items", "Lists page items in the active document (name, type, bounds). Optionally scoped to one layer by name.", { layer: S.string().optional() },
|
|
325
|
+
"var items; if(A.layer){items=_need(A.layer).pageItems;}else{items=_doc().pageItems;} var out=[]; for(var i=0;i<items.length;i++){var it=items[i]; var e={name:it.name,type:String(it.typename)}; try{e.bounds=_bounds(it);}catch(x){} out.push(e);} return out;");
|
|
326
|
+
jsxTool("get_item", "Info about a page item by name: type, bounds, layer, and (for paths) fill/stroke.", { name: S.string() },
|
|
327
|
+
"var it=_needItem(A.name); var r={name:it.name,type:String(it.typename),layer:it.layer.name,hidden:it.hidden,locked:it.locked}; try{r.bounds=_bounds(it);}catch(x){} if(it.typename==='PathItem'){r.filled=it.filled;r.fillColor=_colorOut(it.filled?it.fillColor:null);r.stroked=it.stroked;r.strokeColor=_colorOut(it.stroked?it.strokeColor:null);r.strokeWidth=it.strokeWidth;r.closed=it.closed;} return r;");
|
|
328
|
+
jsxTool("rename_item", "Renames a page item (item names are how other tools address a specific item).", { name: S.string(), newName: S.string() },
|
|
329
|
+
"_needItem(A.name).name=A.newName; return 'ok';");
|
|
330
|
+
jsxTool("delete_item", "Deletes a page item by name.", { name: S.string() },
|
|
331
|
+
"_needItem(A.name).remove(); return 'ok';");
|
|
332
|
+
jsxTool("duplicate_item", "Duplicates a page item. Returns the new item's (auto-generated) name.", { name: S.string(), newName: S.string().optional() },
|
|
333
|
+
"var it=_needItem(A.name); var d=it.duplicate(); if(A.newName)d.name=A.newName; else if(!d.name)d.name=it.name+' copy'; return d.name;");
|
|
334
|
+
jsxTool("set_item_visible", "Shows or hides a page item.", { name: S.string(), visible: S.boolean() },
|
|
335
|
+
"_needItem(A.name).hidden=!A.visible; return 'ok';");
|
|
336
|
+
jsxTool("set_item_locked", "Locks or unlocks a page item.", { name: S.string(), locked: S.boolean() },
|
|
337
|
+
"_needItem(A.name).locked=A.locked; return 'ok';");
|
|
338
|
+
jsxTool("move_item", "Moves a page item to an absolute position (top-left of its bounding box, in points).", { name: S.string(), x: S.number(), y: S.number() },
|
|
339
|
+
"var it=_needItem(A.name); it.position=[A.x, A.y]; return 'ok';");
|
|
340
|
+
jsxTool("translate_item", "Moves a page item relative to its current position, in points.", { name: S.string(), dx: S.number(), dy: S.number() },
|
|
341
|
+
"var it=_needItem(A.name); var p=it.position; it.position=[p[0]+A.dx, p[1]-A.dy]; return 'ok';");
|
|
342
|
+
jsxTool("resize_item", "Scales a page item by percentage (100 = no change) around its center.", { name: S.string(), scaleX: S.number(), scaleY: S.number().optional() },
|
|
343
|
+
"var it=_needItem(A.name); it.resize(A.scaleX, A.scaleY===undefined?A.scaleX:A.scaleY); return 'ok';");
|
|
344
|
+
jsxTool("rotate_item", "Rotates a page item by degrees (positive = counter-clockwise) around its center.", { name: S.string(), angle: S.number() },
|
|
345
|
+
"_needItem(A.name).rotate(A.angle); return 'ok';");
|
|
346
|
+
jsxTool("get_item_bounds", "Geometric bounds (left, top, right, bottom, width, height, in points) of a page item.", { name: S.string() },
|
|
347
|
+
"return _bounds(_needItem(A.name));");
|
|
348
|
+
jsxTool("move_item_to_layer", "Moves a page item into a different layer.", { name: S.string(), layer: S.string() },
|
|
349
|
+
"var it=_needItem(A.name); it.move(_need(A.layer), ElementPlacement.PLACEATEND); return 'ok';");
|
|
350
|
+
jsxTool("bring_to_front", "Brings a page item to the front of its layer's stacking order.", { name: S.string() },
|
|
351
|
+
"_needItem(A.name).zOrder(ZOrderMethod.BRINGTOFRONT); return 'ok';");
|
|
352
|
+
jsxTool("send_to_back", "Sends a page item to the back of its layer's stacking order.", { name: S.string() },
|
|
353
|
+
"_needItem(A.name).zOrder(ZOrderMethod.SENDTOBACK); return 'ok';");
|
|
354
|
+
jsxTool("bring_forward", "Moves a page item one step forward in its layer's stacking order.", { name: S.string() },
|
|
355
|
+
"_needItem(A.name).zOrder(ZOrderMethod.BRINGFORWARD); return 'ok';");
|
|
356
|
+
jsxTool("send_backward", "Moves a page item one step backward in its layer's stacking order.", { name: S.string() },
|
|
357
|
+
"_needItem(A.name).zOrder(ZOrderMethod.SENDBACKWARD); return 'ok';");
|
|
358
|
+
|
|
359
|
+
/* ====================== GROUPING / SELECTION ====================== */
|
|
360
|
+
jsxTool("group_items", "Groups page items together into a new GroupItem. Returns the group's name.", { names: S.array(S.string()), groupName: S.string().optional() },
|
|
361
|
+
"var g=_doc().groupItems.add(); if(A.groupName)g.name=A.groupName; for(var i=0;i<A.names.length;i++){_needItem(A.names[i]).move(g, ElementPlacement.PLACEATEND);} return g.name || '(unnamed group)';");
|
|
362
|
+
jsxTool("ungroup_item", "Ungroups a GroupItem, moving its children up to the group's parent.", { name: S.string() },
|
|
363
|
+
"var g=_needItem(A.name); if(String(g.typename)!=='GroupItem') throw 'Not a group: '+A.name; var parent=g.layer; var kids=[]; for(var i=0;i<g.pageItems.length;i++)kids.push(g.pageItems[i]); for(var j=0;j<kids.length;j++){kids[j].move(parent, ElementPlacement.PLACEATEND);} g.remove(); return kids.length+' item(s) ungrouped';");
|
|
364
|
+
jsxTool("list_group_children", "Lists the direct children of a GroupItem.", { name: S.string() },
|
|
365
|
+
"var g=_needItem(A.name); if(String(g.typename)!=='GroupItem') throw 'Not a group: '+A.name; var out=[]; for(var i=0;i<g.pageItems.length;i++){var it=g.pageItems[i]; out.push({name:it.name,type:String(it.typename)});} return out;");
|
|
366
|
+
jsxTool("select_items", "Sets the active document selection to the given page items (by name). Pass an empty array to clear.", { names: S.array(S.string()) },
|
|
367
|
+
"var sel=[]; for(var i=0;i<A.names.length;i++)sel.push(_needItem(A.names[i])); _doc().selection=sel; return sel.length+' item(s) selected';");
|
|
368
|
+
jsxTool("get_selection", "Lists the page items currently selected in the document.", {},
|
|
369
|
+
"var s=_doc().selection; var out=[]; for(var i=0;i<s.length;i++){out.push({name:s[i].name,type:String(s[i].typename)});} return out;");
|
|
370
|
+
jsxTool("deselect_all", "Clears the active document's selection.", {},
|
|
371
|
+
"_doc().selection=null; return 'ok';");
|
|
372
|
+
|
|
373
|
+
/* ====================== PATHS ====================== */
|
|
374
|
+
jsxTool("create_path_from_points",
|
|
375
|
+
"Creates a PathItem from an array of anchor points ({x,y} in points, document coordinates with Y pointing down as passed here — the tool flips to Illustrator's own Y-up convention internally). Straight segments only (no bezier handles); use set_path_point_handles for curves afterwards.",
|
|
376
|
+
{ points: S.array(S.object({ x: S.number(), y: S.number() })), closed: S.boolean().optional(), name: S.string().optional(), layer: S.string().optional() },
|
|
377
|
+
"var lyr=A.layer?_need(A.layer):_doc().activeLayer; var p=lyr.pathItems.add(); var pts=[]; for(var i=0;i<A.points.length;i++)pts.push([A.points[i].x, -A.points[i].y]); p.setEntirePath(pts); p.closed=!!A.closed; if(A.name)p.name=A.name; return p.name || '(unnamed path)';");
|
|
378
|
+
jsxTool("get_path_points", "Returns a path's anchor points and bezier handles (in the same Y-down convention create_path_from_points takes).", { name: S.string() },
|
|
379
|
+
"var p=_needItem(A.name); if(String(p.typename)!=='PathItem') throw 'Not a path: '+A.name; var out=[]; for(var i=0;i<p.pathPoints.length;i++){var pp=p.pathPoints[i]; out.push({anchor:{x:__pt2(pp.anchor[0]),y:__pt2(-pp.anchor[1])},leftDirection:{x:__pt2(pp.leftDirection[0]),y:__pt2(-pp.leftDirection[1])},rightDirection:{x:__pt2(pp.rightDirection[0]),y:__pt2(-pp.rightDirection[1])},pointType:String(pp.pointType)});} return {closed:p.closed, points:out};");
|
|
380
|
+
jsxTool("set_path_points", "Replaces a path's anchor points wholesale (straight segments; same Y-down convention).", { name: S.string(), points: S.array(S.object({ x: S.number(), y: S.number() })), closed: S.boolean().optional() },
|
|
381
|
+
"var p=_needItem(A.name); if(String(p.typename)!=='PathItem') throw 'Not a path: '+A.name; var pts=[]; for(var i=0;i<A.points.length;i++)pts.push([A.points[i].x, -A.points[i].y]); p.setEntirePath(pts); if(A.closed!==undefined)p.closed=A.closed; return 'ok';");
|
|
382
|
+
jsxTool("set_path_closed", "Opens or closes a path.", { name: S.string(), closed: S.boolean() },
|
|
383
|
+
"var p=_needItem(A.name); p.closed=A.closed; return 'ok';");
|
|
384
|
+
jsxTool("release_compound_path", "Releases a CompoundPathItem back into its individual PathItems.", { name: S.string() },
|
|
385
|
+
"var cp=_needItem(A.name); if(String(cp.typename)!=='CompoundPathItem') throw 'Not a compound path: '+A.name; var parent=cp.layer; var kids=[]; for(var i=0;i<cp.pathItems.length;i++)kids.push(cp.pathItems[i]); for(var j=0;j<kids.length;j++)kids[j].move(parent, ElementPlacement.PLACEATEND); return kids.length+' path(s) released';");
|
|
386
|
+
jsxTool("make_compound_path", "Combines path items into a single CompoundPathItem (e.g. for donut shapes with holes). Returns the compound path's name.", { names: S.array(S.string()), name: S.string().optional() },
|
|
387
|
+
"var sel=[]; for(var i=0;i<A.names.length;i++)sel.push(_needItem(A.names[i])); _doc().selection=sel; app.executeMenuCommand('compoundPath'); var cp=_doc().selection[0]; if(A.name)cp.name=A.name; return cp.name || '(unnamed compound path)';");
|
|
388
|
+
|
|
389
|
+
/* ====================== SHAPES ====================== */
|
|
390
|
+
jsxTool("add_shape_rectangle", "Adds a rectangle. top/left are in points, document coordinates (Y down).", { top: S.number(), left: S.number(), width: S.number(), height: S.number(), name: S.string().optional(), layer: S.string().optional() },
|
|
391
|
+
"var lyr=A.layer?_need(A.layer):_doc().activeLayer; var r=lyr.pathItems.rectangle(_abTop()-A.top, A.left, A.width, A.height); if(A.name)r.name=A.name; return r.name || '(unnamed rectangle)';");
|
|
392
|
+
jsxTool("add_shape_rounded_rectangle", "Adds a rounded rectangle.", { top: S.number(), left: S.number(), width: S.number(), height: S.number(), radius: S.number(), name: S.string().optional(), layer: S.string().optional() },
|
|
393
|
+
"var lyr=A.layer?_need(A.layer):_doc().activeLayer; var r=lyr.pathItems.roundedRectangle(_abTop()-A.top, A.left, A.width, A.height, A.radius, A.radius); if(A.name)r.name=A.name; return r.name || '(unnamed rounded rectangle)';");
|
|
394
|
+
jsxTool("add_shape_ellipse", "Adds an ellipse.", { top: S.number(), left: S.number(), width: S.number(), height: S.number(), name: S.string().optional(), layer: S.string().optional() },
|
|
395
|
+
"var lyr=A.layer?_need(A.layer):_doc().activeLayer; var e=lyr.pathItems.ellipse(_abTop()-A.top, A.left, A.width, A.height); if(A.name)e.name=A.name; return e.name || '(unnamed ellipse)';");
|
|
396
|
+
jsxTool("add_shape_line", "Adds a straight line segment between two points.", { x1: S.number(), y1: S.number(), x2: S.number(), y2: S.number(), name: S.string().optional(), layer: S.string().optional() },
|
|
397
|
+
"var lyr=A.layer?_need(A.layer):_doc().activeLayer; var abT=_abTop(); var l=lyr.pathItems.add(); l.setEntirePath([[A.x1,abT-A.y1],[A.x2,abT-A.y2]]); if(A.name)l.name=A.name; return l.name || '(unnamed line)';");
|
|
398
|
+
jsxTool("add_shape_polygon", "Adds a regular polygon centered at (cx, cy).", { cx: S.number(), cy: S.number(), radius: S.number(), sides: S.number().min(3), name: S.string().optional(), layer: S.string().optional() },
|
|
399
|
+
"var lyr=A.layer?_need(A.layer):_doc().activeLayer; var p=lyr.pathItems.polygon(A.cx, _abTop()-A.cy, A.radius, A.sides); if(A.name)p.name=A.name; return p.name || '(unnamed polygon)';");
|
|
400
|
+
jsxTool("add_shape_star", "Adds a star centered at (cx, cy).", { cx: S.number(), cy: S.number(), outerRadius: S.number(), innerRadius: S.number(), points: S.number().min(3), name: S.string().optional(), layer: S.string().optional() },
|
|
401
|
+
"var lyr=A.layer?_need(A.layer):_doc().activeLayer; var s=lyr.pathItems.star(A.cx, _abTop()-A.cy, A.outerRadius, A.innerRadius, A.points); if(A.name)s.name=A.name; return s.name || '(unnamed star)';");
|
|
402
|
+
|
|
403
|
+
/* ====================== TEXT ====================== */
|
|
404
|
+
jsxTool("add_text_frame", "Adds a point-text frame (text anchored at a single point, grows with content) at (x, y).", { x: S.number(), y: S.number(), contents: S.string(), name: S.string().optional(), layer: S.string().optional() },
|
|
405
|
+
"var lyr=A.layer?_need(A.layer):_doc().activeLayer; var t=lyr.textFrames.pointText([A.x, _abTop()-A.y]); t.contents=A.contents; if(A.name)t.name=A.name; return t.name || '(unnamed text frame)';");
|
|
406
|
+
jsxTool("add_area_text_frame", "Adds an area-text frame: text flows inside a rectangle and wraps.", { top: S.number(), left: S.number(), width: S.number(), height: S.number(), contents: S.string(), name: S.string().optional(), layer: S.string().optional() },
|
|
407
|
+
"var lyr=A.layer?_need(A.layer):_doc().activeLayer; var rect=lyr.pathItems.rectangle(_abTop()-A.top, A.left, A.width, A.height); rect.filled=false; rect.stroked=false; var t=lyr.textFrames.areaText(rect); t.contents=A.contents; if(A.name)t.name=A.name; return t.name || '(unnamed area text)';");
|
|
408
|
+
jsxTool("get_text_contents", "Returns a text frame's string contents.", { name: S.string() },
|
|
409
|
+
"var t=_needItem(A.name); return String(t.contents);");
|
|
410
|
+
jsxTool("set_text_contents", "Sets a text frame's string contents.", { name: S.string(), contents: S.string() },
|
|
411
|
+
"_needItem(A.name).contents=A.contents; return 'ok';");
|
|
412
|
+
jsxTool("set_text_style",
|
|
413
|
+
"Sets character-level style for a text frame's full contents: font (PostScript name, e.g. 'Helvetica-Bold'), size (pt), fill color, and/or tracking.",
|
|
414
|
+
{ name: S.string(), font: S.string().optional(), size: S.number().optional(), color: S.object({ r: S.number(), g: S.number(), b: S.number() }).optional(), tracking: S.number().optional() },
|
|
415
|
+
"var t=_needItem(A.name); var attrs=t.textRange.characterAttributes; if(A.font)attrs.textFont=app.textFonts.getByName(A.font); if(A.size!==undefined)attrs.size=A.size; if(A.color)attrs.fillColor=_color(A.color); if(A.tracking!==undefined)attrs.tracking=A.tracking; return 'ok';");
|
|
416
|
+
jsxTool("set_text_justification", "Sets a text frame's paragraph alignment.", { name: S.string(), align: S.enum(["left", "center", "right", "justify"]) },
|
|
417
|
+
"var map={left:Justification.LEFT,center:Justification.CENTER,right:Justification.RIGHT,justify:Justification.FULLJUSTIFY}; _needItem(A.name).textRange.paragraphAttributes.justification=map[A.align]; return 'ok';");
|
|
418
|
+
jsxTool("list_fonts", "Lists a sample of available font PostScript names (first 200 — Illustrator can have thousands installed).", {},
|
|
419
|
+
"var out=[]; var n=Math.min(app.textFonts.length,200); for(var i=0;i<n;i++)out.push(app.textFonts[i].name); return out;");
|
|
420
|
+
jsxTool("outline_text", "Converts a text frame to outlines (editable vector paths). Returns the resulting group's name — the original text frame is replaced.", { name: S.string(), newName: S.string().optional() },
|
|
421
|
+
"var t=_needItem(A.name); var g=t.createOutline(); if(A.newName)g.name=A.newName; return g.name || '(unnamed outline group)';");
|
|
422
|
+
|
|
423
|
+
/* ====================== COLOR / FILL / STROKE ====================== */
|
|
424
|
+
jsxTool("set_fill_color", "Sets a page item's fill color, or removes the fill if color is omitted.", { name: S.string(), color: S.object({ r: S.number().optional(), g: S.number().optional(), b: S.number().optional(), c: S.number().optional(), m: S.number().optional(), y: S.number().optional(), k: S.number().optional(), gray: S.number().optional() }).optional() },
|
|
425
|
+
"var it=_needItem(A.name); if(!A.color){it.filled=false;}else{it.filled=true; it.fillColor=_color(A.color);} return 'ok';");
|
|
426
|
+
jsxTool("set_stroke_color", "Sets a page item's stroke color, or removes the stroke if color is omitted.", { name: S.string(), color: S.object({ r: S.number().optional(), g: S.number().optional(), b: S.number().optional(), c: S.number().optional(), m: S.number().optional(), y: S.number().optional(), k: S.number().optional(), gray: S.number().optional() }).optional() },
|
|
427
|
+
"var it=_needItem(A.name); if(!A.color){it.stroked=false;}else{it.stroked=true; it.strokeColor=_color(A.color);} return 'ok';");
|
|
428
|
+
jsxTool("set_stroke_width", "Sets a page item's stroke width, in points.", { name: S.string(), width: S.number() },
|
|
429
|
+
"var it=_needItem(A.name); it.stroked=true; it.strokeWidth=A.width; return 'ok';");
|
|
430
|
+
jsxTool("set_opacity", "Sets a page item's opacity (0-100).", { name: S.string(), opacity: S.number().min(0).max(100) },
|
|
431
|
+
"_needItem(A.name).opacity=A.opacity; return 'ok';");
|
|
432
|
+
jsxTool("set_blend_mode", "Sets a page item's blend mode.", { name: S.string(), mode: S.enum(["normal", "multiply", "screen", "overlay", "darken", "lighten", "colorDodge", "colorBurn", "hardLight", "softLight", "difference", "exclusion", "hue", "saturation", "color", "luminosity"]) },
|
|
433
|
+
"var map={normal:BlendModes.NORMAL,multiply:BlendModes.MULTIPLY,screen:BlendModes.SCREEN,overlay:BlendModes.OVERLAY,darken:BlendModes.DARKEN,lighten:BlendModes.LIGHTEN,colorDodge:BlendModes.COLORDODGE,colorBurn:BlendModes.COLORBURN,hardLight:BlendModes.HARDLIGHT,softLight:BlendModes.SOFTLIGHT,difference:BlendModes.DIFFERENCE,exclusion:BlendModes.EXCLUSION,hue:BlendModes.HUE,saturation:BlendModes.SATURATIONBLEND,color:BlendModes.COLORBLEND,luminosity:BlendModes.LUMINOSITY}; _needItem(A.name).blendingMode=map[A.mode]; return 'ok';");
|
|
434
|
+
jsxTool("list_swatches", "Lists the document's color swatches.", {},
|
|
435
|
+
"var out=[]; var sw=_doc().swatches; for(var i=0;i<sw.length;i++){out.push({name:sw[i].name,color:_colorOut(sw[i].color)});} return out;");
|
|
436
|
+
jsxTool("add_swatch", "Adds a new color swatch to the document.", { name: S.string(), color: S.object({ r: S.number().optional(), g: S.number().optional(), b: S.number().optional(), c: S.number().optional(), m: S.number().optional(), y: S.number().optional(), k: S.number().optional(), gray: S.number().optional() }) },
|
|
437
|
+
"var s=_doc().swatches.add(); s.name=A.name; s.color=_color(A.color); return s.name;");
|
|
438
|
+
jsxTool("apply_swatch", "Applies an existing swatch as a page item's fill or stroke.", { name: S.string(), swatch: S.string(), target: S.enum(["fill", "stroke"]).default("fill") },
|
|
439
|
+
"var it=_needItem(A.name); var sw=_doc().swatches.getByName(A.swatch); if(A.target==='stroke'){it.stroked=true; it.strokeColor=sw.color;}else{it.filled=true; it.fillColor=sw.color;} return 'ok';");
|
|
440
|
+
|
|
441
|
+
/* ====================== ARTBOARDS ====================== */
|
|
442
|
+
jsxTool("list_artboards", "Lists the document's artboards (name, bounds).", {},
|
|
443
|
+
"var out=[]; var abs=_doc().artboards; for(var i=0;i<abs.length;i++){var r=abs[i].artboardRect; out.push({index:i,name:abs[i].name,left:__pt2(r[0]),top:__pt2(r[1]),right:__pt2(r[2]),bottom:__pt2(r[3]),width:__pt2(r[2]-r[0]),height:__pt2(r[1]-r[3])});} return out;");
|
|
444
|
+
jsxTool("add_artboard", "Adds a new artboard.", { left: S.number(), top: S.number(), width: S.number(), height: S.number(), name: S.string().optional() },
|
|
445
|
+
"var ab=_doc().artboards.add([A.left, A.top, A.left+A.width, A.top-A.height]); if(A.name)ab.name=A.name; return ab.name;");
|
|
446
|
+
jsxTool("rename_artboard", "Renames an artboard by index.", { index: S.number(), name: S.string() },
|
|
447
|
+
"_doc().artboards[A.index].name=A.name; return 'ok';");
|
|
448
|
+
jsxTool("delete_artboard", "Deletes an artboard by index (a document must keep at least one).", { index: S.number() },
|
|
449
|
+
"_doc().artboards.remove(A.index); return 'ok';");
|
|
450
|
+
jsxTool("set_active_artboard", "Sets which artboard index is active (affects tools that default to 'the active artboard', e.g. get_document_info).", { index: S.number() },
|
|
451
|
+
"_doc().artboards.setActiveArtboardIndex(A.index); return 'ok';");
|
|
452
|
+
|
|
453
|
+
/* ====================== EXPORT ====================== */
|
|
454
|
+
jsxTool("export_png", "Exports the active document (or one artboard) as a transparent PNG-24.", { path: S.string(), scale: S.number().default(100), artboardIndex: S.number().optional(), transparent: S.boolean().default(true) },
|
|
455
|
+
"var opts=new ExportOptionsPNG24(); opts.artBoardClipping=true; opts.horizontalScale=A.scale; opts.verticalScale=A.scale; opts.transparency=A.transparent; if(A.artboardIndex!==undefined)_doc().artboards.setActiveArtboardIndex(A.artboardIndex); _doc().exportFile(new File(A.path), ExportType.PNG24, opts); return 'exported: '+A.path;");
|
|
456
|
+
jsxTool("export_jpg", "Exports the active document (or one artboard) as a JPEG.", { path: S.string(), scale: S.number().default(100), quality: S.number().min(0).max(100).default(90), artboardIndex: S.number().optional() },
|
|
457
|
+
"var opts=new ExportOptionsJPEG(); opts.artBoardClipping=true; opts.horizontalScale=A.scale; opts.verticalScale=A.scale; opts.qualitySetting=A.quality; if(A.artboardIndex!==undefined)_doc().artboards.setActiveArtboardIndex(A.artboardIndex); _doc().exportFile(new File(A.path), ExportType.JPEG, opts); return 'exported: '+A.path;");
|
|
458
|
+
jsxTool("export_svg", "Exports the active document as SVG.", { path: S.string() },
|
|
459
|
+
"var opts=new ExportOptionsSVG(); _doc().exportFile(new File(A.path), ExportType.SVG, opts); return 'exported: '+A.path;");
|
|
460
|
+
jsxTool("export_pdf", "Saves the active document as a PDF (via Save As, not Export — PDF is a native save format in Illustrator).", { path: S.string() },
|
|
461
|
+
"var opts=new PDFSaveOptions(); _doc().saveAs(new File(A.path), opts); return 'saved: '+A.path;");
|
|
462
|
+
jsxTool("save_document_as", "Saves the active document as a new .ai file.", { path: S.string() },
|
|
463
|
+
"_doc().saveAs(new File(A.path)); return 'saved: '+A.path;");
|
|
464
|
+
|
|
465
|
+
/* ====================== GRADIENTS ====================== */
|
|
466
|
+
// GradientColor.angle/.length are confirmed broken in this Illustrator
|
|
467
|
+
// version when set via script (silently ignored, always reads back 0/80
|
|
468
|
+
// regardless of what's assigned) -- verified live, not shipped as params.
|
|
469
|
+
jsxTool("list_gradients", "Lists the document's gradients (name, type, stop count).", {},
|
|
470
|
+
"var out=[]; var gs=_doc().gradients; for(var i=0;i<gs.length;i++){out.push({name:gs[i].name,type:String(gs[i].type),stops:gs[i].gradientStops.length});} return out;");
|
|
471
|
+
jsxTool("create_gradient",
|
|
472
|
+
"Creates a linear or radial gradient with 2+ color stops (position 0-100). Returns the gradient's name.",
|
|
473
|
+
{ name: S.string(), type: S.enum(["linear", "radial"]).default("linear"), stops: S.array(S.object({ position: S.number().min(0).max(100), color: S.object({ r: S.number().optional(), g: S.number().optional(), b: S.number().optional(), c: S.number().optional(), m: S.number().optional(), y: S.number().optional(), k: S.number().optional(), gray: S.number().optional() }), opacity: S.number().min(0).max(100).optional() })).min(2) },
|
|
474
|
+
"var g=_doc().gradients.add(); g.name=A.name; g.type=A.type==='radial'?GradientType.RADIAL:GradientType.LINEAR; var stops=g.gradientStops; while(stops.length<A.stops.length)stops.add(); for(var i=0;i<A.stops.length;i++){var s=stops[i]; s.rampPoint=A.stops[i].position; s.color=_color(A.stops[i].color); if(A.stops[i].opacity!==undefined)s.opacity=A.stops[i].opacity;} return g.name;");
|
|
475
|
+
jsxTool("get_gradient", "Returns a gradient's type and stops.", { name: S.string() },
|
|
476
|
+
"var g=_doc().gradients.getByName(A.name); var out=[]; for(var i=0;i<g.gradientStops.length;i++){var s=g.gradientStops[i]; out.push({position:s.rampPoint,opacity:s.opacity,color:_colorOut(s.color)});} return {type:String(g.type), stops:out};");
|
|
477
|
+
jsxTool("delete_gradient", "Deletes a gradient from the document.", { name: S.string() },
|
|
478
|
+
"_doc().gradients.getByName(A.name).remove(); return 'ok';");
|
|
479
|
+
jsxTool("apply_gradient_fill", "Fills a page item with an existing gradient.", { name: S.string(), gradient: S.string() },
|
|
480
|
+
"var it=_needItem(A.name); var gc=new GradientColor(); gc.gradient=_doc().gradients.getByName(A.gradient); it.filled=true; it.fillColor=gc; return 'ok';");
|
|
481
|
+
jsxTool("apply_gradient_stroke", "Strokes a page item with an existing gradient.", { name: S.string(), gradient: S.string() },
|
|
482
|
+
"var it=_needItem(A.name); var gc=new GradientColor(); gc.gradient=_doc().gradients.getByName(A.gradient); it.stroked=true; it.strokeColor=gc; return 'ok';");
|
|
483
|
+
|
|
484
|
+
/* ====================== SYMBOLS ====================== */
|
|
485
|
+
jsxTool("list_symbols", "Lists the document's symbols.", {},
|
|
486
|
+
"var out=[]; var ss=_doc().symbols; for(var i=0;i<ss.length;i++)out.push(ss[i].name); return out;");
|
|
487
|
+
jsxTool("create_symbol_from_item", "Creates a new symbol definition from a page item. Illustrator replaces the source item in place with an instance of the new symbol.", { name: S.string(), itemName: S.string() },
|
|
488
|
+
"var it=_needItem(A.itemName); var s=_doc().symbols.add(it); s.name=A.name; return s.name;");
|
|
489
|
+
jsxTool("place_symbol_instance", "Places an instance of an existing symbol at (x, y).", { symbol: S.string(), x: S.number(), y: S.number(), name: S.string().optional(), layer: S.string().optional() },
|
|
490
|
+
"var lyr=A.layer?_need(A.layer):_doc().activeLayer; var sym=_needSymbol(A.symbol); var inst=lyr.symbolItems.add(sym); inst.position=[A.x, _abTop()-A.y]; if(A.name)inst.name=A.name; return inst.name || '(unnamed symbol instance)';");
|
|
491
|
+
jsxTool("break_symbol_link", "Breaks a symbol instance's link, converting it back to regular editable artwork. Returns the resulting item's name.", { name: S.string() },
|
|
492
|
+
"var inst=_needItem(A.name); if(String(inst.typename)!=='SymbolItem') throw 'Not a symbol instance: '+A.name; var art=inst.breakLink(); return art.name || '(unnamed)';");
|
|
493
|
+
jsxTool("set_symbol_instance_symbol", "Re-points an existing symbol instance to a different symbol definition.", { name: S.string(), symbol: S.string() },
|
|
494
|
+
"var inst=_needItem(A.name); if(String(inst.typename)!=='SymbolItem') throw 'Not a symbol instance: '+A.name; inst.symbol=_needSymbol(A.symbol); return 'ok';");
|
|
495
|
+
jsxTool("rename_symbol", "Renames a symbol definition.", { name: S.string(), newName: S.string() },
|
|
496
|
+
"_needSymbol(A.name).name=A.newName; return 'ok';");
|
|
497
|
+
jsxTool("delete_symbol", "Deletes a symbol definition from the document (existing instances become regular art).", { name: S.string() },
|
|
498
|
+
"_needSymbol(A.name).remove(); return 'ok';");
|
|
499
|
+
|
|
500
|
+
/* ====================== CLIPPING MASKS ====================== */
|
|
501
|
+
jsxTool("make_clipping_mask",
|
|
502
|
+
"Groups items and uses one of them (maskItemName, must be a path/compound path) as a clipping mask for the rest. Returns the group's name.",
|
|
503
|
+
{ names: S.array(S.string()).min(2), maskItemName: S.string(), groupName: S.string().optional() },
|
|
504
|
+
"var mask=_needItem(A.maskItemName); var g=_doc().groupItems.add(); mask.move(g, ElementPlacement.PLACEATBEGINNING); for(var i=0;i<A.names.length;i++){if(A.names[i]===A.maskItemName)continue; _needItem(A.names[i]).move(g, ElementPlacement.PLACEATEND);} g.clipped=true; if(A.groupName)g.name=A.groupName; return g.name || '(unnamed clip group)';");
|
|
505
|
+
jsxTool("release_clipping_mask", "Releases a clipping group back into its individual items (un-clips and ungroups).", { name: S.string() },
|
|
506
|
+
"var g=_needItem(A.name); if(String(g.typename)!=='GroupItem') throw 'Not a group: '+A.name; g.clipped=false; var parent=g.layer; var kids=[]; for(var i=0;i<g.pageItems.length;i++)kids.push(g.pageItems[i]); for(var j=0;j<kids.length;j++)kids[j].move(parent, ElementPlacement.PLACEATEND); g.remove(); return kids.length+' item(s) released';");
|
|
507
|
+
jsxTool("set_group_clipped", "Low-level: directly sets whether an existing group's topmost item is used as its clipping mask.", { name: S.string(), clipped: S.boolean() },
|
|
508
|
+
"var g=_needItem(A.name); if(String(g.typename)!=='GroupItem') throw 'Not a group: '+A.name; g.clipped=A.clipped; return 'ok';");
|
|
509
|
+
|
|
510
|
+
/* ====================== BRUSHES ====================== */
|
|
511
|
+
jsxTool("list_brushes", "Lists the document's brushes by name (Illustrator's scripting API doesn't expose the brush type -- art/calligraphic/pattern/scatter -- so it isn't included).", {},
|
|
512
|
+
"var out=[]; var bs=_doc().brushes; for(var i=0;i<bs.length;i++)out.push(bs[i].name); return out;");
|
|
513
|
+
jsxTool("apply_brush_to_path", "Applies an existing brush to a path's stroke.", { brush: S.string(), name: S.string() },
|
|
514
|
+
"var it=_needItem(A.name); it.stroked=true; _needBrush(A.brush).applyTo(it); return 'ok';");
|
|
515
|
+
|
|
516
|
+
/* ====================== ALIGN / DISTRIBUTE ====================== */
|
|
517
|
+
// Pure geometry (no executeMenuCommand): computed from each item's own
|
|
518
|
+
// geometricBounds, so it works headlessly and doesn't depend on undocumented
|
|
519
|
+
// internal menu-command IDs.
|
|
520
|
+
jsxTool("align_items",
|
|
521
|
+
"Aligns items to their own combined bounding box on one edge/axis.",
|
|
522
|
+
{ names: S.array(S.string()).min(1), edge: S.enum(["left", "right", "top", "bottom", "centerH", "centerV"]) },
|
|
523
|
+
"var items=_itemsByNames(A.names); var L=Infinity,R=-Infinity,T=-Infinity,B=Infinity; for(var i=0;i<items.length;i++){var b=items[i].geometricBounds; if(b[0]<L)L=b[0]; if(b[2]>R)R=b[2]; if(b[1]>T)T=b[1]; if(b[3]<B)B=b[3];} for(var j=0;j<items.length;j++){var it=items[j],b2=it.geometricBounds,p=it.position; var w=b2[2]-b2[0],h=b2[1]-b2[3]; if(A.edge==='left')it.position=[L,p[1]]; else if(A.edge==='right')it.position=[R-w,p[1]]; else if(A.edge==='top')it.position=[p[0],T]; else if(A.edge==='bottom')it.position=[p[0],B+h]; else if(A.edge==='centerH')it.position=[(L+R)/2-w/2,p[1]]; else if(A.edge==='centerV')it.position=[p[0],(T+B)/2+h/2];} return items.length+' item(s) aligned';");
|
|
524
|
+
jsxTool("distribute_items",
|
|
525
|
+
"Distributes 3+ items with equal spacing between their edges, along one axis.",
|
|
526
|
+
{ names: S.array(S.string()).min(3), axis: S.enum(["horizontal", "vertical"]) },
|
|
527
|
+
"var items=_itemsByNames(A.names); var horiz=A.axis==='horizontal'; items.sort(function(a,b){var ba=a.geometricBounds,bb=b.geometricBounds; return horiz?(ba[0]-bb[0]):(bb[1]-ba[1]);}); var first=items[0].geometricBounds, last=items[items.length-1].geometricBounds; if(horiz){var span=last[2]-first[0]; var totalW=0; for(var i=0;i<items.length;i++){var b=items[i].geometricBounds; totalW+=(b[2]-b[0]);} var gap=(span-totalW)/(items.length-1); var x=first[0]; for(var j=0;j<items.length;j++){var it=items[j],b2=it.geometricBounds,p=it.position; it.position=[x,p[1]]; x+=(b2[2]-b2[0])+gap;}}else{var spanV=first[1]-last[3]; var totalH=0; for(var i2=0;i2<items.length;i2++){var b3=items[i2].geometricBounds; totalH+=(b3[1]-b3[3]);} var gapV=(spanV-totalH)/(items.length-1); var y=first[1]; for(var k=0;k<items.length;k++){var it2=items[k],b4=it2.geometricBounds,p2=it2.position,h=b4[1]-b4[3]; it2.position=[p2[0],y]; y-=h+gapV;}} return items.length+' item(s) distributed';");
|
|
528
|
+
|
|
529
|
+
/* ====================== TRANSFORM (advanced) ====================== */
|
|
530
|
+
jsxTool("flip_item", "Flips a page item horizontally and/or vertically around its own center.", { name: S.string(), horizontal: S.boolean().optional(), vertical: S.boolean().optional() },
|
|
531
|
+
"var it=_needItem(A.name); it.resize(A.horizontal?-100:100, A.vertical?-100:100); return 'ok';");
|
|
532
|
+
jsxTool("shear_item", "Shears (slants) a page item by degrees along one axis, around its own center.", { name: S.string(), angle: S.number(), axis: S.enum(["horizontal", "vertical"]).default("horizontal") },
|
|
533
|
+
"var it=_needItem(A.name); var rad=A.angle*Math.PI/180; var m=app.getIdentityMatrix(); if(A.axis==='horizontal')m.mValueC=Math.tan(rad); else m.mValueB=Math.tan(rad); it.transform(m, true, true, true, true, 100, Transformation.CENTER); return 'ok';");
|
|
534
|
+
|
|
535
|
+
/* ====================== GUIDES ====================== */
|
|
536
|
+
jsxTool("convert_item_to_guide", "Converts a path item into a non-printing ruler guide.", { name: S.string() },
|
|
537
|
+
"var it=_needItem(A.name); it.guides=true; return 'ok';");
|
|
538
|
+
jsxTool("release_guide", "Converts a guide back into a regular, editable path.", { name: S.string() },
|
|
539
|
+
"var it=_needItem(A.name); it.guides=false; return 'ok';");
|
|
540
|
+
|
|
541
|
+
/* ====================== SWATCHES / COLOR GROUPS (advanced) ====================== */
|
|
542
|
+
jsxTool("delete_swatch", "Deletes a color swatch from the document.", { name: S.string() },
|
|
543
|
+
"_needSwatch(A.name).remove(); return 'ok';");
|
|
544
|
+
jsxTool("list_swatch_groups", "Lists the document's swatch groups (color groups) and the swatches in each.", {},
|
|
545
|
+
"var out=[]; var sgs=_doc().swatchGroups; for(var i=0;i<sgs.length;i++){var g=sgs[i]; var names=[]; var sw=g.getAllSwatches(); for(var j=0;j<sw.length;j++)names.push(sw[j].name); out.push({name:g.name,swatches:names});} return out;");
|
|
546
|
+
jsxTool("create_swatch_group", "Creates a new swatch group (color group) containing the given existing swatches.", { name: S.string(), swatches: S.array(S.string()).min(1) },
|
|
547
|
+
"var g=_doc().swatchGroups.add(); g.name=A.name; for(var i=0;i<A.swatches.length;i++)g.addSwatch(_needSwatch(A.swatches[i])); return g.name;");
|
|
548
|
+
jsxTool("add_spot_color", "Creates a new spot color from a CMYK/RGB/gray value and registers it in the document's swatches.", { name: S.string(), color: S.object({ r: S.number().optional(), g: S.number().optional(), b: S.number().optional(), c: S.number().optional(), m: S.number().optional(), y: S.number().optional(), k: S.number().optional(), gray: S.number().optional() }) },
|
|
549
|
+
"var spot=_doc().spots.add(); spot.name=A.name; spot.colorType=ColorModel.SPOT; spot.color=_color(A.color); return spot.name;");
|
|
550
|
+
jsxTool("list_spot_colors", "Lists the document's spot colors.", {},
|
|
551
|
+
"var out=[]; var sp=_doc().spots; for(var i=0;i<sp.length;i++)if(String(sp[i].colorType)==='ColorModel.SPOT')out.push(sp[i].name); return out;");
|
|
552
|
+
|
|
553
|
+
/* ====================== STROKE (advanced) ====================== */
|
|
554
|
+
jsxTool("set_stroke_dashes", "Sets a path's dash pattern (lengths in points, e.g. [4,2] for dash-gap-dash-gap). Pass an empty array for a solid line.", { name: S.string(), dashes: S.array(S.number()) },
|
|
555
|
+
"var it=_needItem(A.name); it.strokeDashes=A.dashes; return 'ok';");
|
|
556
|
+
jsxTool("set_stroke_cap", "Sets a path's stroke end cap style.", { name: S.string(), cap: S.enum(["butt", "round", "projecting"]) },
|
|
557
|
+
"var map={butt:StrokeCap.BUTTENDCAP,round:StrokeCap.ROUNDENDCAP,projecting:StrokeCap.PROJECTINGENDCAP}; _needItem(A.name).strokeCap=map[A.cap]; return 'ok';");
|
|
558
|
+
jsxTool("set_stroke_join", "Sets a path's stroke corner join style.", { name: S.string(), join: S.enum(["miter", "round", "bevel"]) },
|
|
559
|
+
"var map={miter:StrokeJoin.MITERENDJOIN,round:StrokeJoin.ROUNDENDJOIN,bevel:StrokeJoin.BEVELENDJOIN}; _needItem(A.name).strokeJoin=map[A.join]; return 'ok';");
|
|
560
|
+
jsxTool("set_stroke_miter_limit", "Sets a path's miter limit (1-500; only affects miter joins).", { name: S.string(), limit: S.number().min(1).max(500) },
|
|
561
|
+
"_needItem(A.name).strokeMiterLimit=A.limit; return 'ok';");
|
|
562
|
+
|
|
563
|
+
/* ====================== TEXT (advanced) ====================== */
|
|
564
|
+
jsxTool("find_replace_text", "Finds and replaces a substring across every text frame in the active document. Returns how many frames were changed.", { find: S.string(), replace: S.string() },
|
|
565
|
+
"var tf=_doc().textFrames; var count=0; for(var i=0;i<tf.length;i++){var s=String(tf[i].contents); if(s.indexOf(A.find)>=0){tf[i].contents=s.split(A.find).join(A.replace); count++;}} return count+' text frame(s) changed';");
|
|
566
|
+
jsxTool("set_text_leading", "Sets a text frame's line leading (spacing), in points. Turns off auto-leading (text defaults to it), since a leading value assigned while auto-leading is on is silently ignored.", { name: S.string(), leading: S.number() },
|
|
567
|
+
"var attrs=_needItem(A.name).textRange.characterAttributes; attrs.autoLeading=false; attrs.leading=A.leading; return 'ok';");
|
|
568
|
+
jsxTool("list_paragraph_styles", "Lists the document's named paragraph styles.", {},
|
|
569
|
+
"var out=[]; var ps=_doc().paragraphStyles; for(var i=0;i<ps.length;i++)out.push(ps[i].name); return out;");
|
|
570
|
+
jsxTool("list_character_styles", "Lists the document's named character styles.", {},
|
|
571
|
+
"var out=[]; var cs=_doc().characterStyles; for(var i=0;i<cs.length;i++)out.push(cs[i].name); return out;");
|
|
572
|
+
jsxTool("apply_paragraph_style", "Applies a named paragraph style to a text frame's full contents.", { name: S.string(), style: S.string() },
|
|
573
|
+
"var t=_needItem(A.name); _doc().paragraphStyles.getByName(A.style).applyTo(t.textRange, true); return 'ok';");
|
|
574
|
+
jsxTool("apply_character_style", "Applies a named character style to a text frame's full contents.", { name: S.string(), style: S.string() },
|
|
575
|
+
"var t=_needItem(A.name); _doc().characterStyles.getByName(A.style).applyTo(t.textRange, true); return 'ok';");
|
|
576
|
+
|
|
577
|
+
/* ====================== LINKS / PLACED ITEMS ====================== */
|
|
578
|
+
jsxTool("list_placed_items", "Lists placed (linked or embedded) raster/vector files in the document.", {},
|
|
579
|
+
"var out=[]; var pl=_doc().placedItems; for(var i=0;i<pl.length;i++){var p=pl[i]; var f=null; try{f=p.file?p.file.fsName:null;}catch(e){} out.push({name:p.name,file:f});} return out;");
|
|
580
|
+
jsxTool("relink_placed_item", "Repoints a placed item to a different file on disk.", { name: S.string(), path: S.string() },
|
|
581
|
+
"var it=_needItem(A.name); if(String(it.typename)!=='PlacedItem') throw 'Not a placed item: '+A.name; it.file=new File(A.path); return 'ok';");
|
|
582
|
+
jsxTool("embed_placed_item", "Embeds a linked placed item into the document (no longer references the external file).", { name: S.string() },
|
|
583
|
+
"var it=_needItem(A.name); if(String(it.typename)!=='PlacedItem') throw 'Not a placed item: '+A.name; it.embed(); return 'ok';");
|
|
584
|
+
|
|
585
|
+
/* ====================== LAYERS (advanced) ====================== */
|
|
586
|
+
jsxTool("set_layer_color", "Sets a layer's selection/outline color (shown in the Layers panel and as the selection-highlight color for its items), not its artwork.", { name: S.string(), color: S.object({ r: S.number(), g: S.number(), b: S.number() }) },
|
|
587
|
+
"_need(A.name).color=_color(A.color); return 'ok';");
|
|
588
|
+
jsxTool("duplicate_layer", "Duplicates a layer and everything on it (Layer has no direct duplicate() in Illustrator's DOM, so this creates a new layer and duplicates each item into it). Returns the new layer's name.", { name: S.string(), newName: S.string().optional() },
|
|
589
|
+
"var L=_need(A.name); var d=_doc().layers.add(); d.name=A.newName||(L.name+' copy'); d.move(L, ElementPlacement.PLACEAFTER); var kids=[]; for(var i=0;i<L.pageItems.length;i++)kids.push(L.pageItems[i]); for(var j=0;j<kids.length;j++)kids[j].duplicate(d, ElementPlacement.PLACEATEND); return d.name;");
|
|
590
|
+
jsxTool("move_layer", "Reorders a layer to the front, back, or relative to another layer.", { name: S.string(), position: S.enum(["front", "back", "before", "after"]), relativeTo: S.string().optional() },
|
|
591
|
+
"var L=_need(A.name); if(A.position==='front')L.zOrder(ZOrderMethod.BRINGTOFRONT); else if(A.position==='back')L.zOrder(ZOrderMethod.SENDTOBACK); else if(A.position==='before')L.move(_need(A.relativeTo), ElementPlacement.PLACEBEFORE); else if(A.position==='after')L.move(_need(A.relativeTo), ElementPlacement.PLACEAFTER); return 'ok';");
|
|
592
|
+
|
|
593
|
+
/* ====================== SELECTION (advanced) ====================== */
|
|
594
|
+
jsxTool("select_same_fill_color", "Selects every page item in the document whose fill color matches a reference item's fill color exactly.", { referenceName: S.string() },
|
|
595
|
+
"function _colorEq(a,b){if(a.type!==b.type)return false; if(a.type==='rgb')return a.r===b.r&&a.g===b.g&&a.b===b.b; if(a.type==='cmyk')return a.c===b.c&&a.m===b.m&&a.y===b.y&&a.k===b.k; if(a.type==='gray')return a.gray===b.gray; if(a.type==='spot')return a.name===b.name; return true;} var ref=_needItem(A.referenceName); if(!ref.filled) throw 'Reference item has no fill: '+A.referenceName; var refOut=_colorOut(ref.fillColor); var items=_doc().pageItems; var sel=[]; for(var i=0;i<items.length;i++){var it=items[i]; try{if(it.filled && _colorEq(_colorOut(it.fillColor),refOut))sel.push(it);}catch(e){}} _doc().selection=sel; return sel.length+' item(s) selected';");
|
|
596
|
+
jsxTool("invert_selection", "Selects every page item that is NOT currently selected, and deselects everything that was.", {},
|
|
597
|
+
"var cur=_doc().selection; var all=_doc().pageItems; var inv=[]; for(var j=0;j<all.length;j++){var found=false; for(var k=0;k<cur.length;k++)if(cur[k]===all[j]){found=true;break;} if(!found)inv.push(all[j]);} _doc().selection=inv; return inv.length+' item(s) selected';");
|
|
598
|
+
|
|
599
|
+
/* ====================== DOCUMENT (advanced) ====================== */
|
|
600
|
+
jsxTool("get_ruler_units", "Returns the document's ruler units.", {},
|
|
601
|
+
"return String(_doc().rulerUnits);");
|
|
602
|
+
jsxTool("set_ruler_units", "Sets the document's ruler units.", { units: S.enum(["points", "inches", "millimeters", "centimeters", "pixels", "picas"]) },
|
|
603
|
+
"var map={points:RulerUnits.Points,inches:RulerUnits.Inches,millimeters:RulerUnits.Millimeters,centimeters:RulerUnits.Centimeters,pixels:RulerUnits.Pixels,picas:RulerUnits.Picas}; _doc().rulerUnits=map[A.units]; return 'ok';");
|
|
604
|
+
|
|
605
|
+
/* ====================== PATTERNS ====================== */
|
|
606
|
+
jsxTool("list_patterns", "Lists the document's pattern swatches.", {},
|
|
607
|
+
"var out=[]; var ps=_doc().patterns; for(var i=0;i<ps.length;i++)out.push(ps[i].name); return out;");
|
|
608
|
+
jsxTool("apply_pattern_fill", "Fills a page item with an existing pattern swatch.", { name: S.string(), pattern: S.string() },
|
|
609
|
+
"var it=_needItem(A.name); it.filled=true; it.fillColor=_doc().patterns.getByName(A.pattern); return 'ok';");
|
|
610
|
+
|
|
611
|
+
/* ====================== EFFECTS (Live Effects appearance) ======================
|
|
612
|
+
pageItem.applyEffect(xml) is Illustrator's real live-effects API, but Adobe
|
|
613
|
+
never fully documented each effect's XML dict schema -- these 4 use the
|
|
614
|
+
XML shape that's been reliably reported by the scripting community for
|
|
615
|
+
many CC versions. Flagged for a live smoke test (not yet done -- the
|
|
616
|
+
bridge was down while this batch was written) before considering this
|
|
617
|
+
category done; apply_live_effect_raw is the safety valve if any of the
|
|
618
|
+
four turn out wrong for this Illustrator version. */
|
|
619
|
+
jsxTool("apply_live_effect_raw", "Escape hatch: applies an arbitrary live effect by its internal name and raw XML parameter dictionary (e.g. name='Adobe Drop Shadow', xml='<LiveEffect name=\"Adobe Drop Shadow\"><Dict>...</Dict></LiveEffect>'). For effects not covered by a dedicated tool.", { name: S.string(), xml: S.string() },
|
|
620
|
+
"_needItem(A.name).applyEffect(A.xml); return 'ok';");
|
|
621
|
+
jsxTool("apply_drop_shadow_effect", "Applies a live Drop Shadow appearance effect (non-destructive, editable/removable in the Appearance panel). Always black -- the effect's own color/blend-mode parameters rejected every value tried live, so this only exposes what's confirmed working.", { name: S.string(), offsetX: S.number().default(2.117), offsetY: S.number().default(2.117), blur: S.number().default(1.764), opacity: S.number().min(0).max(100).default(75) },
|
|
622
|
+
"var it=_needItem(A.name); var xml='<LiveEffect name=\"Adobe Drop Shadow\"><Dict data=\"R X '+A.offsetX+' R Y '+A.offsetY+' R B '+A.blur+' R op '+A.opacity+' \"/></LiveEffect>'; it.applyEffect(xml); return 'ok';");
|
|
623
|
+
jsxTool("apply_feather_effect", "Applies a live Feather appearance effect (soft-fades the item's edge).", { name: S.string(), radius: S.number().min(0).default(5) },
|
|
624
|
+
"var it=_needItem(A.name); var xml='<LiveEffect name=\"Adobe Feather\"><Dict data=\"R fthr '+A.radius+' \"/></LiveEffect>'; it.applyEffect(xml); return 'ok';");
|
|
625
|
+
jsxTool("apply_round_corners_effect", "Applies a live Round Corners appearance effect to a path.", { name: S.string(), radius: S.number().min(0).default(10) },
|
|
626
|
+
"var it=_needItem(A.name); var xml='<LiveEffect name=\"Adobe Round Corners\"><Dict data=\"R rc_radius '+A.radius+' \"/></LiveEffect>'; it.applyEffect(xml); return 'ok';");
|
|
627
|
+
jsxTool("apply_offset_path_effect", "Applies a live Offset Path appearance effect (grows/shrinks a path's outline).", { name: S.string(), offset: S.number(), joins: S.enum(["miter", "round", "bevel"]).default("miter"), miterLimit: S.number().default(4) },
|
|
628
|
+
"var it=_needItem(A.name); var jmap={miter:0,round:1,bevel:2}; var xml='<LiveEffect name=\"Adobe Offset Path\"><Dict data=\"R offset '+A.offset+' I lineJoin '+jmap[A.joins]+' R miterLimit '+A.miterLimit+' \"/></LiveEffect>'; it.applyEffect(xml); return 'ok';");
|
|
629
|
+
|
|
630
|
+
const NAMED_EFFECTS = {
|
|
631
|
+
innerGlow: "Adobe Inner Glow", outerGlow: "Adobe Outer Glow", scribble: "Adobe Scribble",
|
|
632
|
+
roughen: "Adobe Roughen", transform: "Adobe Transform", tweak: "Adobe Tweak",
|
|
633
|
+
zigZag: "Adobe Zig Zag", freeDistort: "Adobe Free Distort", rasterize: "Adobe Rasterize",
|
|
634
|
+
trimMarks: "Adobe Trim Marks", outlineObject: "Adobe Outline Object",
|
|
635
|
+
gaussianBlur: "Adobe Gaussian Blur", radialBlur: "Adobe Radial Blur", smartBlur: "Adobe Smart Blur", motionBlur: "Adobe Motion Blur",
|
|
636
|
+
coloredPencil: "Adobe Colored Pencil", cutout: "Adobe Cutout", dryBrush: "Adobe Dry Brush", filmGrain: "Adobe Film Grain",
|
|
637
|
+
fresco: "Adobe Fresco", neonGlow: "Adobe Neon Glow", paintDaubs: "Adobe Paint Daubs", paletteKnife: "Adobe Palette Knife",
|
|
638
|
+
plasticWrap: "Adobe Plastic Wrap", posterEdges: "Adobe Poster Edges", roughPastels: "Adobe Rough Pastels",
|
|
639
|
+
smudgeStick: "Adobe Smudge Stick", sponge: "Adobe Sponge", underpainting: "Adobe Underpainting", watercolor: "Adobe Watercolor",
|
|
640
|
+
accentedEdges: "Adobe Accented Edges", angledStrokes: "Adobe Angled Strokes", crosshatch: "Adobe Crosshatch",
|
|
641
|
+
darkStrokes: "Adobe Dark Strokes", inkOutlines: "Adobe Ink Outlines", spatter: "Adobe Spatter",
|
|
642
|
+
sprayedStrokes: "Adobe Sprayed Strokes", sumiE: "Adobe Sumi-e",
|
|
643
|
+
diffuseGlow: "Adobe Diffuse Glow", glass: "Adobe Glass", oceanRipple: "Adobe Ocean Ripple",
|
|
644
|
+
colorHalftone: "Adobe Color Halftone", crystallize: "Adobe Crystallize", mezzotint: "Adobe Mezzotint", pointillize: "Adobe Pointillize",
|
|
645
|
+
basRelief: "Adobe Bas Relief", chalkAndCharcoal: "Adobe Chalk & Charcoal", charcoal: "Adobe Charcoal", chrome: "Adobe Chrome",
|
|
646
|
+
conteCrayon: "Adobe Conte Crayon", graphicPen: "Adobe Graphic Pen", halftonePattern: "Adobe Halftone Pattern",
|
|
647
|
+
notePaper: "Adobe Note Paper", photocopy: "Adobe Photocopy", plaster: "Adobe Plaster", reticulation: "Adobe Reticulation",
|
|
648
|
+
stamp: "Adobe Stamp", tornEdges: "Adobe Torn Edges", waterPaper: "Adobe Water Paper", glowingEdges: "Adobe Glowing Edges",
|
|
649
|
+
craquelure: "Adobe Craquelure", grain: "Adobe Grain", mosaicTiles: "Adobe Mosaic Tiles", patchwork: "Adobe Patchwork",
|
|
650
|
+
stainedGlass: "Adobe Stained Glass", texturizer: "Adobe Texturizer",
|
|
651
|
+
warpArc: "Adobe Warp Arc", warpArcLower: "Adobe Warp Arc Lower", warpArcUpper: "Adobe Warp Arc Upper", warpArch: "Adobe Warp Arch",
|
|
652
|
+
warpBulge: "Adobe Warp Bulge", warpShellLower: "Adobe Warp Shell Lower", warpShellUpper: "Adobe Warp Shell Upper",
|
|
653
|
+
warpFlag: "Adobe Warp Flag", warpWave: "Adobe Warp Wave", warpFish: "Adobe Warp Fish", warpRise: "Adobe Warp Rise",
|
|
654
|
+
warpFisheye: "Adobe Warp Fisheye", warpInflate: "Adobe Warp Inflate", warpSqueeze: "Adobe Warp Squeeze", warpTwist: "Adobe Warp Twist",
|
|
655
|
+
threeDExtrude: "Adobe 3D Extrude", threeDBevel: "Adobe 3D Bevel", threeDRevolve: "Adobe 3D Revolve", threeDRotate: "Adobe 3D Rotate",
|
|
656
|
+
shapeRectangle: "Adobe Shape Rectangle", shapeRoundedRectangle: "Adobe Shape Rounded Rectangle", shapeEllipse: "Adobe Shape Ellipse",
|
|
657
|
+
};
|
|
658
|
+
server.tool("apply_named_effect",
|
|
659
|
+
"Applies one of 87 confirmed-working Illustrator live effects by short name, with default parameters (or a raw XML <Dict> override for fine control -- see apply_live_effect_raw for the format). Covers most of Effect > Illustrator Effects and Effect > Photoshop Effects. Non-destructive/editable in the Appearance panel like the 4 dedicated effect tools (drop shadow, feather, round corners, offset path), which take real typed parameters instead and should be preferred when available.",
|
|
660
|
+
{ name: S.string(), effect: S.enum(Object.keys(NAMED_EFFECTS)), dictXml: S.string().optional() },
|
|
661
|
+
async (a) => {
|
|
662
|
+
const lic = await checkLicense();
|
|
663
|
+
if (!lic.valid) return licenseErrorResponse("apply_named_effect");
|
|
664
|
+
const effectName = NAMED_EFFECTS[a.effect];
|
|
665
|
+
const dict = a.dictXml || "";
|
|
666
|
+
const code = HELPERS + "\nvar A=" + JSON.stringify({ name: a.name }) + ";\n" +
|
|
667
|
+
`_needItem(A.name).applyEffect('<LiveEffect name="${effectName}"><Dict data="${dict}"/></LiveEffect>'); return 'ok';`;
|
|
668
|
+
return text(await rpc("eval", { code }));
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
/* ====================== ACTIONS ====================== */
|
|
672
|
+
jsxTool("play_action", "Plays an existing Illustrator action (from the Actions panel) by name and set name.", { action: S.string(), set: S.string() },
|
|
673
|
+
"app.doScript(A.action, A.set); return 'ok';");
|
|
674
|
+
|
|
675
|
+
// Translate tools/list (descriptions) into the server language. Must run
|
|
676
|
+
// AFTER every tool is registered and BEFORE connect(). No-op in English.
|
|
677
|
+
patchMcpServerToolsList(server, i18n);
|
|
678
|
+
|
|
679
|
+
// ── CLI: --doctor ────────────────────────────────────────────────────────
|
|
680
|
+
const cliArgs = process.argv.slice(2);
|
|
681
|
+
if (cliArgs.includes("--doctor")) {
|
|
682
|
+
(async () => {
|
|
683
|
+
const lines = [];
|
|
684
|
+
const ok = (msg) => lines.push("OK " + msg);
|
|
685
|
+
const fail = (msg) => lines.push("FAIL " + msg);
|
|
686
|
+
const info = (msg) => lines.push("INFO " + msg);
|
|
687
|
+
|
|
688
|
+
lines.push(i18n.t("msg.doctor.title"));
|
|
689
|
+
lines.push("");
|
|
690
|
+
|
|
691
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
692
|
+
if (nodeMajor >= 18) ok(i18n.t("msg.doctor.nodeOk", { version: process.version }));
|
|
693
|
+
else fail(i18n.t("msg.doctor.nodeTooOld", { version: process.version }));
|
|
694
|
+
|
|
695
|
+
if (!LICENSE_KEY) {
|
|
696
|
+
fail(i18n.t("msg.doctor.licenseNotSet"));
|
|
697
|
+
} else {
|
|
698
|
+
const result = await checkLicense();
|
|
699
|
+
if (result.valid) ok(i18n.t("msg.doctor.licenseValid", { plan: result.plan || i18n.t("msg.doctor.licensePlanActive") }));
|
|
700
|
+
else fail(i18n.t("msg.doctor.licenseFailed", { message: result.message || i18n.t("msg.doctor.licenseInvalid") }));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
info(i18n.t("msg.doctor.bridgeConnecting", { url: WS_URL }));
|
|
704
|
+
const bridgeUp = await new Promise((resolve) => {
|
|
705
|
+
let s;
|
|
706
|
+
try { s = new WebSocket(WS_URL); } catch { resolve(false); return; }
|
|
707
|
+
const timer = setTimeout(() => { try { s.terminate(); } catch {} resolve(false); }, 3000);
|
|
708
|
+
s.on("open", () => { clearTimeout(timer); try { s.close(); } catch {} resolve(true); });
|
|
709
|
+
s.on("error", () => { clearTimeout(timer); resolve(false); });
|
|
710
|
+
});
|
|
711
|
+
if (bridgeUp) ok(i18n.t("msg.doctor.bridgeOk"));
|
|
712
|
+
else fail(i18n.t("msg.doctor.bridgeFail"));
|
|
713
|
+
|
|
714
|
+
info(i18n.t("msg.doctor.language", { language: i18n.language, source: i18n.languageSource }));
|
|
715
|
+
|
|
716
|
+
lines.push("");
|
|
717
|
+
lines.push(i18n.t("msg.doctor.support"));
|
|
718
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
719
|
+
process.exit(0);
|
|
720
|
+
})();
|
|
721
|
+
} else {
|
|
722
|
+
|
|
723
|
+
const transport = new StdioServerTransport();
|
|
724
|
+
await server.connect(transport);
|
|
725
|
+
|
|
726
|
+
}
|