@pinepaper.studio/mcp-server 1.6.17 → 1.6.18

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 (62) hide show
  1. package/README.md +92 -0
  2. package/dist/browser/puppeteer-controller.d.ts +61 -1
  3. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  4. package/dist/browser/puppeteer-controller.js +168 -6
  5. package/dist/browser/puppeteer-controller.js.map +1 -1
  6. package/dist/build-info.json +2 -2
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +75 -10
  10. package/dist/index.js.map +1 -1
  11. package/dist/ontology/ontology-validator.d.ts.map +1 -1
  12. package/dist/ontology/ontology-validator.js +9 -5
  13. package/dist/ontology/ontology-validator.js.map +1 -1
  14. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  15. package/dist/ontology/upstream-vocabulary.js +2 -2
  16. package/dist/tools/definitions.d.ts.map +1 -1
  17. package/dist/tools/definitions.js +138 -35
  18. package/dist/tools/definitions.js.map +1 -1
  19. package/dist/tools/engine-lists.d.ts +20 -0
  20. package/dist/tools/engine-lists.d.ts.map +1 -0
  21. package/dist/tools/engine-lists.js +86 -0
  22. package/dist/tools/engine-lists.js.map +1 -0
  23. package/dist/tools/engine-methods.d.ts.map +1 -1
  24. package/dist/tools/engine-methods.js +4 -0
  25. package/dist/tools/engine-methods.js.map +1 -1
  26. package/dist/tools/engine-surface.d.ts +1 -1
  27. package/dist/tools/engine-surface.d.ts.map +1 -1
  28. package/dist/tools/engine-surface.js +20 -4
  29. package/dist/tools/engine-surface.js.map +1 -1
  30. package/dist/tools/handlers/brand-intake.d.ts +95 -0
  31. package/dist/tools/handlers/brand-intake.d.ts.map +1 -0
  32. package/dist/tools/handlers/brand-intake.js +242 -0
  33. package/dist/tools/handlers/brand-intake.js.map +1 -0
  34. package/dist/tools/handlers.d.ts +23 -11
  35. package/dist/tools/handlers.d.ts.map +1 -1
  36. package/dist/tools/handlers.js +226 -10
  37. package/dist/tools/handlers.js.map +1 -1
  38. package/dist/tools/minimal-descriptions.js +4 -4
  39. package/dist/tools/minimal-descriptions.js.map +1 -1
  40. package/dist/tools/shape-params.d.ts +2 -2
  41. package/dist/tools/shape-params.js +2 -2
  42. package/dist/tools/stick-vocabulary.d.ts +1 -1
  43. package/dist/tools/stick-vocabulary.js +1 -1
  44. package/dist/types/code-generator.d.ts +40 -2
  45. package/dist/types/code-generator.d.ts.map +1 -1
  46. package/dist/types/code-generator.js +260 -26
  47. package/dist/types/code-generator.js.map +1 -1
  48. package/dist/types/schemas.d.ts +2170 -698
  49. package/dist/types/schemas.d.ts.map +1 -1
  50. package/dist/types/schemas.js +152 -36
  51. package/dist/types/schemas.js.map +1 -1
  52. package/dist/utils/mcp-log.d.ts +16 -0
  53. package/dist/utils/mcp-log.d.ts.map +1 -0
  54. package/dist/utils/mcp-log.js +10 -0
  55. package/dist/utils/mcp-log.js.map +1 -0
  56. package/dist/utils/progress.d.ts +28 -0
  57. package/dist/utils/progress.d.ts.map +1 -0
  58. package/dist/utils/progress.js +24 -0
  59. package/dist/utils/progress.js.map +1 -0
  60. package/dist/vendor/design/design-style.js +1 -1
  61. package/manifest.json +1 -1
  62. package/package.json +7 -4
@@ -5,6 +5,7 @@
5
5
  * This code is designed to run in the browser context where window.PinePaper is available.
6
6
  */
7
7
  import { normalizeSvgSize } from '../utils/svg-size.js';
8
+ import { WORLD3D_COLOR_PATHS } from '../tools/engine-lists.js';
8
9
  import { REQUIRED_ENGINE_METHODS, OPTIONAL_ENGINE_METHODS } from '../tools/engine-methods.js';
