@pinepaper.studio/mcp-server 1.5.4 → 1.5.5

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.
Files changed (36) hide show
  1. package/dist/cli.js +2 -1
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.js +3 -4
  4. package/dist/index.js.map +1 -1
  5. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  6. package/dist/tools/compact-descriptions.js +0 -1
  7. package/dist/tools/compact-descriptions.js.map +1 -1
  8. package/dist/tools/definitions.d.ts +1 -1
  9. package/dist/tools/definitions.d.ts.map +1 -1
  10. package/dist/tools/definitions.js +19 -546
  11. package/dist/tools/definitions.js.map +1 -1
  12. package/dist/tools/handlers/camera.d.ts.map +1 -1
  13. package/dist/tools/handlers/camera.js +7 -1
  14. package/dist/tools/handlers/camera.js.map +1 -1
  15. package/dist/tools/handlers.d.ts.map +1 -1
  16. package/dist/tools/handlers.js +23 -107
  17. package/dist/tools/handlers.js.map +1 -1
  18. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  19. package/dist/tools/minimal-descriptions.js +3 -15
  20. package/dist/tools/minimal-descriptions.js.map +1 -1
  21. package/dist/tools/toolkits.d.ts.map +1 -1
  22. package/dist/tools/toolkits.js +4 -21
  23. package/dist/tools/toolkits.js.map +1 -1
  24. package/dist/types/code-generator.d.ts +1 -35
  25. package/dist/types/code-generator.d.ts.map +1 -1
  26. package/dist/types/code-generator.js +107 -500
  27. package/dist/types/code-generator.js.map +1 -1
  28. package/dist/types/schemas.d.ts +9 -622
  29. package/dist/types/schemas.d.ts.map +1 -1
  30. package/dist/types/schemas.js +1 -75
  31. package/dist/types/schemas.js.map +1 -1
  32. package/dist/version.d.ts +11 -0
  33. package/dist/version.d.ts.map +1 -0
  34. package/dist/version.js +18 -0
  35. package/dist/version.js.map +1 -0
  36. package/package.json +1 -1
