@efectoapp/mcp-server 0.1.19 → 0.1.22
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 +18 -5
- package/dist/tools/discovery.d.ts.map +1 -1
- package/dist/tools/discovery.js +127 -0
- package/dist/tools/discovery.js.map +1 -1
- package/dist/tools/images.d.ts.map +1 -1
- package/dist/tools/images.js +102 -0
- package/dist/tools/images.js.map +1 -1
- package/dist/tools/output.d.ts +1 -4
- package/dist/tools/output.d.ts.map +1 -1
- package/dist/tools/output.js +51 -64
- package/dist/tools/output.js.map +1 -1
- package/dist/tools/render.d.ts +22 -0
- package/dist/tools/render.d.ts.map +1 -0
- package/dist/tools/render.js +148 -0
- package/dist/tools/render.js.map +1 -0
- package/dist/tools/state.d.ts +91 -2
- package/dist/tools/state.d.ts.map +1 -1
- package/dist/tools/state.js +2013 -73
- package/dist/tools/state.js.map +1 -1
- package/package.json +3 -2
package/dist/tools/state.js
CHANGED
|
@@ -14,9 +14,13 @@
|
|
|
14
14
|
*/
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.stateTools = void 0;
|
|
17
|
+
exports.ensurePoster = ensurePoster;
|
|
17
18
|
exports.resetState = resetState;
|
|
18
19
|
exports.getCurrentState = getCurrentState;
|
|
19
20
|
exports.handleStateTool = handleStateTool;
|
|
21
|
+
const os_1 = require("os");
|
|
22
|
+
const fs_1 = require("fs");
|
|
23
|
+
const path_1 = require("path");
|
|
20
24
|
// Helper to clamp values to valid ranges
|
|
21
25
|
function clamp(value, min, max, fallback) {
|
|
22
26
|
const v = value ?? fallback;
|
|
@@ -24,6 +28,335 @@ function clamp(value, min, max, fallback) {
|
|
|
24
28
|
return fallback;
|
|
25
29
|
return Math.min(max, Math.max(min, v));
|
|
26
30
|
}
|
|
31
|
+
function parseNumericInput(input) {
|
|
32
|
+
if (typeof input === 'number' && Number.isFinite(input))
|
|
33
|
+
return input;
|
|
34
|
+
if (typeof input === 'string') {
|
|
35
|
+
const trimmed = input.trim();
|
|
36
|
+
if (!trimmed)
|
|
37
|
+
return undefined;
|
|
38
|
+
const stripped = trimmed.endsWith('%') ? trimmed.slice(0, -1) : trimmed;
|
|
39
|
+
const parsed = Number.parseFloat(stripped);
|
|
40
|
+
if (Number.isFinite(parsed))
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Palette color lookup map — mirrors lib/color-palettes.ts COLOR_PALETTES.
|
|
47
|
+
* When a paletteId is provided, we resolve it to actual hex colors so the
|
|
48
|
+
* colors array is always in sync and URL-encoded state renders correctly.
|
|
49
|
+
*/
|
|
50
|
+
const PALETTE_COLORS = {
|
|
51
|
+
// Classic 2-color
|
|
52
|
+
noir: ['#000000', '#ffffff'],
|
|
53
|
+
ink: ['#1a1a2e', '#f5f5dc'],
|
|
54
|
+
terminal: ['#001100', '#00ff00'],
|
|
55
|
+
'amber-glow': ['#1a0f00', '#ffcc00'],
|
|
56
|
+
// Retro gaming
|
|
57
|
+
gameboy: ['#0f380f', '#306230', '#8bac0f', '#9bbc0f'],
|
|
58
|
+
commodore: ['#000000', '#6c5eb5', '#959595', '#ffffff'],
|
|
59
|
+
nes: ['#000000', '#b53120', '#6b6b6b', '#fcfcfc'],
|
|
60
|
+
cga: ['#000000', '#aa00aa', '#00aaaa', '#ffffff'],
|
|
61
|
+
// Warm
|
|
62
|
+
'sunset-blvd': ['#1a1423', '#4a1942', '#b33951', '#f5a962', '#fff4e0'],
|
|
63
|
+
campfire: ['#1a0a0a', '#4a0e0e', '#8b1a1a', '#d44d1a', '#ffcc00'],
|
|
64
|
+
'autumn-leaves': ['#2d1b0e', '#6b3e26', '#b5651d', '#e09f3e', '#f5deb3'],
|
|
65
|
+
terracotta: ['#1f1410', '#5c3d2e', '#b36b4f', '#e8a87c', '#faf0e6'],
|
|
66
|
+
'golden-hour': ['#1a1205', '#4a3510', '#8b6914', '#d4a017', '#fff8dc'],
|
|
67
|
+
'rose-gold': ['#2a1a1a', '#6b4040', '#b76e79', '#e8b4bc', '#fff0f5'],
|
|
68
|
+
// Cool
|
|
69
|
+
'deep-sea': ['#0a1628', '#1a3a5c', '#2d6187', '#5ba4c9', '#a8dce8'],
|
|
70
|
+
'arctic-night': ['#0a0a14', '#1a2a4a', '#3a5a8a', '#6a9aca', '#cae8ff'],
|
|
71
|
+
moonlight: ['#0f0f1a', '#2a2a4a', '#4a4a7a', '#8a8ab0', '#e0e0f0'],
|
|
72
|
+
frozen: ['#1a1a2e', '#16213e', '#0f3460', '#53a8b6', '#e8f1f5'],
|
|
73
|
+
twilight: ['#0d0221', '#261447', '#4a2c6a', '#7a4a9a', '#c8a8e8'],
|
|
74
|
+
// Neon
|
|
75
|
+
synthwave: ['#120458', '#7b2cbf', '#e040fb', '#ff6ec7', '#fff59d'],
|
|
76
|
+
vaporwave: ['#1a0a2e', '#3d1a5c', '#ff71ce', '#01cdfe', '#fffb96'],
|
|
77
|
+
cyberpunk: ['#0d0221', '#261447', '#6b2d5c', '#f72585', '#4cc9f0'],
|
|
78
|
+
'tokyo-night': ['#1a1b26', '#414868', '#7aa2f7', '#bb9af7', '#c0caf5'],
|
|
79
|
+
arcade: ['#0d0d0d', '#ff073a', '#39ff14', '#00f0ff', '#ffffff'],
|
|
80
|
+
retrowave: ['#2b1055', '#7f00ff', '#e100ff', '#ff5c8d', '#ffd700'],
|
|
81
|
+
electric: ['#0a0a0a', '#0066ff', '#00ffff', '#ff00ff', '#ffffff'],
|
|
82
|
+
// Earth
|
|
83
|
+
forest: ['#1a2e1a', '#2d4a2d', '#4a7c4a', '#7ab37a', '#c8e6c8'],
|
|
84
|
+
moss: ['#1a1f14', '#3d4a2d', '#6b7b4a', '#a8b88a', '#d4e4bc'],
|
|
85
|
+
desert: ['#2e1f14', '#6b4423', '#c19a6b', '#e6c89c', '#f5e6d3'],
|
|
86
|
+
clay: ['#2a1810', '#5a3828', '#9a6848', '#d4a878', '#f4e4d4'],
|
|
87
|
+
'ocean-floor': ['#0a1a1a', '#1a3a3a', '#2a6a5a', '#4a9a7a', '#8adaaa'],
|
|
88
|
+
// Mono
|
|
89
|
+
grayscale: ['#000000', '#404040', '#808080', '#c0c0c0', '#ffffff'],
|
|
90
|
+
sepia: ['#1a1610', '#3d3020', '#6b5a40', '#a89070', '#e8dcc8'],
|
|
91
|
+
blueprint: ['#001830', '#003060', '#0050a0', '#0080e0', '#e0f0ff'],
|
|
92
|
+
cyanotype: ['#001a2e', '#003050', '#005080', '#0080b0', '#a0d8f0'],
|
|
93
|
+
'green-phosphor': ['#001400', '#003300', '#006600', '#00cc00', '#66ff66'],
|
|
94
|
+
'amber-phosphor': ['#1a0f00', '#3d2400', '#7a4800', '#cc7a00', '#ffcc66'],
|
|
95
|
+
};
|
|
96
|
+
/** Resolve a paletteId to its colors, falling back to the provided default. */
|
|
97
|
+
function resolvePaletteColors(paletteId, fallback) {
|
|
98
|
+
if (!paletteId)
|
|
99
|
+
return fallback;
|
|
100
|
+
return PALETTE_COLORS[paletteId] ?? fallback;
|
|
101
|
+
}
|
|
102
|
+
const TEXT_WORLD_SCALE_FACTOR = 0.15 / 72;
|
|
103
|
+
function getArtboardAspect(aspectRatio) {
|
|
104
|
+
switch (aspectRatio) {
|
|
105
|
+
case '16:9':
|
|
106
|
+
return 16 / 9;
|
|
107
|
+
case '9:16':
|
|
108
|
+
return 9 / 16;
|
|
109
|
+
case '1:1':
|
|
110
|
+
return 1;
|
|
111
|
+
case '4:3':
|
|
112
|
+
return 4 / 3;
|
|
113
|
+
case '3:4':
|
|
114
|
+
return 3 / 4;
|
|
115
|
+
default:
|
|
116
|
+
return 9 / 16;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function parseNormalizedFrameDimension(input) {
|
|
120
|
+
const value = parseNumericInput(input);
|
|
121
|
+
if (value === undefined)
|
|
122
|
+
return undefined;
|
|
123
|
+
if (!Number.isFinite(value))
|
|
124
|
+
return undefined;
|
|
125
|
+
return clamp(value, 0.01, 2, value);
|
|
126
|
+
}
|
|
127
|
+
function textNormalizedToPx(normalized, artboardAspect, axis) {
|
|
128
|
+
if (axis === 'height') {
|
|
129
|
+
return normalized / TEXT_WORLD_SCALE_FACTOR;
|
|
130
|
+
}
|
|
131
|
+
return (normalized * artboardAspect) / TEXT_WORLD_SCALE_FACTOR;
|
|
132
|
+
}
|
|
133
|
+
/** Convert CSS-style % from left (0=left, 50=center, 100=right) to normalized x (-1..1). */
|
|
134
|
+
function cssPercentToX(input, fallbackPct = 50) {
|
|
135
|
+
const value = parseNumericInput(input);
|
|
136
|
+
if (value === undefined)
|
|
137
|
+
return (fallbackPct / 50) - 1;
|
|
138
|
+
// Always interpret as CSS percentage from left edge
|
|
139
|
+
const pct = Math.max(-50, Math.min(150, value));
|
|
140
|
+
return (pct / 50) - 1; // 0→-1, 50→0, 100→1
|
|
141
|
+
}
|
|
142
|
+
/** Convert CSS-style % from top (0=top, 50=center, 100=bottom) to normalized y (-1..1, +up). */
|
|
143
|
+
function cssPercentToY(input, fallbackPct = 50) {
|
|
144
|
+
const value = parseNumericInput(input);
|
|
145
|
+
if (value === undefined)
|
|
146
|
+
return 1 - (fallbackPct / 50);
|
|
147
|
+
// Always interpret as CSS percentage from top edge
|
|
148
|
+
const pct = Math.max(-50, Math.min(150, value));
|
|
149
|
+
return 1 - (pct / 50); // 0→1(top), 50→0(center), 100→-1(bottom)
|
|
150
|
+
}
|
|
151
|
+
/** Convert CSS-style % size (60=60%) to normalized size (0.6).
|
|
152
|
+
* Values 0-1 pass through for backward compat. */
|
|
153
|
+
function cssPercentToSize(input, fallbackPct = 100) {
|
|
154
|
+
const value = parseNumericInput(input);
|
|
155
|
+
if (value === undefined)
|
|
156
|
+
return fallbackPct / 100;
|
|
157
|
+
// Values > 1 are treated as %, values 0-1 pass through
|
|
158
|
+
const normalized = value > 1 && value <= 200 ? value / 100 : value;
|
|
159
|
+
return clamp(normalized, 0.01, 4, normalized);
|
|
160
|
+
}
|
|
161
|
+
function normalizeOpacity(input, fallback) {
|
|
162
|
+
const value = parseNumericInput(input);
|
|
163
|
+
if (value === undefined)
|
|
164
|
+
return fallback;
|
|
165
|
+
const normalized = value > 1 && value <= 100 ? value / 100 : value;
|
|
166
|
+
return clamp(normalized, 0, 1, fallback);
|
|
167
|
+
}
|
|
168
|
+
function normalizeStrokeWidth(input, fallback) {
|
|
169
|
+
const value = parseNumericInput(input);
|
|
170
|
+
if (value === undefined)
|
|
171
|
+
return fallback;
|
|
172
|
+
const normalized = value > 1 ? value / 1000 : value;
|
|
173
|
+
return clamp(normalized, 0, 0.2, fallback);
|
|
174
|
+
}
|
|
175
|
+
function normalizeCornerRadius(input, fallback) {
|
|
176
|
+
const value = parseNumericInput(input);
|
|
177
|
+
if (value === undefined)
|
|
178
|
+
return fallback;
|
|
179
|
+
const normalized = value > 1 ? value / 1000 : value;
|
|
180
|
+
return clamp(normalized, 0, 1, fallback);
|
|
181
|
+
}
|
|
182
|
+
function normalizePositiveInt(input, fallback, min, max) {
|
|
183
|
+
const value = parseNumericInput(input);
|
|
184
|
+
if (value === undefined)
|
|
185
|
+
return fallback;
|
|
186
|
+
const rounded = Math.round(value);
|
|
187
|
+
return Math.min(max, Math.max(min, rounded));
|
|
188
|
+
}
|
|
189
|
+
function parseFlexChildProps(input) {
|
|
190
|
+
const result = {};
|
|
191
|
+
if (typeof input.positioning === 'string') {
|
|
192
|
+
const v = input.positioning.trim().toLowerCase();
|
|
193
|
+
if (v === 'flow' || v === 'absolute')
|
|
194
|
+
result.positioning = v;
|
|
195
|
+
}
|
|
196
|
+
// Per-axis sizing (preferred)
|
|
197
|
+
if (typeof input.flexWidthMode === 'string') {
|
|
198
|
+
const v = input.flexWidthMode.trim().toLowerCase();
|
|
199
|
+
if (v === 'hug' || v === 'fixed' || v === 'fill')
|
|
200
|
+
result.flexWidthMode = v;
|
|
201
|
+
}
|
|
202
|
+
if (typeof input.flexHeightMode === 'string') {
|
|
203
|
+
const v = input.flexHeightMode.trim().toLowerCase();
|
|
204
|
+
if (v === 'hug' || v === 'fixed' || v === 'fill')
|
|
205
|
+
result.flexHeightMode = v;
|
|
206
|
+
}
|
|
207
|
+
// Legacy single-axis: applies to both axes if per-axis not set
|
|
208
|
+
if (typeof input.flexSizeMode === 'string') {
|
|
209
|
+
const v = input.flexSizeMode.trim().toLowerCase();
|
|
210
|
+
if (v === 'hug' || v === 'fixed' || v === 'fill') {
|
|
211
|
+
if (!result.flexWidthMode)
|
|
212
|
+
result.flexWidthMode = v;
|
|
213
|
+
if (!result.flexHeightMode)
|
|
214
|
+
result.flexHeightMode = v;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (typeof input.alignSelf === 'string') {
|
|
218
|
+
const v = input.alignSelf.trim().toLowerCase();
|
|
219
|
+
if (v === 'auto' || v === 'start' || v === 'center' || v === 'end' || v === 'stretch')
|
|
220
|
+
result.alignSelf = v;
|
|
221
|
+
}
|
|
222
|
+
if (input.flexGrow !== undefined) {
|
|
223
|
+
const val = Number(input.flexGrow);
|
|
224
|
+
if (Number.isFinite(val) && val >= 0)
|
|
225
|
+
result.flexGrow = val;
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
// --- Visual property parsers (shadow, blendMode, textTransform, textDecoration) ---
|
|
230
|
+
const VALID_TEXT_TRANSFORMS = ['none', 'uppercase', 'lowercase', 'capitalize'];
|
|
231
|
+
const VALID_TEXT_DECORATIONS = ['none', 'underline', 'line-through', 'overline'];
|
|
232
|
+
const VALID_BLEND_MODES = ['normal', 'multiply', 'screen', 'darken', 'lighten', 'difference'];
|
|
233
|
+
function parseShadowParams(params) {
|
|
234
|
+
const hasShadow = params.shadowOffsetX !== undefined || params.shadowOffsetY !== undefined ||
|
|
235
|
+
params.shadowBlur !== undefined || params.shadowColor !== undefined;
|
|
236
|
+
if (!hasShadow)
|
|
237
|
+
return undefined;
|
|
238
|
+
return {
|
|
239
|
+
offsetX: typeof params.shadowOffsetX === 'number' ? params.shadowOffsetX : 0,
|
|
240
|
+
offsetY: typeof params.shadowOffsetY === 'number' ? params.shadowOffsetY : 4,
|
|
241
|
+
blur: typeof params.shadowBlur === 'number' ? Math.max(0, params.shadowBlur) : 8,
|
|
242
|
+
color: normalizeHexColor(params.shadowColor) || '#000000',
|
|
243
|
+
opacity: typeof params.shadowOpacity === 'number' ? Math.max(0, Math.min(1, params.shadowOpacity)) : 0.5,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function parseBlendMode(params) {
|
|
247
|
+
const mode = params.blendMode;
|
|
248
|
+
if (!mode || !VALID_BLEND_MODES.includes(mode))
|
|
249
|
+
return undefined;
|
|
250
|
+
if (mode === 'normal')
|
|
251
|
+
return undefined; // Don't store default
|
|
252
|
+
return mode;
|
|
253
|
+
}
|
|
254
|
+
function parseTextTransform(params) {
|
|
255
|
+
const val = params.textTransform;
|
|
256
|
+
if (!val || !VALID_TEXT_TRANSFORMS.includes(val))
|
|
257
|
+
return undefined;
|
|
258
|
+
if (val === 'none')
|
|
259
|
+
return undefined;
|
|
260
|
+
return val;
|
|
261
|
+
}
|
|
262
|
+
function parseTextDecoration(params) {
|
|
263
|
+
const result = {};
|
|
264
|
+
const val = params.textDecoration;
|
|
265
|
+
if (val && VALID_TEXT_DECORATIONS.includes(val) && val !== 'none') {
|
|
266
|
+
result.textDecoration = val;
|
|
267
|
+
}
|
|
268
|
+
if (params.textDecorationColor !== undefined) {
|
|
269
|
+
const color = normalizeHexColor(params.textDecorationColor);
|
|
270
|
+
if (color)
|
|
271
|
+
result.textDecorationColor = color;
|
|
272
|
+
}
|
|
273
|
+
if (typeof params.textDecorationThickness === 'number' && params.textDecorationThickness > 0) {
|
|
274
|
+
result.textDecorationThickness = params.textDecorationThickness;
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
/** Parse margin params */
|
|
279
|
+
function parseMarginParams(params) {
|
|
280
|
+
const hasMargin = params.marginTop !== undefined || params.marginRight !== undefined ||
|
|
281
|
+
params.marginBottom !== undefined || params.marginLeft !== undefined;
|
|
282
|
+
if (!hasMargin)
|
|
283
|
+
return undefined;
|
|
284
|
+
return {
|
|
285
|
+
top: typeof params.marginTop === 'number' ? params.marginTop : 0,
|
|
286
|
+
right: typeof params.marginRight === 'number' ? params.marginRight : 0,
|
|
287
|
+
bottom: typeof params.marginBottom === 'number' ? params.marginBottom : 0,
|
|
288
|
+
left: typeof params.marginLeft === 'number' ? params.marginLeft : 0,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/** Parse min/max size constraint params */
|
|
292
|
+
function parseMinMaxConstraints(params) {
|
|
293
|
+
const result = {};
|
|
294
|
+
if (typeof params.minWidth === 'number')
|
|
295
|
+
result.minWidth = Math.max(0, Math.min(1, params.minWidth));
|
|
296
|
+
if (typeof params.maxWidth === 'number')
|
|
297
|
+
result.maxWidth = Math.max(0, Math.min(1, params.maxWidth));
|
|
298
|
+
if (typeof params.minHeight === 'number')
|
|
299
|
+
result.minHeight = Math.max(0, Math.min(1, params.minHeight));
|
|
300
|
+
if (typeof params.maxHeight === 'number')
|
|
301
|
+
result.maxHeight = Math.max(0, Math.min(1, params.maxHeight));
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
/** Parse per-corner radius params */
|
|
305
|
+
function parseCornerRadii(params) {
|
|
306
|
+
const hasPerCorner = params.cornerRadiusTopLeft !== undefined || params.cornerRadiusTopRight !== undefined ||
|
|
307
|
+
params.cornerRadiusBottomRight !== undefined || params.cornerRadiusBottomLeft !== undefined;
|
|
308
|
+
if (!hasPerCorner)
|
|
309
|
+
return undefined;
|
|
310
|
+
return {
|
|
311
|
+
topLeft: typeof params.cornerRadiusTopLeft === 'number' ? Math.max(0, params.cornerRadiusTopLeft) : 0,
|
|
312
|
+
topRight: typeof params.cornerRadiusTopRight === 'number' ? Math.max(0, params.cornerRadiusTopRight) : 0,
|
|
313
|
+
bottomRight: typeof params.cornerRadiusBottomRight === 'number' ? Math.max(0, params.cornerRadiusBottomRight) : 0,
|
|
314
|
+
bottomLeft: typeof params.cornerRadiusBottomLeft === 'number' ? Math.max(0, params.cornerRadiusBottomLeft) : 0,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** Parse transform origin params */
|
|
318
|
+
function parseTransformOrigin(params) {
|
|
319
|
+
const result = {};
|
|
320
|
+
if (typeof params.originX === 'number')
|
|
321
|
+
result.originX = Math.max(0, Math.min(1, params.originX));
|
|
322
|
+
if (typeof params.originY === 'number')
|
|
323
|
+
result.originY = Math.max(0, Math.min(1, params.originY));
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
/** Parse visual properties shared across all layer types (shadow, blendMode, margin, min/max constraints) */
|
|
327
|
+
function parseVisualOverrides(params) {
|
|
328
|
+
const result = {};
|
|
329
|
+
const shadow = parseShadowParams(params);
|
|
330
|
+
if (shadow)
|
|
331
|
+
result.shadow = shadow;
|
|
332
|
+
const blendMode = parseBlendMode(params);
|
|
333
|
+
if (blendMode)
|
|
334
|
+
result.blendMode = blendMode;
|
|
335
|
+
const margin = parseMarginParams(params);
|
|
336
|
+
if (margin)
|
|
337
|
+
result.margin = margin;
|
|
338
|
+
const constraints = parseMinMaxConstraints(params);
|
|
339
|
+
Object.assign(result, constraints);
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
function normalizeHexColor(value) {
|
|
343
|
+
if (typeof value !== 'string')
|
|
344
|
+
return null;
|
|
345
|
+
const trimmed = value.trim();
|
|
346
|
+
if (!trimmed)
|
|
347
|
+
return null;
|
|
348
|
+
const withHash = trimmed.startsWith('#') ? trimmed : `#${trimmed}`;
|
|
349
|
+
if (/^#[0-9a-f]{6}$/i.test(withHash))
|
|
350
|
+
return withHash.toLowerCase();
|
|
351
|
+
if (/^#[0-9a-f]{3}$/i.test(withHash)) {
|
|
352
|
+
const [, r, g, b] = withHash;
|
|
353
|
+
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
function asFiniteNumber(value, fallback) {
|
|
358
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
359
|
+
}
|
|
27
360
|
// Known fonts with their proper CSS values
|
|
28
361
|
const KNOWN_FONTS = {
|
|
29
362
|
'DM Sans': '"DM Sans", sans-serif',
|
|
@@ -66,9 +399,75 @@ function normalizeFontFamily(input) {
|
|
|
66
399
|
}
|
|
67
400
|
// Current poster state (single poster for now)
|
|
68
401
|
let currentPoster = null;
|
|
402
|
+
// --- File-based state persistence ---
|
|
403
|
+
// Some MCP clients spawn a fresh process per tool call, losing in-memory state.
|
|
404
|
+
// We persist to a temp file so state survives process restarts.
|
|
405
|
+
const STATE_FILE_PATH = (0, path_1.join)((0, os_1.tmpdir)(), 'efecto-mcp-state.json');
|
|
406
|
+
const STATE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
407
|
+
/** Write current poster state to the temp file (best-effort). */
|
|
408
|
+
function persistState() {
|
|
409
|
+
if (!currentPoster)
|
|
410
|
+
return;
|
|
411
|
+
try {
|
|
412
|
+
const payload = JSON.stringify({ timestamp: Date.now(), state: currentPoster });
|
|
413
|
+
(0, fs_1.writeFileSync)(STATE_FILE_PATH, payload, 'utf-8');
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// Silently ignore — falls back to in-memory only
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/** Load persisted state from the temp file if valid and not stale. */
|
|
420
|
+
function loadPersistedState() {
|
|
421
|
+
try {
|
|
422
|
+
const raw = (0, fs_1.readFileSync)(STATE_FILE_PATH, 'utf-8');
|
|
423
|
+
const parsed = JSON.parse(raw);
|
|
424
|
+
if (!parsed.state || !parsed.timestamp)
|
|
425
|
+
return null;
|
|
426
|
+
if (Date.now() - parsed.timestamp > STATE_MAX_AGE_MS)
|
|
427
|
+
return null;
|
|
428
|
+
return parsed.state;
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Ensure a poster exists. Restores from persisted file if needed,
|
|
436
|
+
* or auto-creates a default poster as a last resort.
|
|
437
|
+
*/
|
|
438
|
+
function ensurePoster() {
|
|
439
|
+
if (currentPoster)
|
|
440
|
+
return currentPoster;
|
|
441
|
+
// Try restoring from persisted state
|
|
442
|
+
const persisted = loadPersistedState();
|
|
443
|
+
if (persisted) {
|
|
444
|
+
currentPoster = persisted;
|
|
445
|
+
return currentPoster;
|
|
446
|
+
}
|
|
447
|
+
// Auto-create a default poster (same defaults as create_poster)
|
|
448
|
+
currentPoster = {
|
|
449
|
+
canvas: {
|
|
450
|
+
aspectRatio: '9:16',
|
|
451
|
+
backgroundColor: '#1a1a1a',
|
|
452
|
+
layoutMode: 'column',
|
|
453
|
+
alignItems: 'center',
|
|
454
|
+
justifyContent: 'start',
|
|
455
|
+
gap: 0.04,
|
|
456
|
+
padding: { top: 0.08, right: 0.08, bottom: 0.08, left: 0.08 },
|
|
457
|
+
},
|
|
458
|
+
layers: [createDefaultBackgroundLayer('#1a1a1a')],
|
|
459
|
+
postProcesses: [],
|
|
460
|
+
};
|
|
461
|
+
persistState();
|
|
462
|
+
return currentPoster;
|
|
463
|
+
}
|
|
69
464
|
// Reset state (useful for testing)
|
|
70
465
|
function resetState() {
|
|
71
466
|
currentPoster = null;
|
|
467
|
+
try {
|
|
468
|
+
(0, fs_1.unlinkSync)(STATE_FILE_PATH);
|
|
469
|
+
}
|
|
470
|
+
catch { /* ignore */ }
|
|
72
471
|
}
|
|
73
472
|
// Get current state
|
|
74
473
|
function getCurrentState() {
|
|
@@ -85,7 +484,7 @@ function getDefaultAnimationParams(type) {
|
|
|
85
484
|
const base = {
|
|
86
485
|
type,
|
|
87
486
|
enabled: true,
|
|
88
|
-
speed:
|
|
487
|
+
speed: 0.2,
|
|
89
488
|
amplitude: 50,
|
|
90
489
|
staggerDelay: 150,
|
|
91
490
|
staggerMode: 'sequential',
|
|
@@ -289,8 +688,225 @@ function createDefaultBackgroundLayer(backgroundColor) {
|
|
|
289
688
|
},
|
|
290
689
|
contentType: 'solid',
|
|
291
690
|
solidColor: backgroundColor,
|
|
691
|
+
fill: {
|
|
692
|
+
type: 'solid',
|
|
693
|
+
color: backgroundColor,
|
|
694
|
+
opacity: 1,
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
const DEFAULT_LINEAR_GRADIENT_FILL = {
|
|
699
|
+
type: 'linear',
|
|
700
|
+
angle: 135,
|
|
701
|
+
stops: [
|
|
702
|
+
{ color: '#7c3aed', opacity: 1, position: 0 },
|
|
703
|
+
{ color: '#22d3ee', opacity: 1, position: 1 },
|
|
704
|
+
],
|
|
705
|
+
};
|
|
706
|
+
const DEFAULT_RADIAL_GRADIENT_FILL = {
|
|
707
|
+
type: 'radial',
|
|
708
|
+
center: { x: 0.5, y: 0.5 },
|
|
709
|
+
radius: 0.8,
|
|
710
|
+
stops: [
|
|
711
|
+
{ color: '#a78bfa', opacity: 1, position: 0 },
|
|
712
|
+
{ color: '#0f172a', opacity: 1, position: 1 },
|
|
713
|
+
],
|
|
714
|
+
};
|
|
715
|
+
const MIN_GRADIENT_STOPS = 2;
|
|
716
|
+
const MAX_GRADIENT_STOPS = 8;
|
|
717
|
+
function clamp01(value) {
|
|
718
|
+
if (!Number.isFinite(value))
|
|
719
|
+
return 0;
|
|
720
|
+
return Math.min(1, Math.max(0, value));
|
|
721
|
+
}
|
|
722
|
+
function normalizeAngle(value) {
|
|
723
|
+
if (!Number.isFinite(value))
|
|
724
|
+
return 0;
|
|
725
|
+
const normalized = value % 360;
|
|
726
|
+
return normalized < 0 ? normalized + 360 : normalized;
|
|
727
|
+
}
|
|
728
|
+
function clampRadius(value) {
|
|
729
|
+
if (!Number.isFinite(value))
|
|
730
|
+
return DEFAULT_RADIAL_GRADIENT_FILL.radius;
|
|
731
|
+
return Math.min(2, Math.max(0.05, value));
|
|
732
|
+
}
|
|
733
|
+
function isHexColor(value) {
|
|
734
|
+
return /^#[0-9a-fA-F]{3}$/.test(value) || /^#[0-9a-fA-F]{6}$/.test(value);
|
|
735
|
+
}
|
|
736
|
+
function normalizeGradientStop(stop, fallback) {
|
|
737
|
+
return {
|
|
738
|
+
color: isHexColor(stop.color) ? stop.color : fallback.color,
|
|
739
|
+
opacity: clamp01(stop.opacity),
|
|
740
|
+
position: clamp01(stop.position),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function normalizeGradientStops(stops, fallbackStops) {
|
|
744
|
+
const fallbackStart = fallbackStops[0];
|
|
745
|
+
const fallbackEnd = fallbackStops[fallbackStops.length - 1] ?? fallbackStart;
|
|
746
|
+
const input = Array.isArray(stops) ? stops : [];
|
|
747
|
+
const normalized = input
|
|
748
|
+
.map((stop, index) => normalizeGradientStop(stop, fallbackStops[Math.min(index, fallbackStops.length - 1)] ?? fallbackEnd))
|
|
749
|
+
.slice(0, MAX_GRADIENT_STOPS);
|
|
750
|
+
if (normalized.length < MIN_GRADIENT_STOPS) {
|
|
751
|
+
if (normalized.length === 0) {
|
|
752
|
+
normalized.push(normalizeGradientStop(fallbackStart, fallbackStart), normalizeGradientStop(fallbackEnd, fallbackEnd));
|
|
753
|
+
}
|
|
754
|
+
else if (normalized.length === 1) {
|
|
755
|
+
normalized.push(normalizeGradientStop(fallbackEnd, fallbackEnd));
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return normalized
|
|
759
|
+
.map((stop, index) => ({ stop, index }))
|
|
760
|
+
.sort((a, b) => a.stop.position - b.stop.position || a.index - b.index)
|
|
761
|
+
.map(({ stop }) => stop);
|
|
762
|
+
}
|
|
763
|
+
function normalizeLinearGradientFill(fill) {
|
|
764
|
+
const fallback = DEFAULT_LINEAR_GRADIENT_FILL;
|
|
765
|
+
return {
|
|
766
|
+
type: 'linear',
|
|
767
|
+
angle: normalizeAngle(fill.angle),
|
|
768
|
+
stops: normalizeGradientStops(fill.stops, fallback.stops),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function normalizeRadialGradientFill(fill) {
|
|
772
|
+
const fallback = DEFAULT_RADIAL_GRADIENT_FILL;
|
|
773
|
+
return {
|
|
774
|
+
type: 'radial',
|
|
775
|
+
center: {
|
|
776
|
+
x: clamp01(fill.center?.x ?? fallback.center.x),
|
|
777
|
+
y: clamp01(fill.center?.y ?? fallback.center.y),
|
|
778
|
+
},
|
|
779
|
+
radius: clampRadius(fill.radius),
|
|
780
|
+
stops: normalizeGradientStops(fill.stops, fallback.stops),
|
|
292
781
|
};
|
|
293
782
|
}
|
|
783
|
+
function parseGradientStopsInput(input, fallbackStops) {
|
|
784
|
+
if (!Array.isArray(input))
|
|
785
|
+
return null;
|
|
786
|
+
const parsed = input
|
|
787
|
+
.map((item, index) => {
|
|
788
|
+
if (!item || typeof item !== 'object')
|
|
789
|
+
return null;
|
|
790
|
+
const candidate = item;
|
|
791
|
+
const fallback = fallbackStops[Math.min(index, fallbackStops.length - 1)] ?? fallbackStops[0];
|
|
792
|
+
const opacityRaw = typeof candidate.opacity === 'number' ? candidate.opacity : Number.parseFloat(String(candidate.opacity ?? ''));
|
|
793
|
+
const positionRaw = typeof candidate.position === 'number' ? candidate.position : Number.parseFloat(String(candidate.position ?? ''));
|
|
794
|
+
return {
|
|
795
|
+
color: typeof candidate.color === 'string' && isHexColor(candidate.color) ? candidate.color : fallback.color,
|
|
796
|
+
opacity: clamp01(Number.isFinite(opacityRaw) ? opacityRaw : fallback.opacity),
|
|
797
|
+
position: clamp01(Number.isFinite(positionRaw) ? positionRaw : fallback.position),
|
|
798
|
+
};
|
|
799
|
+
})
|
|
800
|
+
.filter((stop) => stop !== null);
|
|
801
|
+
return parsed.length >= MIN_GRADIENT_STOPS ? parsed : null;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Build a Fill object from flat MCP tool params (fillType, gradientStops, etc.).
|
|
805
|
+
* Returns null when no gradient params are present (caller should fall back to solid).
|
|
806
|
+
*/
|
|
807
|
+
function buildFillFromParams(params) {
|
|
808
|
+
const fillType = params.fillType;
|
|
809
|
+
const hasGradientParams = fillType === 'linear' || fillType === 'radial' ||
|
|
810
|
+
params.gradientStyle !== undefined ||
|
|
811
|
+
params.gradientStops !== undefined ||
|
|
812
|
+
params.gradientStartColor !== undefined ||
|
|
813
|
+
params.gradientEndColor !== undefined;
|
|
814
|
+
if (!hasGradientParams)
|
|
815
|
+
return null;
|
|
816
|
+
const gradientStyle = fillType === 'radial' || params.gradientStyle === 'radial' ? 'radial' : 'linear';
|
|
817
|
+
const defaults = gradientStyle === 'radial'
|
|
818
|
+
? DEFAULT_RADIAL_GRADIENT_FILL
|
|
819
|
+
: DEFAULT_LINEAR_GRADIENT_FILL;
|
|
820
|
+
const explicitStops = parseGradientStopsInput(params.gradientStops, defaults.stops);
|
|
821
|
+
const stops = explicitStops ?? [
|
|
822
|
+
{
|
|
823
|
+
color: params.gradientStartColor || defaults.stops[0].color,
|
|
824
|
+
opacity: defaults.stops[0].opacity,
|
|
825
|
+
position: defaults.stops[0].position,
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
color: params.gradientEndColor || defaults.stops[1].color,
|
|
829
|
+
opacity: defaults.stops[1].opacity,
|
|
830
|
+
position: defaults.stops[1].position,
|
|
831
|
+
},
|
|
832
|
+
];
|
|
833
|
+
if (gradientStyle === 'radial') {
|
|
834
|
+
return normalizeRadialGradientFill({
|
|
835
|
+
type: 'radial',
|
|
836
|
+
center: {
|
|
837
|
+
x: Number.isFinite(params.gradientCenterX)
|
|
838
|
+
? params.gradientCenterX
|
|
839
|
+
: DEFAULT_RADIAL_GRADIENT_FILL.center.x,
|
|
840
|
+
y: Number.isFinite(params.gradientCenterY)
|
|
841
|
+
? params.gradientCenterY
|
|
842
|
+
: DEFAULT_RADIAL_GRADIENT_FILL.center.y,
|
|
843
|
+
},
|
|
844
|
+
radius: Number.isFinite(params.gradientRadius)
|
|
845
|
+
? params.gradientRadius
|
|
846
|
+
: DEFAULT_RADIAL_GRADIENT_FILL.radius,
|
|
847
|
+
stops,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
return normalizeLinearGradientFill({
|
|
851
|
+
type: 'linear',
|
|
852
|
+
angle: Number.isFinite(params.gradientAngle)
|
|
853
|
+
? params.gradientAngle
|
|
854
|
+
: DEFAULT_LINEAR_GRADIENT_FILL.angle,
|
|
855
|
+
stops,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
/** Reusable schema properties for gradient fill params — spread into tool schemas. */
|
|
859
|
+
const GRADIENT_FILL_SCHEMA_PROPS = {
|
|
860
|
+
fillType: {
|
|
861
|
+
type: 'string',
|
|
862
|
+
enum: ['solid', 'linear', 'radial'],
|
|
863
|
+
description: 'Fill type: "solid" (default), "linear" gradient, or "radial" gradient',
|
|
864
|
+
},
|
|
865
|
+
gradientStops: {
|
|
866
|
+
type: 'array',
|
|
867
|
+
description: 'Gradient color stops (2-8). Overrides start/end colors when provided.',
|
|
868
|
+
items: {
|
|
869
|
+
type: 'object',
|
|
870
|
+
properties: {
|
|
871
|
+
color: { type: 'string', description: 'Stop color in hex' },
|
|
872
|
+
opacity: { type: 'number', description: 'Stop opacity (0-1)' },
|
|
873
|
+
position: { type: 'number', description: 'Stop position (0-1)' },
|
|
874
|
+
},
|
|
875
|
+
required: ['color', 'position'],
|
|
876
|
+
},
|
|
877
|
+
minItems: 2,
|
|
878
|
+
maxItems: 8,
|
|
879
|
+
},
|
|
880
|
+
gradientAngle: {
|
|
881
|
+
type: 'number',
|
|
882
|
+
description: 'Linear gradient angle in degrees (default 135)',
|
|
883
|
+
},
|
|
884
|
+
gradientStartColor: {
|
|
885
|
+
type: 'string',
|
|
886
|
+
description: 'Gradient start color in hex (shortcut for 2-stop gradient)',
|
|
887
|
+
},
|
|
888
|
+
gradientEndColor: {
|
|
889
|
+
type: 'string',
|
|
890
|
+
description: 'Gradient end color in hex (shortcut for 2-stop gradient)',
|
|
891
|
+
},
|
|
892
|
+
gradientStyle: {
|
|
893
|
+
type: 'string',
|
|
894
|
+
enum: ['linear', 'radial'],
|
|
895
|
+
description: 'Gradient style (alternative to fillType for setting gradient type)',
|
|
896
|
+
},
|
|
897
|
+
gradientCenterX: {
|
|
898
|
+
type: 'number',
|
|
899
|
+
description: 'Radial gradient center X (0-1)',
|
|
900
|
+
},
|
|
901
|
+
gradientCenterY: {
|
|
902
|
+
type: 'number',
|
|
903
|
+
description: 'Radial gradient center Y (0-1)',
|
|
904
|
+
},
|
|
905
|
+
gradientRadius: {
|
|
906
|
+
type: 'number',
|
|
907
|
+
description: 'Radial gradient radius (0.05-2)',
|
|
908
|
+
},
|
|
909
|
+
};
|
|
294
910
|
// Tool definitions
|
|
295
911
|
exports.stateTools = [
|
|
296
912
|
{
|
|
@@ -301,7 +917,7 @@ exports.stateTools = [
|
|
|
301
917
|
properties: {
|
|
302
918
|
aspectRatio: {
|
|
303
919
|
type: 'string',
|
|
304
|
-
enum: ['16:9', '9:16', '1:1', '4:3', 'full'],
|
|
920
|
+
enum: ['16:9', '9:16', '1:1', '4:3', '3:4', 'full'],
|
|
305
921
|
description: 'Canvas aspect ratio (default: 9:16 for portrait poster)',
|
|
306
922
|
default: '9:16',
|
|
307
923
|
},
|
|
@@ -310,19 +926,97 @@ exports.stateTools = [
|
|
|
310
926
|
description: 'Background color in hex format (e.g., "#1a1a1a")',
|
|
311
927
|
default: '#1a1a1a',
|
|
312
928
|
},
|
|
929
|
+
canvasColor: {
|
|
930
|
+
type: 'string',
|
|
931
|
+
description: 'Viewport background color in hex (e.g., "#0a0a0a")',
|
|
932
|
+
},
|
|
933
|
+
overflow: {
|
|
934
|
+
type: 'string',
|
|
935
|
+
enum: ['visible', 'hidden', 'translucent'],
|
|
936
|
+
description: 'Canvas overflow mode',
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
required: [],
|
|
940
|
+
},
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
name: 'clear_poster',
|
|
944
|
+
description: 'Clear all content layers from the poster, keeping only the background. Resets effect and post-processes.',
|
|
945
|
+
inputSchema: {
|
|
946
|
+
type: 'object',
|
|
947
|
+
properties: {},
|
|
948
|
+
required: [],
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
name: 'set_page_layout',
|
|
953
|
+
description: 'Set page/artboard layout mode. Use layoutMode="absolute" for Free placement. Use layoutMode="row" or "column" for Flex layout of top-level layers inside the artboard.',
|
|
954
|
+
inputSchema: {
|
|
955
|
+
type: 'object',
|
|
956
|
+
properties: {
|
|
957
|
+
layoutMode: {
|
|
958
|
+
type: 'string',
|
|
959
|
+
enum: ['absolute', 'row', 'column'],
|
|
960
|
+
description: 'Page layout mode: "absolute" (Free) or flex direction ("row"/"column")',
|
|
961
|
+
},
|
|
962
|
+
justifyContent: {
|
|
963
|
+
type: 'string',
|
|
964
|
+
enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'],
|
|
965
|
+
description: 'Main axis alignment (flex only)',
|
|
966
|
+
},
|
|
967
|
+
alignItems: {
|
|
968
|
+
type: 'string',
|
|
969
|
+
enum: ['start', 'center', 'end'],
|
|
970
|
+
description: 'Cross axis alignment (flex only)',
|
|
971
|
+
},
|
|
972
|
+
wrap: { type: 'boolean', description: 'Wrap to next row (row mode only)', default: false },
|
|
973
|
+
gap: { type: 'number', description: 'Space between children as % of canvas (2=small, 4=medium, 6=large)', default: 0 },
|
|
974
|
+
padding: { type: 'number', description: 'Padding as % of canvas', default: 0 },
|
|
975
|
+
paddingTop: { type: 'number', description: 'Padding top as % of canvas' },
|
|
976
|
+
paddingRight: { type: 'number', description: 'Padding right as % of canvas' },
|
|
977
|
+
paddingBottom: { type: 'number', description: 'Padding bottom as % of canvas' },
|
|
978
|
+
paddingLeft: { type: 'number', description: 'Padding left as % of canvas' },
|
|
979
|
+
},
|
|
980
|
+
required: [],
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
name: 'modify_canvas',
|
|
985
|
+
description: 'Modify the canvas/artboard settings after creation. Change aspect ratio, background color, viewport color, or overflow mode.',
|
|
986
|
+
inputSchema: {
|
|
987
|
+
type: 'object',
|
|
988
|
+
properties: {
|
|
989
|
+
aspectRatio: {
|
|
990
|
+
type: 'string',
|
|
991
|
+
enum: ['16:9', '9:16', '1:1', '4:3', '3:4', 'full'],
|
|
992
|
+
description: 'Canvas aspect ratio',
|
|
993
|
+
},
|
|
994
|
+
backgroundColor: {
|
|
995
|
+
type: 'string',
|
|
996
|
+
description: 'Artboard background color in hex (e.g., "#1a1a1a")',
|
|
997
|
+
},
|
|
998
|
+
canvasColor: {
|
|
999
|
+
type: 'string',
|
|
1000
|
+
description: 'Viewport background color in hex (area outside artboard)',
|
|
1001
|
+
},
|
|
1002
|
+
overflow: {
|
|
1003
|
+
type: 'string',
|
|
1004
|
+
enum: ['visible', 'hidden', 'translucent'],
|
|
1005
|
+
description: 'Canvas overflow mode',
|
|
1006
|
+
},
|
|
313
1007
|
},
|
|
314
1008
|
required: [],
|
|
315
1009
|
},
|
|
316
1010
|
},
|
|
317
1011
|
{
|
|
318
1012
|
name: 'set_background',
|
|
319
|
-
description: 'Configure the background layer (always at index 0). Can be solid
|
|
1013
|
+
description: 'Configure the background layer (always at index 0). Can be solid fill, linear/radial gradient fill, image, or video.',
|
|
320
1014
|
inputSchema: {
|
|
321
1015
|
type: 'object',
|
|
322
1016
|
properties: {
|
|
323
1017
|
type: {
|
|
324
1018
|
type: 'string',
|
|
325
|
-
enum: ['solid', 'image', 'video'],
|
|
1019
|
+
enum: ['solid', 'gradient', 'image', 'video'],
|
|
326
1020
|
description: 'Background content type',
|
|
327
1021
|
default: 'solid',
|
|
328
1022
|
},
|
|
@@ -330,6 +1024,50 @@ exports.stateTools = [
|
|
|
330
1024
|
type: 'string',
|
|
331
1025
|
description: 'Solid background color in hex (for type: solid)',
|
|
332
1026
|
},
|
|
1027
|
+
gradientStartColor: {
|
|
1028
|
+
type: 'string',
|
|
1029
|
+
description: 'Gradient start color in hex (for type: gradient)',
|
|
1030
|
+
},
|
|
1031
|
+
gradientEndColor: {
|
|
1032
|
+
type: 'string',
|
|
1033
|
+
description: 'Gradient end color in hex (for type: gradient)',
|
|
1034
|
+
},
|
|
1035
|
+
gradientStops: {
|
|
1036
|
+
type: 'array',
|
|
1037
|
+
description: 'Optional gradient stops array (2-8 items). If provided, overrides start/end colors.',
|
|
1038
|
+
items: {
|
|
1039
|
+
type: 'object',
|
|
1040
|
+
properties: {
|
|
1041
|
+
color: { type: 'string', description: 'Stop color in hex' },
|
|
1042
|
+
opacity: { type: 'number', description: 'Stop opacity (0-1)' },
|
|
1043
|
+
position: { type: 'number', description: 'Stop position (0-1)' },
|
|
1044
|
+
},
|
|
1045
|
+
required: ['color', 'position'],
|
|
1046
|
+
},
|
|
1047
|
+
minItems: 2,
|
|
1048
|
+
maxItems: 8,
|
|
1049
|
+
},
|
|
1050
|
+
gradientStyle: {
|
|
1051
|
+
type: 'string',
|
|
1052
|
+
enum: ['linear', 'radial'],
|
|
1053
|
+
description: 'Gradient style (for type: gradient)',
|
|
1054
|
+
},
|
|
1055
|
+
gradientAngle: {
|
|
1056
|
+
type: 'number',
|
|
1057
|
+
description: 'Linear gradient angle in degrees (for linear style)',
|
|
1058
|
+
},
|
|
1059
|
+
gradientCenterX: {
|
|
1060
|
+
type: 'number',
|
|
1061
|
+
description: 'Radial center X (0-1, for radial style)',
|
|
1062
|
+
},
|
|
1063
|
+
gradientCenterY: {
|
|
1064
|
+
type: 'number',
|
|
1065
|
+
description: 'Radial center Y (0-1, for radial style)',
|
|
1066
|
+
},
|
|
1067
|
+
gradientRadius: {
|
|
1068
|
+
type: 'number',
|
|
1069
|
+
description: 'Radial radius scale (for radial style)',
|
|
1070
|
+
},
|
|
333
1071
|
imageUrl: {
|
|
334
1072
|
type: 'string',
|
|
335
1073
|
description: 'Image URL (for type: image)',
|
|
@@ -356,26 +1094,66 @@ exports.stateTools = [
|
|
|
356
1094
|
properties: {
|
|
357
1095
|
type: {
|
|
358
1096
|
type: 'string',
|
|
359
|
-
enum: ['text', 'image', 'video'],
|
|
1097
|
+
enum: ['text', 'image', 'video', 'rectangle', 'ellipse', 'polygon', 'star', 'line'],
|
|
360
1098
|
description: 'Type of layer to add',
|
|
361
1099
|
},
|
|
362
1100
|
name: { type: 'string', description: 'Layer name' },
|
|
363
1101
|
// Transform properties (normalized coordinates relative to canvas)
|
|
364
|
-
x: { type: 'number', description: '
|
|
365
|
-
y: { type: 'number', description: '
|
|
366
|
-
width: { type: 'number', description: 'Width (
|
|
367
|
-
height: { type: 'number', description: 'Height
|
|
1102
|
+
x: { type: 'number', description: 'Horizontal position in % from left (0=left edge, 50=center, 100=right edge)', default: 50 },
|
|
1103
|
+
y: { type: 'number', description: 'Vertical position in % from top (0=top edge, 50=center, 100=bottom edge)', default: 50 },
|
|
1104
|
+
width: { type: 'number', description: 'Width as % of canvas (e.g. 60 = 60%)', default: 100 },
|
|
1105
|
+
height: { type: 'number', description: 'Height as % of canvas', default: 100 },
|
|
368
1106
|
rotation: { type: 'number', description: 'Rotation in degrees', default: 0 },
|
|
369
1107
|
opacity: { type: 'number', description: 'Opacity 0-1', default: 1 },
|
|
1108
|
+
// Flex child properties (when inside a flex group or page-level flex)
|
|
1109
|
+
positioning: { type: 'string', enum: ['flow', 'absolute'], description: 'Child positioning inside flex groups. "absolute" opts out of flow layout.' },
|
|
1110
|
+
flexWidthMode: { type: 'string', enum: ['hug', 'fixed', 'fill'], description: 'Child width sizing mode inside flex groups (independent per-axis).' },
|
|
1111
|
+
flexHeightMode: { type: 'string', enum: ['hug', 'fixed', 'fill'], description: 'Child height sizing mode inside flex groups (independent per-axis).' },
|
|
1112
|
+
flexSizeMode: { type: 'string', enum: ['hug', 'fixed', 'fill'], description: 'Legacy: sets both axes. Prefer flexWidthMode/flexHeightMode.' },
|
|
1113
|
+
alignSelf: { type: 'string', enum: ['auto', 'start', 'center', 'end', 'stretch'], description: 'Child cross-axis alignment override inside flex groups.' },
|
|
1114
|
+
flexGrow: { type: 'number', minimum: 0, description: 'Flex grow factor. Controls how fill children share free space proportionally (like CSS flex-grow). Default 1 for fill children.' },
|
|
1115
|
+
// Shape style properties (rectangle/ellipse/polygon/star/line)
|
|
1116
|
+
fillColor: { type: 'string', description: 'Shape fill color in hex (e.g. #ffffff)' },
|
|
1117
|
+
fillOpacity: { type: 'number', description: 'Shape fill opacity (0-1)' },
|
|
1118
|
+
...GRADIENT_FILL_SCHEMA_PROPS,
|
|
1119
|
+
strokeColor: { type: 'string', description: 'Shape stroke color in hex' },
|
|
1120
|
+
strokeWidth: { type: 'number', description: 'Shape stroke width (normalized; e.g. 0.005-0.02)' },
|
|
1121
|
+
strokeOpacity: { type: 'number', description: 'Shape stroke opacity (0-1)' },
|
|
1122
|
+
// Rectangle/Polygon/Star geometry
|
|
1123
|
+
cornerRadius: { type: 'number', description: 'Corner radius (normalized)' },
|
|
1124
|
+
// Polygon geometry
|
|
1125
|
+
sides: { type: 'number', description: 'Polygon sides (3+)' },
|
|
1126
|
+
// Star geometry
|
|
1127
|
+
innerRadiusRatio: { type: 'number', description: 'Star inner radius ratio (0-1)' },
|
|
1128
|
+
points: { type: 'number', description: 'Star points (3+)' },
|
|
1129
|
+
// Line geometry (normalized -1..1 coordinates)
|
|
1130
|
+
startX: { type: 'number', description: 'Line start X in % from left (0=left, 50=center, 100=right)' },
|
|
1131
|
+
startY: { type: 'number', description: 'Line start Y in % from top (0=top, 50=center, 100=bottom)' },
|
|
1132
|
+
endX: { type: 'number', description: 'Line end X in % from left (0=left, 50=center, 100=right)' },
|
|
1133
|
+
endY: { type: 'number', description: 'Line end Y in % from top (0=top, 50=center, 100=bottom)' },
|
|
370
1134
|
// Text layer properties
|
|
371
1135
|
content: { type: 'string', description: 'Text content (required for text layers)' },
|
|
1136
|
+
textType: { type: 'string', enum: ['frame'], description: 'Text mode (always "frame" — point text is deprecated)', default: 'frame' },
|
|
372
1137
|
fontFamily: { type: 'string', description: 'Font family name', default: 'DM Sans' },
|
|
373
1138
|
fontSize: { type: 'number', description: 'Font size in pixels', default: 72 },
|
|
374
1139
|
fontWeight: { type: 'string', enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], description: 'Font weight', default: 'bold' },
|
|
375
1140
|
color: { type: 'string', description: 'Text color in hex', default: '#ffffff' },
|
|
376
1141
|
textAlign: { type: 'string', enum: ['left', 'center', 'right'], description: 'Text alignment', default: 'center' },
|
|
377
|
-
|
|
378
|
-
|
|
1142
|
+
verticalAlign: { type: 'string', enum: ['top', 'middle', 'bottom'], description: 'Vertical text alignment inside the frame. Defaults to "top" for frame text.' },
|
|
1143
|
+
letterSpacing: { type: 'number', description: 'Letter spacing in pixels (negative for tighter, e.g., -1 to -2 for headlines)', default: 0 },
|
|
1144
|
+
lineHeight: { type: 'number', description: 'Line height multiplier (0.9-1.0 for tight headlines, 1.3-1.5 for body)', default: 1.2 },
|
|
1145
|
+
textTransformMode: { type: 'string', enum: ['box'], description: 'Text sizing mode (always "box" — scale mode is deprecated)', default: 'box' },
|
|
1146
|
+
textBoxWidth: { type: 'number', description: 'Width in pixels for text wrapping when using box mode (e.g., 300-600 for readable lines)' },
|
|
1147
|
+
frameWidth: { type: 'number', description: 'Frame width normalized (0-1 relative to artboard width). Convenience alias for frameWidthPx.' },
|
|
1148
|
+
frameHeight: { type: 'number', description: 'Frame height normalized (0-1 relative to artboard height). Convenience alias for frameHeightPx.' },
|
|
1149
|
+
frameWidthPx: { type: 'number', description: 'Frame width in CSS pixels for frame text (resizing changes this, not fontSize)' },
|
|
1150
|
+
frameHeightPx: { type: 'number', description: 'Frame height in CSS pixels for frame text' },
|
|
1151
|
+
autoSize: { type: 'string', enum: ['none', 'height', 'widthAndHeight'], description: 'Auto-size mode for frame text' },
|
|
1152
|
+
// Text CSS properties
|
|
1153
|
+
textTransform: { type: 'string', enum: ['none', 'uppercase', 'lowercase', 'capitalize'], description: 'CSS text-transform (visual only, does not mutate stored content)' },
|
|
1154
|
+
textDecoration: { type: 'string', enum: ['none', 'underline', 'line-through', 'overline'], description: 'CSS text-decoration' },
|
|
1155
|
+
textDecorationColor: { type: 'string', description: 'Decoration line color in hex (defaults to text color)' },
|
|
1156
|
+
textDecorationThickness: { type: 'number', description: 'Decoration line thickness in CSS px' },
|
|
379
1157
|
// Text animation properties (use list_text_animations to see all options)
|
|
380
1158
|
animationType: {
|
|
381
1159
|
type: 'string',
|
|
@@ -449,36 +1227,204 @@ exports.stateTools = [
|
|
|
449
1227
|
// Image/Video properties
|
|
450
1228
|
mediaUrl: { type: 'string', description: 'Media URL (required for image/video layers)' },
|
|
451
1229
|
objectFit: { type: 'string', enum: ['cover', 'contain'], description: 'Object fit mode', default: 'cover' },
|
|
1230
|
+
// Shadow properties (all layer types)
|
|
1231
|
+
shadowOffsetX: { type: 'number', description: 'Shadow horizontal offset in CSS px' },
|
|
1232
|
+
shadowOffsetY: { type: 'number', description: 'Shadow vertical offset in CSS px' },
|
|
1233
|
+
shadowBlur: { type: 'number', description: 'Shadow blur radius in CSS px' },
|
|
1234
|
+
shadowColor: { type: 'string', description: 'Shadow color in hex' },
|
|
1235
|
+
shadowOpacity: { type: 'number', description: 'Shadow opacity (0-1)' },
|
|
1236
|
+
// Blend mode (all layer types)
|
|
1237
|
+
blendMode: { type: 'string', enum: ['normal', 'multiply', 'screen', 'darken', 'lighten', 'difference'], description: 'CSS-like blend mode' },
|
|
1238
|
+
// Margin (all layer types, for flex layout spacing)
|
|
1239
|
+
marginTop: { type: 'number', description: 'Margin top (normalized 0-1)' },
|
|
1240
|
+
marginRight: { type: 'number', description: 'Margin right (normalized 0-1)' },
|
|
1241
|
+
marginBottom: { type: 'number', description: 'Margin bottom (normalized 0-1)' },
|
|
1242
|
+
marginLeft: { type: 'number', description: 'Margin left (normalized 0-1)' },
|
|
1243
|
+
// Min/max size constraints (all layer types)
|
|
1244
|
+
minWidth: { type: 'number', description: 'Minimum width (normalized 0-1)' },
|
|
1245
|
+
maxWidth: { type: 'number', description: 'Maximum width (normalized 0-1)' },
|
|
1246
|
+
minHeight: { type: 'number', description: 'Minimum height (normalized 0-1)' },
|
|
1247
|
+
maxHeight: { type: 'number', description: 'Maximum height (normalized 0-1)' },
|
|
1248
|
+
// Transform origin (all layer types)
|
|
1249
|
+
originX: { type: 'number', description: 'Transform origin X (0=left, 0.5=center, 1=right)' },
|
|
1250
|
+
originY: { type: 'number', description: 'Transform origin Y (0=top, 0.5=center, 1=bottom)' },
|
|
452
1251
|
},
|
|
453
1252
|
required: ['type'],
|
|
454
1253
|
},
|
|
455
1254
|
},
|
|
1255
|
+
{
|
|
1256
|
+
name: 'add_group',
|
|
1257
|
+
description: 'Add a flexbox container with child layers. Works like CSS flexbox: set direction (column/row), alignment, and gap. Children are positioned automatically by the layout engine — do NOT set child x/y. Use this for ALL text layout (heading + subheading, title + date, etc.).',
|
|
1258
|
+
inputSchema: {
|
|
1259
|
+
type: 'object',
|
|
1260
|
+
properties: {
|
|
1261
|
+
name: { type: 'string', description: 'Group name' },
|
|
1262
|
+
layoutMode: { type: 'string', enum: ['row', 'column'], description: 'Flex direction: "column" stacks vertically (default, most common), "row" places side by side', default: 'column' },
|
|
1263
|
+
widthMode: { type: 'string', enum: ['hug', 'fixed'], description: 'Horizontal sizing mode: hug contents or fixed width' },
|
|
1264
|
+
heightMode: { type: 'string', enum: ['hug', 'fixed'], description: 'Vertical sizing mode: hug contents or fixed height' },
|
|
1265
|
+
gap: { type: 'number', description: 'Space between children as % of canvas (2=small, 4=medium, 6=large). Negative values overlap children.', default: 2 },
|
|
1266
|
+
justifyContent: { type: 'string', enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'], description: 'Main-axis alignment (like CSS justify-content)', default: 'start' },
|
|
1267
|
+
alignItems: { type: 'string', enum: ['start', 'center', 'end'], description: 'Cross-axis alignment (like CSS align-items): "start"=left, "center"=centered, "end"=right', default: 'center' },
|
|
1268
|
+
wrap: { type: 'boolean', description: 'Wrap children to next line on overflow (like CSS flex-wrap)', default: false },
|
|
1269
|
+
padding: { type: 'number', description: 'Padding as % of canvas', default: 0 },
|
|
1270
|
+
paddingTop: { type: 'number', description: 'Padding top as % of canvas' },
|
|
1271
|
+
paddingRight: { type: 'number', description: 'Padding right as % of canvas' },
|
|
1272
|
+
paddingBottom: { type: 'number', description: 'Padding bottom as % of canvas' },
|
|
1273
|
+
paddingLeft: { type: 'number', description: 'Padding left as % of canvas' },
|
|
1274
|
+
fillColor: { type: 'string', description: 'Group background color (hex)' },
|
|
1275
|
+
fillOpacity: { type: 'number', description: 'Group background opacity (0-1)' },
|
|
1276
|
+
cornerRadius: { type: 'number', description: 'Corner radius' },
|
|
1277
|
+
x: { type: 'number', description: 'Horizontal position in % from left (0=left edge, 50=center, 100=right edge)', default: 50 },
|
|
1278
|
+
y: { type: 'number', description: 'Vertical position in % from top (0=top edge, 50=center, 100=bottom edge)', default: 50 },
|
|
1279
|
+
width: { type: 'number', description: 'Width as % of canvas (e.g. 60 = 60%)', default: 60 },
|
|
1280
|
+
height: { type: 'number', description: 'Height as % of canvas', default: 40 },
|
|
1281
|
+
rotation: { type: 'number', description: 'Rotation in degrees', default: 0 },
|
|
1282
|
+
opacity: { type: 'number', description: 'Opacity 0-1', default: 1 },
|
|
1283
|
+
children: {
|
|
1284
|
+
type: 'array',
|
|
1285
|
+
description: 'Child layers to add inside the group',
|
|
1286
|
+
items: {
|
|
1287
|
+
type: 'object',
|
|
1288
|
+
properties: {
|
|
1289
|
+
type: { type: 'string', enum: ['text', 'image', 'video', 'rectangle', 'ellipse', 'polygon', 'star', 'line'], description: 'Child layer type' },
|
|
1290
|
+
name: { type: 'string', description: 'Child layer name' },
|
|
1291
|
+
content: { type: 'string', description: 'Text content' },
|
|
1292
|
+
fontFamily: { type: 'string', description: 'Font family' },
|
|
1293
|
+
fontSize: { type: 'number', description: 'Font size in pixels' },
|
|
1294
|
+
fontWeight: { type: 'string', description: 'Font weight' },
|
|
1295
|
+
color: { type: 'string', description: 'Text color in hex' },
|
|
1296
|
+
textAlign: { type: 'string', enum: ['left', 'center', 'right'], description: 'Text alignment' },
|
|
1297
|
+
verticalAlign: { type: 'string', enum: ['top', 'middle', 'bottom'], description: 'Vertical text alignment inside the frame' },
|
|
1298
|
+
letterSpacing: { type: 'number', description: 'Letter spacing' },
|
|
1299
|
+
lineHeight: { type: 'number', description: 'Line height multiplier' },
|
|
1300
|
+
textType: { type: 'string', enum: ['frame'], description: 'Text mode (always "frame")' },
|
|
1301
|
+
textTransformMode: { type: 'string', enum: ['box'], description: 'Text sizing mode (always "box")' },
|
|
1302
|
+
textBoxWidth: { type: 'number', description: 'Width for text wrapping' },
|
|
1303
|
+
frameWidth: { type: 'number', description: 'Frame width normalized (0-1 relative to artboard width). Convenience alias for frameWidthPx.' },
|
|
1304
|
+
frameHeight: { type: 'number', description: 'Frame height normalized (0-1 relative to artboard height). Convenience alias for frameHeightPx.' },
|
|
1305
|
+
frameWidthPx: { type: 'number', description: 'Frame width in CSS pixels' },
|
|
1306
|
+
frameHeightPx: { type: 'number', description: 'Frame height in CSS pixels' },
|
|
1307
|
+
autoSize: { type: 'string', enum: ['none', 'height', 'widthAndHeight'], description: 'Auto-size mode' },
|
|
1308
|
+
textTransform: { type: 'string', enum: ['none', 'uppercase', 'lowercase', 'capitalize'], description: 'CSS text-transform' },
|
|
1309
|
+
textDecoration: { type: 'string', enum: ['none', 'underline', 'line-through', 'overline'], description: 'CSS text-decoration' },
|
|
1310
|
+
textDecorationColor: { type: 'string', description: 'Decoration line color (hex)' },
|
|
1311
|
+
textDecorationThickness: { type: 'number', description: 'Decoration line thickness in CSS px' },
|
|
1312
|
+
mediaUrl: { type: 'string', description: 'Media URL (for image/video)' },
|
|
1313
|
+
objectFit: { type: 'string', enum: ['cover', 'contain'], description: 'Object fit mode' },
|
|
1314
|
+
width: { type: 'number', description: 'Width as % of canvas (e.g. 60 = 60%)' },
|
|
1315
|
+
height: { type: 'number', description: 'Height as % of canvas' },
|
|
1316
|
+
opacity: { type: 'number', description: 'Opacity 0-1' },
|
|
1317
|
+
// Flex child properties
|
|
1318
|
+
positioning: { type: 'string', enum: ['flow', 'absolute'], description: 'Child positioning in group flex layout' },
|
|
1319
|
+
flexWidthMode: { type: 'string', enum: ['hug', 'fixed', 'fill'], description: 'Child width sizing in group flex layout' },
|
|
1320
|
+
flexHeightMode: { type: 'string', enum: ['hug', 'fixed', 'fill'], description: 'Child height sizing in group flex layout' },
|
|
1321
|
+
flexSizeMode: { type: 'string', enum: ['hug', 'fixed', 'fill'], description: 'Legacy: sets both axes. Prefer flexWidthMode/flexHeightMode.' },
|
|
1322
|
+
alignSelf: { type: 'string', enum: ['auto', 'start', 'center', 'end', 'stretch'], description: 'Child cross-axis alignment override in group flex layout' },
|
|
1323
|
+
flexGrow: { type: 'number', minimum: 0, description: 'Flex grow factor for proportional space distribution (like CSS flex-grow). Default 1 for fill children.' },
|
|
1324
|
+
// Shape style
|
|
1325
|
+
fillColor: { type: 'string', description: 'Shape fill color (hex)' },
|
|
1326
|
+
fillOpacity: { type: 'number', description: 'Shape fill opacity (0-1)' },
|
|
1327
|
+
...GRADIENT_FILL_SCHEMA_PROPS,
|
|
1328
|
+
strokeColor: { type: 'string', description: 'Shape stroke color (hex)' },
|
|
1329
|
+
strokeWidth: { type: 'number', description: 'Shape stroke width (normalized; e.g. 0.005-0.02)' },
|
|
1330
|
+
strokeOpacity: { type: 'number', description: 'Shape stroke opacity (0-1)' },
|
|
1331
|
+
// Shape geometry
|
|
1332
|
+
cornerRadius: { type: 'number', description: 'Corner radius (normalized)' },
|
|
1333
|
+
sides: { type: 'number', description: 'Polygon sides (3+)' },
|
|
1334
|
+
innerRadiusRatio: { type: 'number', description: 'Star inner radius ratio (0-1)' },
|
|
1335
|
+
points: { type: 'number', description: 'Star points (3+)' },
|
|
1336
|
+
startX: { type: 'number', description: 'Line start X in % from left (0=left, 50=center, 100=right)' },
|
|
1337
|
+
startY: { type: 'number', description: 'Line start Y in % from top (0=top, 50=center, 100=bottom)' },
|
|
1338
|
+
endX: { type: 'number', description: 'Line end X in % from left (0=left, 50=center, 100=right)' },
|
|
1339
|
+
endY: { type: 'number', description: 'Line end Y in % from top (0=top, 50=center, 100=bottom)' },
|
|
1340
|
+
// Shadow (all layer types)
|
|
1341
|
+
shadowOffsetX: { type: 'number', description: 'Shadow horizontal offset in CSS px' },
|
|
1342
|
+
shadowOffsetY: { type: 'number', description: 'Shadow vertical offset in CSS px' },
|
|
1343
|
+
shadowBlur: { type: 'number', description: 'Shadow blur radius in CSS px' },
|
|
1344
|
+
shadowColor: { type: 'string', description: 'Shadow color in hex' },
|
|
1345
|
+
shadowOpacity: { type: 'number', description: 'Shadow opacity (0-1)' },
|
|
1346
|
+
blendMode: { type: 'string', enum: ['normal', 'multiply', 'screen', 'darken', 'lighten', 'difference'], description: 'CSS-like blend mode' },
|
|
1347
|
+
marginTop: { type: 'number', description: 'Margin top (normalized 0-1)' },
|
|
1348
|
+
marginRight: { type: 'number', description: 'Margin right (normalized 0-1)' },
|
|
1349
|
+
marginBottom: { type: 'number', description: 'Margin bottom (normalized 0-1)' },
|
|
1350
|
+
marginLeft: { type: 'number', description: 'Margin left (normalized 0-1)' },
|
|
1351
|
+
minWidth: { type: 'number', description: 'Minimum width (normalized 0-1)' },
|
|
1352
|
+
maxWidth: { type: 'number', description: 'Maximum width (normalized 0-1)' },
|
|
1353
|
+
minHeight: { type: 'number', description: 'Minimum height (normalized 0-1)' },
|
|
1354
|
+
maxHeight: { type: 'number', description: 'Maximum height (normalized 0-1)' },
|
|
1355
|
+
originX: { type: 'number', description: 'Transform origin X (0=left, 0.5=center, 1=right)' },
|
|
1356
|
+
originY: { type: 'number', description: 'Transform origin Y (0=top, 0.5=center, 1=bottom)' },
|
|
1357
|
+
cornerRadiusTopLeft: { type: 'number', description: 'Per-corner radius: top-left (for rectangle layers)' },
|
|
1358
|
+
cornerRadiusTopRight: { type: 'number', description: 'Per-corner radius: top-right (for rectangle layers)' },
|
|
1359
|
+
cornerRadiusBottomRight: { type: 'number', description: 'Per-corner radius: bottom-right (for rectangle layers)' },
|
|
1360
|
+
cornerRadiusBottomLeft: { type: 'number', description: 'Per-corner radius: bottom-left (for rectangle layers)' },
|
|
1361
|
+
},
|
|
1362
|
+
required: ['type'],
|
|
1363
|
+
},
|
|
1364
|
+
},
|
|
1365
|
+
},
|
|
1366
|
+
required: ['children'],
|
|
1367
|
+
},
|
|
1368
|
+
},
|
|
456
1369
|
{
|
|
457
1370
|
name: 'modify_layer',
|
|
458
|
-
description: 'Modify an existing layer by ID',
|
|
1371
|
+
description: 'Modify an existing layer by ID. Supports text, image, video, group, and shape properties.',
|
|
459
1372
|
inputSchema: {
|
|
460
1373
|
type: 'object',
|
|
461
1374
|
properties: {
|
|
462
1375
|
layerId: { type: 'string', description: 'ID of the layer to modify' },
|
|
463
|
-
// Transform properties (
|
|
464
|
-
x: { type: 'number', description: '
|
|
465
|
-
y: { type: 'number', description: '
|
|
466
|
-
width: { type: 'number', description: 'Width' },
|
|
467
|
-
height: { type: 'number', description: 'Height' },
|
|
1376
|
+
// Transform properties (CSS % coordinates)
|
|
1377
|
+
x: { type: 'number', description: 'Horizontal position in % from left (0=left edge, 50=center, 100=right edge)' },
|
|
1378
|
+
y: { type: 'number', description: 'Vertical position in % from top (0=top edge, 50=center, 100=bottom edge)' },
|
|
1379
|
+
width: { type: 'number', description: 'Width as % of canvas (e.g. 60 = 60%)' },
|
|
1380
|
+
height: { type: 'number', description: 'Height as % of canvas' },
|
|
468
1381
|
rotation: { type: 'number', description: 'Rotation in degrees' },
|
|
469
1382
|
opacity: { type: 'number', description: 'Opacity 0-1' },
|
|
470
1383
|
visible: { type: 'boolean', description: 'Layer visibility' },
|
|
471
1384
|
locked: { type: 'boolean', description: 'Layer lock state' },
|
|
472
1385
|
name: { type: 'string', description: 'Layer name' },
|
|
1386
|
+
// Flex child properties
|
|
1387
|
+
positioning: { type: 'string', enum: ['flow', 'absolute'], description: 'Child positioning mode inside flex groups' },
|
|
1388
|
+
flexWidthMode: { type: 'string', enum: ['hug', 'fixed', 'fill'], description: 'Child width sizing mode inside flex groups (independent per-axis).' },
|
|
1389
|
+
flexHeightMode: { type: 'string', enum: ['hug', 'fixed', 'fill'], description: 'Child height sizing mode inside flex groups (independent per-axis).' },
|
|
1390
|
+
flexSizeMode: { type: 'string', enum: ['hug', 'fixed', 'fill'], description: 'Legacy: sets both axes. Prefer flexWidthMode/flexHeightMode.' },
|
|
1391
|
+
alignSelf: { type: 'string', enum: ['auto', 'start', 'center', 'end', 'stretch'], description: 'Child cross-axis alignment override inside flex groups' },
|
|
1392
|
+
flexGrow: { type: 'number', minimum: 0, description: 'Flex grow factor for proportional space distribution (like CSS flex-grow). Default 1 for fill children.' },
|
|
1393
|
+
// Shape style properties (rectangle/ellipse/polygon/star/line)
|
|
1394
|
+
strokeColor: { type: 'string', description: 'Shape stroke color (hex)' },
|
|
1395
|
+
strokeWidth: { type: 'number', description: 'Shape stroke width (normalized; e.g. 0.005-0.02)' },
|
|
1396
|
+
strokeOpacity: { type: 'number', description: 'Shape stroke opacity (0-1)' },
|
|
1397
|
+
// Shape geometry
|
|
1398
|
+
sides: { type: 'number', description: 'Polygon sides (3+)' },
|
|
1399
|
+
innerRadiusRatio: { type: 'number', description: 'Star inner radius ratio (0-1)' },
|
|
1400
|
+
points: { type: 'number', description: 'Star points (3+)' },
|
|
1401
|
+
startX: { type: 'number', description: 'Line start X in % from left (0=left, 50=center, 100=right)' },
|
|
1402
|
+
startY: { type: 'number', description: 'Line start Y in % from top (0=top, 50=center, 100=bottom)' },
|
|
1403
|
+
endX: { type: 'number', description: 'Line end X in % from left (0=left, 50=center, 100=right)' },
|
|
1404
|
+
endY: { type: 'number', description: 'Line end Y in % from top (0=top, 50=center, 100=bottom)' },
|
|
473
1405
|
// Text layer properties
|
|
474
1406
|
content: { type: 'string' },
|
|
1407
|
+
textType: { type: 'string', enum: ['frame'], description: 'Text mode (always "frame")' },
|
|
475
1408
|
fontFamily: { type: 'string' },
|
|
476
1409
|
fontSize: { type: 'number' },
|
|
477
1410
|
fontWeight: { type: 'string' },
|
|
478
1411
|
color: { type: 'string' },
|
|
479
1412
|
textAlign: { type: 'string' },
|
|
1413
|
+
verticalAlign: { type: 'string', enum: ['top', 'middle', 'bottom'], description: 'Vertical text alignment inside the frame' },
|
|
480
1414
|
letterSpacing: { type: 'number' },
|
|
481
1415
|
lineHeight: { type: 'number' },
|
|
1416
|
+
textTransformMode: { type: 'string', enum: ['box'] },
|
|
1417
|
+
textBoxWidth: { type: 'number' },
|
|
1418
|
+
frameWidth: { type: 'number', description: 'Frame width normalized (0-1 relative to artboard width). Convenience alias for frameWidthPx.' },
|
|
1419
|
+
frameHeight: { type: 'number', description: 'Frame height normalized (0-1 relative to artboard height). Convenience alias for frameHeightPx.' },
|
|
1420
|
+
frameWidthPx: { type: 'number' },
|
|
1421
|
+
frameHeightPx: { type: 'number' },
|
|
1422
|
+
autoSize: { type: 'string', enum: ['none', 'height', 'widthAndHeight'] },
|
|
1423
|
+
// Text CSS properties
|
|
1424
|
+
textTransform: { type: 'string', enum: ['none', 'uppercase', 'lowercase', 'capitalize'], description: 'CSS text-transform (visual only, does not mutate stored content)' },
|
|
1425
|
+
textDecoration: { type: 'string', enum: ['none', 'underline', 'line-through', 'overline'], description: 'CSS text-decoration' },
|
|
1426
|
+
textDecorationColor: { type: 'string', description: 'Decoration line color in hex (defaults to text color)' },
|
|
1427
|
+
textDecorationThickness: { type: 'number', description: 'Decoration line thickness in CSS px' },
|
|
482
1428
|
// Text animation properties (all type-specific params supported - see add_layer for descriptions)
|
|
483
1429
|
animationType: {
|
|
484
1430
|
type: 'string',
|
|
@@ -535,6 +1481,49 @@ exports.stateTools = [
|
|
|
535
1481
|
// Media properties
|
|
536
1482
|
mediaUrl: { type: 'string' },
|
|
537
1483
|
objectFit: { type: 'string' },
|
|
1484
|
+
// Group layout properties
|
|
1485
|
+
layoutMode: { type: 'string', enum: ['row', 'column'], description: 'Group layout direction' },
|
|
1486
|
+
widthMode: { type: 'string', enum: ['hug', 'fixed'], description: 'Group horizontal sizing mode' },
|
|
1487
|
+
heightMode: { type: 'string', enum: ['hug', 'fixed'], description: 'Group vertical sizing mode' },
|
|
1488
|
+
gap: { type: 'number', description: 'Space between children as % of canvas (2=small, 4=medium, 6=large)' },
|
|
1489
|
+
justifyContent: { type: 'string', enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'], description: 'Group main-axis alignment' },
|
|
1490
|
+
alignItems: { type: 'string', enum: ['start', 'center', 'end'], description: 'Group cross-axis alignment' },
|
|
1491
|
+
wrap: { type: 'boolean', description: 'Group wrap children' },
|
|
1492
|
+
padding: { type: 'number', description: 'Padding as % of canvas' },
|
|
1493
|
+
paddingTop: { type: 'number', description: 'Padding top as % of canvas' },
|
|
1494
|
+
paddingRight: { type: 'number', description: 'Padding right as % of canvas' },
|
|
1495
|
+
paddingBottom: { type: 'number', description: 'Padding bottom as % of canvas' },
|
|
1496
|
+
paddingLeft: { type: 'number', description: 'Padding left as % of canvas' },
|
|
1497
|
+
fillColor: { type: 'string', description: 'Fill color (group background OR shape fill)' },
|
|
1498
|
+
fillOpacity: { type: 'number', description: 'Fill opacity (group background OR shape fill)' },
|
|
1499
|
+
...GRADIENT_FILL_SCHEMA_PROPS,
|
|
1500
|
+
cornerRadius: { type: 'number', description: 'Corner radius (group OR shape)' },
|
|
1501
|
+
// Shadow properties (all layer types)
|
|
1502
|
+
shadowOffsetX: { type: 'number', description: 'Shadow horizontal offset in CSS px' },
|
|
1503
|
+
shadowOffsetY: { type: 'number', description: 'Shadow vertical offset in CSS px' },
|
|
1504
|
+
shadowBlur: { type: 'number', description: 'Shadow blur radius in CSS px' },
|
|
1505
|
+
shadowColor: { type: 'string', description: 'Shadow color in hex' },
|
|
1506
|
+
shadowOpacity: { type: 'number', description: 'Shadow opacity (0-1)' },
|
|
1507
|
+
// Blend mode (all layer types)
|
|
1508
|
+
blendMode: { type: 'string', enum: ['normal', 'multiply', 'screen', 'darken', 'lighten', 'difference'], description: 'CSS-like blend mode' },
|
|
1509
|
+
// Margin (all layer types)
|
|
1510
|
+
marginTop: { type: 'number', description: 'Margin top (normalized 0-1)' },
|
|
1511
|
+
marginRight: { type: 'number', description: 'Margin right (normalized 0-1)' },
|
|
1512
|
+
marginBottom: { type: 'number', description: 'Margin bottom (normalized 0-1)' },
|
|
1513
|
+
marginLeft: { type: 'number', description: 'Margin left (normalized 0-1)' },
|
|
1514
|
+
// Min/max size constraints (all layer types)
|
|
1515
|
+
minWidth: { type: 'number', description: 'Minimum width (normalized 0-1)' },
|
|
1516
|
+
maxWidth: { type: 'number', description: 'Maximum width (normalized 0-1)' },
|
|
1517
|
+
minHeight: { type: 'number', description: 'Minimum height (normalized 0-1)' },
|
|
1518
|
+
maxHeight: { type: 'number', description: 'Maximum height (normalized 0-1)' },
|
|
1519
|
+
// Transform origin (all layer types)
|
|
1520
|
+
originX: { type: 'number', description: 'Transform origin X (0=left, 0.5=center, 1=right)' },
|
|
1521
|
+
originY: { type: 'number', description: 'Transform origin Y (0=top, 0.5=center, 1=bottom)' },
|
|
1522
|
+
// Per-corner radius (rectangle and group layers)
|
|
1523
|
+
cornerRadiusTopLeft: { type: 'number', description: 'Per-corner radius: top-left' },
|
|
1524
|
+
cornerRadiusTopRight: { type: 'number', description: 'Per-corner radius: top-right' },
|
|
1525
|
+
cornerRadiusBottomRight: { type: 'number', description: 'Per-corner radius: bottom-right' },
|
|
1526
|
+
cornerRadiusBottomLeft: { type: 'number', description: 'Per-corner radius: bottom-left' },
|
|
538
1527
|
},
|
|
539
1528
|
required: ['layerId'],
|
|
540
1529
|
},
|
|
@@ -560,12 +1549,15 @@ IMPORTANT - Two effect pipelines:
|
|
|
560
1549
|
'dither-floyd-steinberg', 'dither-atkinson', 'dither-jarvis-judice-ninke',
|
|
561
1550
|
'dither-stucki', 'dither-burkes', 'dither-sierra', 'dither-two-row-sierra',
|
|
562
1551
|
'dither-sierra-lite',
|
|
1552
|
+
'color-separation',
|
|
563
1553
|
// Halftone (WebGPU - supports post-processes)
|
|
564
1554
|
'halftone-mono', 'halftone-cmyk',
|
|
565
1555
|
// Glitch (WebGPU - supports post-processes)
|
|
566
1556
|
'glitch-chromatic', 'glitch-digital', 'glitch-vhs', 'glitch-weird',
|
|
567
1557
|
// Art (WebGPU - supports post-processes)
|
|
568
1558
|
'art-kuwahara', 'art-crosshatch', 'art-lineart', 'art-engraving', 'art-stipple',
|
|
1559
|
+
// Special (WebGPU - supports post-processes)
|
|
1560
|
+
'special-warp',
|
|
569
1561
|
],
|
|
570
1562
|
description: 'Effect ID to apply',
|
|
571
1563
|
},
|
|
@@ -611,7 +1603,7 @@ Common combinations:
|
|
|
611
1603
|
'scanlines', 'vignette', 'chromatic-aberration', 'curvature',
|
|
612
1604
|
'grain', 'noise', 'pixelate', 'wave', 'rgb-glitch', 'brightness-contrast',
|
|
613
1605
|
'color-tint', 'palette', 'jitter', 'bloom', 'dot-screen', 'sepia',
|
|
614
|
-
'grid', 'light-beams', 'motion-blur',
|
|
1606
|
+
'grid', 'light-beams', 'motion-blur', 'blob-tracking',
|
|
615
1607
|
],
|
|
616
1608
|
description: 'Post-process type',
|
|
617
1609
|
},
|
|
@@ -669,6 +1661,17 @@ Common combinations:
|
|
|
669
1661
|
density: { type: 'number', description: 'Light-beams: ray spread (0.5-2)', default: 1 },
|
|
670
1662
|
animated: { type: 'boolean', description: 'Light-beams/Grid: enable animation', default: false },
|
|
671
1663
|
particleAmount: { type: 'number', description: 'Light-beams: particle density (0-1)', default: 0.5 },
|
|
1664
|
+
// Blob Tracking
|
|
1665
|
+
rectScale: { type: 'number', description: 'Blob-tracking: rectangle size multiplier (0.3-3)', default: 1 },
|
|
1666
|
+
zoom: { type: 'number', description: 'Blob-tracking: zoom factor inside rectangles (0.2-1.0)', default: 0.6 },
|
|
1667
|
+
invertChance: { type: 'number', description: 'Blob-tracking: chance of inverting colors (0-1)', default: 0.08 },
|
|
1668
|
+
animSpeed: { type: 'number', description: 'Blob-tracking: animation speed (0-3)', default: 1 },
|
|
1669
|
+
lineColor: { type: 'string', description: 'Blob-tracking: connecting line color (hex)', default: '#ffffff' },
|
|
1670
|
+
lineOpacity: { type: 'number', description: 'Blob-tracking: line opacity (0-1)', default: 0.7 },
|
|
1671
|
+
rectColor: { type: 'string', description: 'Blob-tracking: rectangle outline color (hex)', default: '#ffff00' },
|
|
1672
|
+
rectOpacity: { type: 'number', description: 'Blob-tracking: rectangle opacity (0-1)', default: 1 },
|
|
1673
|
+
showLabels: { type: 'boolean', description: 'Blob-tracking: show tracking ID labels', default: true },
|
|
1674
|
+
glow: { type: 'number', description: 'Blob-tracking: glow intensity (0-1)', default: 0.3 },
|
|
672
1675
|
},
|
|
673
1676
|
required: ['type'],
|
|
674
1677
|
},
|
|
@@ -684,6 +1687,22 @@ Common combinations:
|
|
|
684
1687
|
required: ['layerId'],
|
|
685
1688
|
},
|
|
686
1689
|
},
|
|
1690
|
+
{
|
|
1691
|
+
name: 'move_layer',
|
|
1692
|
+
description: 'Move a layer in the layer stack (z-order). Cannot move the background layer (index 0).',
|
|
1693
|
+
inputSchema: {
|
|
1694
|
+
type: 'object',
|
|
1695
|
+
properties: {
|
|
1696
|
+
layerId: { type: 'string', description: 'ID of the layer to move' },
|
|
1697
|
+
direction: {
|
|
1698
|
+
type: 'string',
|
|
1699
|
+
enum: ['up', 'down', 'top', 'bottom'],
|
|
1700
|
+
description: 'Where to move it: up/down = one step, top/bottom = to front/back',
|
|
1701
|
+
},
|
|
1702
|
+
},
|
|
1703
|
+
required: ['layerId', 'direction'],
|
|
1704
|
+
},
|
|
1705
|
+
},
|
|
687
1706
|
{
|
|
688
1707
|
name: 'get_state',
|
|
689
1708
|
description: 'Get the current poster state as LayerShareState JSON (for debugging or verification)',
|
|
@@ -733,11 +1752,24 @@ async function handleStateTool(name, args) {
|
|
|
733
1752
|
case 'create_poster': {
|
|
734
1753
|
const aspectRatio = params.aspectRatio || '9:16';
|
|
735
1754
|
const backgroundColor = params.backgroundColor || '#1a1a1a';
|
|
1755
|
+
const canvasColor = params.canvasColor;
|
|
1756
|
+
const overflow = params.overflow;
|
|
736
1757
|
currentPoster = {
|
|
737
|
-
canvas: {
|
|
1758
|
+
canvas: {
|
|
1759
|
+
aspectRatio,
|
|
1760
|
+
backgroundColor,
|
|
1761
|
+
...(canvasColor && { canvasColor }),
|
|
1762
|
+
...(overflow && { overflow }),
|
|
1763
|
+
layoutMode: 'column',
|
|
1764
|
+
alignItems: 'center',
|
|
1765
|
+
justifyContent: 'start',
|
|
1766
|
+
gap: 0.04,
|
|
1767
|
+
padding: { top: 0.08, right: 0.08, bottom: 0.08, left: 0.08 },
|
|
1768
|
+
},
|
|
738
1769
|
layers: [createDefaultBackgroundLayer(backgroundColor)],
|
|
739
1770
|
postProcesses: [],
|
|
740
1771
|
};
|
|
1772
|
+
persistState();
|
|
741
1773
|
return {
|
|
742
1774
|
content: [
|
|
743
1775
|
{
|
|
@@ -752,12 +1784,110 @@ async function handleStateTool(name, args) {
|
|
|
752
1784
|
],
|
|
753
1785
|
};
|
|
754
1786
|
}
|
|
755
|
-
case '
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1787
|
+
case 'clear_poster': {
|
|
1788
|
+
ensurePoster();
|
|
1789
|
+
const bgLayer = currentPoster.layers.find((l) => l.type === 'background');
|
|
1790
|
+
const defaultBg = createDefaultBackgroundLayer(currentPoster.canvas.backgroundColor || '#1a1a1a');
|
|
1791
|
+
currentPoster = {
|
|
1792
|
+
canvas: { ...currentPoster.canvas },
|
|
1793
|
+
layers: bgLayer ? [bgLayer] : [defaultBg],
|
|
1794
|
+
postProcesses: [],
|
|
1795
|
+
};
|
|
1796
|
+
persistState();
|
|
1797
|
+
return {
|
|
1798
|
+
content: [{
|
|
1799
|
+
type: 'text',
|
|
1800
|
+
text: JSON.stringify({
|
|
1801
|
+
success: true,
|
|
1802
|
+
message: 'Poster cleared — all content layers, effects, and post-processes removed.',
|
|
1803
|
+
}),
|
|
1804
|
+
}],
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
case 'set_page_layout': {
|
|
1808
|
+
ensurePoster();
|
|
1809
|
+
const layoutMode = params.layoutMode;
|
|
1810
|
+
if (layoutMode !== undefined) {
|
|
1811
|
+
if (layoutMode !== 'absolute' && layoutMode !== 'row' && layoutMode !== 'column') {
|
|
1812
|
+
return {
|
|
1813
|
+
content: [{ type: 'text', text: 'Error: layoutMode must be one of: absolute, row, column' }],
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
currentPoster.canvas.layoutMode = layoutMode;
|
|
1817
|
+
}
|
|
1818
|
+
if (params.justifyContent !== undefined) {
|
|
1819
|
+
currentPoster.canvas.justifyContent = params.justifyContent;
|
|
1820
|
+
}
|
|
1821
|
+
if (params.alignItems !== undefined) {
|
|
1822
|
+
currentPoster.canvas.alignItems = params.alignItems;
|
|
1823
|
+
}
|
|
1824
|
+
if (params.wrap !== undefined) {
|
|
1825
|
+
currentPoster.canvas.wrap = Boolean(params.wrap);
|
|
1826
|
+
}
|
|
1827
|
+
if (params.gap !== undefined) {
|
|
1828
|
+
currentPoster.canvas.gap = cssPercentToSize(params.gap, (currentPoster.canvas.gap ?? 0) * 100);
|
|
1829
|
+
}
|
|
1830
|
+
if (params.padding !== undefined ||
|
|
1831
|
+
params.paddingTop !== undefined ||
|
|
1832
|
+
params.paddingRight !== undefined ||
|
|
1833
|
+
params.paddingBottom !== undefined ||
|
|
1834
|
+
params.paddingLeft !== undefined) {
|
|
1835
|
+
const existing = currentPoster.canvas.padding || { top: 0, right: 0, bottom: 0, left: 0 };
|
|
1836
|
+
const uniformPct = params.padding !== undefined ? parseNumericInput(params.padding) : undefined;
|
|
1837
|
+
const uniform = uniformPct !== undefined ? cssPercentToSize(uniformPct, 0) : undefined;
|
|
1838
|
+
const nextPadding = {
|
|
1839
|
+
top: params.paddingTop !== undefined ? cssPercentToSize(params.paddingTop, 0) : uniform ?? existing.top,
|
|
1840
|
+
right: params.paddingRight !== undefined ? cssPercentToSize(params.paddingRight, 0) : uniform ?? existing.right,
|
|
1841
|
+
bottom: params.paddingBottom !== undefined ? cssPercentToSize(params.paddingBottom, 0) : uniform ?? existing.bottom,
|
|
1842
|
+
left: params.paddingLeft !== undefined ? cssPercentToSize(params.paddingLeft, 0) : uniform ?? existing.left,
|
|
759
1843
|
};
|
|
1844
|
+
currentPoster.canvas.padding = nextPadding;
|
|
760
1845
|
}
|
|
1846
|
+
persistState();
|
|
1847
|
+
return {
|
|
1848
|
+
content: [
|
|
1849
|
+
{
|
|
1850
|
+
type: 'text',
|
|
1851
|
+
text: JSON.stringify({
|
|
1852
|
+
success: true,
|
|
1853
|
+
message: `Page layout updated (${currentPoster.canvas.layoutMode ?? 'absolute'})`,
|
|
1854
|
+
canvas: currentPoster.canvas,
|
|
1855
|
+
}, null, 2),
|
|
1856
|
+
},
|
|
1857
|
+
],
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
case 'modify_canvas': {
|
|
1861
|
+
ensurePoster();
|
|
1862
|
+
const canvas = currentPoster.canvas;
|
|
1863
|
+
if (params.aspectRatio)
|
|
1864
|
+
canvas.aspectRatio = params.aspectRatio;
|
|
1865
|
+
if (params.backgroundColor)
|
|
1866
|
+
canvas.backgroundColor = params.backgroundColor;
|
|
1867
|
+
if (params.canvasColor !== undefined)
|
|
1868
|
+
canvas.canvasColor = params.canvasColor;
|
|
1869
|
+
if (params.overflow !== undefined)
|
|
1870
|
+
canvas.overflow = params.overflow;
|
|
1871
|
+
persistState();
|
|
1872
|
+
return {
|
|
1873
|
+
content: [
|
|
1874
|
+
{
|
|
1875
|
+
type: 'text',
|
|
1876
|
+
text: JSON.stringify({
|
|
1877
|
+
success: true,
|
|
1878
|
+
canvas: {
|
|
1879
|
+
aspectRatio: canvas.aspectRatio,
|
|
1880
|
+
backgroundColor: canvas.backgroundColor,
|
|
1881
|
+
canvasColor: canvas.canvasColor,
|
|
1882
|
+
overflow: canvas.overflow,
|
|
1883
|
+
},
|
|
1884
|
+
}, null, 2),
|
|
1885
|
+
},
|
|
1886
|
+
],
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
case 'set_background': {
|
|
1890
|
+
ensurePoster();
|
|
761
1891
|
// Background is always at index 0
|
|
762
1892
|
const bgLayer = currentPoster.layers[0];
|
|
763
1893
|
if (bgLayer.type !== 'background') {
|
|
@@ -765,13 +1895,34 @@ async function handleStateTool(name, args) {
|
|
|
765
1895
|
content: [{ type: 'text', text: 'Error: Background layer not found at index 0' }],
|
|
766
1896
|
};
|
|
767
1897
|
}
|
|
768
|
-
const
|
|
1898
|
+
const requestedType = params.type || 'solid';
|
|
1899
|
+
const contentType = requestedType === 'gradient' ? 'solid' : requestedType;
|
|
769
1900
|
bgLayer.contentType = contentType;
|
|
770
|
-
if (
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1901
|
+
if (requestedType === 'gradient') {
|
|
1902
|
+
const gradient = buildFillFromParams(params)
|
|
1903
|
+
?? normalizeLinearGradientFill(DEFAULT_LINEAR_GRADIENT_FILL);
|
|
1904
|
+
bgLayer.fill = gradient;
|
|
1905
|
+
bgLayer.solidColor = gradient.stops[0].color;
|
|
1906
|
+
currentPoster.canvas.backgroundColor = gradient.stops[0].color;
|
|
1907
|
+
delete bgLayer.inputMedia;
|
|
1908
|
+
}
|
|
1909
|
+
else if (contentType === 'solid') {
|
|
1910
|
+
const solidColor = params.color ||
|
|
1911
|
+
(bgLayer.fill?.type === 'solid'
|
|
1912
|
+
? bgLayer.fill.color
|
|
1913
|
+
: bgLayer.fill
|
|
1914
|
+
? bgLayer.fill.stops[0]?.color
|
|
1915
|
+
: undefined) ||
|
|
1916
|
+
bgLayer.solidColor ||
|
|
1917
|
+
currentPoster.canvas.backgroundColor ||
|
|
1918
|
+
'#5c5c5c';
|
|
1919
|
+
bgLayer.solidColor = solidColor;
|
|
1920
|
+
bgLayer.fill = {
|
|
1921
|
+
type: 'solid',
|
|
1922
|
+
color: solidColor,
|
|
1923
|
+
opacity: 1,
|
|
1924
|
+
};
|
|
1925
|
+
currentPoster.canvas.backgroundColor = solidColor;
|
|
775
1926
|
delete bgLayer.inputMedia;
|
|
776
1927
|
}
|
|
777
1928
|
else if (contentType === 'image' && params.imageUrl) {
|
|
@@ -794,6 +1945,7 @@ async function handleStateTool(name, args) {
|
|
|
794
1945
|
objectFit: params.objectFit || 'cover',
|
|
795
1946
|
};
|
|
796
1947
|
}
|
|
1948
|
+
persistState();
|
|
797
1949
|
return {
|
|
798
1950
|
content: [
|
|
799
1951
|
{
|
|
@@ -808,11 +1960,7 @@ async function handleStateTool(name, args) {
|
|
|
808
1960
|
};
|
|
809
1961
|
}
|
|
810
1962
|
case 'add_layer': {
|
|
811
|
-
|
|
812
|
-
return {
|
|
813
|
-
content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
|
|
814
|
-
};
|
|
815
|
-
}
|
|
1963
|
+
ensurePoster();
|
|
816
1964
|
const layerType = params.type;
|
|
817
1965
|
if (!layerType) {
|
|
818
1966
|
return {
|
|
@@ -824,38 +1972,76 @@ async function handleStateTool(name, args) {
|
|
|
824
1972
|
content: [{ type: 'text', text: 'Error: Use set_background to modify the background layer' }],
|
|
825
1973
|
};
|
|
826
1974
|
}
|
|
1975
|
+
const isShapeType = ['rectangle', 'ellipse', 'polygon', 'star', 'line'].includes(layerType);
|
|
827
1976
|
const layerId = generateId();
|
|
1977
|
+
const visualOverrides = parseVisualOverrides(params);
|
|
1978
|
+
const originOverrides = parseTransformOrigin(params);
|
|
828
1979
|
const baseLayer = {
|
|
829
1980
|
id: layerId,
|
|
830
1981
|
type: layerType,
|
|
831
1982
|
name: params.name || `${layerType.charAt(0).toUpperCase() + layerType.slice(1)} Layer`,
|
|
832
1983
|
visible: true,
|
|
833
1984
|
locked: false,
|
|
1985
|
+
...parseFlexChildProps(params),
|
|
1986
|
+
...visualOverrides,
|
|
834
1987
|
transform: {
|
|
835
|
-
x: params.x
|
|
836
|
-
y: params.y
|
|
837
|
-
|
|
838
|
-
|
|
1988
|
+
x: cssPercentToX(params.x, 50),
|
|
1989
|
+
y: cssPercentToY(params.y, 50),
|
|
1990
|
+
// Shapes store their intrinsic size in geometry fields; keep transform scale at 1.
|
|
1991
|
+
width: isShapeType ? 1 : cssPercentToSize(params.width, 100),
|
|
1992
|
+
height: isShapeType ? 1 : cssPercentToSize(params.height, 100),
|
|
839
1993
|
rotation: params.rotation ?? 0,
|
|
840
|
-
opacity: params.opacity
|
|
1994
|
+
opacity: normalizeOpacity(params.opacity, 1),
|
|
1995
|
+
...originOverrides,
|
|
841
1996
|
},
|
|
842
1997
|
};
|
|
843
1998
|
let layer;
|
|
844
1999
|
if (layerType === 'text') {
|
|
845
2000
|
const animation = buildAnimationSettings(params);
|
|
846
|
-
|
|
2001
|
+
const artboardAspect = getArtboardAspect(currentPoster.canvas.aspectRatio);
|
|
2002
|
+
const content = params.content || 'Text';
|
|
2003
|
+
const fontSize = params.fontSize || 72;
|
|
2004
|
+
const frameWidthNormalized = parseNormalizedFrameDimension(params.frameWidth);
|
|
2005
|
+
const frameHeightNormalized = parseNormalizedFrameDimension(params.frameHeight);
|
|
2006
|
+
const frameWidthPxFromNormalized = frameWidthNormalized !== undefined
|
|
2007
|
+
? textNormalizedToPx(frameWidthNormalized, artboardAspect, 'width')
|
|
2008
|
+
: undefined;
|
|
2009
|
+
const frameHeightPxFromNormalized = frameHeightNormalized !== undefined
|
|
2010
|
+
? textNormalizedToPx(frameHeightNormalized, artboardAspect, 'height')
|
|
2011
|
+
: undefined;
|
|
2012
|
+
// All text is frame text — silently convert any 'point'/'scale' requests
|
|
2013
|
+
let frameWidthPx = typeof params.frameWidthPx === 'number' ? params.frameWidthPx : frameWidthPxFromNormalized;
|
|
2014
|
+
const frameHeightPx = typeof params.frameHeightPx === 'number' ? params.frameHeightPx : frameHeightPxFromNormalized;
|
|
2015
|
+
const textBoxWidth = typeof params.textBoxWidth === 'number' ? params.textBoxWidth : undefined;
|
|
2016
|
+
const requestedAutoSize = typeof params.autoSize === 'string'
|
|
2017
|
+
? params.autoSize
|
|
2018
|
+
: undefined;
|
|
2019
|
+
const autoSize = requestedAutoSize ?? 'height';
|
|
2020
|
+
if (frameWidthPx == null && textBoxWidth == null) {
|
|
2021
|
+
const defaultFrameWidthNormalized = fontSize >= 72 ? 0.88 : 0.78;
|
|
2022
|
+
frameWidthPx = textNormalizedToPx(defaultFrameWidthNormalized, artboardAspect, 'width');
|
|
2023
|
+
}
|
|
847
2024
|
layer = {
|
|
848
2025
|
...baseLayer,
|
|
849
2026
|
type: 'text',
|
|
850
|
-
content
|
|
2027
|
+
content,
|
|
2028
|
+
textType: 'frame',
|
|
851
2029
|
fontFamily: normalizeFontFamily(params.fontFamily),
|
|
852
|
-
fontSize
|
|
2030
|
+
fontSize,
|
|
853
2031
|
fontWeight: normalizeFontWeight(params.fontWeight),
|
|
854
2032
|
color: params.color || '#ffffff',
|
|
855
2033
|
textAlign: params.textAlign || 'center',
|
|
2034
|
+
...(params.verticalAlign ? { verticalAlign: params.verticalAlign } : {}),
|
|
856
2035
|
letterSpacing: params.letterSpacing ?? 0,
|
|
857
2036
|
lineHeight: params.lineHeight ?? 1.2,
|
|
858
|
-
|
|
2037
|
+
textTransformMode: 'box',
|
|
2038
|
+
...(typeof textBoxWidth === 'number' ? { textBoxWidth } : {}),
|
|
2039
|
+
...(typeof frameWidthPx === 'number' ? { frameWidthPx } : {}),
|
|
2040
|
+
...(typeof frameHeightPx === 'number' ? { frameHeightPx } : {}),
|
|
2041
|
+
...(typeof autoSize === 'string' ? { autoSize } : {}),
|
|
2042
|
+
...(animation ? { animation } : {}),
|
|
2043
|
+
...(parseTextTransform(params) ? { textTransform: parseTextTransform(params) } : {}),
|
|
2044
|
+
...parseTextDecoration(params),
|
|
859
2045
|
};
|
|
860
2046
|
}
|
|
861
2047
|
else if (layerType === 'image') {
|
|
@@ -892,10 +2078,140 @@ async function handleStateTool(name, args) {
|
|
|
892
2078
|
playbackSpeed: 1,
|
|
893
2079
|
};
|
|
894
2080
|
}
|
|
2081
|
+
else if (layerType === 'rectangle') {
|
|
2082
|
+
const fillColor = normalizeHexColor(params.fillColor) || '#3b82f6';
|
|
2083
|
+
const fillOpacity = normalizeOpacity(params.fillOpacity, 1);
|
|
2084
|
+
const strokeColor = normalizeHexColor(params.strokeColor) || '#000000';
|
|
2085
|
+
const gradientFill = buildFillFromParams(params);
|
|
2086
|
+
layer = {
|
|
2087
|
+
...baseLayer,
|
|
2088
|
+
type: 'rectangle',
|
|
2089
|
+
style: {
|
|
2090
|
+
fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
|
|
2091
|
+
fillType: 'solid',
|
|
2092
|
+
fillColor,
|
|
2093
|
+
fillOpacity,
|
|
2094
|
+
strokeColor,
|
|
2095
|
+
strokeWidth: normalizeStrokeWidth(params.strokeWidth, 0),
|
|
2096
|
+
strokeOpacity: normalizeOpacity(params.strokeOpacity, 1),
|
|
2097
|
+
strokeCap: 'round',
|
|
2098
|
+
strokeJoin: 'round',
|
|
2099
|
+
},
|
|
2100
|
+
shapeWidth: cssPercentToSize(params.width, 20),
|
|
2101
|
+
shapeHeight: cssPercentToSize(params.height, 20),
|
|
2102
|
+
cornerRadius: normalizeCornerRadius(params.cornerRadius, 0),
|
|
2103
|
+
...(parseCornerRadii(params) ? { cornerRadii: parseCornerRadii(params) } : {}),
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
else if (layerType === 'ellipse') {
|
|
2107
|
+
const fillColor = normalizeHexColor(params.fillColor) || '#3b82f6';
|
|
2108
|
+
const fillOpacity = normalizeOpacity(params.fillOpacity, 1);
|
|
2109
|
+
const strokeColor = normalizeHexColor(params.strokeColor) || '#000000';
|
|
2110
|
+
const w = cssPercentToSize(params.width, 20);
|
|
2111
|
+
const h = cssPercentToSize(params.height, 20);
|
|
2112
|
+
const gradientFill = buildFillFromParams(params);
|
|
2113
|
+
layer = {
|
|
2114
|
+
...baseLayer,
|
|
2115
|
+
type: 'ellipse',
|
|
2116
|
+
style: {
|
|
2117
|
+
fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
|
|
2118
|
+
fillType: 'solid',
|
|
2119
|
+
fillColor,
|
|
2120
|
+
fillOpacity,
|
|
2121
|
+
strokeColor,
|
|
2122
|
+
strokeWidth: normalizeStrokeWidth(params.strokeWidth, 0),
|
|
2123
|
+
strokeOpacity: normalizeOpacity(params.strokeOpacity, 1),
|
|
2124
|
+
strokeCap: 'round',
|
|
2125
|
+
strokeJoin: 'round',
|
|
2126
|
+
},
|
|
2127
|
+
radiusX: w / 2,
|
|
2128
|
+
radiusY: h / 2,
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
else if (layerType === 'polygon') {
|
|
2132
|
+
const fillColor = normalizeHexColor(params.fillColor) || '#3b82f6';
|
|
2133
|
+
const fillOpacity = normalizeOpacity(params.fillOpacity, 1);
|
|
2134
|
+
const strokeColor = normalizeHexColor(params.strokeColor) || '#000000';
|
|
2135
|
+
const w = cssPercentToSize(params.width, 20);
|
|
2136
|
+
const h = cssPercentToSize(params.height, 20);
|
|
2137
|
+
const gradientFill = buildFillFromParams(params);
|
|
2138
|
+
layer = {
|
|
2139
|
+
...baseLayer,
|
|
2140
|
+
type: 'polygon',
|
|
2141
|
+
style: {
|
|
2142
|
+
fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
|
|
2143
|
+
fillType: 'solid',
|
|
2144
|
+
fillColor,
|
|
2145
|
+
fillOpacity,
|
|
2146
|
+
strokeColor,
|
|
2147
|
+
strokeWidth: normalizeStrokeWidth(params.strokeWidth, 0),
|
|
2148
|
+
strokeOpacity: normalizeOpacity(params.strokeOpacity, 1),
|
|
2149
|
+
strokeCap: 'round',
|
|
2150
|
+
strokeJoin: 'round',
|
|
2151
|
+
},
|
|
2152
|
+
radius: Math.min(w, h) / 2,
|
|
2153
|
+
sides: normalizePositiveInt(params.sides, 6, 3, 32),
|
|
2154
|
+
cornerRadius: normalizeCornerRadius(params.cornerRadius, 0),
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
else if (layerType === 'star') {
|
|
2158
|
+
const fillColor = normalizeHexColor(params.fillColor) || '#3b82f6';
|
|
2159
|
+
const fillOpacity = normalizeOpacity(params.fillOpacity, 1);
|
|
2160
|
+
const strokeColor = normalizeHexColor(params.strokeColor) || '#000000';
|
|
2161
|
+
const w = cssPercentToSize(params.width, 20);
|
|
2162
|
+
const h = cssPercentToSize(params.height, 20);
|
|
2163
|
+
const gradientFill = buildFillFromParams(params);
|
|
2164
|
+
layer = {
|
|
2165
|
+
...baseLayer,
|
|
2166
|
+
type: 'star',
|
|
2167
|
+
style: {
|
|
2168
|
+
fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
|
|
2169
|
+
fillType: 'solid',
|
|
2170
|
+
fillColor,
|
|
2171
|
+
fillOpacity,
|
|
2172
|
+
strokeColor,
|
|
2173
|
+
strokeWidth: normalizeStrokeWidth(params.strokeWidth, 0),
|
|
2174
|
+
strokeOpacity: normalizeOpacity(params.strokeOpacity, 1),
|
|
2175
|
+
strokeCap: 'round',
|
|
2176
|
+
strokeJoin: 'round',
|
|
2177
|
+
},
|
|
2178
|
+
outerRadius: Math.min(w, h) / 2,
|
|
2179
|
+
innerRadiusRatio: clamp(parseNumericInput(params.innerRadiusRatio), 0.05, 0.95, 0.5),
|
|
2180
|
+
points: normalizePositiveInt(params.points, 5, 3, 32),
|
|
2181
|
+
cornerRadius: normalizeCornerRadius(params.cornerRadius, 0),
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
else if (layerType === 'line') {
|
|
2185
|
+
const strokeColor = normalizeHexColor(params.strokeColor) || '#000000';
|
|
2186
|
+
const strokeWidth = normalizeStrokeWidth(params.strokeWidth, 0.005);
|
|
2187
|
+
const strokeOpacity = normalizeOpacity(params.strokeOpacity, 1);
|
|
2188
|
+
const halfW = clamp(parseNumericInput(params.width), 0.01, 1, 0.6);
|
|
2189
|
+
const halfH = clamp(parseNumericInput(params.height), 0, 1, 0);
|
|
2190
|
+
const startX = cssPercentToX(params.startX, (-halfW + 1) * 50);
|
|
2191
|
+
const endX = cssPercentToX(params.endX, (halfW + 1) * 50);
|
|
2192
|
+
const startY = cssPercentToY(params.startY, (1 + halfH) * 50);
|
|
2193
|
+
const endY = cssPercentToY(params.endY, (1 - halfH) * 50);
|
|
2194
|
+
layer = {
|
|
2195
|
+
...baseLayer,
|
|
2196
|
+
type: 'line',
|
|
2197
|
+
style: {
|
|
2198
|
+
strokeColor,
|
|
2199
|
+
strokeWidth,
|
|
2200
|
+
strokeOpacity,
|
|
2201
|
+
strokeCap: 'round',
|
|
2202
|
+
strokeJoin: 'round',
|
|
2203
|
+
},
|
|
2204
|
+
startX,
|
|
2205
|
+
startY,
|
|
2206
|
+
endX,
|
|
2207
|
+
endY,
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
895
2210
|
else {
|
|
896
2211
|
layer = baseLayer;
|
|
897
2212
|
}
|
|
898
2213
|
currentPoster.layers.push(layer);
|
|
2214
|
+
persistState();
|
|
899
2215
|
return {
|
|
900
2216
|
content: [
|
|
901
2217
|
{
|
|
@@ -910,12 +2226,308 @@ async function handleStateTool(name, args) {
|
|
|
910
2226
|
],
|
|
911
2227
|
};
|
|
912
2228
|
}
|
|
913
|
-
case '
|
|
914
|
-
|
|
2229
|
+
case 'add_group': {
|
|
2230
|
+
ensurePoster();
|
|
2231
|
+
const children = params.children;
|
|
2232
|
+
if (!children || !Array.isArray(children) || children.length === 0) {
|
|
915
2233
|
return {
|
|
916
|
-
content: [{ type: 'text', text: 'Error:
|
|
2234
|
+
content: [{ type: 'text', text: 'Error: children array is required and must have at least 1 child' }],
|
|
917
2235
|
};
|
|
918
2236
|
}
|
|
2237
|
+
const groupId = generateId();
|
|
2238
|
+
const childLayers = [];
|
|
2239
|
+
const artboardAspect = getArtboardAspect(currentPoster.canvas.aspectRatio);
|
|
2240
|
+
for (const childParams of children) {
|
|
2241
|
+
const childType = childParams.type || 'text';
|
|
2242
|
+
const isChildShapeType = ['rectangle', 'ellipse', 'polygon', 'star', 'line'].includes(childType);
|
|
2243
|
+
const childId = generateId();
|
|
2244
|
+
const childVisualOverrides = parseVisualOverrides(childParams);
|
|
2245
|
+
const childOriginOverrides = parseTransformOrigin(childParams);
|
|
2246
|
+
const childBase = {
|
|
2247
|
+
id: childId,
|
|
2248
|
+
type: childType,
|
|
2249
|
+
name: childParams.name || `${childType.charAt(0).toUpperCase() + childType.slice(1)} Layer`,
|
|
2250
|
+
visible: true,
|
|
2251
|
+
locked: false,
|
|
2252
|
+
...parseFlexChildProps(childParams),
|
|
2253
|
+
...childVisualOverrides,
|
|
2254
|
+
transform: {
|
|
2255
|
+
x: 0,
|
|
2256
|
+
y: 0,
|
|
2257
|
+
// Shapes store intrinsic size in geometry fields; keep transform scale at 1.
|
|
2258
|
+
width: isChildShapeType ? 1 : cssPercentToSize(childParams.width, 100),
|
|
2259
|
+
height: isChildShapeType ? 1 : cssPercentToSize(childParams.height, 100),
|
|
2260
|
+
rotation: 0,
|
|
2261
|
+
opacity: normalizeOpacity(childParams.opacity, 1),
|
|
2262
|
+
...childOriginOverrides,
|
|
2263
|
+
},
|
|
2264
|
+
};
|
|
2265
|
+
if (childType === 'text') {
|
|
2266
|
+
const frameWidthNormalized = parseNormalizedFrameDimension(childParams.frameWidth);
|
|
2267
|
+
const frameHeightNormalized = parseNormalizedFrameDimension(childParams.frameHeight);
|
|
2268
|
+
const frameWidthPxFromNormalized = frameWidthNormalized !== undefined
|
|
2269
|
+
? textNormalizedToPx(frameWidthNormalized, artboardAspect, 'width')
|
|
2270
|
+
: undefined;
|
|
2271
|
+
const frameHeightPxFromNormalized = frameHeightNormalized !== undefined
|
|
2272
|
+
? textNormalizedToPx(frameHeightNormalized, artboardAspect, 'height')
|
|
2273
|
+
: undefined;
|
|
2274
|
+
// All text is frame text
|
|
2275
|
+
let frameWidthPx = typeof childParams.frameWidthPx === 'number' ? childParams.frameWidthPx : frameWidthPxFromNormalized;
|
|
2276
|
+
const frameHeightPx = typeof childParams.frameHeightPx === 'number' ? childParams.frameHeightPx : frameHeightPxFromNormalized;
|
|
2277
|
+
const textBoxWidth = typeof childParams.textBoxWidth === 'number' ? childParams.textBoxWidth : undefined;
|
|
2278
|
+
const childFontSize = childParams.fontSize || 72;
|
|
2279
|
+
// Default frameWidthPx for group text children (same as add_layer)
|
|
2280
|
+
if (frameWidthPx == null && textBoxWidth == null) {
|
|
2281
|
+
const defaultFrameWidthNormalized = childFontSize >= 72 ? 0.88 : 0.78;
|
|
2282
|
+
frameWidthPx = textNormalizedToPx(defaultFrameWidthNormalized, artboardAspect, 'width');
|
|
2283
|
+
}
|
|
2284
|
+
childLayers.push({
|
|
2285
|
+
...childBase,
|
|
2286
|
+
type: 'text',
|
|
2287
|
+
content: childParams.content || 'Text',
|
|
2288
|
+
textType: 'frame',
|
|
2289
|
+
fontFamily: normalizeFontFamily(childParams.fontFamily),
|
|
2290
|
+
fontSize: childFontSize,
|
|
2291
|
+
fontWeight: normalizeFontWeight(childParams.fontWeight),
|
|
2292
|
+
color: childParams.color || '#ffffff',
|
|
2293
|
+
textAlign: childParams.textAlign || 'center',
|
|
2294
|
+
...(childParams.verticalAlign ? { verticalAlign: childParams.verticalAlign } : {}),
|
|
2295
|
+
letterSpacing: childParams.letterSpacing ?? 0,
|
|
2296
|
+
lineHeight: childParams.lineHeight ?? 1.2,
|
|
2297
|
+
textTransformMode: 'box',
|
|
2298
|
+
...(textBoxWidth != null ? { textBoxWidth } : {}),
|
|
2299
|
+
frameWidthPx,
|
|
2300
|
+
...(typeof frameHeightPx === 'number' ? { frameHeightPx } : {}),
|
|
2301
|
+
...(childParams.autoSize ? { autoSize: childParams.autoSize } : { autoSize: 'height' }),
|
|
2302
|
+
...(parseTextTransform(childParams) ? { textTransform: parseTextTransform(childParams) } : {}),
|
|
2303
|
+
...parseTextDecoration(childParams),
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
else if (childType === 'image') {
|
|
2307
|
+
if (!childParams.mediaUrl)
|
|
2308
|
+
continue;
|
|
2309
|
+
childLayers.push({
|
|
2310
|
+
...childBase,
|
|
2311
|
+
type: 'image',
|
|
2312
|
+
mediaUrl: childParams.mediaUrl,
|
|
2313
|
+
objectFit: childParams.objectFit || 'cover',
|
|
2314
|
+
brightness: 1,
|
|
2315
|
+
contrast: 1,
|
|
2316
|
+
saturation: 1,
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
else if (childType === 'video') {
|
|
2320
|
+
if (!childParams.mediaUrl)
|
|
2321
|
+
continue;
|
|
2322
|
+
childLayers.push({
|
|
2323
|
+
...childBase,
|
|
2324
|
+
type: 'video',
|
|
2325
|
+
mediaUrl: childParams.mediaUrl,
|
|
2326
|
+
objectFit: childParams.objectFit || 'cover',
|
|
2327
|
+
brightness: 1,
|
|
2328
|
+
contrast: 1,
|
|
2329
|
+
saturation: 1,
|
|
2330
|
+
loop: true,
|
|
2331
|
+
playbackSpeed: 1,
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
else if (childType === 'rectangle') {
|
|
2335
|
+
const fillColor = normalizeHexColor(childParams.fillColor) || '#3b82f6';
|
|
2336
|
+
const fillOpacity = normalizeOpacity(childParams.fillOpacity, 1);
|
|
2337
|
+
const strokeColor = normalizeHexColor(childParams.strokeColor) || '#000000';
|
|
2338
|
+
const gradientFill = buildFillFromParams(childParams);
|
|
2339
|
+
childLayers.push({
|
|
2340
|
+
...childBase,
|
|
2341
|
+
type: 'rectangle',
|
|
2342
|
+
style: {
|
|
2343
|
+
fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
|
|
2344
|
+
fillType: 'solid',
|
|
2345
|
+
fillColor,
|
|
2346
|
+
fillOpacity,
|
|
2347
|
+
strokeColor,
|
|
2348
|
+
strokeWidth: normalizeStrokeWidth(childParams.strokeWidth, 0),
|
|
2349
|
+
strokeOpacity: normalizeOpacity(childParams.strokeOpacity, 1),
|
|
2350
|
+
strokeCap: 'round',
|
|
2351
|
+
strokeJoin: 'round',
|
|
2352
|
+
},
|
|
2353
|
+
shapeWidth: cssPercentToSize(childParams.width, 20),
|
|
2354
|
+
shapeHeight: cssPercentToSize(childParams.height, 20),
|
|
2355
|
+
cornerRadius: normalizeCornerRadius(childParams.cornerRadius, 0),
|
|
2356
|
+
...(parseCornerRadii(childParams) ? { cornerRadii: parseCornerRadii(childParams) } : {}),
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
else if (childType === 'ellipse') {
|
|
2360
|
+
const fillColor = normalizeHexColor(childParams.fillColor) || '#3b82f6';
|
|
2361
|
+
const fillOpacity = normalizeOpacity(childParams.fillOpacity, 1);
|
|
2362
|
+
const strokeColor = normalizeHexColor(childParams.strokeColor) || '#000000';
|
|
2363
|
+
const w = cssPercentToSize(childParams.width, 20);
|
|
2364
|
+
const h = cssPercentToSize(childParams.height, 20);
|
|
2365
|
+
const gradientFill = buildFillFromParams(childParams);
|
|
2366
|
+
childLayers.push({
|
|
2367
|
+
...childBase,
|
|
2368
|
+
type: 'ellipse',
|
|
2369
|
+
style: {
|
|
2370
|
+
fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
|
|
2371
|
+
fillType: 'solid',
|
|
2372
|
+
fillColor,
|
|
2373
|
+
fillOpacity,
|
|
2374
|
+
strokeColor,
|
|
2375
|
+
strokeWidth: normalizeStrokeWidth(childParams.strokeWidth, 0),
|
|
2376
|
+
strokeOpacity: normalizeOpacity(childParams.strokeOpacity, 1),
|
|
2377
|
+
strokeCap: 'round',
|
|
2378
|
+
strokeJoin: 'round',
|
|
2379
|
+
},
|
|
2380
|
+
radiusX: w / 2,
|
|
2381
|
+
radiusY: h / 2,
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
else if (childType === 'polygon') {
|
|
2385
|
+
const fillColor = normalizeHexColor(childParams.fillColor) || '#3b82f6';
|
|
2386
|
+
const fillOpacity = normalizeOpacity(childParams.fillOpacity, 1);
|
|
2387
|
+
const strokeColor = normalizeHexColor(childParams.strokeColor) || '#000000';
|
|
2388
|
+
const w = cssPercentToSize(childParams.width, 20);
|
|
2389
|
+
const h = cssPercentToSize(childParams.height, 20);
|
|
2390
|
+
const gradientFill = buildFillFromParams(childParams);
|
|
2391
|
+
childLayers.push({
|
|
2392
|
+
...childBase,
|
|
2393
|
+
type: 'polygon',
|
|
2394
|
+
style: {
|
|
2395
|
+
fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
|
|
2396
|
+
fillType: 'solid',
|
|
2397
|
+
fillColor,
|
|
2398
|
+
fillOpacity,
|
|
2399
|
+
strokeColor,
|
|
2400
|
+
strokeWidth: normalizeStrokeWidth(childParams.strokeWidth, 0),
|
|
2401
|
+
strokeOpacity: normalizeOpacity(childParams.strokeOpacity, 1),
|
|
2402
|
+
strokeCap: 'round',
|
|
2403
|
+
strokeJoin: 'round',
|
|
2404
|
+
},
|
|
2405
|
+
radius: Math.min(w, h) / 2,
|
|
2406
|
+
sides: normalizePositiveInt(childParams.sides, 6, 3, 32),
|
|
2407
|
+
cornerRadius: normalizeCornerRadius(childParams.cornerRadius, 0),
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
else if (childType === 'star') {
|
|
2411
|
+
const fillColor = normalizeHexColor(childParams.fillColor) || '#3b82f6';
|
|
2412
|
+
const fillOpacity = normalizeOpacity(childParams.fillOpacity, 1);
|
|
2413
|
+
const strokeColor = normalizeHexColor(childParams.strokeColor) || '#000000';
|
|
2414
|
+
const w = cssPercentToSize(childParams.width, 20);
|
|
2415
|
+
const h = cssPercentToSize(childParams.height, 20);
|
|
2416
|
+
const gradientFill = buildFillFromParams(childParams);
|
|
2417
|
+
childLayers.push({
|
|
2418
|
+
...childBase,
|
|
2419
|
+
type: 'star',
|
|
2420
|
+
style: {
|
|
2421
|
+
fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
|
|
2422
|
+
fillType: 'solid',
|
|
2423
|
+
fillColor,
|
|
2424
|
+
fillOpacity,
|
|
2425
|
+
strokeColor,
|
|
2426
|
+
strokeWidth: normalizeStrokeWidth(childParams.strokeWidth, 0),
|
|
2427
|
+
strokeOpacity: normalizeOpacity(childParams.strokeOpacity, 1),
|
|
2428
|
+
strokeCap: 'round',
|
|
2429
|
+
strokeJoin: 'round',
|
|
2430
|
+
},
|
|
2431
|
+
outerRadius: Math.min(w, h) / 2,
|
|
2432
|
+
innerRadiusRatio: clamp(parseNumericInput(childParams.innerRadiusRatio), 0.05, 0.95, 0.5),
|
|
2433
|
+
points: normalizePositiveInt(childParams.points, 5, 3, 32),
|
|
2434
|
+
cornerRadius: normalizeCornerRadius(childParams.cornerRadius, 0),
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
else if (childType === 'line') {
|
|
2438
|
+
const strokeColor = normalizeHexColor(childParams.strokeColor) || '#000000';
|
|
2439
|
+
const strokeWidth = normalizeStrokeWidth(childParams.strokeWidth, 0.005);
|
|
2440
|
+
const strokeOpacity = normalizeOpacity(childParams.strokeOpacity, 1);
|
|
2441
|
+
const halfW = clamp(parseNumericInput(childParams.width), 0.01, 1, 0.6);
|
|
2442
|
+
const halfH = clamp(parseNumericInput(childParams.height), 0, 1, 0);
|
|
2443
|
+
const startX = cssPercentToX(childParams.startX, (-halfW + 1) * 50);
|
|
2444
|
+
const endX = cssPercentToX(childParams.endX, (halfW + 1) * 50);
|
|
2445
|
+
const startY = cssPercentToY(childParams.startY, (1 + halfH) * 50);
|
|
2446
|
+
const endY = cssPercentToY(childParams.endY, (1 - halfH) * 50);
|
|
2447
|
+
childLayers.push({
|
|
2448
|
+
...childBase,
|
|
2449
|
+
type: 'line',
|
|
2450
|
+
style: {
|
|
2451
|
+
strokeColor,
|
|
2452
|
+
strokeWidth,
|
|
2453
|
+
strokeOpacity,
|
|
2454
|
+
strokeCap: 'round',
|
|
2455
|
+
strokeJoin: 'round',
|
|
2456
|
+
},
|
|
2457
|
+
startX,
|
|
2458
|
+
startY,
|
|
2459
|
+
endX,
|
|
2460
|
+
endY,
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
if (childLayers.length === 0) {
|
|
2465
|
+
return {
|
|
2466
|
+
content: [{ type: 'text', text: 'Error: No valid child layers could be created' }],
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
// Build padding — convert from CSS % to internal normalized values
|
|
2470
|
+
const uniformPaddingPct = params.padding ?? 0;
|
|
2471
|
+
const uniformPadding = cssPercentToSize(uniformPaddingPct, 0);
|
|
2472
|
+
const groupPadding = {
|
|
2473
|
+
top: cssPercentToSize(params.paddingTop ?? uniformPaddingPct, 0),
|
|
2474
|
+
right: cssPercentToSize(params.paddingRight ?? uniformPaddingPct, 0),
|
|
2475
|
+
bottom: cssPercentToSize(params.paddingBottom ?? uniformPaddingPct, 0),
|
|
2476
|
+
left: cssPercentToSize(params.paddingLeft ?? uniformPaddingPct, 0),
|
|
2477
|
+
};
|
|
2478
|
+
const layoutMode = params.layoutMode || 'column';
|
|
2479
|
+
const widthMode = params.widthMode ?? (params.width !== undefined ? 'fixed' : 'hug');
|
|
2480
|
+
const heightMode = params.heightMode ?? (params.height !== undefined ? 'fixed' : 'hug');
|
|
2481
|
+
const baseWidth = cssPercentToSize(params.width, 60);
|
|
2482
|
+
const baseHeight = cssPercentToSize(params.height, 40);
|
|
2483
|
+
const groupLayer = {
|
|
2484
|
+
id: groupId,
|
|
2485
|
+
type: 'group',
|
|
2486
|
+
name: params.name || 'Group',
|
|
2487
|
+
visible: true,
|
|
2488
|
+
locked: false,
|
|
2489
|
+
transform: {
|
|
2490
|
+
x: cssPercentToX(params.x, 50),
|
|
2491
|
+
y: cssPercentToY(params.y, 50),
|
|
2492
|
+
width: 1,
|
|
2493
|
+
height: 1,
|
|
2494
|
+
rotation: params.rotation ?? 0,
|
|
2495
|
+
opacity: normalizeOpacity(params.opacity, 1),
|
|
2496
|
+
},
|
|
2497
|
+
children: childLayers,
|
|
2498
|
+
expanded: true,
|
|
2499
|
+
baseWidth,
|
|
2500
|
+
baseHeight,
|
|
2501
|
+
layoutMode,
|
|
2502
|
+
widthMode,
|
|
2503
|
+
heightMode,
|
|
2504
|
+
gap: cssPercentToSize(params.gap, 2),
|
|
2505
|
+
justifyContent: params.justifyContent ?? 'start',
|
|
2506
|
+
alignItems: params.alignItems ?? 'center',
|
|
2507
|
+
wrap: params.wrap ?? false,
|
|
2508
|
+
padding: groupPadding,
|
|
2509
|
+
...(params.fillColor ? { fillColor: params.fillColor } : {}),
|
|
2510
|
+
...(params.fillOpacity !== undefined ? { fillOpacity: params.fillOpacity } : {}),
|
|
2511
|
+
...(params.cornerRadius !== undefined ? { cornerRadius: params.cornerRadius } : {}),
|
|
2512
|
+
};
|
|
2513
|
+
currentPoster.layers.push(groupLayer);
|
|
2514
|
+
persistState();
|
|
2515
|
+
return {
|
|
2516
|
+
content: [
|
|
2517
|
+
{
|
|
2518
|
+
type: 'text',
|
|
2519
|
+
text: JSON.stringify({
|
|
2520
|
+
success: true,
|
|
2521
|
+
message: `Group added with ${childLayers.length} children (${groupLayer.layoutMode} layout)`,
|
|
2522
|
+
group: groupLayer,
|
|
2523
|
+
totalLayers: currentPoster.layers.length,
|
|
2524
|
+
}, null, 2),
|
|
2525
|
+
},
|
|
2526
|
+
],
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
case 'modify_layer': {
|
|
2530
|
+
ensurePoster();
|
|
919
2531
|
const layerId = params.layerId;
|
|
920
2532
|
if (!layerId) {
|
|
921
2533
|
return {
|
|
@@ -929,11 +2541,28 @@ async function handleStateTool(name, args) {
|
|
|
929
2541
|
};
|
|
930
2542
|
}
|
|
931
2543
|
const layer = currentPoster.layers[layerIndex];
|
|
2544
|
+
const isShapeType = ['rectangle', 'ellipse', 'polygon', 'star', 'line'].includes(layer.type);
|
|
932
2545
|
// Update transform properties
|
|
933
|
-
const transformKeys =
|
|
2546
|
+
const transformKeys = isShapeType
|
|
2547
|
+
? ['x', 'y', 'rotation', 'opacity']
|
|
2548
|
+
: ['x', 'y', 'width', 'height', 'rotation', 'opacity'];
|
|
934
2549
|
for (const key of transformKeys) {
|
|
935
2550
|
if (params[key] !== undefined) {
|
|
936
|
-
|
|
2551
|
+
if (key === 'x') {
|
|
2552
|
+
layer.transform[key] = cssPercentToX(params[key], (layer.transform[key] + 1) * 50);
|
|
2553
|
+
}
|
|
2554
|
+
else if (key === 'y') {
|
|
2555
|
+
layer.transform[key] = cssPercentToY(params[key], (1 - layer.transform[key]) * 50);
|
|
2556
|
+
}
|
|
2557
|
+
else if (key === 'opacity') {
|
|
2558
|
+
layer.transform[key] = normalizeOpacity(params[key], layer.transform[key]);
|
|
2559
|
+
}
|
|
2560
|
+
else if (key === 'width' || key === 'height') {
|
|
2561
|
+
layer.transform[key] = cssPercentToSize(params[key], layer.transform[key] * 100);
|
|
2562
|
+
}
|
|
2563
|
+
else {
|
|
2564
|
+
layer.transform[key] = params[key];
|
|
2565
|
+
}
|
|
937
2566
|
}
|
|
938
2567
|
}
|
|
939
2568
|
// Update base properties
|
|
@@ -943,9 +2572,46 @@ async function handleStateTool(name, args) {
|
|
|
943
2572
|
layer.locked = params.locked;
|
|
944
2573
|
if (params.name !== undefined)
|
|
945
2574
|
layer.name = params.name;
|
|
2575
|
+
// Update flex-child properties
|
|
2576
|
+
const flexProps = parseFlexChildProps(params);
|
|
2577
|
+
if (flexProps.positioning !== undefined)
|
|
2578
|
+
layer.positioning = flexProps.positioning;
|
|
2579
|
+
if (flexProps.flexWidthMode !== undefined)
|
|
2580
|
+
layer.flexWidthMode = flexProps.flexWidthMode;
|
|
2581
|
+
if (flexProps.flexHeightMode !== undefined)
|
|
2582
|
+
layer.flexHeightMode = flexProps.flexHeightMode;
|
|
2583
|
+
if (flexProps.alignSelf !== undefined)
|
|
2584
|
+
layer.alignSelf = flexProps.alignSelf;
|
|
2585
|
+
if (flexProps.flexGrow !== undefined)
|
|
2586
|
+
layer.flexGrow = flexProps.flexGrow;
|
|
2587
|
+
// Update visual props (shadow, blendMode, margin, min/max, transform origin — all layer types)
|
|
2588
|
+
const shadow = parseShadowParams(params);
|
|
2589
|
+
if (shadow)
|
|
2590
|
+
layer.shadow = shadow;
|
|
2591
|
+
const blendMode = parseBlendMode(params);
|
|
2592
|
+
if (blendMode !== undefined)
|
|
2593
|
+
layer.blendMode = blendMode;
|
|
2594
|
+
const margin = parseMarginParams(params);
|
|
2595
|
+
if (margin)
|
|
2596
|
+
layer.margin = margin;
|
|
2597
|
+
const constraints = parseMinMaxConstraints(params);
|
|
2598
|
+
if (constraints.minWidth !== undefined)
|
|
2599
|
+
layer.minWidth = constraints.minWidth;
|
|
2600
|
+
if (constraints.maxWidth !== undefined)
|
|
2601
|
+
layer.maxWidth = constraints.maxWidth;
|
|
2602
|
+
if (constraints.minHeight !== undefined)
|
|
2603
|
+
layer.minHeight = constraints.minHeight;
|
|
2604
|
+
if (constraints.maxHeight !== undefined)
|
|
2605
|
+
layer.maxHeight = constraints.maxHeight;
|
|
2606
|
+
const originOverrides = parseTransformOrigin(params);
|
|
2607
|
+
if (originOverrides.originX !== undefined)
|
|
2608
|
+
layer.transform.originX = originOverrides.originX;
|
|
2609
|
+
if (originOverrides.originY !== undefined)
|
|
2610
|
+
layer.transform.originY = originOverrides.originY;
|
|
946
2611
|
// Update text layer properties
|
|
947
2612
|
if (layer.type === 'text') {
|
|
948
2613
|
const textLayer = layer;
|
|
2614
|
+
const artboardAspect = getArtboardAspect(currentPoster.canvas.aspectRatio);
|
|
949
2615
|
if (params.content !== undefined)
|
|
950
2616
|
textLayer.content = params.content;
|
|
951
2617
|
if (params.fontFamily !== undefined)
|
|
@@ -958,10 +2624,46 @@ async function handleStateTool(name, args) {
|
|
|
958
2624
|
textLayer.color = params.color;
|
|
959
2625
|
if (params.textAlign !== undefined)
|
|
960
2626
|
textLayer.textAlign = params.textAlign;
|
|
2627
|
+
if (params.verticalAlign !== undefined)
|
|
2628
|
+
textLayer.verticalAlign = params.verticalAlign;
|
|
961
2629
|
if (params.letterSpacing !== undefined)
|
|
962
2630
|
textLayer.letterSpacing = params.letterSpacing;
|
|
963
2631
|
if (params.lineHeight !== undefined)
|
|
964
2632
|
textLayer.lineHeight = params.lineHeight;
|
|
2633
|
+
// All text is frame text — silently ignore any 'point'/'scale' requests
|
|
2634
|
+
textLayer.textType = 'frame';
|
|
2635
|
+
textLayer.textTransformMode = 'box';
|
|
2636
|
+
if (params.textBoxWidth !== undefined)
|
|
2637
|
+
textLayer.textBoxWidth = params.textBoxWidth;
|
|
2638
|
+
if (params.frameWidthPx !== undefined)
|
|
2639
|
+
textLayer.frameWidthPx = params.frameWidthPx;
|
|
2640
|
+
if (params.frameHeightPx !== undefined)
|
|
2641
|
+
textLayer.frameHeightPx = params.frameHeightPx;
|
|
2642
|
+
if (params.autoSize !== undefined)
|
|
2643
|
+
textLayer.autoSize = params.autoSize;
|
|
2644
|
+
// Text CSS properties
|
|
2645
|
+
const tt = parseTextTransform(params);
|
|
2646
|
+
if (tt !== undefined)
|
|
2647
|
+
textLayer.textTransform = tt;
|
|
2648
|
+
else if (params.textTransform === 'none')
|
|
2649
|
+
textLayer.textTransform = undefined;
|
|
2650
|
+
const td = parseTextDecoration(params);
|
|
2651
|
+
if (td.textDecoration !== undefined)
|
|
2652
|
+
textLayer.textDecoration = td.textDecoration;
|
|
2653
|
+
else if (params.textDecoration === 'none')
|
|
2654
|
+
textLayer.textDecoration = undefined;
|
|
2655
|
+
if (td.textDecorationColor !== undefined)
|
|
2656
|
+
textLayer.textDecorationColor = td.textDecorationColor;
|
|
2657
|
+
if (td.textDecorationThickness !== undefined)
|
|
2658
|
+
textLayer.textDecorationThickness = td.textDecorationThickness;
|
|
2659
|
+
const frameWidthNormalized = parseNormalizedFrameDimension(params.frameWidth);
|
|
2660
|
+
if (frameWidthNormalized !== undefined && params.frameWidthPx === undefined) {
|
|
2661
|
+
textLayer.frameWidthPx = textNormalizedToPx(frameWidthNormalized, artboardAspect, 'width');
|
|
2662
|
+
}
|
|
2663
|
+
const frameHeightNormalized = parseNormalizedFrameDimension(params.frameHeight);
|
|
2664
|
+
if (frameHeightNormalized !== undefined && params.frameHeightPx === undefined) {
|
|
2665
|
+
textLayer.frameHeightPx = textNormalizedToPx(frameHeightNormalized, artboardAspect, 'height');
|
|
2666
|
+
}
|
|
965
2667
|
// Update animation settings
|
|
966
2668
|
if (params.animationType !== undefined) {
|
|
967
2669
|
if (params.animationType === 'none') {
|
|
@@ -1095,6 +2797,177 @@ async function handleStateTool(name, args) {
|
|
|
1095
2797
|
if (params.objectFit !== undefined)
|
|
1096
2798
|
mediaLayer.objectFit = params.objectFit;
|
|
1097
2799
|
}
|
|
2800
|
+
// Update group layout properties
|
|
2801
|
+
if (layer.type === 'group') {
|
|
2802
|
+
const groupLayer = layer;
|
|
2803
|
+
if (params.layoutMode !== undefined)
|
|
2804
|
+
groupLayer.layoutMode = params.layoutMode;
|
|
2805
|
+
if (params.widthMode !== undefined)
|
|
2806
|
+
groupLayer.widthMode = params.widthMode;
|
|
2807
|
+
if (params.heightMode !== undefined)
|
|
2808
|
+
groupLayer.heightMode = params.heightMode;
|
|
2809
|
+
if (params.gap !== undefined)
|
|
2810
|
+
groupLayer.gap = cssPercentToSize(params.gap, (groupLayer.gap ?? 0.02) * 100);
|
|
2811
|
+
if (params.justifyContent !== undefined)
|
|
2812
|
+
groupLayer.justifyContent = params.justifyContent;
|
|
2813
|
+
if (params.alignItems !== undefined)
|
|
2814
|
+
groupLayer.alignItems = params.alignItems;
|
|
2815
|
+
if (params.wrap !== undefined)
|
|
2816
|
+
groupLayer.wrap = params.wrap;
|
|
2817
|
+
if (params.fillColor !== undefined)
|
|
2818
|
+
groupLayer.fillColor = params.fillColor;
|
|
2819
|
+
if (params.fillOpacity !== undefined)
|
|
2820
|
+
groupLayer.fillOpacity = params.fillOpacity;
|
|
2821
|
+
if (params.cornerRadius !== undefined)
|
|
2822
|
+
groupLayer.cornerRadius = params.cornerRadius;
|
|
2823
|
+
const groupCornerRadii = parseCornerRadii(params);
|
|
2824
|
+
if (groupCornerRadii)
|
|
2825
|
+
groupLayer.cornerRadii = groupCornerRadii;
|
|
2826
|
+
// Padding: uniform or individual overrides (CSS % to normalized)
|
|
2827
|
+
if (params.padding !== undefined || params.paddingTop !== undefined || params.paddingRight !== undefined || params.paddingBottom !== undefined || params.paddingLeft !== undefined) {
|
|
2828
|
+
const existing = groupLayer.padding || { top: 0, right: 0, bottom: 0, left: 0 };
|
|
2829
|
+
const uniformPct = params.padding !== undefined ? parseNumericInput(params.padding) : undefined;
|
|
2830
|
+
const uniform = uniformPct !== undefined ? cssPercentToSize(uniformPct, 0) : undefined;
|
|
2831
|
+
groupLayer.padding = {
|
|
2832
|
+
top: params.paddingTop !== undefined ? cssPercentToSize(params.paddingTop, 0) : uniform ?? existing.top,
|
|
2833
|
+
right: params.paddingRight !== undefined ? cssPercentToSize(params.paddingRight, 0) : uniform ?? existing.right,
|
|
2834
|
+
bottom: params.paddingBottom !== undefined ? cssPercentToSize(params.paddingBottom, 0) : uniform ?? existing.bottom,
|
|
2835
|
+
left: params.paddingLeft !== undefined ? cssPercentToSize(params.paddingLeft, 0) : uniform ?? existing.left,
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
// Allow updating baseWidth/baseHeight via width/height params
|
|
2839
|
+
if (params.width !== undefined) {
|
|
2840
|
+
groupLayer.baseWidth = cssPercentToSize(params.width, groupLayer.baseWidth * 100);
|
|
2841
|
+
if (params.widthMode === undefined) {
|
|
2842
|
+
groupLayer.widthMode = 'fixed';
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
if (params.height !== undefined) {
|
|
2846
|
+
groupLayer.baseHeight = cssPercentToSize(params.height, groupLayer.baseHeight * 100);
|
|
2847
|
+
if (params.heightMode === undefined) {
|
|
2848
|
+
groupLayer.heightMode = 'fixed';
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
// Update shape properties (geometry + style)
|
|
2853
|
+
if (isShapeType) {
|
|
2854
|
+
const anyLayer = layer;
|
|
2855
|
+
// Style (common)
|
|
2856
|
+
if (!anyLayer.style)
|
|
2857
|
+
anyLayer.style = {};
|
|
2858
|
+
if (params.fillColor !== undefined) {
|
|
2859
|
+
const existing = typeof anyLayer.style.fillColor === 'string' ? anyLayer.style.fillColor : '#3b82f6';
|
|
2860
|
+
anyLayer.style.fillColor = normalizeHexColor(params.fillColor) || existing;
|
|
2861
|
+
}
|
|
2862
|
+
if (params.fillOpacity !== undefined) {
|
|
2863
|
+
const existing = asFiniteNumber(anyLayer.style.fillOpacity, 1);
|
|
2864
|
+
anyLayer.style.fillOpacity = normalizeOpacity(params.fillOpacity, existing);
|
|
2865
|
+
}
|
|
2866
|
+
if (params.strokeColor !== undefined) {
|
|
2867
|
+
const existing = typeof anyLayer.style.strokeColor === 'string' ? anyLayer.style.strokeColor : '#000000';
|
|
2868
|
+
anyLayer.style.strokeColor = normalizeHexColor(params.strokeColor) || existing;
|
|
2869
|
+
}
|
|
2870
|
+
if (params.strokeWidth !== undefined) {
|
|
2871
|
+
const existing = asFiniteNumber(anyLayer.style.strokeWidth, 0);
|
|
2872
|
+
anyLayer.style.strokeWidth = normalizeStrokeWidth(params.strokeWidth, existing);
|
|
2873
|
+
}
|
|
2874
|
+
if (params.strokeOpacity !== undefined) {
|
|
2875
|
+
const existing = asFiniteNumber(anyLayer.style.strokeOpacity, 1);
|
|
2876
|
+
anyLayer.style.strokeOpacity = normalizeOpacity(params.strokeOpacity, existing);
|
|
2877
|
+
}
|
|
2878
|
+
// Gradient fill update
|
|
2879
|
+
const modifyGradientFill = buildFillFromParams(params);
|
|
2880
|
+
if (modifyGradientFill) {
|
|
2881
|
+
anyLayer.style.fill = modifyGradientFill;
|
|
2882
|
+
anyLayer.style.fillType = 'solid';
|
|
2883
|
+
}
|
|
2884
|
+
else if (params.fillColor !== undefined || params.fillOpacity !== undefined) {
|
|
2885
|
+
// Sync style.fill as solid when only color/opacity changed
|
|
2886
|
+
const currentFillColor = typeof anyLayer.style.fillColor === 'string' ? anyLayer.style.fillColor : '#3b82f6';
|
|
2887
|
+
const currentFillOpacity = asFiniteNumber(anyLayer.style.fillOpacity, 1);
|
|
2888
|
+
anyLayer.style.fill = { type: 'solid', color: currentFillColor, opacity: currentFillOpacity };
|
|
2889
|
+
anyLayer.style.fillType = 'solid';
|
|
2890
|
+
}
|
|
2891
|
+
if (anyLayer.style.fillType === undefined && layer.type !== 'line')
|
|
2892
|
+
anyLayer.style.fillType = 'solid';
|
|
2893
|
+
if (anyLayer.style.strokeCap === undefined)
|
|
2894
|
+
anyLayer.style.strokeCap = 'round';
|
|
2895
|
+
if (anyLayer.style.strokeJoin === undefined)
|
|
2896
|
+
anyLayer.style.strokeJoin = 'round';
|
|
2897
|
+
if (layer.type === 'rectangle') {
|
|
2898
|
+
if (params.width !== undefined)
|
|
2899
|
+
anyLayer.shapeWidth = cssPercentToSize(params.width, asFiniteNumber(anyLayer.shapeWidth, 0.2) * 100);
|
|
2900
|
+
if (params.height !== undefined)
|
|
2901
|
+
anyLayer.shapeHeight = cssPercentToSize(params.height, asFiniteNumber(anyLayer.shapeHeight, 0.2) * 100);
|
|
2902
|
+
if (params.cornerRadius !== undefined)
|
|
2903
|
+
anyLayer.cornerRadius = normalizeCornerRadius(params.cornerRadius, asFiniteNumber(anyLayer.cornerRadius, 0));
|
|
2904
|
+
const rectCornerRadii = parseCornerRadii(params);
|
|
2905
|
+
if (rectCornerRadii)
|
|
2906
|
+
anyLayer.cornerRadii = rectCornerRadii;
|
|
2907
|
+
}
|
|
2908
|
+
else if (layer.type === 'ellipse') {
|
|
2909
|
+
if (params.width !== undefined)
|
|
2910
|
+
anyLayer.radiusX = cssPercentToSize(params.width, asFiniteNumber(anyLayer.radiusX, 0.1) * 2 * 100) / 2;
|
|
2911
|
+
if (params.height !== undefined)
|
|
2912
|
+
anyLayer.radiusY = cssPercentToSize(params.height, asFiniteNumber(anyLayer.radiusY, 0.1) * 2 * 100) / 2;
|
|
2913
|
+
}
|
|
2914
|
+
else if (layer.type === 'polygon') {
|
|
2915
|
+
if (params.width !== undefined || params.height !== undefined) {
|
|
2916
|
+
const baseRadius = asFiniteNumber(anyLayer.radius, 0.1);
|
|
2917
|
+
const w = params.width !== undefined ? cssPercentToSize(params.width, baseRadius * 2 * 100) : undefined;
|
|
2918
|
+
const h = params.height !== undefined ? cssPercentToSize(params.height, baseRadius * 2 * 100) : undefined;
|
|
2919
|
+
const size = Math.min(w ?? baseRadius * 2, h ?? baseRadius * 2);
|
|
2920
|
+
anyLayer.radius = size / 2;
|
|
2921
|
+
}
|
|
2922
|
+
if (params.sides !== undefined)
|
|
2923
|
+
anyLayer.sides = normalizePositiveInt(params.sides, asFiniteNumber(anyLayer.sides, 6), 3, 64);
|
|
2924
|
+
if (params.cornerRadius !== undefined)
|
|
2925
|
+
anyLayer.cornerRadius = normalizeCornerRadius(params.cornerRadius, asFiniteNumber(anyLayer.cornerRadius, 0));
|
|
2926
|
+
}
|
|
2927
|
+
else if (layer.type === 'star') {
|
|
2928
|
+
if (params.width !== undefined || params.height !== undefined) {
|
|
2929
|
+
const baseOuter = asFiniteNumber(anyLayer.outerRadius, 0.1);
|
|
2930
|
+
const w = params.width !== undefined ? cssPercentToSize(params.width, baseOuter * 2 * 100) : undefined;
|
|
2931
|
+
const h = params.height !== undefined ? cssPercentToSize(params.height, baseOuter * 2 * 100) : undefined;
|
|
2932
|
+
const size = Math.min(w ?? baseOuter * 2, h ?? baseOuter * 2);
|
|
2933
|
+
anyLayer.outerRadius = size / 2;
|
|
2934
|
+
}
|
|
2935
|
+
if (params.innerRadiusRatio !== undefined) {
|
|
2936
|
+
anyLayer.innerRadiusRatio = clamp(parseNumericInput(params.innerRadiusRatio), 0.05, 0.95, asFiniteNumber(anyLayer.innerRadiusRatio, 0.5));
|
|
2937
|
+
}
|
|
2938
|
+
if (params.points !== undefined)
|
|
2939
|
+
anyLayer.points = normalizePositiveInt(params.points, asFiniteNumber(anyLayer.points, 5), 3, 64);
|
|
2940
|
+
if (params.cornerRadius !== undefined)
|
|
2941
|
+
anyLayer.cornerRadius = normalizeCornerRadius(params.cornerRadius, asFiniteNumber(anyLayer.cornerRadius, 0));
|
|
2942
|
+
}
|
|
2943
|
+
else if (layer.type === 'line') {
|
|
2944
|
+
if (params.startX !== undefined)
|
|
2945
|
+
anyLayer.startX = cssPercentToX(params.startX, (asFiniteNumber(anyLayer.startX, -0.6) + 1) * 50);
|
|
2946
|
+
if (params.endX !== undefined)
|
|
2947
|
+
anyLayer.endX = cssPercentToX(params.endX, (asFiniteNumber(anyLayer.endX, 0.6) + 1) * 50);
|
|
2948
|
+
if (params.startY !== undefined)
|
|
2949
|
+
anyLayer.startY = cssPercentToY(params.startY, (1 - asFiniteNumber(anyLayer.startY, 0)) * 50);
|
|
2950
|
+
if (params.endY !== undefined)
|
|
2951
|
+
anyLayer.endY = cssPercentToY(params.endY, (1 - asFiniteNumber(anyLayer.endY, 0)) * 50);
|
|
2952
|
+
// Convenience sizing: adjust around 0 when width/height provided without explicit endpoints.
|
|
2953
|
+
if ((params.width !== undefined || params.height !== undefined) && params.startX === undefined && params.endX === undefined && params.startY === undefined && params.endY === undefined) {
|
|
2954
|
+
const prevHalfW = Math.max(0.01, Math.abs(asFiniteNumber(anyLayer.endX, 0.6) - asFiniteNumber(anyLayer.startX, -0.6)) / 2);
|
|
2955
|
+
const prevHalfH = Math.abs(asFiniteNumber(anyLayer.endY, 0) - asFiniteNumber(anyLayer.startY, 0)) / 2;
|
|
2956
|
+
const halfW = clamp(parseNumericInput(params.width), 0.01, 1, prevHalfW);
|
|
2957
|
+
const halfH = clamp(parseNumericInput(params.height), 0, 1, prevHalfH);
|
|
2958
|
+
anyLayer.startX = -halfW;
|
|
2959
|
+
anyLayer.endX = halfW;
|
|
2960
|
+
anyLayer.startY = -halfH;
|
|
2961
|
+
anyLayer.endY = halfH;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
// Keep transform scale at 1 for deterministic geometry sizing.
|
|
2965
|
+
if (params.width !== undefined || params.height !== undefined || params.startX !== undefined || params.startY !== undefined || params.endX !== undefined || params.endY !== undefined) {
|
|
2966
|
+
layer.transform.width = 1;
|
|
2967
|
+
layer.transform.height = 1;
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
persistState();
|
|
1098
2971
|
return {
|
|
1099
2972
|
content: [
|
|
1100
2973
|
{
|
|
@@ -1109,11 +2982,7 @@ async function handleStateTool(name, args) {
|
|
|
1109
2982
|
};
|
|
1110
2983
|
}
|
|
1111
2984
|
case 'apply_effect': {
|
|
1112
|
-
|
|
1113
|
-
return {
|
|
1114
|
-
content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
2985
|
+
ensurePoster();
|
|
1117
2986
|
const effectId = params.effectId;
|
|
1118
2987
|
if (!effectId) {
|
|
1119
2988
|
return {
|
|
@@ -1122,6 +2991,7 @@ async function handleStateTool(name, args) {
|
|
|
1122
2991
|
}
|
|
1123
2992
|
if (effectId === 'none') {
|
|
1124
2993
|
delete currentPoster.effect;
|
|
2994
|
+
persistState();
|
|
1125
2995
|
return {
|
|
1126
2996
|
content: [
|
|
1127
2997
|
{
|
|
@@ -1188,12 +3058,15 @@ async function handleStateTool(name, args) {
|
|
|
1188
3058
|
};
|
|
1189
3059
|
// Dither has BUILT-IN palette and bloom (WebGL pipeline)
|
|
1190
3060
|
// These don't use post-processes - settings are on the effect itself
|
|
3061
|
+
const ditherPaletteId = params.paletteId ?? null;
|
|
3062
|
+
const ditherDefaultColors = ['#000000', '#ffffff'];
|
|
3063
|
+
// When paletteId is set, resolve to actual colors so the URL encodes them correctly
|
|
3064
|
+
const ditherColors = params.colors ?? resolvePaletteColors(ditherPaletteId, ditherDefaultColors);
|
|
1191
3065
|
effect.dither = {
|
|
1192
3066
|
pattern: patternMap[effectId] || 'floydSteinberg',
|
|
1193
3067
|
pixelation: clamp(params.pixelation, 1, 20, 3),
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
colors: params.colors ?? ['#000000', '#ffffff'],
|
|
3068
|
+
paletteId: ditherPaletteId,
|
|
3069
|
+
colors: ditherColors,
|
|
1197
3070
|
brightness: clamp(params.brightness, 0, 2, 1),
|
|
1198
3071
|
contrast: clamp(params.contrast, 0.5, 2, 1.2),
|
|
1199
3072
|
threshold: 1.0,
|
|
@@ -1250,12 +3123,12 @@ async function handleStateTool(name, args) {
|
|
|
1250
3123
|
}
|
|
1251
3124
|
else if (settingsKey === 'chromatic') {
|
|
1252
3125
|
effect.chromatic = {
|
|
1253
|
-
shape: '
|
|
3126
|
+
shape: 'square',
|
|
1254
3127
|
radius: 4,
|
|
1255
3128
|
rotateR: 15,
|
|
1256
3129
|
rotateG: 75,
|
|
1257
3130
|
rotateB: 0,
|
|
1258
|
-
scatter:
|
|
3131
|
+
scatter: 1,
|
|
1259
3132
|
blending: 1,
|
|
1260
3133
|
blendingMode: 'linear',
|
|
1261
3134
|
greyscale: false,
|
|
@@ -1273,12 +3146,12 @@ async function handleStateTool(name, args) {
|
|
|
1273
3146
|
// Digital glitch defaults
|
|
1274
3147
|
blockSize: 0.5,
|
|
1275
3148
|
displacement: 0.5,
|
|
1276
|
-
blockOpacity:
|
|
3149
|
+
blockOpacity: 0,
|
|
1277
3150
|
colorSplit: 0.5,
|
|
1278
3151
|
lineTear: 0.5,
|
|
1279
3152
|
pixelate: 0.3,
|
|
1280
3153
|
// Weird glitch defaults
|
|
1281
|
-
glitchChance: 0.
|
|
3154
|
+
glitchChance: 0.66,
|
|
1282
3155
|
glitchSpeed: 7.0,
|
|
1283
3156
|
sliceDensity: 14.0,
|
|
1284
3157
|
sliceStrength: 0.38,
|
|
@@ -1366,6 +3239,7 @@ async function handleStateTool(name, args) {
|
|
|
1366
3239
|
};
|
|
1367
3240
|
}
|
|
1368
3241
|
currentPoster.effect = effect;
|
|
3242
|
+
persistState();
|
|
1369
3243
|
return {
|
|
1370
3244
|
content: [
|
|
1371
3245
|
{
|
|
@@ -1380,11 +3254,7 @@ async function handleStateTool(name, args) {
|
|
|
1380
3254
|
};
|
|
1381
3255
|
}
|
|
1382
3256
|
case 'add_postprocess': {
|
|
1383
|
-
|
|
1384
|
-
return {
|
|
1385
|
-
content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
|
|
1386
|
-
};
|
|
1387
|
-
}
|
|
3257
|
+
ensurePoster();
|
|
1388
3258
|
const ppType = params.type;
|
|
1389
3259
|
if (!ppType) {
|
|
1390
3260
|
return {
|
|
@@ -1430,6 +3300,18 @@ async function handleStateTool(name, args) {
|
|
|
1430
3300
|
'decay', // light-beams
|
|
1431
3301
|
'density', // light-beams
|
|
1432
3302
|
'particleAmount', // light-beams
|
|
3303
|
+
// Blob tracking
|
|
3304
|
+
'detection', // blob-tracking
|
|
3305
|
+
'rectScale', // blob-tracking
|
|
3306
|
+
'zoom', // blob-tracking
|
|
3307
|
+
'invertChance', // blob-tracking
|
|
3308
|
+
'animSpeed', // blob-tracking
|
|
3309
|
+
'lineColor', // blob-tracking
|
|
3310
|
+
'lineOpacity', // blob-tracking
|
|
3311
|
+
'rectColor', // blob-tracking
|
|
3312
|
+
'rectOpacity', // blob-tracking
|
|
3313
|
+
'showLabels', // blob-tracking
|
|
3314
|
+
'glow', // blob-tracking
|
|
1433
3315
|
];
|
|
1434
3316
|
const allSettingKeys = [...commonKeys, ...additionalKeys];
|
|
1435
3317
|
for (const key of allSettingKeys) {
|
|
@@ -1442,6 +3324,11 @@ async function handleStateTool(name, args) {
|
|
|
1442
3324
|
settings.type = settings.blurType;
|
|
1443
3325
|
delete settings.blurType;
|
|
1444
3326
|
}
|
|
3327
|
+
// Palette post-process: resolve paletteId to colors if colors weren't explicitly provided
|
|
3328
|
+
if (ppType === 'palette' && settings.paletteId && !params.colors) {
|
|
3329
|
+
const resolved = resolvePaletteColors(settings.paletteId, ['#000000', '#ffffff']);
|
|
3330
|
+
settings.colors = resolved;
|
|
3331
|
+
}
|
|
1445
3332
|
const postProcess = {
|
|
1446
3333
|
id: ppId,
|
|
1447
3334
|
type: ppType,
|
|
@@ -1449,6 +3336,7 @@ async function handleStateTool(name, args) {
|
|
|
1449
3336
|
settings,
|
|
1450
3337
|
};
|
|
1451
3338
|
currentPoster.postProcesses.push(postProcess);
|
|
3339
|
+
persistState();
|
|
1452
3340
|
// Build response with warning if dither is active
|
|
1453
3341
|
const response = {
|
|
1454
3342
|
success: true,
|
|
@@ -1471,11 +3359,7 @@ async function handleStateTool(name, args) {
|
|
|
1471
3359
|
};
|
|
1472
3360
|
}
|
|
1473
3361
|
case 'remove_layer': {
|
|
1474
|
-
|
|
1475
|
-
return {
|
|
1476
|
-
content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
3362
|
+
ensurePoster();
|
|
1479
3363
|
const layerId = params.layerId;
|
|
1480
3364
|
if (!layerId) {
|
|
1481
3365
|
return {
|
|
@@ -1494,6 +3378,7 @@ async function handleStateTool(name, args) {
|
|
|
1494
3378
|
};
|
|
1495
3379
|
}
|
|
1496
3380
|
currentPoster.layers.splice(layerIndex, 1);
|
|
3381
|
+
persistState();
|
|
1497
3382
|
return {
|
|
1498
3383
|
content: [
|
|
1499
3384
|
{
|
|
@@ -1507,12 +3392,67 @@ async function handleStateTool(name, args) {
|
|
|
1507
3392
|
],
|
|
1508
3393
|
};
|
|
1509
3394
|
}
|
|
1510
|
-
case '
|
|
1511
|
-
|
|
3395
|
+
case 'move_layer': {
|
|
3396
|
+
ensurePoster();
|
|
3397
|
+
const poster = currentPoster;
|
|
3398
|
+
const layerId = params.layerId;
|
|
3399
|
+
const direction = params.direction;
|
|
3400
|
+
if (!layerId) {
|
|
1512
3401
|
return {
|
|
1513
|
-
content: [{ type: 'text', text: '
|
|
3402
|
+
content: [{ type: 'text', text: 'Error: layerId is required' }],
|
|
1514
3403
|
};
|
|
1515
3404
|
}
|
|
3405
|
+
const layerIndex = poster.layers.findIndex((l) => l.id === layerId);
|
|
3406
|
+
if (layerIndex < 0) {
|
|
3407
|
+
return {
|
|
3408
|
+
content: [{ type: 'text', text: `Error: Layer not found: ${layerId}` }],
|
|
3409
|
+
};
|
|
3410
|
+
}
|
|
3411
|
+
if (layerIndex === 0) {
|
|
3412
|
+
return {
|
|
3413
|
+
content: [{ type: 'text', text: 'Error: Cannot move background layer' }],
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
const clampIndex = (index) => Math.max(1, Math.min(index, poster.layers.length - 1));
|
|
3417
|
+
const moveTo = (toIndex) => {
|
|
3418
|
+
const nextIndex = clampIndex(toIndex);
|
|
3419
|
+
if (nextIndex === layerIndex)
|
|
3420
|
+
return false;
|
|
3421
|
+
const [layer] = poster.layers.splice(layerIndex, 1);
|
|
3422
|
+
poster.layers.splice(nextIndex, 0, layer);
|
|
3423
|
+
return true;
|
|
3424
|
+
};
|
|
3425
|
+
let moved = false;
|
|
3426
|
+
if (direction === 'up')
|
|
3427
|
+
moved = moveTo(layerIndex + 1);
|
|
3428
|
+
else if (direction === 'down')
|
|
3429
|
+
moved = moveTo(layerIndex - 1);
|
|
3430
|
+
else if (direction === 'top')
|
|
3431
|
+
moved = moveTo(poster.layers.length - 1);
|
|
3432
|
+
else if (direction === 'bottom')
|
|
3433
|
+
moved = moveTo(1);
|
|
3434
|
+
else {
|
|
3435
|
+
return {
|
|
3436
|
+
content: [{ type: 'text', text: 'Error: direction must be one of: up, down, top, bottom' }],
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
persistState();
|
|
3440
|
+
return {
|
|
3441
|
+
content: [
|
|
3442
|
+
{
|
|
3443
|
+
type: 'text',
|
|
3444
|
+
text: JSON.stringify({
|
|
3445
|
+
success: true,
|
|
3446
|
+
message: moved ? `Layer moved (${direction})` : `Layer already at ${direction}`,
|
|
3447
|
+
layerId,
|
|
3448
|
+
totalLayers: poster.layers.length,
|
|
3449
|
+
}, null, 2),
|
|
3450
|
+
},
|
|
3451
|
+
],
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
case 'get_state': {
|
|
3455
|
+
ensurePoster();
|
|
1516
3456
|
return {
|
|
1517
3457
|
content: [
|
|
1518
3458
|
{
|