9
10
  import { ACCEPTED_CREATE_PARAMS, NORMALIZE_PARAM_READS, MODIFY_CHANGE_READS } from '../tools/shape-params.js';
10
11
  import { generateP5DrawCode } from '../tools/p5-compat/p5-helpers.js';
@@ -24,7 +25,7 @@ GlobeEnableInputSchema, GlobeRotateToInputSchema, GlobeSpinInputSchema, WorldTou
24
25
  // Custom relation/code types
25
26
  RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
26
27
  // Mask types
27
- ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, } from './schemas.js';
28
+ ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, } from './schemas.js';
28
29
  import { OntologyCompiler } from '../ontology/ontology-compiler.js';
29
30
  // =============================================================================
30
31
  // HELPER FUNCTIONS
@@ -2213,6 +2214,39 @@ function world3dColor(value) {
2213
2214
  return value;
2214
2215
  return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255);
2215
2216
  }
2217
+ /**
2218
+ * The world spec's colour fields: GENERATED from FxTool WORLD_SCHEMA's kind
2219
+ * 'color' entries (scripts/sync-engine-lists.mjs). None of them is named
2220
+ * `color`, so world3dColors never touched them and a hex `env.zenith` reached
2221
+ * the engine as a string, where the validator refused it.
2222
+ */
2223
+ export { WORLD3D_COLOR_PATHS };
2224
+ /** A hex colour to [r, g, b] 0..1; anything else unchanged (arrays too: light colours may exceed 1). */
2225
+ export function world3dHex(value) {
2226
+ return typeof value === 'string' ? world3dColor(value) : value;
2227
+ }
2228
+ /** Convert hex strings at the world spec's colour paths (configure / create). */
2229
+ export function world3dSpecColors(spec) {
2230
+ if (!spec || typeof spec !== 'object' || Array.isArray(spec))
2231
+ return spec;
2232
+ const out = JSON.parse(JSON.stringify(spec));
2233
+ for (const path of WORLD3D_COLOR_PATHS) {
2234
+ const [group, key] = path.split('.');
2235
+ const g = out[group];
2236
+ if (g && typeof g === 'object' && key in g)
2237
+ g[key] = world3dHex(g[key]);
2238
+ }
2239
+ return out;
2240
+ }
2241
+ /** A light or material spec with hex colours (color, emissive, sheenColor) converted; an array is left exactly as given. */
2242
+ function world3dHexColor(spec) {
2243
+ const out = { ...(spec ?? {}) };
2244
+ for (const key of ['color', 'emissive', 'sheenColor']) {
2245
+ if (typeof out[key] === 'string')
2246
+ out[key] = world3dColor(out[key]);
2247
+ }
2248
+ return out;
2249
+ }
2216
2250
  /** Recursively convert every `color` field in a world3d payload. */
2217
2251
  function world3dColors(value) {
2218
2252
  if (Array.isArray(value))
@@ -2225,6 +2259,60 @@ function world3dColors(value) {
2225
2259
  }
2226
2260
  return out;
2227
2261
  }
