@efectoapp/mcp-server 0.1.21 → 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 +6 -3
- package/dist/tools/output.d.ts +1 -4
- package/dist/tools/output.d.ts.map +1 -1
- package/dist/tools/output.js +49 -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 +58 -1
- package/dist/tools/state.d.ts.map +1 -1
- package/dist/tools/state.js +1598 -192
- 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() {
|
|
@@ -401,6 +800,113 @@ function parseGradientStopsInput(input, fallbackStops) {
|
|
|
401
800
|
.filter((stop) => stop !== null);
|
|
402
801
|
return parsed.length >= MIN_GRADIENT_STOPS ? parsed : null;
|
|
403
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
|
+
};
|
|
404
910
|
// Tool definitions
|
|
405
911
|
exports.stateTools = [
|
|
406
912
|
{
|
|
@@ -420,6 +926,84 @@ exports.stateTools = [
|
|
|
420
926
|
description: 'Background color in hex format (e.g., "#1a1a1a")',
|
|
421
927
|
default: '#1a1a1a',
|
|
422
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
|
+
},
|
|
423
1007
|
},
|
|
424
1008
|
required: [],
|
|
425
1009
|
},
|
|
@@ -510,32 +1094,66 @@ exports.stateTools = [
|
|
|
510
1094
|
properties: {
|
|
511
1095
|
type: {
|
|
512
1096
|
type: 'string',
|
|
513
|
-
enum: ['text', 'image', 'video'],
|
|
1097
|
+
enum: ['text', 'image', 'video', 'rectangle', 'ellipse', 'polygon', 'star', 'line'],
|
|
514
1098
|
description: 'Type of layer to add',
|
|
515
1099
|
},
|
|
516
1100
|
name: { type: 'string', description: 'Layer name' },
|
|
517
1101
|
// Transform properties (normalized coordinates relative to canvas)
|
|
518
|
-
x: { type: 'number', description: '
|
|
519
|
-
y: { type: 'number', description: '
|
|
520
|
-
width: { type: 'number', description: 'Width (
|
|
521
|
-
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 },
|
|
522
1106
|
rotation: { type: 'number', description: 'Rotation in degrees', default: 0 },
|
|
523
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)' },
|
|
524
1134
|
// Text layer properties
|
|
525
1135
|
content: { type: 'string', description: 'Text content (required for text layers)' },
|
|
526
|
-
textType: { type: 'string', enum: ['
|
|
1136
|
+
textType: { type: 'string', enum: ['frame'], description: 'Text mode (always "frame" — point text is deprecated)', default: 'frame' },
|
|
527
1137
|
fontFamily: { type: 'string', description: 'Font family name', default: 'DM Sans' },
|
|
528
1138
|
fontSize: { type: 'number', description: 'Font size in pixels', default: 72 },
|
|
529
1139
|
fontWeight: { type: 'string', enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], description: 'Font weight', default: 'bold' },
|
|
530
1140
|
color: { type: 'string', description: 'Text color in hex', default: '#ffffff' },
|
|
531
1141
|
textAlign: { type: 'string', enum: ['left', 'center', 'right'], description: 'Text alignment', default: 'center' },
|
|
1142
|
+
verticalAlign: { type: 'string', enum: ['top', 'middle', 'bottom'], description: 'Vertical text alignment inside the frame. Defaults to "top" for frame text.' },
|
|
532
1143
|
letterSpacing: { type: 'number', description: 'Letter spacing in pixels (negative for tighter, e.g., -1 to -2 for headlines)', default: 0 },
|
|
533
1144
|
lineHeight: { type: 'number', description: 'Line height multiplier (0.9-1.0 for tight headlines, 1.3-1.5 for body)', default: 1.2 },
|
|
534
|
-
textTransformMode: { type: 'string', enum: ['
|
|
1145
|
+
textTransformMode: { type: 'string', enum: ['box'], description: 'Text sizing mode (always "box" — scale mode is deprecated)', default: 'box' },
|
|
535
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.' },
|
|
536
1149
|
frameWidthPx: { type: 'number', description: 'Frame width in CSS pixels for frame text (resizing changes this, not fontSize)' },
|
|
537
1150
|
frameHeightPx: { type: 'number', description: 'Frame height in CSS pixels for frame text' },
|
|
538
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' },
|
|
539
1157
|
// Text animation properties (use list_text_animations to see all options)
|
|
540
1158
|
animationType: {
|
|
541
1159
|
type: 'string',
|
|
@@ -609,6 +1227,27 @@ exports.stateTools = [
|
|
|
609
1227
|
// Image/Video properties
|
|
610
1228
|
mediaUrl: { type: 'string', description: 'Media URL (required for image/video layers)' },
|
|
611
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)' },
|
|
612
1251
|
},
|
|
613
1252
|
required: ['type'],
|
|
614
1253
|
},
|
|
@@ -623,22 +1262,22 @@ exports.stateTools = [
|
|
|
623
1262
|
layoutMode: { type: 'string', enum: ['row', 'column'], description: 'Flex direction: "column" stacks vertically (default, most common), "row" places side by side', default: 'column' },
|
|
624
1263
|
widthMode: { type: 'string', enum: ['hug', 'fixed'], description: 'Horizontal sizing mode: hug contents or fixed width' },
|
|
625
1264
|
heightMode: { type: 'string', enum: ['hug', 'fixed'], description: 'Vertical sizing mode: hug contents or fixed height' },
|
|
626
|
-
gap: { type: 'number', description: 'Space between children
|
|
1265
|
+
gap: { type: 'number', description: 'Space between children as % of canvas (2=small, 4=medium, 6=large). Negative values overlap children.', default: 2 },
|
|
627
1266
|
justifyContent: { type: 'string', enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'], description: 'Main-axis alignment (like CSS justify-content)', default: 'start' },
|
|
628
1267
|
alignItems: { type: 'string', enum: ['start', 'center', 'end'], description: 'Cross-axis alignment (like CSS align-items): "start"=left, "center"=centered, "end"=right', default: 'center' },
|
|
629
1268
|
wrap: { type: 'boolean', description: 'Wrap children to next line on overflow (like CSS flex-wrap)', default: false },
|
|
630
|
-
padding: { type: 'number', description: '
|
|
631
|
-
paddingTop: { type: 'number', description: '
|
|
632
|
-
paddingRight: { type: 'number', description: '
|
|
633
|
-
paddingBottom: { type: 'number', description: '
|
|
634
|
-
paddingLeft: { type: 'number', description: '
|
|
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' },
|
|
635
1274
|
fillColor: { type: 'string', description: 'Group background color (hex)' },
|
|
636
1275
|
fillOpacity: { type: 'number', description: 'Group background opacity (0-1)' },
|
|
637
1276
|
cornerRadius: { type: 'number', description: 'Corner radius' },
|
|
638
|
-
x: { type: 'number', description: '
|
|
639
|
-
y: { type: 'number', description: '
|
|
640
|
-
width: { type: 'number', description: '
|
|
641
|
-
height: { type: 'number', description: '
|
|
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 },
|
|
642
1281
|
rotation: { type: 'number', description: 'Rotation in degrees', default: 0 },
|
|
643
1282
|
opacity: { type: 'number', description: 'Opacity 0-1', default: 1 },
|
|
644
1283
|
children: {
|
|
@@ -647,7 +1286,7 @@ exports.stateTools = [
|
|
|
647
1286
|
items: {
|
|
648
1287
|
type: 'object',
|
|
649
1288
|
properties: {
|
|
650
|
-
type: { type: 'string', enum: ['text', 'image', 'video'], description: 'Child layer type' },
|
|
1289
|
+
type: { type: 'string', enum: ['text', 'image', 'video', 'rectangle', 'ellipse', 'polygon', 'star', 'line'], description: 'Child layer type' },
|
|
651
1290
|
name: { type: 'string', description: 'Child layer name' },
|
|
652
1291
|
content: { type: 'string', description: 'Text content' },
|
|
653
1292
|
fontFamily: { type: 'string', description: 'Font family' },
|
|
@@ -655,19 +1294,70 @@ exports.stateTools = [
|
|
|
655
1294
|
fontWeight: { type: 'string', description: 'Font weight' },
|
|
656
1295
|
color: { type: 'string', description: 'Text color in hex' },
|
|
657
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' },
|
|
658
1298
|
letterSpacing: { type: 'number', description: 'Letter spacing' },
|
|
659
1299
|
lineHeight: { type: 'number', description: 'Line height multiplier' },
|
|
660
|
-
textType: { type: 'string', enum: ['
|
|
661
|
-
textTransformMode: { type: 'string', enum: ['
|
|
1300
|
+
textType: { type: 'string', enum: ['frame'], description: 'Text mode (always "frame")' },
|
|
1301
|
+
textTransformMode: { type: 'string', enum: ['box'], description: 'Text sizing mode (always "box")' },
|
|
662
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.' },
|
|
663
1305
|
frameWidthPx: { type: 'number', description: 'Frame width in CSS pixels' },
|
|
664
1306
|
frameHeightPx: { type: 'number', description: 'Frame height in CSS pixels' },
|
|
665
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' },
|
|
666
1312
|
mediaUrl: { type: 'string', description: 'Media URL (for image/video)' },
|
|
667
1313
|
objectFit: { type: 'string', enum: ['cover', 'contain'], description: 'Object fit mode' },
|
|
668
|
-
width: { type: 'number', description: '
|
|
669
|
-
height: { type: 'number', description: '
|
|
1314
|
+
width: { type: 'number', description: 'Width as % of canvas (e.g. 60 = 60%)' },
|
|
1315
|
+
height: { type: 'number', description: 'Height as % of canvas' },
|
|
670
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)' },
|
|
671
1361
|
},
|
|
672
1362
|
required: ['type'],
|
|
673
1363
|
},
|
|
@@ -678,36 +1368,63 @@ exports.stateTools = [
|
|
|
678
1368
|
},
|
|
679
1369
|
{
|
|
680
1370
|
name: 'modify_layer',
|
|
681
|
-
description: 'Modify an existing layer by ID. Supports text, image, video, and
|
|
1371
|
+
description: 'Modify an existing layer by ID. Supports text, image, video, group, and shape properties.',
|
|
682
1372
|
inputSchema: {
|
|
683
1373
|
type: 'object',
|
|
684
1374
|
properties: {
|
|
685
1375
|
layerId: { type: 'string', description: 'ID of the layer to modify' },
|
|
686
|
-
// Transform properties (
|
|
687
|
-
x: { type: 'number', description: '
|
|
688
|
-
y: { type: 'number', description: '
|
|
689
|
-
width: { type: 'number', description: 'Width' },
|
|
690
|
-
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' },
|
|
691
1381
|
rotation: { type: 'number', description: 'Rotation in degrees' },
|
|
692
1382
|
opacity: { type: 'number', description: 'Opacity 0-1' },
|
|
693
1383
|
visible: { type: 'boolean', description: 'Layer visibility' },
|
|
694
1384
|
locked: { type: 'boolean', description: 'Layer lock state' },
|
|
695
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)' },
|
|
696
1405
|
// Text layer properties
|
|
697
1406
|
content: { type: 'string' },
|
|
698
|
-
textType: { type: 'string', enum: ['
|
|
1407
|
+
textType: { type: 'string', enum: ['frame'], description: 'Text mode (always "frame")' },
|
|
699
1408
|
fontFamily: { type: 'string' },
|
|
700
1409
|
fontSize: { type: 'number' },
|
|
701
1410
|
fontWeight: { type: 'string' },
|
|
702
1411
|
color: { type: 'string' },
|
|
703
1412
|
textAlign: { type: 'string' },
|
|
1413
|
+
verticalAlign: { type: 'string', enum: ['top', 'middle', 'bottom'], description: 'Vertical text alignment inside the frame' },
|
|
704
1414
|
letterSpacing: { type: 'number' },
|
|
705
1415
|
lineHeight: { type: 'number' },
|
|
706
|
-
textTransformMode: { type: 'string', enum: ['
|
|
1416
|
+
textTransformMode: { type: 'string', enum: ['box'] },
|
|
707
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.' },
|
|
708
1420
|
frameWidthPx: { type: 'number' },
|
|
709
1421
|
frameHeightPx: { type: 'number' },
|
|
710
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' },
|
|
711
1428
|
// Text animation properties (all type-specific params supported - see add_layer for descriptions)
|
|
712
1429
|
animationType: {
|
|
713
1430
|
type: 'string',
|
|
@@ -768,18 +1485,45 @@ exports.stateTools = [
|
|
|
768
1485
|
layoutMode: { type: 'string', enum: ['row', 'column'], description: 'Group layout direction' },
|
|
769
1486
|
widthMode: { type: 'string', enum: ['hug', 'fixed'], description: 'Group horizontal sizing mode' },
|
|
770
1487
|
heightMode: { type: 'string', enum: ['hug', 'fixed'], description: 'Group vertical sizing mode' },
|
|
771
|
-
gap: { type: 'number', description: '
|
|
1488
|
+
gap: { type: 'number', description: 'Space between children as % of canvas (2=small, 4=medium, 6=large)' },
|
|
772
1489
|
justifyContent: { type: 'string', enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'], description: 'Group main-axis alignment' },
|
|
773
1490
|
alignItems: { type: 'string', enum: ['start', 'center', 'end'], description: 'Group cross-axis alignment' },
|
|
774
1491
|
wrap: { type: 'boolean', description: 'Group wrap children' },
|
|
775
|
-
padding: { type: 'number', description: '
|
|
776
|
-
paddingTop: { type: 'number', description: '
|
|
777
|
-
paddingRight: { type: 'number', description: '
|
|
778
|
-
paddingBottom: { type: 'number', description: '
|
|
779
|
-
paddingLeft: { type: 'number', description: '
|
|
780
|
-
fillColor: { type: 'string', description: '
|
|
781
|
-
fillOpacity: { type: 'number', description: '
|
|
782
|
-
|
|
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' },
|
|
783
1527
|
},
|
|
784
1528
|
required: ['layerId'],
|
|
785
1529
|
},
|
|
@@ -805,12 +1549,15 @@ IMPORTANT - Two effect pipelines:
|
|
|
805
1549
|
'dither-floyd-steinberg', 'dither-atkinson', 'dither-jarvis-judice-ninke',
|
|
806
1550
|
'dither-stucki', 'dither-burkes', 'dither-sierra', 'dither-two-row-sierra',
|
|
807
1551
|
'dither-sierra-lite',
|
|
1552
|
+
'color-separation',
|
|
808
1553
|
// Halftone (WebGPU - supports post-processes)
|
|
809
1554
|
'halftone-mono', 'halftone-cmyk',
|
|
810
1555
|
// Glitch (WebGPU - supports post-processes)
|
|
811
1556
|
'glitch-chromatic', 'glitch-digital', 'glitch-vhs', 'glitch-weird',
|
|
812
1557
|
// Art (WebGPU - supports post-processes)
|
|
813
1558
|
'art-kuwahara', 'art-crosshatch', 'art-lineart', 'art-engraving', 'art-stipple',
|
|
1559
|
+
// Special (WebGPU - supports post-processes)
|
|
1560
|
+
'special-warp',
|
|
814
1561
|
],
|
|
815
1562
|
description: 'Effect ID to apply',
|
|
816
1563
|
},
|
|
@@ -856,7 +1603,7 @@ Common combinations:
|
|
|
856
1603
|
'scanlines', 'vignette', 'chromatic-aberration', 'curvature',
|
|
857
1604
|
'grain', 'noise', 'pixelate', 'wave', 'rgb-glitch', 'brightness-contrast',
|
|
858
1605
|
'color-tint', 'palette', 'jitter', 'bloom', 'dot-screen', 'sepia',
|
|
859
|
-
'grid', 'light-beams', 'motion-blur',
|
|
1606
|
+
'grid', 'light-beams', 'motion-blur', 'blob-tracking',
|
|
860
1607
|
],
|
|
861
1608
|
description: 'Post-process type',
|
|
862
1609
|
},
|
|
@@ -914,6 +1661,17 @@ Common combinations:
|
|
|
914
1661
|
density: { type: 'number', description: 'Light-beams: ray spread (0.5-2)', default: 1 },
|
|
915
1662
|
animated: { type: 'boolean', description: 'Light-beams/Grid: enable animation', default: false },
|
|
916
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 },
|
|
917
1675
|
},
|
|
918
1676
|
required: ['type'],
|
|
919
1677
|
},
|
|
@@ -929,6 +1687,22 @@ Common combinations:
|
|
|
929
1687
|
required: ['layerId'],
|
|
930
1688
|
},
|
|
931
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
|
+
},
|
|
932
1706
|
{
|
|
933
1707
|
name: 'get_state',
|
|
934
1708
|
description: 'Get the current poster state as LayerShareState JSON (for debugging or verification)',
|
|
@@ -978,11 +1752,24 @@ async function handleStateTool(name, args) {
|
|
|
978
1752
|
case 'create_poster': {
|
|
979
1753
|
const aspectRatio = params.aspectRatio || '9:16';
|
|
980
1754
|
const backgroundColor = params.backgroundColor || '#1a1a1a';
|
|
1755
|
+
const canvasColor = params.canvasColor;
|
|
1756
|
+
const overflow = params.overflow;
|
|
981
1757
|
currentPoster = {
|
|
982
|
-
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
|
+
},
|
|
983
1769
|
layers: [createDefaultBackgroundLayer(backgroundColor)],
|
|
984
1770
|
postProcesses: [],
|
|
985
1771
|
};
|
|
1772
|
+
persistState();
|
|
986
1773
|
return {
|
|
987
1774
|
content: [
|
|
988
1775
|
{
|
|
@@ -997,12 +1784,110 @@ async function handleStateTool(name, args) {
|
|
|
997
1784
|
],
|
|
998
1785
|
};
|
|
999
1786
|
}
|
|
1000
|
-
case '
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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,
|
|
1004
1843
|
};
|
|
1844
|
+
currentPoster.canvas.padding = nextPadding;
|
|
1005
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();
|
|
1006
1891
|
// Background is always at index 0
|
|
1007
1892
|
const bgLayer = currentPoster.layers[0];
|
|
1008
1893
|
if (bgLayer.type !== 'background') {
|
|
@@ -1014,53 +1899,8 @@ async function handleStateTool(name, args) {
|
|
|
1014
1899
|
const contentType = requestedType === 'gradient' ? 'solid' : requestedType;
|
|
1015
1900
|
bgLayer.contentType = contentType;
|
|
1016
1901
|
if (requestedType === 'gradient') {
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
const gradient = gradientStyle === 'radial'
|
|
1020
|
-
? normalizeRadialGradientFill({
|
|
1021
|
-
type: 'radial',
|
|
1022
|
-
center: {
|
|
1023
|
-
x: Number.isFinite(params.gradientCenterX)
|
|
1024
|
-
? params.gradientCenterX
|
|
1025
|
-
: DEFAULT_RADIAL_GRADIENT_FILL.center.x,
|
|
1026
|
-
y: Number.isFinite(params.gradientCenterY)
|
|
1027
|
-
? params.gradientCenterY
|
|
1028
|
-
: DEFAULT_RADIAL_GRADIENT_FILL.center.y,
|
|
1029
|
-
},
|
|
1030
|
-
radius: Number.isFinite(params.gradientRadius)
|
|
1031
|
-
? params.gradientRadius
|
|
1032
|
-
: DEFAULT_RADIAL_GRADIENT_FILL.radius,
|
|
1033
|
-
stops: explicitStops ?? [
|
|
1034
|
-
{
|
|
1035
|
-
color: params.gradientStartColor || DEFAULT_RADIAL_GRADIENT_FILL.stops[0].color,
|
|
1036
|
-
opacity: DEFAULT_RADIAL_GRADIENT_FILL.stops[0].opacity,
|
|
1037
|
-
position: DEFAULT_RADIAL_GRADIENT_FILL.stops[0].position,
|
|
1038
|
-
},
|
|
1039
|
-
{
|
|
1040
|
-
color: params.gradientEndColor || DEFAULT_RADIAL_GRADIENT_FILL.stops[1].color,
|
|
1041
|
-
opacity: DEFAULT_RADIAL_GRADIENT_FILL.stops[1].opacity,
|
|
1042
|
-
position: DEFAULT_RADIAL_GRADIENT_FILL.stops[1].position,
|
|
1043
|
-
},
|
|
1044
|
-
],
|
|
1045
|
-
})
|
|
1046
|
-
: normalizeLinearGradientFill({
|
|
1047
|
-
type: 'linear',
|
|
1048
|
-
angle: Number.isFinite(params.gradientAngle)
|
|
1049
|
-
? params.gradientAngle
|
|
1050
|
-
: DEFAULT_LINEAR_GRADIENT_FILL.angle,
|
|
1051
|
-
stops: explicitStops ?? [
|
|
1052
|
-
{
|
|
1053
|
-
color: params.gradientStartColor || DEFAULT_LINEAR_GRADIENT_FILL.stops[0].color,
|
|
1054
|
-
opacity: DEFAULT_LINEAR_GRADIENT_FILL.stops[0].opacity,
|
|
1055
|
-
position: DEFAULT_LINEAR_GRADIENT_FILL.stops[0].position,
|
|
1056
|
-
},
|
|
1057
|
-
{
|
|
1058
|
-
color: params.gradientEndColor || DEFAULT_LINEAR_GRADIENT_FILL.stops[1].color,
|
|
1059
|
-
opacity: DEFAULT_LINEAR_GRADIENT_FILL.stops[1].opacity,
|
|
1060
|
-
position: DEFAULT_LINEAR_GRADIENT_FILL.stops[1].position,
|
|
1061
|
-
},
|
|
1062
|
-
],
|
|
1063
|
-
});
|
|
1902
|
+
const gradient = buildFillFromParams(params)
|
|
1903
|
+
?? normalizeLinearGradientFill(DEFAULT_LINEAR_GRADIENT_FILL);
|
|
1064
1904
|
bgLayer.fill = gradient;
|
|
1065
1905
|
bgLayer.solidColor = gradient.stops[0].color;
|
|
1066
1906
|
currentPoster.canvas.backgroundColor = gradient.stops[0].color;
|
|
@@ -1105,6 +1945,7 @@ async function handleStateTool(name, args) {
|
|
|
1105
1945
|
objectFit: params.objectFit || 'cover',
|
|
1106
1946
|
};
|
|
1107
1947
|
}
|
|
1948
|
+
persistState();
|
|
1108
1949
|
return {
|
|
1109
1950
|
content: [
|
|
1110
1951
|
{
|
|
@@ -1119,11 +1960,7 @@ async function handleStateTool(name, args) {
|
|
|
1119
1960
|
};
|
|
1120
1961
|
}
|
|
1121
1962
|
case 'add_layer': {
|
|
1122
|
-
|
|
1123
|
-
return {
|
|
1124
|
-
content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
|
|
1125
|
-
};
|
|
1126
|
-
}
|
|
1963
|
+
ensurePoster();
|
|
1127
1964
|
const layerType = params.type;
|
|
1128
1965
|
if (!layerType) {
|
|
1129
1966
|
return {
|
|
@@ -1135,49 +1972,76 @@ async function handleStateTool(name, args) {
|
|
|
1135
1972
|
content: [{ type: 'text', text: 'Error: Use set_background to modify the background layer' }],
|
|
1136
1973
|
};
|
|
1137
1974
|
}
|
|
1975
|
+
const isShapeType = ['rectangle', 'ellipse', 'polygon', 'star', 'line'].includes(layerType);
|
|
1138
1976
|
const layerId = generateId();
|
|
1977
|
+
const visualOverrides = parseVisualOverrides(params);
|
|
1978
|
+
const originOverrides = parseTransformOrigin(params);
|
|
1139
1979
|
const baseLayer = {
|
|
1140
1980
|
id: layerId,
|
|
1141
1981
|
type: layerType,
|
|
1142
1982
|
name: params.name || `${layerType.charAt(0).toUpperCase() + layerType.slice(1)} Layer`,
|
|
1143
1983
|
visible: true,
|
|
1144
1984
|
locked: false,
|
|
1985
|
+
...parseFlexChildProps(params),
|
|
1986
|
+
...visualOverrides,
|
|
1145
1987
|
transform: {
|
|
1146
|
-
x: params.x
|
|
1147
|
-
y: params.y
|
|
1148
|
-
|
|
1149
|
-
|
|
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),
|
|
1150
1993
|
rotation: params.rotation ?? 0,
|
|
1151
|
-
opacity: params.opacity
|
|
1994
|
+
opacity: normalizeOpacity(params.opacity, 1),
|
|
1995
|
+
...originOverrides,
|
|
1152
1996
|
},
|
|
1153
1997
|
};
|
|
1154
1998
|
let layer;
|
|
1155
1999
|
if (layerType === 'text') {
|
|
1156
2000
|
const animation = buildAnimationSettings(params);
|
|
1157
|
-
|
|
1158
|
-
const
|
|
1159
|
-
const
|
|
1160
|
-
const
|
|
1161
|
-
const
|
|
1162
|
-
|
|
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
|
+
}
|
|
1163
2024
|
layer = {
|
|
1164
2025
|
...baseLayer,
|
|
1165
2026
|
type: 'text',
|
|
1166
|
-
content
|
|
1167
|
-
textType,
|
|
2027
|
+
content,
|
|
2028
|
+
textType: 'frame',
|
|
1168
2029
|
fontFamily: normalizeFontFamily(params.fontFamily),
|
|
1169
|
-
fontSize
|
|
2030
|
+
fontSize,
|
|
1170
2031
|
fontWeight: normalizeFontWeight(params.fontWeight),
|
|
1171
2032
|
color: params.color || '#ffffff',
|
|
1172
2033
|
textAlign: params.textAlign || 'center',
|
|
2034
|
+
...(params.verticalAlign ? { verticalAlign: params.verticalAlign } : {}),
|
|
1173
2035
|
letterSpacing: params.letterSpacing ?? 0,
|
|
1174
2036
|
lineHeight: params.lineHeight ?? 1.2,
|
|
1175
|
-
textTransformMode,
|
|
1176
|
-
...(
|
|
1177
|
-
...(
|
|
1178
|
-
...(
|
|
1179
|
-
...(
|
|
2037
|
+
textTransformMode: 'box',
|
|
2038
|
+
...(typeof textBoxWidth === 'number' ? { textBoxWidth } : {}),
|
|
2039
|
+
...(typeof frameWidthPx === 'number' ? { frameWidthPx } : {}),
|
|
2040
|
+
...(typeof frameHeightPx === 'number' ? { frameHeightPx } : {}),
|
|
2041
|
+
...(typeof autoSize === 'string' ? { autoSize } : {}),
|
|
1180
2042
|
...(animation ? { animation } : {}),
|
|
2043
|
+
...(parseTextTransform(params) ? { textTransform: parseTextTransform(params) } : {}),
|
|
2044
|
+
...parseTextDecoration(params),
|
|
1181
2045
|
};
|
|
1182
2046
|
}
|
|
1183
2047
|
else if (layerType === 'image') {
|
|
@@ -1214,10 +2078,140 @@ async function handleStateTool(name, args) {
|
|
|
1214
2078
|
playbackSpeed: 1,
|
|
1215
2079
|
};
|
|
1216
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
|
+
}
|
|
1217
2210
|
else {
|
|
1218
2211
|
layer = baseLayer;
|
|
1219
2212
|
}
|
|
1220
2213
|
currentPoster.layers.push(layer);
|
|
2214
|
+
persistState();
|
|
1221
2215
|
return {
|
|
1222
2216
|
content: [
|
|
1223
2217
|
{
|
|
@@ -1233,11 +2227,7 @@ async function handleStateTool(name, args) {
|
|
|
1233
2227
|
};
|
|
1234
2228
|
}
|
|
1235
2229
|
case 'add_group': {
|
|
1236
|
-
|
|
1237
|
-
return {
|
|
1238
|
-
content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
2230
|
+
ensurePoster();
|
|
1241
2231
|
const children = params.children;
|
|
1242
2232
|
if (!children || !Array.isArray(children) || children.length === 0) {
|
|
1243
2233
|
return {
|
|
@@ -1246,46 +2236,71 @@ async function handleStateTool(name, args) {
|
|
|
1246
2236
|
}
|
|
1247
2237
|
const groupId = generateId();
|
|
1248
2238
|
const childLayers = [];
|
|
2239
|
+
const artboardAspect = getArtboardAspect(currentPoster.canvas.aspectRatio);
|
|
1249
2240
|
for (const childParams of children) {
|
|
1250
2241
|
const childType = childParams.type || 'text';
|
|
2242
|
+
const isChildShapeType = ['rectangle', 'ellipse', 'polygon', 'star', 'line'].includes(childType);
|
|
1251
2243
|
const childId = generateId();
|
|
2244
|
+
const childVisualOverrides = parseVisualOverrides(childParams);
|
|
2245
|
+
const childOriginOverrides = parseTransformOrigin(childParams);
|
|
1252
2246
|
const childBase = {
|
|
1253
2247
|
id: childId,
|
|
1254
2248
|
type: childType,
|
|
1255
2249
|
name: childParams.name || `${childType.charAt(0).toUpperCase() + childType.slice(1)} Layer`,
|
|
1256
2250
|
visible: true,
|
|
1257
2251
|
locked: false,
|
|
2252
|
+
...parseFlexChildProps(childParams),
|
|
2253
|
+
...childVisualOverrides,
|
|
1258
2254
|
transform: {
|
|
1259
2255
|
x: 0,
|
|
1260
2256
|
y: 0,
|
|
1261
|
-
|
|
1262
|
-
|
|
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),
|
|
1263
2260
|
rotation: 0,
|
|
1264
|
-
opacity: childParams.opacity
|
|
2261
|
+
opacity: normalizeOpacity(childParams.opacity, 1),
|
|
2262
|
+
...childOriginOverrides,
|
|
1265
2263
|
},
|
|
1266
2264
|
};
|
|
1267
2265
|
if (childType === 'text') {
|
|
1268
|
-
const
|
|
1269
|
-
const
|
|
1270
|
-
const
|
|
1271
|
-
|
|
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
|
+
}
|
|
1272
2284
|
childLayers.push({
|
|
1273
2285
|
...childBase,
|
|
1274
2286
|
type: 'text',
|
|
1275
2287
|
content: childParams.content || 'Text',
|
|
1276
|
-
textType,
|
|
2288
|
+
textType: 'frame',
|
|
1277
2289
|
fontFamily: normalizeFontFamily(childParams.fontFamily),
|
|
1278
|
-
fontSize:
|
|
2290
|
+
fontSize: childFontSize,
|
|
1279
2291
|
fontWeight: normalizeFontWeight(childParams.fontWeight),
|
|
1280
2292
|
color: childParams.color || '#ffffff',
|
|
1281
2293
|
textAlign: childParams.textAlign || 'center',
|
|
2294
|
+
...(childParams.verticalAlign ? { verticalAlign: childParams.verticalAlign } : {}),
|
|
1282
2295
|
letterSpacing: childParams.letterSpacing ?? 0,
|
|
1283
2296
|
lineHeight: childParams.lineHeight ?? 1.2,
|
|
1284
|
-
textTransformMode,
|
|
1285
|
-
...(
|
|
1286
|
-
|
|
1287
|
-
...(
|
|
1288
|
-
...(childParams.autoSize ? { autoSize: childParams.autoSize } : {}),
|
|
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),
|
|
1289
2304
|
});
|
|
1290
2305
|
}
|
|
1291
2306
|
else if (childType === 'image') {
|
|
@@ -1316,25 +2331,155 @@ async function handleStateTool(name, args) {
|
|
|
1316
2331
|
playbackSpeed: 1,
|
|
1317
2332
|
});
|
|
1318
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
|
+
}
|
|
1319
2463
|
}
|
|
1320
2464
|
if (childLayers.length === 0) {
|
|
1321
2465
|
return {
|
|
1322
2466
|
content: [{ type: 'text', text: 'Error: No valid child layers could be created' }],
|
|
1323
2467
|
};
|
|
1324
2468
|
}
|
|
1325
|
-
// Build padding
|
|
1326
|
-
const
|
|
2469
|
+
// Build padding — convert from CSS % to internal normalized values
|
|
2470
|
+
const uniformPaddingPct = params.padding ?? 0;
|
|
2471
|
+
const uniformPadding = cssPercentToSize(uniformPaddingPct, 0);
|
|
1327
2472
|
const groupPadding = {
|
|
1328
|
-
top: params.paddingTop ??
|
|
1329
|
-
right: params.paddingRight ??
|
|
1330
|
-
bottom: params.paddingBottom ??
|
|
1331
|
-
left: params.paddingLeft ??
|
|
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),
|
|
1332
2477
|
};
|
|
1333
2478
|
const layoutMode = params.layoutMode || 'column';
|
|
1334
2479
|
const widthMode = params.widthMode ?? (params.width !== undefined ? 'fixed' : 'hug');
|
|
1335
2480
|
const heightMode = params.heightMode ?? (params.height !== undefined ? 'fixed' : 'hug');
|
|
1336
|
-
const baseWidth = params.width
|
|
1337
|
-
const baseHeight = params.height
|
|
2481
|
+
const baseWidth = cssPercentToSize(params.width, 60);
|
|
2482
|
+
const baseHeight = cssPercentToSize(params.height, 40);
|
|
1338
2483
|
const groupLayer = {
|
|
1339
2484
|
id: groupId,
|
|
1340
2485
|
type: 'group',
|
|
@@ -1342,12 +2487,12 @@ async function handleStateTool(name, args) {
|
|
|
1342
2487
|
visible: true,
|
|
1343
2488
|
locked: false,
|
|
1344
2489
|
transform: {
|
|
1345
|
-
x: params.x
|
|
1346
|
-
y: params.y
|
|
2490
|
+
x: cssPercentToX(params.x, 50),
|
|
2491
|
+
y: cssPercentToY(params.y, 50),
|
|
1347
2492
|
width: 1,
|
|
1348
2493
|
height: 1,
|
|
1349
2494
|
rotation: params.rotation ?? 0,
|
|
1350
|
-
opacity: params.opacity
|
|
2495
|
+
opacity: normalizeOpacity(params.opacity, 1),
|
|
1351
2496
|
},
|
|
1352
2497
|
children: childLayers,
|
|
1353
2498
|
expanded: true,
|
|
@@ -1356,7 +2501,7 @@ async function handleStateTool(name, args) {
|
|
|
1356
2501
|
layoutMode,
|
|
1357
2502
|
widthMode,
|
|
1358
2503
|
heightMode,
|
|
1359
|
-
gap: params.gap
|
|
2504
|
+
gap: cssPercentToSize(params.gap, 2),
|
|
1360
2505
|
justifyContent: params.justifyContent ?? 'start',
|
|
1361
2506
|
alignItems: params.alignItems ?? 'center',
|
|
1362
2507
|
wrap: params.wrap ?? false,
|
|
@@ -1366,6 +2511,7 @@ async function handleStateTool(name, args) {
|
|
|
1366
2511
|
...(params.cornerRadius !== undefined ? { cornerRadius: params.cornerRadius } : {}),
|
|
1367
2512
|
};
|
|
1368
2513
|
currentPoster.layers.push(groupLayer);
|
|
2514
|
+
persistState();
|
|
1369
2515
|
return {
|
|
1370
2516
|
content: [
|
|
1371
2517
|
{
|
|
@@ -1381,11 +2527,7 @@ async function handleStateTool(name, args) {
|
|
|
1381
2527
|
};
|
|
1382
2528
|
}
|
|
1383
2529
|
case 'modify_layer': {
|
|
1384
|
-
|
|
1385
|
-
return {
|
|
1386
|
-
content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
|
|
1387
|
-
};
|
|
1388
|
-
}
|
|
2530
|
+
ensurePoster();
|
|
1389
2531
|
const layerId = params.layerId;
|
|
1390
2532
|
if (!layerId) {
|
|
1391
2533
|
return {
|
|
@@ -1399,11 +2541,28 @@ async function handleStateTool(name, args) {
|
|
|
1399
2541
|
};
|
|
1400
2542
|
}
|
|
1401
2543
|
const layer = currentPoster.layers[layerIndex];
|
|
2544
|
+
const isShapeType = ['rectangle', 'ellipse', 'polygon', 'star', 'line'].includes(layer.type);
|
|
1402
2545
|
// Update transform properties
|
|
1403
|
-
const transformKeys =
|
|
2546
|
+
const transformKeys = isShapeType
|
|
2547
|
+
? ['x', 'y', 'rotation', 'opacity']
|
|
2548
|
+
: ['x', 'y', 'width', 'height', 'rotation', 'opacity'];
|
|
1404
2549
|
for (const key of transformKeys) {
|
|
1405
2550
|
if (params[key] !== undefined) {
|
|
1406
|
-
|
|
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
|
+
}
|
|
1407
2566
|
}
|
|
1408
2567
|
}
|
|
1409
2568
|
// Update base properties
|
|
@@ -1413,9 +2572,46 @@ async function handleStateTool(name, args) {
|
|
|
1413
2572
|
layer.locked = params.locked;
|
|
1414
2573
|
if (params.name !== undefined)
|
|
1415
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;
|
|
1416
2611
|
// Update text layer properties
|
|
1417
2612
|
if (layer.type === 'text') {
|
|
1418
2613
|
const textLayer = layer;
|
|
2614
|
+
const artboardAspect = getArtboardAspect(currentPoster.canvas.aspectRatio);
|
|
1419
2615
|
if (params.content !== undefined)
|
|
1420
2616
|
textLayer.content = params.content;
|
|
1421
2617
|
if (params.fontFamily !== undefined)
|
|
@@ -1428,18 +2624,15 @@ async function handleStateTool(name, args) {
|
|
|
1428
2624
|
textLayer.color = params.color;
|
|
1429
2625
|
if (params.textAlign !== undefined)
|
|
1430
2626
|
textLayer.textAlign = params.textAlign;
|
|
2627
|
+
if (params.verticalAlign !== undefined)
|
|
2628
|
+
textLayer.verticalAlign = params.verticalAlign;
|
|
1431
2629
|
if (params.letterSpacing !== undefined)
|
|
1432
2630
|
textLayer.letterSpacing = params.letterSpacing;
|
|
1433
2631
|
if (params.lineHeight !== undefined)
|
|
1434
2632
|
textLayer.lineHeight = params.lineHeight;
|
|
1435
|
-
//
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
? params.textType
|
|
1439
|
-
: (params.textTransformMode === 'scale' ? 'point' : 'frame');
|
|
1440
|
-
textLayer.textType = resolvedTextType;
|
|
1441
|
-
textLayer.textTransformMode = resolvedTextType === 'point' ? 'scale' : 'box';
|
|
1442
|
-
}
|
|
2633
|
+
// All text is frame text — silently ignore any 'point'/'scale' requests
|
|
2634
|
+
textLayer.textType = 'frame';
|
|
2635
|
+
textLayer.textTransformMode = 'box';
|
|
1443
2636
|
if (params.textBoxWidth !== undefined)
|
|
1444
2637
|
textLayer.textBoxWidth = params.textBoxWidth;
|
|
1445
2638
|
if (params.frameWidthPx !== undefined)
|
|
@@ -1448,6 +2641,29 @@ async function handleStateTool(name, args) {
|
|
|
1448
2641
|
textLayer.frameHeightPx = params.frameHeightPx;
|
|
1449
2642
|
if (params.autoSize !== undefined)
|
|
1450
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
|
+
}
|
|
1451
2667
|
// Update animation settings
|
|
1452
2668
|
if (params.animationType !== undefined) {
|
|
1453
2669
|
if (params.animationType === 'none') {
|
|
@@ -1591,7 +2807,7 @@ async function handleStateTool(name, args) {
|
|
|
1591
2807
|
if (params.heightMode !== undefined)
|
|
1592
2808
|
groupLayer.heightMode = params.heightMode;
|
|
1593
2809
|
if (params.gap !== undefined)
|
|
1594
|
-
groupLayer.gap = params.gap;
|
|
2810
|
+
groupLayer.gap = cssPercentToSize(params.gap, (groupLayer.gap ?? 0.02) * 100);
|
|
1595
2811
|
if (params.justifyContent !== undefined)
|
|
1596
2812
|
groupLayer.justifyContent = params.justifyContent;
|
|
1597
2813
|
if (params.alignItems !== undefined)
|
|
@@ -1604,31 +2820,154 @@ async function handleStateTool(name, args) {
|
|
|
1604
2820
|
groupLayer.fillOpacity = params.fillOpacity;
|
|
1605
2821
|
if (params.cornerRadius !== undefined)
|
|
1606
2822
|
groupLayer.cornerRadius = params.cornerRadius;
|
|
1607
|
-
|
|
2823
|
+
const groupCornerRadii = parseCornerRadii(params);
|
|
2824
|
+
if (groupCornerRadii)
|
|
2825
|
+
groupLayer.cornerRadii = groupCornerRadii;
|
|
2826
|
+
// Padding: uniform or individual overrides (CSS % to normalized)
|
|
1608
2827
|
if (params.padding !== undefined || params.paddingTop !== undefined || params.paddingRight !== undefined || params.paddingBottom !== undefined || params.paddingLeft !== undefined) {
|
|
1609
2828
|
const existing = groupLayer.padding || { top: 0, right: 0, bottom: 0, left: 0 };
|
|
1610
|
-
const
|
|
2829
|
+
const uniformPct = params.padding !== undefined ? parseNumericInput(params.padding) : undefined;
|
|
2830
|
+
const uniform = uniformPct !== undefined ? cssPercentToSize(uniformPct, 0) : undefined;
|
|
1611
2831
|
groupLayer.padding = {
|
|
1612
|
-
top: params.paddingTop
|
|
1613
|
-
right: params.paddingRight
|
|
1614
|
-
bottom: params.paddingBottom
|
|
1615
|
-
left: params.paddingLeft
|
|
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,
|
|
1616
2836
|
};
|
|
1617
2837
|
}
|
|
1618
2838
|
// Allow updating baseWidth/baseHeight via width/height params
|
|
1619
2839
|
if (params.width !== undefined) {
|
|
1620
|
-
groupLayer.baseWidth = params.width;
|
|
2840
|
+
groupLayer.baseWidth = cssPercentToSize(params.width, groupLayer.baseWidth * 100);
|
|
1621
2841
|
if (params.widthMode === undefined) {
|
|
1622
2842
|
groupLayer.widthMode = 'fixed';
|
|
1623
2843
|
}
|
|
1624
2844
|
}
|
|
1625
2845
|
if (params.height !== undefined) {
|
|
1626
|
-
groupLayer.baseHeight = params.height;
|
|
2846
|
+
groupLayer.baseHeight = cssPercentToSize(params.height, groupLayer.baseHeight * 100);
|
|
1627
2847
|
if (params.heightMode === undefined) {
|
|
1628
2848
|
groupLayer.heightMode = 'fixed';
|
|
1629
2849
|
}
|
|
1630
2850
|
}
|
|
1631
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();
|
|
1632
2971
|
return {
|
|
1633
2972
|
content: [
|
|
1634
2973
|
{
|
|
@@ -1643,11 +2982,7 @@ async function handleStateTool(name, args) {
|
|
|
1643
2982
|
};
|
|
1644
2983
|
}
|
|
1645
2984
|
case 'apply_effect': {
|
|
1646
|
-
|
|
1647
|
-
return {
|
|
1648
|
-
content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
|
|
1649
|
-
};
|
|
1650
|
-
}
|
|
2985
|
+
ensurePoster();
|
|
1651
2986
|
const effectId = params.effectId;
|
|
1652
2987
|
if (!effectId) {
|
|
1653
2988
|
return {
|
|
@@ -1656,6 +2991,7 @@ async function handleStateTool(name, args) {
|
|
|
1656
2991
|
}
|
|
1657
2992
|
if (effectId === 'none') {
|
|
1658
2993
|
delete currentPoster.effect;
|
|
2994
|
+
persistState();
|
|
1659
2995
|
return {
|
|
1660
2996
|
content: [
|
|
1661
2997
|
{
|
|
@@ -1722,12 +3058,15 @@ async function handleStateTool(name, args) {
|
|
|
1722
3058
|
};
|
|
1723
3059
|
// Dither has BUILT-IN palette and bloom (WebGL pipeline)
|
|
1724
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);
|
|
1725
3065
|
effect.dither = {
|
|
1726
3066
|
pattern: patternMap[effectId] || 'floydSteinberg',
|
|
1727
3067
|
pixelation: clamp(params.pixelation, 1, 20, 3),
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
colors: params.colors ?? ['#000000', '#ffffff'],
|
|
3068
|
+
paletteId: ditherPaletteId,
|
|
3069
|
+
colors: ditherColors,
|
|
1731
3070
|
brightness: clamp(params.brightness, 0, 2, 1),
|
|
1732
3071
|
contrast: clamp(params.contrast, 0.5, 2, 1.2),
|
|
1733
3072
|
threshold: 1.0,
|
|
@@ -1900,6 +3239,7 @@ async function handleStateTool(name, args) {
|
|
|
1900
3239
|
};
|
|
1901
3240
|
}
|
|
1902
3241
|
currentPoster.effect = effect;
|
|
3242
|
+
persistState();
|
|
1903
3243
|
return {
|
|
1904
3244
|
content: [
|
|
1905
3245
|
{
|
|
@@ -1914,11 +3254,7 @@ async function handleStateTool(name, args) {
|
|
|
1914
3254
|
};
|
|
1915
3255
|
}
|
|
1916
3256
|
case 'add_postprocess': {
|
|
1917
|
-
|
|
1918
|
-
return {
|
|
1919
|
-
content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
|
|
1920
|
-
};
|
|
1921
|
-
}
|
|
3257
|
+
ensurePoster();
|
|
1922
3258
|
const ppType = params.type;
|
|
1923
3259
|
if (!ppType) {
|
|
1924
3260
|
return {
|
|
@@ -1964,6 +3300,18 @@ async function handleStateTool(name, args) {
|
|
|
1964
3300
|
'decay', // light-beams
|
|
1965
3301
|
'density', // light-beams
|
|
1966
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
|
|
1967
3315
|
];
|
|
1968
3316
|
const allSettingKeys = [...commonKeys, ...additionalKeys];
|
|
1969
3317
|
for (const key of allSettingKeys) {
|
|
@@ -1976,6 +3324,11 @@ async function handleStateTool(name, args) {
|
|
|
1976
3324
|
settings.type = settings.blurType;
|
|
1977
3325
|
delete settings.blurType;
|
|
1978
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
|
+
}
|
|
1979
3332
|
const postProcess = {
|
|
1980
3333
|
id: ppId,
|
|
1981
3334
|
type: ppType,
|
|
@@ -1983,6 +3336,7 @@ async function handleStateTool(name, args) {
|
|
|
1983
3336
|
settings,
|
|
1984
3337
|
};
|
|
1985
3338
|
currentPoster.postProcesses.push(postProcess);
|
|
3339
|
+
persistState();
|
|
1986
3340
|
// Build response with warning if dither is active
|
|
1987
3341
|
const response = {
|
|
1988
3342
|
success: true,
|
|
@@ -2005,11 +3359,7 @@ async function handleStateTool(name, args) {
|
|
|
2005
3359
|
};
|
|
2006
3360
|
}
|
|
2007
3361
|
case 'remove_layer': {
|
|
2008
|
-
|
|
2009
|
-
return {
|
|
2010
|
-
content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
|
|
2011
|
-
};
|
|
2012
|
-
}
|
|
3362
|
+
ensurePoster();
|
|
2013
3363
|
const layerId = params.layerId;
|
|
2014
3364
|
if (!layerId) {
|
|
2015
3365
|
return {
|
|
@@ -2028,6 +3378,7 @@ async function handleStateTool(name, args) {
|
|
|
2028
3378
|
};
|
|
2029
3379
|
}
|
|
2030
3380
|
currentPoster.layers.splice(layerIndex, 1);
|
|
3381
|
+
persistState();
|
|
2031
3382
|
return {
|
|
2032
3383
|
content: [
|
|
2033
3384
|
{
|
|
@@ -2041,12 +3392,67 @@ async function handleStateTool(name, args) {
|
|
|
2041
3392
|
],
|
|
2042
3393
|
};
|
|
2043
3394
|
}
|
|
2044
|
-
case '
|
|
2045
|
-
|
|
3395
|
+
case 'move_layer': {
|
|
3396
|
+
ensurePoster();
|
|
3397
|
+
const poster = currentPoster;
|
|
3398
|
+
const layerId = params.layerId;
|
|
3399
|
+
const direction = params.direction;
|
|
3400
|
+
if (!layerId) {
|
|
3401
|
+
return {
|
|
3402
|
+
content: [{ type: 'text', text: 'Error: layerId is required' }],
|
|
3403
|
+
};
|
|
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) {
|
|
2046
3412
|
return {
|
|
2047
|
-
content: [{ type: 'text', text: '
|
|
3413
|
+
content: [{ type: 'text', text: 'Error: Cannot move background layer' }],
|
|
2048
3414
|
};
|
|
2049
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();
|
|
2050
3456
|
return {
|
|
2051
3457
|
content: [
|
|
2052
3458
|
{
|