@@ -10,10 +10,6 @@ import { CreateItemInputSchema, ModifyItemInputSchema, AddRelationInputSchema, A
10
10
  CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
11
11
  // Agent flow mode types
12
12
  AgentStartJobInputSchema, AgentEndJobInputSchema, AgentResetInputSchema, AgentBatchExecuteInputSchema, AgentExportInputSchema, AgentAnalyzeInputSchema,
13
- // Interactive/Trigger types
14
- AddTriggerInputSchema, RemoveTriggerInputSchema, QueryTriggersInputSchema,
15
- // Quiz types
16
- CreateQuizInputSchema, GetQuizStateInputSchema, ResetQuizInputSchema,
17
13
  // Letter collage types
18
14
  CreateLetterCollageInputSchema, AnimateLetterCollageInputSchema,
19
15
  // Map types
@@ -292,11 +288,27 @@ app.animate(item, {
292
288
  ({ success: true, itemId: '${itemId}', animationType: '${animationType}' });
293
289
  `.trim();
294
290
  }
291
+ /**
292
+ * PinePaper's keyframe interpolator only tweens `position` when it is an [x, y]
293
+ * ARRAY. Callers (and other agents) routinely pass a { x, y } object, which is
294
+ * silently ignored — the item then stays frozen for the whole animation. Accept
295
+ * both shapes by normalizing { x, y } -> [x, y] before the keyframes are emitted.
296
+ */
297
+ function normalizeKeyframePositions(keyframes) {
298
+ return (keyframes || []).map((kf) => {
299
+ const props = kf?.properties;
300
+ const p = props?.position;
301
+ if (p && !Array.isArray(p) && typeof p === 'object' && 'x' in p && 'y' in p) {
302
+ return { ...kf, properties: { ...props, position: [p.x, p.y] } };
303
+ }
304
+ return kf;
305
+ });
306
+ }
295
307
  /**
296
308
  * Template for keyframe animation
297
309
  */
298
310
  function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint) {
299
- const keyframesJson = JSON.stringify(keyframes, null, 2);
311
+ const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
300
312
  const calculatedDuration = duration || Math.max(...keyframes.map(k => k.time));
301
313
  const opts = { duration: calculatedDuration, loop };
302
314
  if (timeOffset !== undefined)
@@ -456,35 +468,29 @@ const size = app.getCanvasSize();
456
468
  */
457
469
  function generateClearCanvasCode() {
458
470
  return `
459
- // Clear all items from canvas
460
- const entries = app.itemRegistry.getAll();
461
- const itemIds = entries.map(entry => entry.id || entry.itemId);
462
- let removedCount = 0;
463
-
464
- for (const itemId of itemIds) {
465
- try {
466
- const item = app.getItemById(itemId);
467
- if (item) {
468
- item.remove();
469
- app.itemRegistry.remove(itemId);
470
- removedCount++;
471
- }
472
- } catch (e) {
473
- console.warn('Failed to remove item:', itemId, e);
474
- }
475
- }
476
-
477
- // Also clear any relations
478
- if (app.clearAllRelations) {
479
- app.clearAllRelations();
471
+ // Clear all items from canvas. Prefer app.clearCanvas() — the canonical
472
+ // reset that also tears down relations, the (virtual) camera animation,
473
+ // generators, effects and background. The old manual loop here was broken:
474
+ // it keyed item lookups on entry.id/entry.itemId (the registry getAll()
475
+ // entries don't expose those — getItemById wants the registry id), so it
476
+ // removed 0 items, and it called the non-existent app.clearAllRelations.
477
+ const before = app.itemRegistry ? app.itemRegistry.getAll().length : 0;
478
+
479
+ if (typeof app.clearCanvas === 'function') {
480
+ app.clearCanvas();
481
+ } else {
482
+ // Fallback for older builds without clearCanvas().
483
+ if (app.textItemGroup) app.textItemGroup.removeChildren();
484
+ if (app.patternGroup) app.patternGroup.removeChildren();
485
+ if (app.itemRegistry) app.itemRegistry.clear();
486
+ if (app.relationRegistry) app.relationRegistry.clear();
480
487
  }
481
488
 
482
- app.historyManager.saveState();
489
+ if (app.historyManager) app.historyManager.saveState();
483
490
 
484
- // Verify canvas is empty
485
- const remaining = app.itemRegistry.getAll().length;
491
+ const remaining = app.itemRegistry ? app.itemRegistry.getAll().length : 0;
486
492
 
487
- ({ success: true, removedCount, remainingItems: remaining });
493
+ ({ success: true, removedCount: Math.max(0, before - remaining), remainingItems: remaining });
488
494
  `.trim();
489
495
  }
490
496
  /**
@@ -1178,13 +1184,9 @@ if (typeof app.generators !== 'undefined' && app.generators['${backgroundGenerat
1178
1184
  const sourceId = nameToId['${source}'];
1179
1185
  const targetId = nameToId['${target}'];
1180
1186
  if (sourceId && targetId) {
1181
- const sourceItem = app.itemRegistry.get(sourceId);
1182
- const targetItem = app.itemRegistry.get(targetId);
1183
- if (sourceItem && targetItem) {
1184
- const params = ${paramsStr};
1185
- app.relationManager.addRelation(sourceItem, targetItem, '${type}', params);
1186
- results.relations.push({ source: '${source}', target: '${target}', type: '${type}' });
1187
- }
1187
+ const params = ${paramsStr};
1188
+ app.addRelation(sourceId, targetId, '${type}', params);
1189
+ results.relations.push({ source: '${source}', target: '${target}', type: '${type}' });
1188
1190
  }
1189
1191
  })();
1190
1192
  `);
@@ -1601,14 +1603,7 @@ throw new Error('Unknown diagram mode action: ${action}');
1601
1603
  if (item.shadowColor || item.shadowBlur) analysis.hasShadows = true;
1602
1604
  });
1603
1605
 
1604
- // Check animation manager for registered animations
1605
- if (app.animationManager) {
1606
- const animations = app.animationManager.getAll ? app.animationManager.getAll() : (app.animationManager.animations || []);
1607
- if (animations.length > 0) {
1608
- analysis.hasAnimations = true;
1609
- animationSet.add('managed');
1610
- }
1611
- }
1606
+ // FxTool has no animationManager — animations are detected via item.data.animationType walk above
1612
1607
  // Check timeline for active animations
1613
1608
  if (app.timeline && (app.timeline.isPlaying || app.timeline.animations?.length > 0)) {
1614
1609
  analysis.hasAnimations = true;
@@ -1779,16 +1774,29 @@ throw new Error('Unknown diagram mode action: ${action}');
1779
1774
  */
1780
1775
  generateBatchOperationCode(op, index) {
1781
1776
  switch (op.type) {
1782
- case 'create':
1777
+ case 'create': {
1783
1778
  const pos = op.position || { x: 400, y: 300 };
1784
- const props = JSON.stringify(op.properties || {});
1785
- return `
1779
+ const createProps = (op.properties || {});
1780
+ const props = JSON.stringify(createProps);
1781
+ let createCode = `
1786
1782
  const item = app.create('${op.itemType}', { position: { x: ${pos.x}, y: ${pos.y} }, ...${props} });
1787
- const itemId = item.data && item.data.id ? item.data.id : app.registerItem(item, '${op.itemType}', { source: 'mcp-batch' });
1783
+ const itemId = item.data && item.data.id ? item.data.id : app.registerItem(item, '${op.itemType}', { source: 'mcp-batch' });`;
1784
+ // app.create() does not read blendMode/opacity from its params, but the
1785
+ // underlying Paper.js item supports them directly — apply post-create so
1786
+ // callers can pass them inline in `properties` and have them take effect.
1787
+ if (createProps.blendMode !== undefined) {
1788
+ createCode += `\nif ('blendMode' in item) item.blendMode = ${JSON.stringify(createProps.blendMode)};`;
1789
+ }
1790
+ if (createProps.opacity !== undefined) {
1791
+ createCode += `\nif ('opacity' in item) item.opacity = ${JSON.stringify(createProps.opacity)};`;
1792
+ }
1793
+ createCode += `
1788
1794
  // Ensure item is visible above backgrounds/generators
1789
1795
  if (item.bringToFront) item.bringToFront();
1790
1796
  return { itemId };
1791
1797
  `;
1798
+ return createCode;
1799
+ }
1792
1800
  case 'modify':
1793
1801
  const itemRef = op.itemId?.startsWith('$')
1794
1802
  ? `itemIds[${op.itemId.substring(1)}]`
@@ -1869,7 +1877,7 @@ return { success: true, width: ${w}, height: ${h} };
1869
1877
  const kfItemRef = op.itemId?.startsWith('$')
1870
1878
  ? `itemIds[${op.itemId.substring(1)}]`
1871
1879
  : `'${op.itemId}'`;
1872
- const kfJson = JSON.stringify(op.keyframes || []);
1880
+ const kfJson = JSON.stringify(normalizeKeyframePositions(op.keyframes || []));
1873
1881
  const kfDuration = op.duration || (op.keyframes?.length ? Math.max(...op.keyframes.map(k => k.time)) : 5);
1874
1882
  const kfLoop = op.loop ?? false;
1875
1883
  return `
@@ -1883,16 +1891,14 @@ return { itemId: targetId, duration: ${kfDuration}, loop: ${kfLoop} };
1883
1891
  ? `itemIds[${op.itemId.substring(1)}]`
1884
1892
  : `'${op.itemId}'`;
1885
1893
  const maskOpts = JSON.stringify(op.maskOptions || {});
1894
+ const preset = op.maskPreset || op.maskType || 'wipeLeft';
1886
1895
  return `
1887
1896
  const targetId = ${maskItemRef};
1888
- if (!app.maskSystem) throw new Error('Mask system not available');
1889
- const result = app.maskSystem.applyAnimatedMask(targetId, {
1890
- preset: ${op.maskPreset ? `'${op.maskPreset}'` : 'null'},
1891
- maskType: ${op.maskType ? `'${op.maskType}'` : 'null'},
1892
- options: ${maskOpts},
1893
- maskOptions: ${maskOpts}
1894
- });
1895
- return result;
1897
+ const item = app.getItemById(targetId);
1898
+ if (!item) throw new Error('Item not found: ' + targetId);
1899
+ if (!app.applyAnimatedMask) throw new Error('Mask system not available');
1900
+ const maskedGroup = app.applyAnimatedMask(item, '${preset}', ${maskOpts});
1901
+ return { success: !!maskedGroup, itemId: targetId, preset: '${preset}' };
1896
1902
  `;
1897
1903
  }
1898
1904
  case 'apply_effect': {
@@ -1940,8 +1946,9 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
1940
1946
  */
1941
1947
  generateAgentExport(input) {
1942
1948
  const validated = AgentExportInputSchema.parse(input);
1943
- const { platform, format, quality, framing } = validated;
1949
+ const { platform, format, quality, framing, duration } = validated;
1944
1950
  const qualityLevel = quality || 'standard';
1951
+ const videoDuration = duration ?? 5;
1945
1952
  // Quality settings (bitrate in bps for VideoEncoder)
1946
1953
  const qualitySettings = {
1947
1954
  draft: { compression: 0.6, fps: 15, dpi: 72, bitrate: 2_000_000 },
@@ -2055,7 +2062,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
2055
2062
  // ExportEngine._quickExportVideo overrides quality with undefined for
2056
2063
  // mp4/webm, _calculateBitrate yields NaN, and VideoEncoder.configure
2057
2064
  // rejects with 'malformed bitrate value'.
2058
- const baseVideoSettings = { format, fps: settings.fps, quality: settings.compression, duration: 5 };
2065
+ const baseVideoSettings = { format, fps: settings.fps, quality: settings.compression, duration: ${videoDuration} };
2059
2066
  const blobToDataUrl = (b) => new Promise(resolve => {
2060
2067
  const r = new FileReader();
2061
2068
  r.onloadend = () => resolve(r.result);
@@ -2178,14 +2185,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
2178
2185
  if (item.shadowColor || item.shadowBlur) hasShadow = true;
2179
2186
  });
2180
2187
 
2181
- // Check animation manager for registered animations
2182
- if (app.animationManager) {
2183
- const animations = app.animationManager.getAll ? app.animationManager.getAll() : (app.animationManager.animations || []);
2184
- if (animations.length > 0) {
2185
- analysis.hasAnimations = true;
2186
- animationSet.add('managed');
2187
- }
2188
- }
2188
+ // FxTool has no animationManager — animations are detected via item.data.animationType walk above
2189
2189
  // Check timeline for active animations
2190
2190
  if (app.timeline && (app.timeline.isPlaying || app.timeline.animations?.length > 0)) {
2191
2191
  analysis.hasAnimations = true;
@@ -2249,257 +2249,6 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
2249
2249
  suggestedPlatforms
2250
2250
  };
2251
2251
  })();
2252
- `.trim();
2253
- }
2254
- // ===========================================================================
2255
- // INTERACTIVE TRIGGER CODE GENERATORS
2256
- // ===========================================================================
2257
- /**
2258
- * Generate code for adding a trigger
2259
- */
2260
- generateAddTrigger(input) {
2261
- const validated = AddTriggerInputSchema.parse(input);
2262
- const { itemId, event, actions, condition, timelineOffset } = validated;
2263
- const actionsStr = JSON.stringify(actions);
2264
- const conditionStr = condition ? `'${condition.replace(/'/g, "\\'")}'` : 'null';
2265
- const offsetStr = timelineOffset !== undefined ? timelineOffset : 'null';
2266
- return `
2267
- // Add trigger to ${itemId}
2268
- const item = app.getItemById('${itemId}');
2269
- if (!item) throw new Error('Item not found: ${itemId}');
2270
-
2271
- const trigger = {
2272
- event: '${event}',
2273
- actions: ${actionsStr},
2274
- condition: ${conditionStr},
2275
- timelineOffset: ${offsetStr}
2276
- };
2277
-
2278
- // Initialize triggers array if needed
2279
- if (!item.data) item.data = {};
2280
- if (!item.data.triggers) item.data.triggers = [];
2281
-
2282
- // Add trigger
2283
- item.data.triggers.push(trigger);
2284
-
2285
- // Register event handler
2286
- if (app.triggerManager) {
2287
- app.triggerManager.registerTrigger(item, trigger);
2288
- }
2289
-
2290
- app.historyManager.saveState();
2291
- ({ success: true, itemId: '${itemId}', event: '${event}', triggerId: item.data.triggers.length - 1 });
2292
- `.trim();
2293
- }
2294
- /**
2295
- * Generate code for removing a trigger
2296
- */
2297
- generateRemoveTrigger(input) {
2298
- const validated = RemoveTriggerInputSchema.parse(input);
2299
- const { itemId, event, removeAll } = validated;
2300
- if (removeAll) {
2301
- return `
2302
- // Remove all triggers from ${itemId}
2303
- const item = app.getItemById('${itemId}');
2304
- if (!item) throw new Error('Item not found: ${itemId}');
2305
-
2306
- const removedCount = item.data?.triggers?.length || 0;
2307
- if (item.data) item.data.triggers = [];
2308
-
2309
- if (app.triggerManager) {
2310
- app.triggerManager.unregisterAll(item);
2311
- }
2312
-
2313
- app.historyManager.saveState();
2314
- ({ success: true, itemId: '${itemId}', removedCount, removeAll: true });
2315
- `.trim();
2316
- }
2317
- return `
2318
- // Remove ${event} trigger from ${itemId}
2319
- const item = app.getItemById('${itemId}');
2320
- if (!item) throw new Error('Item not found: ${itemId}');
2321
-
2322
- let removedCount = 0;
2323
- if (item.data?.triggers) {
2324
- const before = item.data.triggers.length;
2325
- item.data.triggers = item.data.triggers.filter(t => t.event !== '${event}');
2326
- removedCount = before - item.data.triggers.length;
2327
- }
2328
-
2329
- if (app.triggerManager) {
2330
- app.triggerManager.unregisterEvent(item, '${event}');
2331
- }
2332
-
2333
- app.historyManager.saveState();
2334
- ({ success: true, itemId: '${itemId}', event: '${event}', removedCount });
2335
- `.trim();
2336
- }
2337
- /**
2338
- * Generate code for querying triggers
2339
- */
2340
- generateQueryTriggers(input) {
2341
- const validated = QueryTriggersInputSchema.parse(input);
2342
- const { itemId, event } = validated;
2343
- if (itemId) {
2344
- return `
2345
- // Query triggers for ${itemId}
2346
- const item = app.getItemById('${itemId}');
2347
- if (!item) throw new Error('Item not found: ${itemId}');
2348
-
2349
- let triggers = item.data?.triggers || [];
2350
- ${event ? `triggers = triggers.filter(t => t.event === '${event}');` : ''}
2351
-
2352
- ({ success: true, itemId: '${itemId}', triggers, count: triggers.length });
2353
- `.trim();
2354
- }
2355
- return `
2356
- // Query all triggers
2357
- const allTriggers = [];
2358
- const items = app.itemRegistry ? app.itemRegistry.getAll() : [];
2359
-
2360
- items.forEach(entry => {
2361
- const item = entry.item;
2362
- if (item.data?.triggers?.length > 0) {
2363
- let triggers = item.data.triggers;
2364
- ${event ? `triggers = triggers.filter(t => t.event === '${event}');` : ''}
2365
- triggers.forEach((trigger, idx) => {
2366
- allTriggers.push({
2367
- itemId: entry.id,
2368
- triggerId: idx,
2369
- ...trigger
2370
- });
2371
- });
2372
- }
2373
- });
2374
-
2375
- ({ success: true, triggers: allTriggers, count: allTriggers.length${event ? `, event: '${event}'` : ''} });
2376
- `.trim();
2377
- }
2378
- // ===========================================================================
2379
- // QUIZ CODE GENERATORS
2380
- // ===========================================================================
2381
- /**
2382
- * Generate code for creating a quiz
2383
- */
2384
- generateCreateQuiz(input) {
2385
- const validated = CreateQuizInputSchema.parse(input);
2386
- const { title, questions, passingScore, showScore, allowRetry, shuffleQuestions, shuffleOptions } = validated;
2387
- const quizConfig = JSON.stringify({
2388
- title: title || 'Untitled Quiz',
2389
- questions,
2390
- passingScore: passingScore || 70,
2391
- showScore: showScore !== false,
2392
- allowRetry: allowRetry !== false,
2393
- shuffleQuestions: shuffleQuestions || false,
2394
- shuffleOptions: shuffleOptions || false,
2395
- }, null, 2);
2396
- return `
2397
- // Create quiz
2398
- const quizConfig = ${quizConfig};
2399
-
2400
- // Initialize quiz manager if needed
2401
- if (!app.quizManager) {
2402
- app.quizManager = {
2403
- quizzes: [],
2404
- activeQuiz: null,
2405
- createQuiz: function(config) {
2406
- const quiz = {
2407
- id: 'quiz_' + Date.now(),
2408
- ...config,
2409
- state: {
2410
- currentQuestion: 0,
2411
- answers: [],
2412
- score: 0,
2413
- maxScore: config.questions.reduce((sum, q) => sum + q.points, 0),
2414
- complete: false
2415
- }
2416
- };
2417
- this.quizzes.push(quiz);
2418
- this.activeQuiz = quiz;
2419
- return quiz;
2420
- }
2421
- };
2422
- }
2423
-
2424
- const quiz = app.quizManager.createQuiz(quizConfig);
2425
- app.historyManager.saveState();
2426
-
2427
- ({
2428
- success: true,
2429
- quizId: quiz.id,
2430
- title: quiz.title,
2431
- questionCount: quiz.questions.length,
2432
- maxScore: quiz.state.maxScore,
2433
- passingScore: quiz.passingScore
2434
- });
2435
- `.trim();
2436
- }
2437
- /**
2438
- * Generate code for getting quiz state
2439
- */
2440
- generateGetQuizState(input) {
2441
- const validated = GetQuizStateInputSchema.parse(input);
2442
- const { quizId } = validated;
2443
- const quizRef = quizId
2444
- ? `app.quizManager.quizzes.find(q => q.id === '${quizId}')`
2445
- : 'app.quizManager?.activeQuiz';
2446
- return `
2447
- // Get quiz state
2448
- if (!app.quizManager) {
2449
- return { success: false, error: 'No quiz manager initialized' };
2450
- }
2451
-
2452
- const quiz = ${quizRef};
2453
- if (!quiz) {
2454
- return { success: false, error: 'Quiz not found' };
2455
- }
2456
-
2457
- ({
2458
- success: true,
2459
- quizId: quiz.id,
2460
- title: quiz.title,
2461
- currentQuestion: quiz.state.currentQuestion,
2462
- totalQuestions: quiz.questions.length,
2463
- score: quiz.state.score,
2464
- maxScore: quiz.state.maxScore,
2465
- answers: quiz.state.answers,
2466
- complete: quiz.state.complete,
2467
- passed: quiz.state.complete ? (quiz.state.score / quiz.state.maxScore * 100) >= quiz.passingScore : null
2468
- });
2469
- `.trim();
2470
- }
2471
- /**
2472
- * Generate code for resetting a quiz
2473
- */
2474
- generateResetQuiz(input) {
2475
- const validated = ResetQuizInputSchema.parse(input);
2476
- const { quizId } = validated;
2477
- const quizRef = quizId
2478
- ? `app.quizManager.quizzes.find(q => q.id === '${quizId}')`
2479
- : 'app.quizManager?.activeQuiz';
2480
- return `
2481
- // Reset quiz
2482
- if (!app.quizManager) {
2483
- return { success: false, error: 'No quiz manager initialized' };
2484
- }
2485
-
2486
- const quiz = ${quizRef};
2487
- if (!quiz) {
2488
- return { success: false, error: 'Quiz not found' };
2489
- }
2490
-
2491
- // Reset state
2492
- quiz.state = {
2493
- currentQuestion: 0,
2494
- answers: [],
2495
- score: 0,
2496
- maxScore: quiz.questions.reduce((sum, q) => sum + q.points, 0),
2497
- complete: false
2498
- };
2499
-
2500
- app.historyManager.saveState();
2501
-
2502
- ({ success: true, quizId: quiz.id, reset: true });
2503
2252
  `.trim();
2504
2253
  }
2505
2254
  // ===========================================================================
@@ -2588,80 +2337,6 @@ app.historyManager.saveState();
2588
2337
 
2589
2338
  return result;
2590
2339
  })();