2262
+ /** At most this many tiles in one contact sheet: past it, the tiles are too small to judge. */
2263
+ export const SHEET_MAX_TILES = 48;
2264
+ /**
2265
+ * The times a contact sheet captures, and a label for each. Pure, so the
2266
+ * modes are testable without a browser. Explicit `times` win; otherwise
2267
+ * every / strip / loopSeam. Rounded to the millisecond, clamped at 0.
2268
+ */
2269
+ export function sheetTimes(times, sheet) {
2270
+ const r = (t) => Math.max(0, Math.round(t * 1000) / 1000);
2271
+ const fmt = (t) => `${t.toFixed(2)}s`;
2272
+ let out = [];
2273
+ let labels = null;
2274
+ if (times && times.length) {
2275
+ out = times.map(r);
2276
+ }
2277
+ else if (sheet.strip) {
2278
+ const { at, count, step } = sheet.strip;
2279
+ const first = at - Math.floor(count / 2) * step;
2280
+ out = Array.from({ length: count }, (_, i) => r(first + i * step));
2281
+ }
2282
+ else if (sheet.loopSeam && sheet.duration) {
2283
+ // The frame BEFORE the loop wraps, then the first: a jump between them is the stutter.
2284
+ out = [r(sheet.duration - 1 / 30), 0];
2285
+ labels = [`end ${fmt(out[0])}`, 'start 0.00s'];
2286
+ }
2287
+ else if (sheet.every && sheet.duration) {
2288
+ for (let t = 0; t < sheet.duration - 1e-9 && out.length < SHEET_MAX_TILES; t += sheet.every)
2289
+ out.push(r(t));
2290
+ }
2291
+ out = out.slice(0, SHEET_MAX_TILES);
2292
+ return { times: out, labels: labels ?? out.map(fmt) };
2293
+ }
2294
+ /**
2295
+ * The critique an agent applies to a contact sheet. One scale, the same every
2296
+ * time, so two passes over one film can be compared.
2297
+ */
2298
+ export const SHEET_RUBRIC = {
2299
+ scale: 'Score each 1-10. Under 7 on any line is a fix before export.',
2300
+ criteria: [
2301
+ 'hook: does something worth watching happen in the first 2 s?',
2302
+ 'phone readability: at 360 px wide, can every word be read?',
2303
+ 'motion quality: do moves ease in and out, with nothing jumping or sliding at constant speed?',
2304
+ 'variety: does something change every 2-4 s, with no stretch that looks the same?',
2305
+ 'composition: is there a clear subject, with nothing crammed against an edge?',
2306
+ 'brand accuracy: are the colours, fonts and logo the brand\'s own?',
2307
+ 'sound sync: do cuts and hits land on the beat (check against pinepaper_beat_cuts)?',
2308
+ ],
2309
+ checklist: [
2310
+ 'text overlapping text or a subject',
2311
+ 'dead beats: adjacent tiles that are the same picture',
2312
+ 'labels or logos cut off in a corner',
2313
+ 'a jump between the last and first frame of a loop (loopSeam)',
2314
+ ],
2315
+ };
2228
2316
  export class PinePaperCodeGenerator {
2229
2317
  /**
2230
2318
  * Generate code for creating an item
@@ -3997,7 +4085,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
3997
4085
  }
3998
4086
  generateAgentExport(input) {
3999
4087
  const validated = AgentExportInputSchema.parse(input);
4000
- const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, transparent, alphaQuantizer, deterministic, ad: adOpts, frames } = validated;
4088
+ const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, transparent, alphaQuantizer, deterministic, motionBlur, ad: adOpts, frames } = validated;
4001
4089
  // Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
4002
4090
  const videoEncodeOpts = {};
4003
4091
  if (broadcast)
@@ -4019,6 +4107,10 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
4019
4107
  // the only bytes that varied, and this pins them.
4020
4108
  if (deterministic)
4021
4109
  videoEncodeOpts.deterministic = true;
4110
+ // Motion blur (FxTool #55): the engine forwards it on every mp4/webm path
4111
+ // (quickExport, the agent paths, the export store, videoExporter.export).
4112
+ if (motionBlur)
4113
+ videoEncodeOpts.motionBlur = motionBlur;
4022
4114
  // A transparent WebM (FxTool 1833b397) rides the store route too.
4023
4115
  const webmAlpha = format === 'webm' && transparent === true;
4024
4116
  if (webmAlpha) {
@@ -4308,7 +4400,10 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
4308
4400
  const bg = app.canvasEl && app.canvasEl.style && app.canvasEl.style.backgroundColor;
4309
4401
  const hasColor = !!bg && !/^(transparent|rgba\\([^)]*,\\s*0\\))$/i.test(bg);
4310
4402
  const hasBgItems = !!(app.patternGroup && app.patternGroup.children && app.patternGroup.children.length);
4311
- if (hasColor || hasBgItems) return [];
4403
+ // A 3D world under the canvas IS the background: since FxTool #50
4404
+ // (747315e3) jpg/webp stills composite it, so nothing turns white/black.
4405
+ const hasWorld = !!app._world3d;
4406
+ if (hasColor || hasBgItems || hasWorld) return [];
4312
4407
  return [{ code: 'alpha_dropped', message: fmt === 'apng'
4313
4408
  ? 'apng with transparent: false and no background colour: the frames are filled BLACK. Set a background colour, or drop transparent: false to keep the alpha.'
4314
4409
  : fmt === 'jpg'
@@ -4365,7 +4460,9 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
4365
4460
  fps: settings.fps,
4366
4461
  quality,
4367
4462
  width: dimensions.width,
4368
- height: dimensions.height,
4463
+ height: dimensions.height,${motionBlur ? `
4464
+ // The estimate reports renders (frames x subframes) and renderCost.
4465
+ motionBlur: ${JSON.stringify(motionBlur)},` : ''}
4369
4466
  });
4370
4467
  return {
4371
4468
  success: true,
@@ -5230,6 +5327,17 @@ ${Object.keys(videoEncodeOpts).length ? ` if (result && result.success) {
5230
5327
  if (result.fidelity.note) delete result.fidelity.note;
5231
5328
  }
5232
5329
  }
5330
+ ` : ''}${motionBlur ? ` // What the exporter actually did with motionBlur (FxTool #55 records it as
5331
+ // lastMotionBlur, with renders = frames x subframes). A studio that predates
5332
+ // it ignores the option silently, so that is said by name.
5333
+ if (result && result.success) {
5334
+ const __mb = __vx && __vx.lastMotionBlur;
5335
+ if (__mb) result.motionBlur = __mb;
5336
+ else {
5337
+ result.fidelity = result.fidelity || { warnings: [] };
5338
+ result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'motion_blur_not_applied', message: 'motionBlur was asked for, but this studio did not report applying it: it predates motion blur (FxTool #55), so the video has none.' }]);
5339
+ }
5340
+ }
5233
5341
  ` : ''} const __ar = __vx && __vx.lastAudioReport;
5234
5342
  if (result && result.success && __ar && typeof __ar === 'object') {
5235
5343
  result.audio = __ar;
@@ -6861,19 +6969,25 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
6861
6969
  `.trim();
6862
6970
  }
6863
6971
  generateGetAvailableEasings() {
6972
+ // The engine's own list wins: listAnimatableProperties().easings is its
6973
+ // EASING_NAMES, read from the easing table itself. The static list is the
6974
+ // fallback for studios that predate it (fixture-pinned, easing-parity test).
6864
6975
  return `
6865
6976
  // Get available easing functions
6866
6977
  (function() {
6978
+ const fallback = ${JSON.stringify(KEYFRAME_EASINGS)};
6979
+ const descriptions = ${JSON.stringify(EASING_DESCRIPTIONS)};
6980
+ let live = null;
6981
+ try {
6982
+ const r = typeof app !== 'undefined' && app && typeof app.listAnimatableProperties === 'function'
6983
+ ? app.listAnimatableProperties() : null;
6984
+ if (r && Array.isArray(r.easings) && r.easings.length) live = r.easings;
6985
+ } catch (e) { live = null; }
6867
6986
  return {
6868
- easings: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'bounce', 'elastic'],
6869
- descriptions: {
6870
- linear: 'Constant speed',
6871
- easeIn: 'Slow start, fast end',
6872
- easeOut: 'Fast start, slow end',
6873
- easeInOut: 'Slow start and end',
6874
- bounce: 'Bouncing effect at end',
6875
- elastic: 'Spring-like overshoot'
6876
- }
6987
+ easings: live || fallback,
6988
+ source: live ? 'engine' : 'mcp-server fallback list',
6989
+ descriptions,
6990
+ exportNote: 'Every easing renders exactly in video, GIF and APNG. Lottie and CSS get the closest single Bezier: springPlayful loses its wobble, and the in-out quart/quint/expo/circ curves are within 2-4%. SMIL is exact for springs and those four.'
6877
6991
  };
6878
6992
  })();
6879
6993
  `.trim();
@@ -9704,7 +9818,7 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
9704
9818
  })();`.trim();
9705
9819
  }
9706
9820
  generateCaptureFrames(input) {
9707
- const timesJson = JSON.stringify(input.times);
9821
+ const timesJson = JSON.stringify(input.times ?? []);
9708
9822
  const seed = input.seed !== undefined ? input.seed : 0;
9709
9823
  const includeDataUrls = !!input.includeDataUrls;
9710
9824
  return `
@@ -9763,6 +9877,69 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
9763
9877
  ...(__firstSeek ? { firstSeek: __firstSeek } : {}),
9764
9878
  ...(__firstSeek && __firstSeek.timedOut > 0 ? { videoWarning: 'the video seek for the first frame timed out: its video may show an earlier picture. Retry after the clip has loaded.' } : {}),
9765
9879
  frames: frames };
9880
+ })();`.trim();
9881
+ }
9882
+ /**
9883
+ * A contact sheet (G3): capture at the planned times and tile them into one
9884
+ * image in the page. On a studio with sized capture (FxTool #42:
9885
+ * captureFramesAt(times, { width, format })) the engine renders each frame
9886
+ * small; on an older one the full frame is scaled while tiling.
9887
+ */
9888
+ generateCaptureSheet(input) {
9889
+ const sheet = input.sheet;
9890
+ const { times, labels } = sheetTimes(input.times, sheet);
9891
+ const n = times.length;
9892
+ const columns = sheet.columns ?? Math.min(6, Math.max(1, Math.ceil(Math.sqrt(n))));
9893
+ const mime = sheet.format === 'png' ? 'image/png' : 'image/jpeg';
9894
+ const S = (v) => JSON.stringify(v);
9895
+ return `
9896
+ // Contact sheet of ${n} frame(s)
9897
+ (async function() {
9898
+ if (typeof app.captureFramesAt !== 'function') {
9899
+ return { success: false, error: 'app.captureFramesAt unavailable — update FxTool to a build with the deterministic capture entrypoint' };
9900
+ }
9901
+ const times = ${S(times)};
9902
+ const labels = ${S(labels)};
9903
+ const tileW = ${sheet.tileWidth};
9904
+ const columns = ${columns};
9905
+ const sized = typeof app._frameCapture === 'function';
9906
+ const cap = typeof app.captureFramesAtAsync === 'function' ? app.captureFramesAtAsync.bind(app) : app.captureFramesAt.bind(app);
9907
+ const urls = sized
9908
+ ? await cap(times, { seed: ${input.seed ?? 0}, width: tileW, format: 'png' })
9909
+ : await cap(times, { seed: ${input.seed ?? 0}, capture: function(c) {
9910
+ return typeof app.captureFrameDataURL === 'function' ? app.captureFrameDataURL() : (c && c.toDataURL ? c.toDataURL() : '');
9911
+ } });
9912
+ const imgs = await Promise.all(urls.map(function(u) {
9913
+ return new Promise(function(res) { const im = new Image(); im.onload = function() { res(im); }; im.onerror = function() { res(null); }; im.src = u; });
9914
+ }));
9915
+ const first = imgs.find(function(im) { return im && im.naturalWidth; });
9916
+ if (!first) return { success: false, error: 'no frame could be captured' };
9917
+ const tileH = Math.max(1, Math.round(tileW * first.naturalHeight / first.naturalWidth));
9918
+ const rows = Math.ceil(imgs.length / columns);
9919
+ const gap = 4;
9920
+ const cv = document.createElement('canvas');
9921
+ cv.width = columns * tileW + (columns + 1) * gap;
9922
+ cv.height = rows * tileH + (rows + 1) * gap;
9923
+ const g = cv.getContext('2d');
9924
+ g.fillStyle = '#202020'; g.fillRect(0, 0, cv.width, cv.height);
9925
+ imgs.forEach(function(im, i) {
9926
+ const x = gap + (i % columns) * (tileW + gap);
9927
+ const y = gap + Math.floor(i / columns) * (tileH + gap);
9928
+ if (im) g.drawImage(im, x, y, tileW, tileH);
9929
+ else { g.fillStyle = '#802020'; g.fillRect(x, y, tileW, tileH); }
9930
+ if (${sheet.labels ? 'true' : 'false'}) {
9931
+ const text = labels[i];
9932
+ g.font = '600 13px system-ui, sans-serif';
9933
+ const w = g.measureText(text).width + 10;
9934
+ g.fillStyle = 'rgba(0,0,0,0.65)'; g.fillRect(x + 4, y + 4, w, 20);
9935
+ g.fillStyle = '#fff'; g.fillText(text, x + 9, y + 19);
9936
+ }
9937
+ });
9938
+ const dataUrl = cv.toDataURL(${S(mime)}${sheet.format === 'png' ? '' : ', 0.82'});
9939
+ return { success: true, dataUrl: dataUrl, tiles: imgs.length, columns: columns, rows: rows,
9940
+ width: cv.width, height: cv.height, tileWidth: tileW, times: times,
9941
+ missing: imgs.filter(function(im) { return !im; }).length,
9942
+ sizedCapture: sized };
9766
9943
  })();`.trim();
9767
9944
  }
9768
9945
  /**
@@ -10142,6 +10319,45 @@ ${guard}
10142
10319
  // The facade throws on a missing raster/matte rather than returning {ok:false}.
10143
10320
  return { success: false, error: e && e.message ? e.message : String(e) };
10144
10321
  }
10322
+ })();`.trim();
10323
+ }
10324
+ case 'analyze_reference': {
10325
+ const S = (v) => JSON.stringify(v);
10326
+ return `
10327
+ // Measure a reference video's FORMAT: cuts, shot lengths, colours, pacing, beat
10328
+ (async function() {
10329
+ if (typeof app.analyzeReferenceVideo !== 'function') { return { success: false, error: 'app.analyzeReferenceVideo unavailable — update FxTool' }; }
10330
+ const r = await app.analyzeReferenceVideo(${S(input.id)}, { thumbnails: ${input.thumbnails ? 'true' : 'false'} });
10331
+ if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'the reference could not be analysed' }; }
10332
+ // Kept in the page for remake_from_reference, so the analysis does not have
10333
+ // to travel back through the agent.
10334
+ window.__ppLastReference = { analysis: r, id: ${S(input.id)} };
10335
+ const shots = Array.isArray(r.shots) ? r.shots : [];
10336
+ return { success: true, referenceId: ${S(input.id)}, duration: r.duration, aspect: r.aspect, width: r.width, height: r.height,
10337
+ cuts: r.cuts, shots: shots, pacing: r.pacing, audio: r.audio, card: r.card,
10338
+ ...(r.truncated ? { truncated: true } : {}),
10339
+ next: 'remake_from_reference with texts: your own words, ' + shots.length + ' (one per shot). Nothing of the footage, its words or its sound is copied.' };
10340
+ })();`.trim();
10341
+ }
10342
+ case 'remake_from_reference': {
10343
+ const S = (v) => JSON.stringify(v);
10344
+ const opts = {
10345
+ texts: input.texts ?? [],
10346
+ ...(input.duration !== undefined ? { duration: input.duration } : {}),
10347
+ ...(input.clear !== undefined ? { clear: input.clear } : {}),
10348
+ };
10349
+ return `
10350
+ // Build a new piece on the analysed reference's structure, in your own words
10351
+ (async function() {
10352
+ if (typeof app.remakeFromReference !== 'function') { return { success: false, error: 'app.remakeFromReference unavailable — update FxTool' }; }
10353
+ const last = window.__ppLastReference;
10354
+ if (!last || !last.analysis) { return { success: false, error: 'no analysed reference in this session: call pinepaper_media analyze_reference first' }; }
10355
+ // reference: the analysed clip stays in the project MUTED; without it the
10356
+ // reference's own sound is exported under the new cards.
10357
+ const r = await app.remakeFromReference(last.analysis, Object.assign(${S(opts)}, { reference: last.id }));
10358
+ return r && r.ok
10359
+ ? { success: true, action: 'remake_from_reference', duration: r.duration, shots: r.shots, items: r.items, referenceMuted: last.id }
10360
+ : { success: false, error: (r && r.error) || 'the remake could not be built' };
10145
10361
  })();`.trim();
10146
10362
  }
10147
10363
  case 'stop_live_matte': {
@@ -10313,6 +10529,21 @@ ${guard}
10313
10529
  const S = (v) => JSON.stringify(v);
10314
10530
  const wrap = (comment, body) => `\n// ${comment}\n(function() {\n${body}\n})();`.trim();
10315
10531
  switch (input.action) {
10532
+ case 'apply':
10533
+ // One engine entry point (FxTool #54): app.applyMedium dispatches to the
10534
+ // medium's own method (DesignMedia MEDIA[key].apply) and refuses an absent
10535
+ // or brush-only medium with the reason. The Paper item it returns is not
10536
+ // passed back (it cannot cross the bridge); its id is.
10537
+ return `
10538
+ // Medium: apply ${input.medium}
10539
+ (async function() {
10540
+ if (typeof app.applyMedium !== 'function') { return { success: false, error: 'app.applyMedium unavailable — update FxTool' }; }
10541
+ const r = await app.applyMedium(${S(input.itemId)}, ${S(input.medium)}, ${S(input.options ?? {})});
10542
+ if (!r || r.ok === false) {
10543
+ return { success: false, error: (r && (r.error || r.reason)) || 'the medium could not be applied', medium: ${S(input.medium)}, ...(r && r.fidelity ? { fidelity: r.fidelity } : {}) };
10544
+ }
10545
+ return { success: true, action: 'apply', medium: r.medium || ${S(input.medium)}, fidelity: r.fidelity, itemId: r.id, ...(r.note ? { note: r.note } : {}) };
10546
+ })();`.trim();
10316
10547
  case 'list_media':
10317
10548
  return wrap('Medium: list', ` if (typeof app.listDesignMedia !== 'function') { return { success: false, error: 'medium axis unavailable — update FxTool' }; }
10318
10549
  const media = app.listDesignMedia();
@@ -10629,12 +10860,15 @@ ${guard}
10629
10860
  // World3D: create — terrain, sky, shadows, walkable character, under the Paper canvas
10630
10861
  (async function() {
10631
10862
  if (typeof app.createWorld3D !== 'function') { return { success: false, error: 'app.createWorld3D unavailable — update FxTool to a world3d-capable build' }; }
10632
- await app.createWorld3D(${S(world3dColors(input.spec ?? 'forest'))}, ${opts});
10863
+ await app.createWorld3D(${S(world3dSpecColors(world3dColors(input.spec ?? 'forest')))}, ${opts});
10633
10864
  // describe() is the world's own parameter schema — return the preset list +
10634
10865
  // top-level keys so the agent knows what configure can touch, without the
10635
10866
  // full multi-KB schema on every create.
10636
10867
  const d = typeof app.describeWorld3D === 'function' ? await app.describeWorld3D() : null;
10637
- return { success: true, action: 'create', spec: ${S(input.spec ?? 'forest')}, configurableKeys: d ? Object.keys(d.params || d) : [] };
10868
+ // params is WORLD_SCHEMA: an ARRAY of {path, kind, doc}. Object.keys on it
10869
+ // returned "0", "1", "2"… — the indexes, not a single usable key.
10870
+ const keys = !d ? [] : Array.isArray(d.params) ? d.params.map(function (e) { return e && e.path; }).filter(Boolean) : Object.keys(d.params || d);
10871
+ return { success: true, action: 'create', spec: ${S(input.spec ?? 'forest')}, configurableKeys: keys };
10638
10872
  })();`.trim();
10639
10873
  }
10640
10874
  case 'describe':
@@ -10650,9 +10884,9 @@ ${guard}
10650
10884
  (function() {
10651
10885
  if (typeof app.configureWorld3D !== 'function') { return { success: false, error: 'app.configureWorld3D unavailable — update FxTool' }; }
10652
10886
  ${needWorld}
10653
- const r = app.configureWorld3D(${S(world3dColors(input.patch))});
10887
+ const r = app.configureWorld3D(${S(world3dSpecColors(world3dColors(input.patch)))});
10654
10888
  // The validator names the right key on a wrong one — forward it verbatim.
10655
- return r && r.ok ? { success: true, action: 'configure' } : { success: false, error: (r && r.error) || 'configure failed' };
10889
+ return r && r.ok ? { success: true, action: 'configure' } : { success: false, error: (r && (r.error || (Array.isArray(r.errors) && r.errors.length ? r.errors.join('; ') : '') || r.reason)) || 'configure failed' };
10656
10890
  })();`.trim();
10657
10891
  case 'add_actor': {
10658
10892
  const actor = S({
@@ -10765,7 +10999,7 @@ ${needWorld}
10765
10999
  // @engine-methods extrudeToMesh latheToMesh importOBJToWorld importGLTFToWorld
10766
11000
  if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool to a world3d-capable build' }; }
10767
11001
  const r = await app.${fn}(${S(input.pathId)}, ${S(mesh)});
10768
- if (!r || r.ok === false) { return { success: false, error: (r && r.reason) || 'the world refused the mesh' }; }
11002
+ if (!r || r.ok === false) { return { success: false, error: (r && (r.error || (Array.isArray(r.errors) && r.errors.length ? r.errors.join('; ') : '') || r.reason)) || 'the world refused the mesh' }; }
10769
11003
  return { success: true, meshId: r.id, op: ${S(input.action === 'extrude_path' ? 'extrude' : 'lathe')}, sourceId: ${S(input.pathId)} };
10770
11004
  })();`.trim();
10771
11005
  }
@@ -10792,8 +11026,8 @@ ${needWorld}
10792
11026
  (function() {
10793
11027
  if (typeof app.addWorldLight !== 'function') { return { success: false, error: 'app.addWorldLight unavailable — update FxTool' }; }
10794
11028
  ${needWorld}
10795
- const r = app.addWorldLight(${S(input.light ?? {})});
10796
- if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the world refused the light' }; }
11029
+ const r = app.addWorldLight(${S(world3dHexColor(input.light))});
11030
+ if (!r || r.ok === false) { return { success: false, error: (r && (r.error || (Array.isArray(r.errors) && r.errors.length ? r.errors.join('; ') : '') || r.reason)) || 'the world refused the light' }; }
10797
11031
  return { success: true, lightId: r.id, light: r.light };
10798
11032
  })();`.trim();
10799
11033
  case 'set_light':
@@ -10801,7 +11035,7 @@ ${needWorld}
10801
11035
  // World3D: edit a light in place
10802
11036
  (function() {
10803
11037
  if (typeof app.setWorldLight !== 'function') { return { success: false, error: 'app.setWorldLight unavailable — update FxTool' }; }
10804
- return { success: !!app.setWorldLight(${S(input.lightId)}, ${S(input.light ?? {})}) };
11038
+ return { success: !!app.setWorldLight(${S(input.lightId)}, ${S(world3dHexColor(input.light))}) };
10805
11039
  })();`.trim();
10806
11040
  case 'remove_light':
10807
11041
  return `
@@ -10824,8 +11058,8 @@ ${needWorld}
10824
11058
  (function() {
10825
11059
  if (typeof app.addWorldMaterial !== 'function') { return { success: false, error: 'app.addWorldMaterial unavailable — update FxTool' }; }
10826
11060
  ${needWorld}
10827
- const r = app.addWorldMaterial(${S(input.material ?? {})});
10828
- if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the world refused the material' }; }
11061
+ const r = app.addWorldMaterial(${S(world3dHexColor(input.material))});
11062
+ if (!r || r.ok === false) { return { success: false, error: (r && (r.error || (Array.isArray(r.errors) && r.errors.length ? r.errors.join('; ') : '') || r.reason)) || 'the world refused the material' }; }
10829
11063
  return { success: true, materialId: r.id, material: r.material };
10830
11064
  })();`.trim();
10831
11065
  case 'set_material':
@@ -10833,7 +11067,7 @@ ${needWorld}
10833
11067
  // World3D: patch a material — every object referencing it changes next frame
10834
11068
  (function() {
10835
11069
  if (typeof app.setWorldMaterial !== 'function') { return { success: false, error: 'app.setWorldMaterial unavailable — update FxTool' }; }
10836
- return { success: !!app.setWorldMaterial(${S(input.materialId)}, ${S(input.material ?? {})}) };
11070
+ return { success: !!app.setWorldMaterial(${S(input.materialId)}, ${S(world3dHexColor(input.material))}) };
10837
11071
  })();`.trim();
10838
11072
  case 'remove_material':
10839
11073
  return `
@@ -10892,7 +11126,7 @@ ${needWorld}
10892
11126
  (function() {
10893
11127
  if (typeof app.setWorldNavTarget !== 'function') { return { success: false, error: 'app.setWorldNavTarget unavailable — update FxTool' }; }
10894
11128
  const r = app.setWorldNavTarget(${S(input.navTarget)});
10895
- if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the nav target was refused' }; }
11129
+ if (!r || r.ok === false) { return { success: false, error: (r && (r.error || (Array.isArray(r.errors) && r.errors.length ? r.errors.join('; ') : '') || r.reason)) || 'the nav target was refused' }; }
10896
11130
  return { success: true, navTarget: ${S(input.navTarget)} };
10897
11131
  })();`.trim();
10898
11132
  case 'get_nav_target':