@pinepaper.studio/mcp-server 1.6.16 → 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.
- package/README.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.hi.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +142 -4
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/browser/puppeteer-controller.d.ts +61 -1
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +168 -6
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +75 -10
- package/dist/index.js.map +1 -1
- package/dist/ontology/ontology-validator.d.ts.map +1 -1
- package/dist/ontology/ontology-validator.js +9 -5
- package/dist/ontology/ontology-validator.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.js +2 -2
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +198 -47
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +20 -0
- package/dist/tools/engine-lists.d.ts.map +1 -0
- package/dist/tools/engine-lists.js +86 -0
- package/dist/tools/engine-lists.js.map +1 -0
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +8 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +20 -4
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/beats.d.ts +64 -0
- package/dist/tools/handlers/beats.d.ts.map +1 -0
- package/dist/tools/handlers/beats.js +83 -0
- package/dist/tools/handlers/beats.js.map +1 -0
- package/dist/tools/handlers/brand-intake.d.ts +95 -0
- package/dist/tools/handlers/brand-intake.d.ts.map +1 -0
- package/dist/tools/handlers/brand-intake.js +242 -0
- package/dist/tools/handlers/brand-intake.js.map +1 -0
- package/dist/tools/handlers.d.ts +23 -11
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +265 -14
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +5 -3
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.js +2 -2
- package/dist/tools/stick-vocabulary.d.ts +1 -1
- package/dist/tools/stick-vocabulary.js +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +2 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +63 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +533 -49
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +2199 -640
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +186 -50
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/a11y.d.ts +2 -0
- package/dist/utils/a11y.d.ts.map +1 -1
- package/dist/utils/a11y.js +1 -1
- package/dist/utils/a11y.js.map +1 -1
- package/dist/utils/beats.d.ts +58 -0
- package/dist/utils/beats.d.ts.map +1 -0
- package/dist/utils/beats.js +110 -0
- package/dist/utils/beats.js.map +1 -0
- package/dist/utils/mcp-log.d.ts +16 -0
- package/dist/utils/mcp-log.d.ts.map +1 -0
- package/dist/utils/mcp-log.js +10 -0
- package/dist/utils/mcp-log.js.map +1 -0
- package/dist/utils/progress.d.ts +28 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +24 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/svg-size.d.ts +22 -0
- package/dist/utils/svg-size.d.ts.map +1 -0
- package/dist/utils/svg-size.js +43 -0
- package/dist/utils/svg-size.js.map +1 -0
- package/dist/vendor/design/design-style.js +1 -1
- package/manifest.json +9 -1
- package/package.json +7 -4
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Converts MCP tool inputs to executable PinePaper JavaScript code.
|
|
5
5
|
* This code is designed to run in the browser context where window.PinePaper is available.
|
|
6
6
|
*/
|
|
7
|
+
import { normalizeSvgSize } from '../utils/svg-size.js';
|
|
8
|
+
import { WORLD3D_COLOR_PATHS } from '../tools/engine-lists.js';
|
|
7
9
|
import { REQUIRED_ENGINE_METHODS, OPTIONAL_ENGINE_METHODS } from '../tools/engine-methods.js';
|
|
8
10
|
import { ACCEPTED_CREATE_PARAMS, NORMALIZE_PARAM_READS, MODIFY_CHANGE_READS } from '../tools/shape-params.js';
|
|
9
11
|
import { generateP5DrawCode } from '../tools/p5-compat/p5-helpers.js';
|
|
@@ -23,7 +25,7 @@ GlobeEnableInputSchema, GlobeRotateToInputSchema, GlobeSpinInputSchema, WorldTou
|
|
|
23
25
|
// Custom relation/code types
|
|
24
26
|
RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
|
|
25
27
|
// Mask types
|
|
26
|
-
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';
|
|
27
29
|
import { OntologyCompiler } from '../ontology/ontology-compiler.js';
|
|
28
30
|
// =============================================================================
|
|
29
31
|
// HELPER FUNCTIONS
|
|
@@ -1697,7 +1699,8 @@ function generateSetCanvasSizeCode(width, height, preset) {
|
|
|
1697
1699
|
// echoing the request back.
|
|
1698
1700
|
return { success: true, width: (r && r.width) || ${width}, height: (r && r.height) || ${height},
|
|
1699
1701
|
...(r && r.preset ? { preset: r.preset } : {}),
|
|
1700
|
-
...(r && r.unbounded !== undefined ? { unbounded: r.unbounded } : {})
|
|
1702
|
+
...(r && r.unbounded !== undefined ? { unbounded: r.unbounded } : {})${!preset && (width > 8192 || height > 8192) ? `,
|
|
1703
|
+
note: 'above 8192 px per side: Chrome and Firefox render it (the studio allows up to 16384 safely), but Safari and iOS may produce a blank or corrupt canvas — export from Chrome.'` : ''} };
|
|
1701
1704
|
})();
|
|
1702
1705
|
`.trim();
|
|
1703
1706
|
}
|
|
@@ -2211,6 +2214,39 @@ function world3dColor(value) {
|
|
|
2211
2214
|
return value;
|
|
2212
2215
|
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255);
|
|
2213
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
|
+
}
|
|
2214
2250
|
/** Recursively convert every `color` field in a world3d payload. */
|
|
2215
2251
|
function world3dColors(value) {
|
|
2216
2252
|
if (Array.isArray(value))
|
|
@@ -2223,6 +2259,60 @@ function world3dColors(value) {
|
|
|
2223
2259
|
}
|
|
2224
2260
|
return out;
|
|
2225
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
|
+
};
|
|
2226
2316
|
export class PinePaperCodeGenerator {
|
|
2227
2317
|
/**
|
|
2228
2318
|
* Generate code for creating an item
|
|
@@ -2250,6 +2340,9 @@ export class PinePaperCodeGenerator {
|
|
|
2250
2340
|
*/
|
|
2251
2341
|
generateModifyItem(input) {
|
|
2252
2342
|
const validated = ModifyItemInputSchema.parse(input);
|
|
2343
|
+
if (validated.atTime !== undefined) {
|
|
2344
|
+
return generateAutoKeyCode(validated.itemId, validated.properties, validated.atTime, validated.easing);
|
|
2345
|
+
}
|
|
2253
2346
|
return generateModifyItemCode(validated.itemId, validated.properties, validated.data);
|
|
2254
2347
|
}
|
|
2255
2348
|
/**
|
|
@@ -2466,12 +2559,21 @@ ${INLINE_REMOTE_IMAGES}
|
|
|
2466
2559
|
imported.scale(${scale});
|
|
2467
2560
|
const itemId = app.registerItem(imported, 'svg-import', { source: 'mcp' });
|
|
2468
2561
|
app.historyManager.saveState();
|
|
2562
|
+
const _b = imported.bounds;
|
|
2563
|
+
const _tiny = _b && _b.width < 16 && _b.height < 16;
|
|
2469
2564
|
return { success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} },
|
|
2565
|
+
size: _b ? { width: Math.round(_b.width * 10) / 10, height: Math.round(_b.height * 10) / 10 } : null,
|
|
2566
|
+
...(_tiny ? { warning: 'the SVG came in at ' + _b.width.toFixed(1) + 'x' + _b.height.toFixed(1) + ' px — too small to see (a root sized in em or % reads as 1 px). Pass scale, or give the SVG a width / height in px.' } : {}),
|
|
2470
2567
|
...(_inl.notes.length ? { imageWarnings: _inl.notes } : {}) };
|
|
2471
2568
|
})();
|
|
2472
2569
|
`.trim();
|
|
2473
2570
|
}
|
|
2474
2571
|
if (svgString) {
|
|
2572
|
+
// A root sized in em / % (icon sets) renders ~5 px: sized from the
|
|
2573
|
+
// viewBox instead, and the result says so (see utils/svg-size.ts).
|
|
2574
|
+
const sized = normalizeSvgSize(svgString);
|
|
2575
|
+
svgString = sized.svg;
|
|
2576
|
+
const sizeFix = sized.fix;
|
|
2475
2577
|
// Escape the SVG string for embedding in code
|
|
2476
2578
|
const escapedSvg = svgString.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
2477
2579
|
return `
|
|
@@ -2490,7 +2592,14 @@ imported.position = new paper.Point(${position.x}, ${position.y});
|
|
|
2490
2592
|
imported.scale(${scale});
|
|
2491
2593
|
const itemId = app.registerItem(imported, 'svg-import', { source: 'mcp' });
|
|
2492
2594
|
app.historyManager.saveState();
|
|
2595
|
+
// A SPECK IS NOT A SUCCESS WORTH REPORTING QUIETLY: under 16 px both ways,
|
|
2596
|
+
// the import is said to be tiny, with the fix.
|
|
2597
|
+
const _b = imported.bounds;
|
|
2598
|
+
const _tiny = _b && _b.width < 16 && _b.height < 16;
|
|
2493
2599
|
return { success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} },
|
|
2600
|
+
size: _b ? { width: Math.round(_b.width * 10) / 10, height: Math.round(_b.height * 10) / 10 } : null,
|
|
2601
|
+
${sizeFix ? `sizeFix: ${JSON.stringify(sizeFix)},` : ''}
|
|
2602
|
+
...(_tiny ? { warning: 'the SVG came in at ' + _b.width.toFixed(1) + 'x' + _b.height.toFixed(1) + ' px — too small to see. Pass scale, or give the SVG a width / height in px.' } : {}),
|
|
2494
2603
|
...(_inl.notes.length ? { imageWarnings: _inl.notes } : {}) };
|
|
2495
2604
|
})();
|
|
2496
2605
|
`.trim();
|
|
@@ -3976,7 +4085,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3976
4085
|
}
|
|
3977
4086
|
generateAgentExport(input) {
|
|
3978
4087
|
const validated = AgentExportInputSchema.parse(input);
|
|
3979
|
-
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 } = 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;
|
|
3980
4089
|
// Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
|
|
3981
4090
|
const videoEncodeOpts = {};
|
|
3982
4091
|
if (broadcast)
|
|
@@ -3998,6 +4107,10 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3998
4107
|
// the only bytes that varied, and this pins them.
|
|
3999
4108
|
if (deterministic)
|
|
4000
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;
|
|
4001
4114
|
// A transparent WebM (FxTool 1833b397) rides the store route too.
|
|
4002
4115
|
const webmAlpha = format === 'webm' && transparent === true;
|
|
4003
4116
|
if (webmAlpha) {
|
|
@@ -4007,13 +4120,22 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4007
4120
|
}
|
|
4008
4121
|
const encodeOptsJs = Object.keys(videoEncodeOpts).length ? `, ...${JSON.stringify(videoEncodeOpts)}` : '';
|
|
4009
4122
|
const qualityLevel = quality || 'standard';
|
|
4010
|
-
|
|
4123
|
+
let videoDuration = duration ?? 5;
|
|
4011
4124
|
// Quality settings (bitrate in bps for VideoEncoder)
|
|
4012
4125
|
const qualitySettings = {
|
|
4013
4126
|
draft: { compression: 0.6, fps: 15, dpi: 72, bitrate: 2_000_000 },
|
|
4014
4127
|
standard: { compression: 0.85, fps: 30, dpi: 150, bitrate: 5_000_000 },
|
|
4015
4128
|
high: { compression: 0.95, fps: 60, dpi: 300, bitrate: 8_000_000 },
|
|
4016
4129
|
}[qualityLevel];
|
|
4130
|
+
// FRAMES, EXACTLY (showcase film, 11.1). The engine makes ceil(duration x
|
|
4131
|
+
// fps) frames for mp4 / webm / gif and floor(...) for apng, so 7.91667 s
|
|
4132
|
+
// at 24 fps was 191 frames, not 190 — and N / fps itself can land a hair
|
|
4133
|
+
// above N in floating point (190 / 24 * 24 > 190). The duration is nudged
|
|
4134
|
+
// a microsecond toward the side that rounds to N.
|
|
4135
|
+
if (frames !== undefined) {
|
|
4136
|
+
const fpsEff = fps ?? qualitySettings.fps;
|
|
4137
|
+
videoDuration = frames / fpsEff + (format === 'apng' ? 1e-6 : -1e-6);
|
|
4138
|
+
}
|
|
4017
4139
|
// Platform presets
|
|
4018
4140
|
const platformPresets = {
|
|
4019
4141
|
'instagram': { width: 1080, height: 1080, staticFormat: 'png', animatedFormat: 'mp4' },
|
|
@@ -4278,7 +4400,10 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4278
4400
|
const bg = app.canvasEl && app.canvasEl.style && app.canvasEl.style.backgroundColor;
|
|
4279
4401
|
const hasColor = !!bg && !/^(transparent|rgba\\([^)]*,\\s*0\\))$/i.test(bg);
|
|
4280
4402
|
const hasBgItems = !!(app.patternGroup && app.patternGroup.children && app.patternGroup.children.length);
|
|
4281
|
-
|
|
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 [];
|
|
4282
4407
|
return [{ code: 'alpha_dropped', message: fmt === 'apng'
|
|
4283
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.'
|
|
4284
4409
|
: fmt === 'jpg'
|
|
@@ -4335,7 +4460,9 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4335
4460
|
fps: settings.fps,
|
|
4336
4461
|
quality,
|
|
4337
4462
|
width: dimensions.width,
|
|
4338
|
-
height: dimensions.height
|
|
4463
|
+
height: dimensions.height,${motionBlur ? `
|
|
4464
|
+
// The estimate reports renders (frames x subframes) and renderCost.
|
|
4465
|
+
motionBlur: ${JSON.stringify(motionBlur)},` : ''}
|
|
4339
4466
|
});
|
|
4340
4467
|
return {
|
|
4341
4468
|
success: true,
|
|
@@ -4494,6 +4621,9 @@ ${stillTime !== undefined ? `
|
|
|
4494
4621
|
// a neighbouring card's overflowing headline. Older studios ignore the
|
|
4495
4622
|
// fourth argument, and leave lastRegionExcluded unset.
|
|
4496
4623
|
if ('lastRegionExcluded' in app) app.lastRegionExcluded = null;
|
|
4624
|
+
// A video in the region is waited for at this time (round 12, 12.5):
|
|
4625
|
+
// the render drew before the clip's seek landed.
|
|
4626
|
+
if (typeof app.awaitMediaAt === 'function') await app.awaitMediaAt(${stillTime !== undefined ? stillTime : "typeof app.playbackTime === 'number' ? app.playbackTime : 0"});
|
|
4497
4627
|
const regionUrl = app.renderRegionToDataURL(new paper.Rectangle(${region.x}, ${region.y}, ${region.width}, ${region.height}), rw, rh${region.excludeForeign === false ? '' : ', { excludeForeign: true }'});
|
|
4498
4628
|
const __excluded = Array.isArray(app.lastRegionExcluded) ? app.lastRegionExcluded.filter(function(id) { return id; }) : null;
|
|
4499
4629
|
// ITEMS THAT CROSS THE REGION'S EDGE (round 9 II, 1.78). In the sheet
|
|
@@ -4723,7 +4853,12 @@ ${stillTime !== undefined ? `
|
|
|
4723
4853
|
+ ' MB inline ceiling, and this studio has no app.exportEngine.exportToStore to page it out of — update FxTool, or lower duration/quality',
|
|
4724
4854
|
};
|
|
4725
4855
|
}
|
|
4726
|
-
|
|
4856
|
+
// The container the file IS: an encoder that cannot do H.264 at this
|
|
4857
|
+
// size writes VP9 WebM (round 12: mp4 at 12000 px came back WebM),
|
|
4858
|
+
// and naming it .mp4 breaks players. Said below in fidelity.
|
|
4859
|
+
const __mime = (blob.type && String(blob.type).split(';')[0]) || videoMimeType;
|
|
4860
|
+
const __fmt = __mime === 'video/webm' ? 'webm' : __mime === 'video/mp4' ? 'mp4' : format;
|
|
4861
|
+
return { success: true, platform, format: __fmt, framing, data: await blobToDataUrl(blob), mimeType: __mime, size: blob.size };
|
|
4727
4862
|
};
|
|
4728
4863
|
|
|
4729
4864
|
// Camera framing requires going direct to videoExporter so width/height
|
|
@@ -5072,6 +5207,56 @@ ${stillTime !== undefined ? `
|
|
|
5072
5207
|
// The applied value is the engine's own report where it gives one
|
|
5073
5208
|
// (5adfcf1c: lastVideoReport.headroom), so the result and the report agree.
|
|
5074
5209
|
if (result && result.success) result.broadcastHeadroom = { value: (__vr && typeof __vr.headroom === 'number') ? __vr.headroom : 12, defaulted: true, note: 'broadcast defaults to 12 luma codes of headroom, which keeps encoder ringing legal; pass broadcastHeadroom: 0 for the full range.' };` : ''}
|
|
5210
|
+
// FROZEN OUTPUT (round 12, 12.3): constant bitrate at 3552x1080 answered
|
|
5211
|
+
// success with 600 identical frames. The file cannot be decoded here on the
|
|
5212
|
+
// store route, so this is a size check: an animated scene whose video
|
|
5213
|
+
// carries under 0.002 bits per pixel per frame is almost certainly one
|
|
5214
|
+
// frame repeated. Labelled as a suspicion, with how to confirm it.
|
|
5215
|
+
if (result && result.success && (result.format === 'mp4' || result.format === 'webm') && result.fidelity && result.fidelity.checked && result.fidelity.checked.animated && typeof result.size === 'number') {
|
|
5216
|
+
const __d = result.dimensions || dimensions;
|
|
5217
|
+
const __frames = Math.max(1, Math.round(${videoDuration} * (settings.fps || 30)));
|
|
5218
|
+
const __bpp = (result.size * 8) / Math.max(1, __d.width * __d.height * __frames);
|
|
5219
|
+
// The studio's own count of distinct frames it fed the encoder, where it
|
|
5220
|
+
// reports one, decides; the size is only the fallback (a small dot moving on
|
|
5221
|
+
// a flat field is also a tiny file).
|
|
5222
|
+
const __vf = __vr && __vr.frames;
|
|
5223
|
+
const __distinct = __vf && typeof __vf.distinct === 'number' ? __vf.distinct : null;
|
|
5224
|
+
// The studio's decoded-output test (output_frozen) is the real answer for
|
|
5225
|
+
// an encoder freeze; the size guess stays out when it has spoken.
|
|
5226
|
+
const __engineFrozen = !!(__vr && Array.isArray(__vr.warnings) && __vr.warnings.some(function(w) { return w && w.code === 'output_frozen'; }));
|
|
5227
|
+
if (!__engineFrozen && (__distinct !== null ? __distinct <= 1 : __bpp < 0.002)) {
|
|
5228
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'possibly_frozen',
|
|
5229
|
+
message: __distinct !== null
|
|
5230
|
+
? 'the scene animates, but the studio rendered ' + __distinct + ' distinct frame(s) for the encoder — the animation did not play during the export (not an encoder fault). Check that the animation runs on the timeline over the exported range.'
|
|
5231
|
+
: 'the video carries ' + __bpp.toFixed(5) + ' bits per pixel per frame (' + result.size + ' bytes for ' + __frames + ' frames at ' + __d.width + 'x' + __d.height + ') — the encoder has most likely repeated one frame. Check two frames (ffprobe, or pinepaper_capture_frames against the scene); a smaller size, bitrateMode variable, or format webm usually avoids it.' }]);
|
|
5232
|
+
if (result.fidelity.note) delete result.fidelity.note;
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
// THE STUDIO'S OWN CODED VIDEO WARNINGS (e.g. an output-side freeze the
|
|
5236
|
+
// input-frame count cannot see): passed through when they carry a code this
|
|
5237
|
+
// tool does not already raise from the numbers. Plain-string warnings are
|
|
5238
|
+
// the bitrate / luma texts, raised above from their numbers instead.
|
|
5239
|
+
if (result && result.success && __vr && Array.isArray(__vr.warnings)) {
|
|
5240
|
+
// format_substituted is NOT here: the studio's own (agreed with FxTool)
|
|
5241
|
+
// is the better-informed one, so it passes and this tool's is skipped.
|
|
5242
|
+
const __mine = ['bitrate_below_floor', 'luma_out_of_range', 'determinism_not_pinned', 'dimensions_rounded', 'possibly_frozen', 'alpha_not_applied', 'alpha_keyframe_mismatch'];
|
|
5243
|
+
const __extra = __vr.warnings.filter(function(w) { return w && typeof w === 'object' && typeof w.code === 'string' && __mine.indexOf(w.code) < 0; })
|
|
5244
|
+
.map(function(w) { return { code: w.code, message: String(w.message || w.code) }; });
|
|
5245
|
+
if (__extra.length) {
|
|
5246
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5247
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat(__extra);
|
|
5248
|
+
if (result.fidelity.note) delete result.fidelity.note;
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
// A DIFFERENT CONTAINER THAN ASKED (round 12, 12.7): the file is named by
|
|
5252
|
+
// what it is, and the substitution is said, not left to a player to find.
|
|
5253
|
+
const __studioSaidFormat = !!(__vr && Array.isArray(__vr.warnings) && __vr.warnings.some(function(w) { return w && w.code === 'format_substituted'; }));
|
|
5254
|
+
if (!__studioSaidFormat && result && result.success && (result.format === 'mp4' || result.format === 'webm') && result.format !== format && (format === 'mp4' || format === 'webm')) {
|
|
5255
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5256
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'format_substituted',
|
|
5257
|
+
message: format + ' was asked for, but the browser encoder could not make ' + format + ' at ' + ((result.dimensions && result.dimensions.width) || '') + 'x' + ((result.dimensions && result.dimensions.height) || '') + ((__vr && __vr.codecFallback && __vr.codecFallback.reason) ? ' (' + __vr.codecFallback.reason + ')' : '') + ', so the file is ' + result.format + ' (saved with that extension). For ' + format + ' at this size, export smaller (scale) and upscale, or re-encode the ' + result.format + '.' }]);
|
|
5258
|
+
if (result.fidelity.note) delete result.fidelity.note;
|
|
5259
|
+
}
|
|
5075
5260
|
// AN ODD SIDE ROUNDED UP (FxTool e64a5260). H.264 needs even dimensions, so
|
|
5076
5261
|
// 1200x675 came out 1200x676 — and platform specs are exact-pixel. The
|
|
5077
5262
|
// result's dimensions become what the file IS, and fidelity says why.
|
|
@@ -5125,7 +5310,10 @@ ${Object.keys(videoEncodeOpts).length ? ` if (result && result.success) {
|
|
|
5125
5310
|
const __vw = [];
|
|
5126
5311
|
// The engine joins its warnings into one string (bitrate floor, then
|
|
5127
5312
|
// luma), so each is raised from its own numbers, not from that text.
|
|
5128
|
-
|
|
5313
|
+
// The studio's own measured bitrate_not_honoured, where it sends one,
|
|
5314
|
+
// replaces this estimate.
|
|
5315
|
+
const __engineBitrate = !!(__vr && Array.isArray(__vr.warnings) && __vr.warnings.some(function(w) { return w && w.code === 'bitrate_not_honoured'; }));
|
|
5316
|
+
if (!__engineBitrate && __vr && typeof __vr.achievedBitrate === 'number' && typeof __vr.bitrateFloor === 'number' && __vr.achievedBitrate < __vr.bitrateFloor * 0.95) {
|
|
5129
5317
|
__vw.push({ code: 'bitrate_below_floor', message: 'achieved ' + (__vr.achievedBitrate / 1e6).toFixed(2) + ' Mbps against a ' + (__vr.bitrateFloor / 1e6).toFixed(1) + ' Mbps floor: the browser encoder treats the bitrate as a ceiling and does not pad simple content. Re-encode for delivery (e.g. ffmpeg -b:v with -minrate / -bufsize) if the spec requires the floor.' });
|
|
5130
5318
|
}
|
|
5131
5319
|
if (!__vr || typeof __vr !== 'object') {
|
|
@@ -5139,6 +5327,17 @@ ${Object.keys(videoEncodeOpts).length ? ` if (result && result.success) {
|
|
|
5139
5327
|
if (result.fidelity.note) delete result.fidelity.note;
|
|
5140
5328
|
}
|
|
5141
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
|
+
}
|
|
5142
5341
|
` : ''} const __ar = __vx && __vx.lastAudioReport;
|
|
5143
5342
|
if (result && result.success && __ar && typeof __ar === 'object') {
|
|
5144
5343
|
result.audio = __ar;
|
|
@@ -6770,19 +6969,25 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6770
6969
|
`.trim();
|
|
6771
6970
|
}
|
|
6772
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).
|
|
6773
6975
|
return `
|
|
6774
6976
|
// Get available easing functions
|
|
6775
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; }
|
|
6776
6986
|
return {
|
|
6777
|
-
easings:
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
easeOut: 'Fast start, slow end',
|
|
6782
|
-
easeInOut: 'Slow start and end',
|
|
6783
|
-
bounce: 'Bouncing effect at end',
|
|
6784
|
-
elastic: 'Spring-like overshoot'
|
|
6785
|
-
}
|
|
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.'
|
|
6786
6991
|
};
|
|
6787
6992
|
})();
|
|
6788
6993
|
`.trim();
|
|
@@ -6905,7 +7110,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6905
7110
|
`.trim();
|
|
6906
7111
|
}
|
|
6907
7112
|
generateImportImage(input) {
|
|
6908
|
-
const { url, position, maxWidth, maxHeight, mask, smoothing } = input;
|
|
7113
|
+
const { url, position, maxWidth, maxHeight, mask, smoothing, maxEdge, nativeSize } = input;
|
|
6909
7114
|
// Each fragment used to be written with a LEADING comma, as if it followed
|
|
6910
7115
|
// an entry that no longer exists — so the first one opened the object with
|
|
6911
7116
|
// `{, position: …}` and the emitted code was a syntax error. Passing any
|
|
@@ -6916,6 +7121,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6916
7121
|
position ? `position: { x: ${position.x}, y: ${position.y} }` : '',
|
|
6917
7122
|
maxWidth !== undefined ? `maxWidth: ${maxWidth}` : '',
|
|
6918
7123
|
maxHeight !== undefined ? `maxHeight: ${maxHeight}` : '',
|
|
7124
|
+
nativeSize ? 'nativeSize: true' : '',
|
|
6919
7125
|
].filter(Boolean);
|
|
6920
7126
|
const optsLiteral = optParts.length ? `{ ${optParts.join(', ')} }` : '{}';
|
|
6921
7127
|
return `
|
|
@@ -6971,7 +7177,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6971
7177
|
fr.readAsDataURL(blob);
|
|
6972
7178
|
});
|
|
6973
7179
|
}
|
|
6974
|
-
const entry = await app.imageTools.uploadFromURL(src);
|
|
7180
|
+
const entry = await app.imageTools.uploadFromURL(src${maxEdge !== undefined ? `, { maxEdge: ${maxEdge === 'native' ? 'Infinity' : maxEdge} }` : ''});
|
|
6975
7181
|
const opts = ${optsLiteral};
|
|
6976
7182
|
const raster = await app.imageTools.placeImage(entry.id, Object.keys(opts).length > 0 ? opts : undefined);
|
|
6977
7183
|
${smoothing ? ` // Pixel art wants 'off': Paper's Raster draws with smoothing 'low' by
|
|
@@ -7010,7 +7216,13 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7010
7216
|
success: true,
|
|
7011
7217
|
itemId: itemId,
|
|
7012
7218
|
message: 'Image imported and placed on canvas.',
|
|
7013
|
-
bounds: { x: b.x, y: b.y, width: b.width, height: b.height }${mask ? `,\n mask: '${mask}'` : ''}
|
|
7219
|
+
bounds: { x: b.x, y: b.y, width: b.width, height: b.height }${mask ? `,\n mask: '${mask}'` : ''},
|
|
7220
|
+
// Stored smaller than its source (the studio's cap): said, with the
|
|
7221
|
+
// way to keep every pixel. The studio reports native size where it can.
|
|
7222
|
+
...((entry && entry.nativeWidth && entry.width && entry.nativeWidth > entry.width)
|
|
7223
|
+
? { stored: { width: entry.width, height: entry.height }, native: { width: entry.nativeWidth, height: entry.nativeHeight },
|
|
7224
|
+
note: 'the studio stored this image at ' + entry.width + 'x' + entry.height + ' (its source is ' + entry.nativeWidth + 'x' + entry.nativeHeight + '). Pass maxEdge: "native" to keep every pixel.' }
|
|
7225
|
+
: {})
|
|
7014
7226
|
};
|
|
7015
7227
|
} catch (e) {
|
|
7016
7228
|
return { error: 'Failed to import image: ' + e.message };
|
|
@@ -9525,24 +9737,55 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9525
9737
|
const n = Math.min(1200, Math.max(3, Math.round(duration * fps)));
|
|
9526
9738
|
return `
|
|
9527
9739
|
// Accessibility measurements (${checks.join(', ')})
|
|
9528
|
-
(function() {
|
|
9740
|
+
(async function() {
|
|
9529
9741
|
const out = { success: true, fps: ${fps}, duration: ${duration} };
|
|
9530
9742
|
const rgba = function(c) {
|
|
9531
9743
|
if (!c || c.gradient || typeof c.red !== 'number') return null;
|
|
9532
9744
|
return { r: Math.round(c.red * 255), g: Math.round(c.green * 255), b: Math.round(c.blue * 255), a: typeof c.alpha === 'number' ? c.alpha : 1 };
|
|
9533
9745
|
};
|
|
9534
9746
|
out.page = (app.canvasEl && app.canvasEl.style && app.canvasEl.style.backgroundColor) || null;
|
|
9535
|
-
${checks.includes('contrast') ? `
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
const
|
|
9543
|
-
|
|
9544
|
-
|
|
9747
|
+
${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text is judged where it is on screen.
|
|
9748
|
+
// Reading only the current playhead found no visible text in a scene of
|
|
9749
|
+
// timed captions and passed with nothing checked. The times: 0, inside each
|
|
9750
|
+
// text's lifetime, every keyframe time, and every second of the duration —
|
|
9751
|
+
// the playhead is moved, lifetimes applied, and put back.
|
|
9752
|
+
const all = (app.itemRegistry && typeof app.itemRegistry.getAll === 'function') ? app.itemRegistry.getAll() : [];
|
|
9753
|
+
const snap = function(t) {
|
|
9754
|
+
const texts = all.filter(function(e) { return e && e.item && e.item.className === 'PointText' && e.item.visible !== false && e.item.opacity !== 0 && String(e.item.content || '').trim() !== ''; });
|
|
9755
|
+
const solids = all.filter(function(e) { return e && e.item && e.item.className !== 'PointText' && e.item.visible !== false && rgba(e.item.fillColor) && e.item.bounds; });
|
|
9756
|
+
return texts.map(function(e) {
|
|
9757
|
+
const it = e.item, c = it.bounds.center;
|
|
9758
|
+
const under = solids.filter(function(s) { return s.item.bounds.contains(c) && typeof s.item.isBelow === 'function' && s.item.isBelow(it); });
|
|
9759
|
+
const top = under.reduce(function(best, s) { return (!best || (typeof s.item.isAbove === 'function' && s.item.isAbove(best.item))) ? s : best; }, null);
|
|
9760
|
+
const w = String(it.fontWeight || '');
|
|
9761
|
+
return { id: e.itemId, t: t, content: String(it.content), fill: rgba(it.fillColor), fontSize: it.fontSize || 0,
|
|
9762
|
+
bold: w === 'bold' || Number(w) >= 700, behind: top ? rgba(top.item.fillColor) : null, behindId: top ? top.itemId : undefined };
|
|
9763
|
+
});
|
|
9764
|
+
};
|
|
9765
|
+
const times = [0];
|
|
9766
|
+
all.forEach(function(e) {
|
|
9767
|
+
const d = (e && e.item && e.item.data) || {};
|
|
9768
|
+
if (typeof d.bornAt === 'number') times.push(d.bornAt + Math.min(0.1, (typeof d.ttl === 'number' ? d.ttl : 1) / 2));
|
|
9769
|
+
if (Array.isArray(d.keyframes)) d.keyframes.forEach(function(k) { if (k && typeof k.time === 'number') times.push(k.time); });
|
|
9545
9770
|
});
|
|
9771
|
+
Array.from({ length: Math.floor(${duration}) + 1 }, function(_, i) { times.push(i); });
|
|
9772
|
+
const uniq = times.filter(function(t, i) { return t >= 0 && t <= ${duration} + 1e-6 && times.indexOf(t) === i; }).sort(function(a, b) { return a - b; }).slice(0, 60);
|
|
9773
|
+
const L = globalThis.__ppMcp;
|
|
9774
|
+
const at = function(t) {
|
|
9775
|
+
if (typeof app.setPlaybackTime === 'function') app.setPlaybackTime(t);
|
|
9776
|
+
if (L && L.lifetimeApp === app && typeof L.applyLifetimes === 'function') { try { L.applyLifetimes(t); } catch (_) { /* sampled as is */ } }
|
|
9777
|
+
if (typeof paper !== 'undefined' && paper.view && typeof paper.view.update === 'function') paper.view.update();
|
|
9778
|
+
};
|
|
9779
|
+
const prevT = typeof app.playbackTime === 'number' ? app.playbackTime : 0;
|
|
9780
|
+
out.texts = [];
|
|
9781
|
+
if (typeof app.setPlaybackTime === 'function') {
|
|
9782
|
+
uniq.forEach(function(t) { at(t); out.texts = out.texts.concat(snap(t)); });
|
|
9783
|
+
at(prevT);
|
|
9784
|
+
out.sampledTimes = uniq.length;
|
|
9785
|
+
} else {
|
|
9786
|
+
out.texts = snap(prevT);
|
|
9787
|
+
out.sampledTimes = 1;
|
|
9788
|
+
}
|
|
9546
9789
|
` : ''}${checks.includes('flash') ? ` if (typeof app.captureFramesAt !== 'function') {
|
|
9547
9790
|
out.flashUnavailable = 'this studio has no deterministic frame capture (app.captureFramesAt), so flashing was not checked.';
|
|
9548
9791
|
} else {
|
|
@@ -9552,7 +9795,9 @@ ${checks.includes('contrast') ? ` const all = (app.itemRegistry && typeof app.i
|
|
|
9552
9795
|
const ctx = small.getContext('2d', { willReadFrequently: true });
|
|
9553
9796
|
const lut = Array.from({ length: 256 }, function(_, v) { const x = v / 255; return x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); });
|
|
9554
9797
|
const times = Array.from({ length: ${n} }, function(_, i) { return i / ${fps}; });
|
|
9555
|
-
|
|
9798
|
+
// Waits for video per frame where the studio can (12.6).
|
|
9799
|
+
const __cap = typeof app.captureFramesAtAsync === 'function' ? app.captureFramesAtAsync.bind(app) : app.captureFramesAt.bind(app);
|
|
9800
|
+
out.series = await __cap(times, { capture: function(c) {
|
|
9556
9801
|
// Transparent canvas shows the page's CSS background, so composite on it.
|
|
9557
9802
|
ctx.fillStyle = out.page || '#ffffff';
|
|
9558
9803
|
ctx.fillRect(0, 0, W, H);
|
|
@@ -9573,12 +9818,12 @@ ${checks.includes('contrast') ? ` const all = (app.itemRegistry && typeof app.i
|
|
|
9573
9818
|
})();`.trim();
|
|
9574
9819
|
}
|
|
9575
9820
|
generateCaptureFrames(input) {
|
|
9576
|
-
const timesJson = JSON.stringify(input.times);
|
|
9821
|
+
const timesJson = JSON.stringify(input.times ?? []);
|
|
9577
9822
|
const seed = input.seed !== undefined ? input.seed : 0;
|
|
9578
9823
|
const includeDataUrls = !!input.includeDataUrls;
|
|
9579
9824
|
return `
|
|
9580
9825
|
// Deterministic frame capture
|
|
9581
|
-
(function() {
|
|
9826
|
+
(async function() {
|
|
9582
9827
|
if (typeof app.captureFramesAt !== 'function') {
|
|
9583
9828
|
return { success: false, error: 'app.captureFramesAt unavailable — update FxTool to a build with the deterministic capture entrypoint' };
|
|
9584
9829
|
}
|
|
@@ -9595,7 +9840,24 @@ ${checks.includes('contrast') ? ` const all = (app.itemRegistry && typeof app.i
|
|
|
9595
9840
|
// plain 9-text scene (round 7 X, #7) on studios without hashFrame. A native
|
|
9596
9841
|
// reduce is not a generated loop and is not counted; same djb2-xor.
|
|
9597
9842
|
: function(s) { return Array.prototype.reduce.call(s, function(h, c) { return (((h << 5) + h) ^ c.charCodeAt(0)) >>> 0; }, 5381).toString(16); };
|
|
9598
|
-
|
|
9843
|
+
// A VIDEO ON SCREEN IS WAITED FOR (round 12, 12.6): the sync capture drew
|
|
9844
|
+
// before a clip's seek landed, so a frame showed the previous time's
|
|
9845
|
+
// picture. captureFramesAtAsync waits per frame; older studios keep the
|
|
9846
|
+
// sync one (and say video-not-synced themselves).
|
|
9847
|
+
//
|
|
9848
|
+
// What is REPORTED is what happened, not what exists (retest of 009f00e: a
|
|
9849
|
+
// "videoSynced: true" capture right after an upload showed the video black).
|
|
9850
|
+
// videoWait says whether the studio waited per frame; firstSeek is the
|
|
9851
|
+
// studio's own answer for the first time (seeked / timedOut / skipped).
|
|
9852
|
+
// A seek that times out later is an engine warning (video-seek-timeout),
|
|
9853
|
+
// forwarded in governor.engineWarnings.
|
|
9854
|
+
const __waits = typeof app.captureFramesAtAsync === 'function';
|
|
9855
|
+
let __firstSeek = null;
|
|
9856
|
+
if (typeof app.awaitMediaAt === 'function' && ${timesJson}.length) {
|
|
9857
|
+
try { __firstSeek = await app.awaitMediaAt(${timesJson}[0]); } catch (e) { __firstSeek = { error: String(e && e.message || e) }; }
|
|
9858
|
+
}
|
|
9859
|
+
const __cap = __waits ? app.captureFramesAtAsync.bind(app) : app.captureFramesAt.bind(app);
|
|
9860
|
+
const frames = await __cap(${timesJson}, {
|
|
9599
9861
|
seed: ${seed},
|
|
9600
9862
|
capture: function(c, t, i) {
|
|
9601
9863
|
// Prefer app.captureFrameDataURL(): it renders only the used region, so an
|
|
@@ -9610,7 +9872,74 @@ ${checks.includes('contrast') ? ` const all = (app.itemRegistry && typeof app.i
|
|
|
9610
9872
|
}
|
|
9611
9873
|
});
|
|
9612
9874
|
const uniqueHashes = new Set(frames.map(function(f) { return f.hash; })).size;
|
|
9613
|
-
return { success: true, seed: ${seed}, frameCount: frames.length, uniqueHashes: uniqueHashes, allIdentical: uniqueHashes <= 1,
|
|
9875
|
+
return { success: true, seed: ${seed}, frameCount: frames.length, uniqueHashes: uniqueHashes, allIdentical: uniqueHashes <= 1,
|
|
9876
|
+
videoWait: __waits ? 'awaited per frame' : 'not supported by this studio (frames may show a video before its seek lands)',
|
|
9877
|
+
...(__firstSeek ? { firstSeek: __firstSeek } : {}),
|
|
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.' } : {}),
|
|
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 };
|
|
9614
9943
|
})();`.trim();
|
|
9615
9944
|
}
|
|
9616
9945
|
/**
|
|
@@ -9758,6 +10087,7 @@ ${checks.includes('contrast') ? ` const all = (app.itemRegistry && typeof app.i
|
|
|
9758
10087
|
const opts = JSON.stringify({
|
|
9759
10088
|
...(input.position ? { position: input.position } : {}),
|
|
9760
10089
|
...(input.scale !== undefined ? { scale: input.scale } : {}),
|
|
10090
|
+
...(input.fit !== undefined ? { fit: input.fit } : {}),
|
|
9761
10091
|
...(input.timeOffset !== undefined ? { timeOffset: input.timeOffset } : {}),
|
|
9762
10092
|
...(input.clipInPoint !== undefined ? { clipInPoint: input.clipInPoint } : {}),
|
|
9763
10093
|
...(input.clipOutPoint !== undefined ? { clipOutPoint: input.clipOutPoint } : {}),
|
|
@@ -9989,6 +10319,45 @@ ${guard}
|
|
|
9989
10319
|
// The facade throws on a missing raster/matte rather than returning {ok:false}.
|
|
9990
10320
|
return { success: false, error: e && e.message ? e.message : String(e) };
|
|
9991
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' };
|
|
9992
10361
|
})();`.trim();
|
|
9993
10362
|
}
|
|
9994
10363
|
case 'stop_live_matte': {
|
|
@@ -10160,6 +10529,21 @@ ${guard}
|
|
|
10160
10529
|
const S = (v) => JSON.stringify(v);
|
|
10161
10530
|
const wrap = (comment, body) => `\n// ${comment}\n(function() {\n${body}\n})();`.trim();
|
|
10162
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();
|
|
10163
10547
|
case 'list_media':
|
|
10164
10548
|
return wrap('Medium: list', ` if (typeof app.listDesignMedia !== 'function') { return { success: false, error: 'medium axis unavailable — update FxTool' }; }
|
|
10165
10549
|
const media = app.listDesignMedia();
|
|
@@ -10476,12 +10860,15 @@ ${guard}
|
|
|
10476
10860
|
// World3D: create — terrain, sky, shadows, walkable character, under the Paper canvas
|
|
10477
10861
|
(async function() {
|
|
10478
10862
|
if (typeof app.createWorld3D !== 'function') { return { success: false, error: 'app.createWorld3D unavailable — update FxTool to a world3d-capable build' }; }
|
|
10479
|
-
await app.createWorld3D(${S(world3dColors(input.spec ?? 'forest'))}, ${opts});
|
|
10863
|
+
await app.createWorld3D(${S(world3dSpecColors(world3dColors(input.spec ?? 'forest')))}, ${opts});
|
|
10480
10864
|
// describe() is the world's own parameter schema — return the preset list +
|
|
10481
10865
|
// top-level keys so the agent knows what configure can touch, without the
|
|
10482
10866
|
// full multi-KB schema on every create.
|
|
10483
10867
|
const d = typeof app.describeWorld3D === 'function' ? await app.describeWorld3D() : null;
|
|
10484
|
-
|
|
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 };
|
|
10485
10872
|
})();`.trim();
|
|
10486
10873
|
}
|
|
10487
10874
|
case 'describe':
|
|
@@ -10497,9 +10884,9 @@ ${guard}
|
|
|
10497
10884
|
(function() {
|
|
10498
10885
|
if (typeof app.configureWorld3D !== 'function') { return { success: false, error: 'app.configureWorld3D unavailable — update FxTool' }; }
|
|
10499
10886
|
${needWorld}
|
|
10500
|
-
const r = app.configureWorld3D(${S(world3dColors(input.patch))});
|
|
10887
|
+
const r = app.configureWorld3D(${S(world3dSpecColors(world3dColors(input.patch)))});
|
|
10501
10888
|
// The validator names the right key on a wrong one — forward it verbatim.
|
|
10502
|
-
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' };
|
|
10503
10890
|
})();`.trim();
|
|
10504
10891
|
case 'add_actor': {
|
|
10505
10892
|
const actor = S({
|
|
@@ -10612,7 +10999,7 @@ ${needWorld}
|
|
|
10612
10999
|
// @engine-methods extrudeToMesh latheToMesh importOBJToWorld importGLTFToWorld
|
|
10613
11000
|
if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool to a world3d-capable build' }; }
|
|
10614
11001
|
const r = await app.${fn}(${S(input.pathId)}, ${S(mesh)});
|
|
10615
|
-
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' }; }
|
|
10616
11003
|
return { success: true, meshId: r.id, op: ${S(input.action === 'extrude_path' ? 'extrude' : 'lathe')}, sourceId: ${S(input.pathId)} };
|
|
10617
11004
|
})();`.trim();
|
|
10618
11005
|
}
|
|
@@ -10639,8 +11026,8 @@ ${needWorld}
|
|
|
10639
11026
|
(function() {
|
|
10640
11027
|
if (typeof app.addWorldLight !== 'function') { return { success: false, error: 'app.addWorldLight unavailable — update FxTool' }; }
|
|
10641
11028
|
${needWorld}
|
|
10642
|
-
const r = app.addWorldLight(${S(input.light
|
|
10643
|
-
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' }; }
|
|
10644
11031
|
return { success: true, lightId: r.id, light: r.light };
|
|
10645
11032
|
})();`.trim();
|
|
10646
11033
|
case 'set_light':
|
|
@@ -10648,7 +11035,7 @@ ${needWorld}
|
|
|
10648
11035
|
// World3D: edit a light in place
|
|
10649
11036
|
(function() {
|
|
10650
11037
|
if (typeof app.setWorldLight !== 'function') { return { success: false, error: 'app.setWorldLight unavailable — update FxTool' }; }
|
|
10651
|
-
return { success: !!app.setWorldLight(${S(input.lightId)}, ${S(input.light
|
|
11038
|
+
return { success: !!app.setWorldLight(${S(input.lightId)}, ${S(world3dHexColor(input.light))}) };
|
|
10652
11039
|
})();`.trim();
|
|
10653
11040
|
case 'remove_light':
|
|
10654
11041
|
return `
|
|
@@ -10671,8 +11058,8 @@ ${needWorld}
|
|
|
10671
11058
|
(function() {
|
|
10672
11059
|
if (typeof app.addWorldMaterial !== 'function') { return { success: false, error: 'app.addWorldMaterial unavailable — update FxTool' }; }
|
|
10673
11060
|
${needWorld}
|
|
10674
|
-
const r = app.addWorldMaterial(${S(input.material
|
|
10675
|
-
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' }; }
|
|
10676
11063
|
return { success: true, materialId: r.id, material: r.material };
|
|
10677
11064
|
})();`.trim();
|
|
10678
11065
|
case 'set_material':
|
|
@@ -10680,7 +11067,7 @@ ${needWorld}
|
|
|
10680
11067
|
// World3D: patch a material — every object referencing it changes next frame
|
|
10681
11068
|
(function() {
|
|
10682
11069
|
if (typeof app.setWorldMaterial !== 'function') { return { success: false, error: 'app.setWorldMaterial unavailable — update FxTool' }; }
|
|
10683
|
-
return { success: !!app.setWorldMaterial(${S(input.materialId)}, ${S(input.material
|
|
11070
|
+
return { success: !!app.setWorldMaterial(${S(input.materialId)}, ${S(world3dHexColor(input.material))}) };
|
|
10684
11071
|
})();`.trim();
|
|
10685
11072
|
case 'remove_material':
|
|
10686
11073
|
return `
|
|
@@ -10739,7 +11126,7 @@ ${needWorld}
|
|
|
10739
11126
|
(function() {
|
|
10740
11127
|
if (typeof app.setWorldNavTarget !== 'function') { return { success: false, error: 'app.setWorldNavTarget unavailable — update FxTool' }; }
|
|
10741
11128
|
const r = app.setWorldNavTarget(${S(input.navTarget)});
|
|
10742
|
-
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' }; }
|
|
10743
11130
|
return { success: true, navTarget: ${S(input.navTarget)} };
|
|
10744
11131
|
})();`.trim();
|
|
10745
11132
|
case 'get_nav_target':
|
|
@@ -11239,7 +11626,10 @@ ${guard(fn)}
|
|
|
11239
11626
|
}
|
|
11240
11627
|
case 'render_soundtrack': {
|
|
11241
11628
|
const opts = {
|
|
11242
|
-
|
|
11629
|
+
// A top-level duration was stripped by the schema and the render
|
|
11630
|
+
// used its default length, silently; both spellings are read now.
|
|
11631
|
+
...(typeof input.duration === 'number' ? { duration: input.duration }
|
|
11632
|
+
: typeof input.options?.duration === 'number' ? { duration: input.options.duration } : {}),
|
|
11243
11633
|
...(input.sampleRate !== undefined ? { sampleRate: input.sampleRate } : {}),
|
|
11244
11634
|
...(input.bitDepth !== undefined ? { bitDepth: input.bitDepth } : {}),
|
|
11245
11635
|
};
|
|
@@ -12134,4 +12524,98 @@ return {
|
|
|
12134
12524
|
fonts: fonts,
|
|
12135
12525
|
};`;
|
|
12136
12526
|
}
|
|
12527
|
+
/**
|
|
12528
|
+
* AUTO-KEY FROM AN AGENT (plan C5a): "at t, make it orange" writes a key at t,
|
|
12529
|
+
* the way a user's drag does with auto-key on — not a static edit.
|
|
12530
|
+
*
|
|
12531
|
+
* The key is MERGED into the item's track (addAnimation mode 'merge'): other
|
|
12532
|
+
* keys stay. So the track stays whole:
|
|
12533
|
+
* - the new key also carries every OTHER animated property's value at t
|
|
12534
|
+
* (getInterpolatedState), because a key missing a property the others
|
|
12535
|
+
* animate can break that property's curve;
|
|
12536
|
+
* - a property the track did not animate yet is added to every existing key
|
|
12537
|
+
* (its old value before t, the new one from t), and a first key at 0 holds
|
|
12538
|
+
* the old value, so the change eases in from what it was instead of
|
|
12539
|
+
* jumping.
|
|
12540
|
+
* No duration is passed: addAnimation starts timeline playback when given one.
|
|
12541
|
+
* The result names each property's previous value and what was written.
|
|
12542
|
+
*/
|
|
12543
|
+
export function generateAutoKeyCode(itemId, props, atTime, easing) {
|
|
12544
|
+
return `
|
|
12545
|
+
// Auto-key ${itemId} at ${atTime}s
|
|
12546
|
+
(function() {
|
|
12547
|
+
const item = app.getItemById(${JSON.stringify(itemId)});
|
|
12548
|
+
if (!item) return { success: false, error: ${JSON.stringify(`no item "${itemId}" on the canvas — check the id with pinepaper_get_items.`)} };
|
|
12549
|
+
if (typeof app.addAnimation !== 'function') return { success: false, error: 'this studio cannot write keyframes (no addAnimation).' };
|
|
12550
|
+
const AT = ${atTime};
|
|
12551
|
+
const NEW = ${JSON.stringify(props)};
|
|
12552
|
+
const track = (item.data && Array.isArray(item.data.keyframes)) ? item.data.keyframes : [];
|
|
12553
|
+
const animated = track.reduce(function(a, k) { Object.keys((k && k.properties) || {}).forEach(function(p) { if (a.indexOf(p) < 0) a.push(p); }); return a; }, []);
|
|
12554
|
+
// The static value of a property the track does not animate yet.
|
|
12555
|
+
const read = function(p) {
|
|
12556
|
+
const c = function(v) { return v && typeof v.toCSS === 'function' ? v.toCSS(true) : v; };
|
|
12557
|
+
if (p === 'x') return item.position && item.position.x;
|
|
12558
|
+
if (p === 'y') return item.position && item.position.y;
|
|
12559
|
+
if (p === 'scale') return item.scaling ? item.scaling.x : 1;
|
|
12560
|
+
if (p === 'scaleX') return item.scaling ? item.scaling.x : 1;
|
|
12561
|
+
if (p === 'scaleY') return item.scaling ? item.scaling.y : 1;
|
|
12562
|
+
if (p === 'color' || p === 'fillColor') return c(item.fillColor);
|
|
12563
|
+
if (p === 'strokeColor') return c(item.strokeColor);
|
|
12564
|
+
if (p === 'width') return item.bounds && item.bounds.width;
|
|
12565
|
+
if (p === 'height') return item.bounds && item.bounds.height;
|
|
12566
|
+
const v = item[p];
|
|
12567
|
+
return (v === undefined || typeof v === 'function') ? undefined : c(v);
|
|
12568
|
+
};
|
|
12569
|
+
const state = (track.length && typeof app.getInterpolatedState === 'function') ? (app.getInterpolatedState(item, AT) || {}) : {};
|
|
12570
|
+
const byTime = {};
|
|
12571
|
+
const put = function(t, p, v, e) { const k = byTime[t] || (byTime[t] = { time: t, properties: {} }); k.properties[p] = v; if (e) k.easing = e; };
|
|
12572
|
+
const changes = [];
|
|
12573
|
+
Object.keys(NEW).forEach(function(p) {
|
|
12574
|
+
const wasAnimated = animated.indexOf(p) >= 0;
|
|
12575
|
+
const before = wasAnimated ? state[p] : read(p);
|
|
12576
|
+
if (!wasAnimated && before !== undefined) {
|
|
12577
|
+
track.forEach(function(k) { put(k.time, p, k.time < AT ? before : NEW[p]); });
|
|
12578
|
+
if (!track.length && AT > 0) put(0, p, before);
|
|
12579
|
+
}
|
|
12580
|
+
changes.push({ property: p, before: before === undefined ? null : before, after: NEW[p], easedFrom: before === undefined ? 'unknown — keyed at t only' : (wasAnimated ? 'the track' : 'its value before t') });
|
|
12581
|
+
});
|
|
12582
|
+
// The key at t: the new values, plus every other animated property as it is at t.
|
|
12583
|
+
animated.forEach(function(p) { if (!(p in NEW) && state[p] !== undefined) put(AT, p, state[p]); });
|
|
12584
|
+
Object.keys(NEW).forEach(function(p) { put(AT, p, NEW[p], ${JSON.stringify(easing ?? null)}); });
|
|
12585
|
+
const keys = Object.keys(byTime).map(function(t) { return byTime[t]; }).sort(function(a, b) { return a.time - b.time; });
|
|
12586
|
+
const beforeTimes = track.map(function(k) { return k.time; });
|
|
12587
|
+
app.addAnimation(${JSON.stringify(itemId)}, keys, { mode: 'merge' });
|
|
12588
|
+
const after = (item.data && Array.isArray(item.data.keyframes)) ? item.data.keyframes : [];
|
|
12589
|
+
const kept = beforeTimes.every(function(t) { return after.some(function(k) { return k.time === t; }); });
|
|
12590
|
+
if (!kept) return { success: false, error: 'this studio replaced the track instead of merging into it (no merge mode): the earlier keys are gone. Put every key in one keyframe_animate call.', atTime: AT };
|
|
12591
|
+
if (app.historyManager) app.historyManager.saveState();
|
|
12592
|
+
return { success: true, itemId: ${JSON.stringify(itemId)}, atTime: AT, autoKeyed: true, changes: changes, keys: after.length,
|
|
12593
|
+
note: 'written as keyframes (a temporal edit), not a static change; pinepaper_query_mutations shows the series.' };
|
|
12594
|
+
})();`.trim();
|
|
12595
|
+
}
|
|
12596
|
+
/**
|
|
12597
|
+
* A property's time series (plan C5a): the item's own keyframe track, read
|
|
12598
|
+
* back. Until the engine keeps a mutation log (C3) this is the keys, not the
|
|
12599
|
+
* history of who changed what — the result says so.
|
|
12600
|
+
*/
|
|
12601
|
+
export function generateQueryMutationsCode(itemId, property, range, fps = 30) {
|
|
12602
|
+
return `
|
|
12603
|
+
// Keyframe series for ${itemId}${property ? '.' + property : ''}
|
|
12604
|
+
(function() {
|
|
12605
|
+
const item = app.getItemById(${JSON.stringify(itemId)});
|
|
12606
|
+
if (!item) return { success: false, error: ${JSON.stringify(`no item "${itemId}" on the canvas.`)} };
|
|
12607
|
+
const P = ${JSON.stringify(property ?? null)}, R = ${JSON.stringify(range ?? null)}, FPS = ${fps};
|
|
12608
|
+
const track = (item.data && Array.isArray(item.data.keyframes)) ? item.data.keyframes : [];
|
|
12609
|
+
const series = {};
|
|
12610
|
+
track.forEach(function(k) {
|
|
12611
|
+
if (R && (k.time < R[0] || k.time > R[1])) return;
|
|
12612
|
+
Object.keys((k && k.properties) || {}).forEach(function(p) {
|
|
12613
|
+
if (P && p !== P) return;
|
|
12614
|
+
(series[p] = series[p] || []).push({ t: k.time, frame: Math.round(k.time * FPS), value: k.properties[p], easing: k.easing || null });
|
|
12615
|
+
});
|
|
12616
|
+
});
|
|
12617
|
+
return { success: true, itemId: ${JSON.stringify(itemId)}, source: 'keyframes', series: series, keys: track.length,
|
|
12618
|
+
note: 'the item\\'s keyframe track, read back. Who changed what and the before value per edit need the studio\\'s mutation log, which this studio does not expose yet.' };
|
|
12619
|
+
})();`.trim();
|
|
12620
|
+
}
|
|
12137
12621
|
//# sourceMappingURL=code-generator.js.map
|