2591
- `.trim();
2592
- }
2593
- /**
2594
- * Generate code to get letter collage options
2595
- */
2596
- generateGetLetterCollageOptions() {
2597
- return `
2598
- // Get letter collage options
2599
- (function() {
2600
- if (!app.getLetterCollageOptions) {
2601
- // Return static options if function not available
2602
- return {
2603
- success: true,
2604
- styles: ['tile', 'magazine', 'paperCut', 'fold', 'gradient', 'image'],
2605
- tilePalettes: {
2606
- game: ['wordle', 'scrabble'],
2607
- vibrant: ['candy', 'neon', 'rainbow'],
2608
- soft: ['pastel', 'cotton'],
2609
- natural: ['earth', 'ocean', 'forest', 'sunset'],
2610
- professional: ['corporate', 'minimal', 'slate'],
2611
- seasonal: ['christmas', 'halloween', 'spring'],
2612
- magazine: ['magazine', 'newspaper', 'vintage'],
2613
- paperCraft: ['paperCraft', 'origami', 'craftPaper']
2614
- },
2615
- gradientPalettes: ['rainbow', 'sunset', 'ocean', 'fire', 'gold', 'rose', 'ice', 'cyberpunk', 'neonGlow', 'purplePink'],
2616
- gradientDirections: ['vertical', 'horizontal', 'diagonal', 'radial']
2617
- };
2618
- }
2619
-
2620
- return app.getLetterCollageOptions();
2621
- })();
2622
- `.trim();
2623
- }
2624
- // ===========================================================================
2625
- // CANVAS PRESETS CODE GENERATORS
2626
- // ===========================================================================
2627
- /**
2628
- * Generate code to get canvas presets
2629
- */
2630
- generateGetCanvasPresets() {
2631
- return `
2632
- // Get canvas presets
2633
- (function() {
2634
- if (app.getCanvasPresets) {
2635
- return app.getCanvasPresets();
2636
- }
2637
-
2638
- // Return static presets if function not available
2639
- return {
2640
- success: true,
2641
- presets: [
2642
- { key: 'youtube-thumbnail', name: 'YouTube Thumbnail', width: 1280, height: 720, aspectRatio: '16:9', category: 'video' },
2643
- { key: 'youtube-short', name: 'YouTube Shorts', width: 1080, height: 1920, aspectRatio: '9:16', category: 'video' },
2644
- { key: 'tiktok', name: 'TikTok', width: 1080, height: 1920, aspectRatio: '9:16', category: 'video' },
2645
- { key: 'instagram-story', name: 'Instagram Story', width: 1080, height: 1920, aspectRatio: '9:16', category: 'social' },
2646
- { key: 'instagram-post', name: 'Instagram Post (Square)', width: 1080, height: 1080, aspectRatio: '1:1', category: 'social' },
2647
- { key: 'instagram-landscape', name: 'Instagram Landscape', width: 1080, height: 566, aspectRatio: '1.91:1', category: 'social' },
2648
- { key: 'instagram-portrait', name: 'Instagram Portrait', width: 1080, height: 1350, aspectRatio: '4:5', category: 'social' },
2649
- { key: 'facebook-post', name: 'Facebook Post', width: 1200, height: 630, aspectRatio: '1.91:1', category: 'social' },
2650
- { key: 'facebook-cover', name: 'Facebook Cover', width: 820, height: 312, aspectRatio: '2.63:1', category: 'social' },
2651
- { key: 'facebook-story', name: 'Facebook Story', width: 1080, height: 1920, aspectRatio: '9:16', category: 'social' },
2652
- { key: 'twitter-post', name: 'Twitter/X Post', width: 1200, height: 675, aspectRatio: '16:9', category: 'social' },
2653
- { key: 'twitter-header', name: 'Twitter/X Header', width: 1500, height: 500, aspectRatio: '3:1', category: 'social' },
2654
- { key: 'linkedin-post', name: 'LinkedIn Post', width: 1200, height: 627, aspectRatio: '1.91:1', category: 'social' },
2655
- { key: 'linkedin-banner', name: 'LinkedIn Banner', width: 1584, height: 396, aspectRatio: '4:1', category: 'social' },
2656
- { key: 'pinterest-pin', name: 'Pinterest Pin', width: 1000, height: 1500, aspectRatio: '2:3', category: 'social' },
2657
- { key: 'presentation-16x9', name: 'Presentation 16:9', width: 1920, height: 1080, aspectRatio: '16:9', category: 'presentation' },
2658
- { key: 'presentation-4x3', name: 'Presentation 4:3', width: 1024, height: 768, aspectRatio: '4:3', category: 'presentation' },
2659
- { key: 'hd-720p', name: 'HD 720p', width: 1280, height: 720, aspectRatio: '16:9', category: 'video' },
2660
- { key: 'full-hd-1080p', name: 'Full HD 1080p', width: 1920, height: 1080, aspectRatio: '16:9', category: 'video' },
2661
- { key: 'default', name: 'Default', width: 800, height: 600, aspectRatio: '4:3', category: 'general' }
2662
- ]
2663
- };
2664
- })();
2665
2340
  `.trim();
2666
2341
  }
2667
2342
  // ===========================================================================
@@ -3446,22 +3121,27 @@ app.historyManager.saveState();
3446
3121
  const optionsStr = validated.options ? JSON.stringify(validated.options) : '{}';
3447
3122
  const keyframesStr = validated.keyframes ? JSON.stringify(validated.keyframes) : 'null';
3448
3123
  const maskOptionsStr = validated.maskOptions ? JSON.stringify(validated.maskOptions) : '{}';
3124
+ const preset = validated.preset || validated.maskType || 'wipeLeft';
3125
+ // Per-preset options. Custom keyframes are an animated-mask extension —
3126
+ // when supplied, fall through to maskingSystem.applyCustomMask instead.
3449
3127
  return `
3450
3128
  // Apply animated mask to ${validated.itemId}
3451
3129
  (function() {
3452
- if (!app.maskSystem) {
3130
+ if (!app.applyAnimatedMask) {
3453
3131
  return { success: false, error: 'Mask system not available' };
3454
3132
  }
3133
+ const item = app.getItemById('${validated.itemId}');
3134
+ if (!item) return { success: false, error: 'Item not found: ${validated.itemId}' };
3455
3135
 
3456
3136
  try {
3457
- const result = app.maskSystem.applyAnimatedMask('${validated.itemId}', {
3458
- preset: ${validated.preset ? `'${validated.preset}'` : 'null'},
3459
- maskType: ${validated.maskType ? `'${validated.maskType}'` : 'null'},
3460
- keyframes: ${keyframesStr},
3461
- options: ${optionsStr},
3462
- maskOptions: ${maskOptionsStr}
3463
- });
3464
- return result;
3137
+ const mergedOpts = Object.assign({}, ${optionsStr}, ${maskOptionsStr});
3138
+ let maskedGroup;
3139
+ if (${keyframesStr} && app.maskingSystem && app.maskingSystem.applyCustomMask) {
3140
+ maskedGroup = app.maskingSystem.applyCustomMask(item, '${preset}', ${keyframesStr}, mergedOpts);
3141
+ } else {
3142
+ maskedGroup = app.applyAnimatedMask(item, '${preset}', mergedOpts);
3143
+ }
3144
+ return { success: !!maskedGroup, itemId: '${validated.itemId}', preset: '${preset}' };
3465
3145
  } catch (error) {
3466
3146
  return { success: false, error: error.message };
3467
3147
  }
@@ -3475,17 +3155,15 @@ app.historyManager.saveState();
3475
3155
  return `
3476
3156
  // Apply custom mask to ${validated.itemId}
3477
3157
  (function() {
3478
- if (!app.maskSystem) {
3158
+ if (!app.maskingSystem || !app.maskingSystem.applyCustomMask) {
3479
3159
  return { success: false, error: 'Mask system not available' };
3480
3160
  }
3161
+ const item = app.getItemById('${validated.itemId}');
3162
+ if (!item) return { success: false, error: 'Item not found: ${validated.itemId}' };
3481
3163
 
3482
3164
  try {
3483
- const result = app.maskSystem.applyCustomMask('${validated.itemId}', {
3484
- maskType: '${validated.maskType}',
3485
- keyframes: ${keyframesStr},
3486
- maskOptions: ${maskOptionsStr}
3487
- });
3488
- return result;
3165
+ const maskedGroup = app.maskingSystem.applyCustomMask(item, '${validated.maskType}', ${keyframesStr}, ${maskOptionsStr});
3166
+ return { success: !!maskedGroup, itemId: '${validated.itemId}', maskType: '${validated.maskType}' };
3489
3167
  } catch (error) {
3490
3168
  return { success: false, error: error.message };
3491
3169
  }
@@ -3497,12 +3175,17 @@ app.historyManager.saveState();
3497
3175
  return `
3498
3176
  // Remove mask from ${validated.itemId}
3499
3177
  (function() {
3500
- if (!app.maskSystem) {
3178
+ if (!app.removeMask) {
3501
3179
  return { success: false, error: 'Mask system not available' };
3502
3180
  }
3181
+ // The item id may point at either the masked group OR the original item;
3182
+ // app.removeMask expects the masked group, so resolve and walk up if needed.
3183
+ const item = app.getItemById('${validated.itemId}');
3184
+ if (!item) return { success: false, error: 'Item not found: ${validated.itemId}' };
3185
+ const maskedGroup = (item.data && item.data.isMaskedGroup) ? item : (item.parent && item.parent.data && item.parent.data.isMaskedGroup ? item.parent : item);
3503
3186
 
3504
3187
  try {
3505
- const result = app.maskSystem.removeMask('${validated.itemId}');
3188
+ app.removeMask(maskedGroup);
3506
3189
  return { success: true, itemId: '${validated.itemId}', message: 'Mask removed' };
3507
3190
  } catch (error) {
3508
3191
  return { success: false, error: error.message };
@@ -3596,10 +3279,12 @@ app.historyManager.saveState();
3596
3279
  })();
3597
3280
  `.trim();
3598
3281
  }
3282
+ // =============================================================================
3283
+ // TEMPLATE
3284
+ // =============================================================================
3599
3285
  generateApplyTemplate(input) {
3600
3286
  const { templateId, category, listOnly } = input;
3601
3287
  if (listOnly || !templateId) {
3602
- // List mode: return available templates, optionally filtered by category
3603
3288
  const categoryFilter = category ? `'${category}'` : 'null';
3604
3289
  return `
3605
3290
  // List available templates
@@ -3625,7 +3310,6 @@ app.historyManager.saveState();
3625
3310
  })();
3626
3311
  `.trim();
3627
3312
  }
3628
- // Load mode: apply a specific template
3629
3313
  return `
3630
3314
  // Apply template: ${templateId}
3631
3315
  (async function() {
@@ -3901,7 +3585,7 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
3901
3585
  // Select all items
3902
3586
  (function() {
3903
3587
  app.selectAll();
3904
- const items = app.getSelection().map(i => i.data?.itemId || i.name || i.id);
3588
+ const items = app.getSelectedItems().map(i => i.data?.itemId || i.name || i.id);
3905
3589
  return { success: true, action: 'select_all', count: items.length, items };
3906
3590
  })();`.trim();
3907
3591
  case 'deselect_all':
@@ -3915,7 +3599,7 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
3915
3599
  return `
3916
3600
  // Get current selection
3917
3601
  (function() {
3918
- const items = app.getSelection().map(i => ({
3602
+ const items = app.getSelectedItems().map(i => ({
3919
3603
  itemId: i.data?.itemId || i.name || i.id,
3920
3604
  type: i.data?.itemType || i.className,
3921
3605
  bounds: i.bounds ? { x: i.bounds.x, y: i.bounds.y, width: i.bounds.width, height: i.bounds.height } : null
@@ -3926,7 +3610,7 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
3926
3610
  return `
3927
3611
  // Delete selected items
3928
3612
  (function() {
3929
- const selection = app.getSelection();
3613
+ const selection = app.getSelectedItems();
3930
3614
  const deleted = selection.map(i => i.data?.itemId || i.name || i.id);
3931
3615
  app.deleteSelected();
3932
3616
  return { success: true, action: 'delete_selected', deleted, count: deleted.length };
@@ -4160,29 +3844,6 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
4160
3844
  return `(function() { return { error: 'Unknown precomp action: ${input.action}' }; })();`;
4161
3845
  }
4162
3846
  }
4163
- generateView(input) {
4164
- switch (input.action) {
4165
- case 'fit': {
4166
- const mode = input.mode || 'content';
4167
- const padding = input.padding ?? 20;
4168
- return `
4169
- // Fit view to ${mode}
4170
- (function() {
4171
- app.fitView('${mode}', ${padding});
4172
- return { success: true, action: 'fit', mode: '${mode}', padding: ${padding} };
4173
- })();`.trim();
4174
- }
4175
- case 'get_state':
4176
- return `
4177
- // Get current view state
4178
- (function() {
4179
- const state = app.getViewState();
4180
- return { success: true, action: 'get_state', ...state };
4181
- })();`.trim();
4182
- default:
4183
- return `(function() { return { error: 'Unknown view action: ${input.action}' }; })();`;
4184
- }
4185
- }
4186
3847
  generateBackground(input) {
4187
3848
  switch (input.action) {
4188
3849
  case 'set': {
@@ -4226,8 +3887,9 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
4226
3887
  return `
4227
3888
  // Get background info
4228
3889
  (function() {
4229
- const bg = app.getBackground();
4230
- return { success: true, action: 'get', ...bg };
3890
+ // FxTool exposes getBackgroundMode() returning the mode string only.
3891
+ const mode = app.getBackgroundMode ? app.getBackgroundMode() : null;
3892
+ return { success: true, action: 'get', mode };
4231
3893
  })();`.trim();
4232
3894
  default:
4233
3895
  return `(function() { return { error: 'Unknown background action: ${input.action}' }; })();`;
@@ -4435,61 +4097,6 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
4435
4097
  }
4436
4098
  }
4437
4099
  // ===========================================================================
4438
- // STORAGE
4439
- // ===========================================================================
4440
- generateStorage(input) {
4441
- const guard = `if (!app.storageManager) return { error: 'StorageManager not available' };`;
4442
- switch (input.action) {
4443
- case 'save': {
4444
- const name = input.name ? JSON.stringify(input.name) : "'Untitled Project'";
4445
- const thumbnailCode = input.thumbnail
4446
- ? `const thumbnail = app.exportThumbnail ? app.exportThumbnail() : null;\n if (thumbnail) project.thumbnail = thumbnail;`
4447
- : '';
4448
- return `
4449
- // Save project
4450
- (async function() {
4451
- ${guard}
4452
- const project = { name: ${name}, data: app.serialize() };
4453
- ${thumbnailCode}
4454
- const projectId = await app.storageManager.saveProject(project);
4455
- return { success: true, action: 'save', projectId, name: ${name} };
4456
- })();`.trim();
4457
- }
4458
- case 'load': {
4459
- const projId = JSON.stringify(input.projectId || '');
4460
- return `
4461
- // Load project
4462
- (async function() {
4463
- ${guard}
4464
- const project = await app.storageManager.loadProject(${projId});
4465
- if (!project) return { error: 'Project not found: ' + ${projId} };
4466
- app.deserialize(project.data);
4467
- return { success: true, action: 'load', projectId: ${projId}, name: project.name };
4468
- })();`.trim();
4469
- }
4470
- case 'list':
4471
- return `
4472
- // List saved projects
4473
- (async function() {
4474
- ${guard}
4475
- const projects = await app.storageManager.listProjects();
4476
- return { success: true, action: 'list', projects: projects.map(p => ({ id: p.id, name: p.name, updatedAt: p.updatedAt })), count: projects.length };
4477
- })();`.trim();
4478
- case 'delete': {
4479
- const projId = JSON.stringify(input.projectId || '');
4480
- return `
4481
- // Delete project
4482
- (async function() {
4483
- ${guard}
4484
- await app.storageManager.deleteProject(${projId});
4485
- return { success: true, action: 'delete', projectId: ${projId} };
4486
- })();`.trim();
4487
- }
4488
- default:
4489
- return `(function() { return { error: 'Unknown storage action: ${input.action}' }; })();`;
4490
- }
4491
- }
4492
- // ===========================================================================
4493
4100
  // INTERACTION
4494
4101
  // ===========================================================================
4495
4102
  generateInteraction(input) {
@@ -4559,8 +4166,8 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
4559
4166
  return `
4560
4167
  // Export widget (pp:PinePaper ontology JSON)
4561
4168
  (async function() {
4562
- if (!app.exportWidget) return { error: 'Widget export not available' };
4563
- const result = await app.exportWidget(${JSON.stringify(opts)});
4169
+ if (!app.exportEngine || !app.exportEngine.exportWidget) return { error: 'Widget export not available' };
4170
+ const result = await app.exportEngine.exportWidget(${JSON.stringify(opts)});
4564
4171
  return { success: true, json: result.json, filename: result.filename, embedCode: result.embedCode, metadata: result.data?.metadata };
4565
4172
  })();`.trim();
4566
4173
  }
@@ -4573,8 +4180,8 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} const ite
4573
4180
  return `
4574
4181
  // Export widget as self-contained HTML
4575
4182
  (async function() {
4576
- if (!app.exportWidgetHTML) return { error: 'Widget HTML export not available' };
4577
- const result = await app.exportWidgetHTML(${JSON.stringify(opts)});
4183
+ if (!app.exportEngine || !app.exportEngine.exportWidgetHTML) return { error: 'Widget HTML export not available' };
4184
+ const result = await app.exportEngine.exportWidgetHTML(${JSON.stringify(opts)});
4578
4185
  return { success: true, html: result.html, estimatedSize: result.estimatedSize, analysis: { itemTypes: [...result.analysis.itemTypes], relationTypes: [...result.analysis.relationTypes], hasSimpleAnimations: result.analysis.hasSimpleAnimations, hasKeyframeAnimations: result.analysis.hasKeyframeAnimations, hasMasks: result.analysis.hasMasks } };
4579
4186
  })();`.trim();
4580
4187
  }