@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.
- package/README.md +92 -0
- 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 +138 -35
- 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 +4 -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/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 +226 -10
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.js +4 -4
- 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/types/code-generator.d.ts +40 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +260 -26
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +2170 -698
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +152 -36
- package/dist/types/schemas.js.map +1 -1
- 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/vendor/design/design-style.js +1 -1
- package/manifest.json +1 -1
- 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
|
-
|
|
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:
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
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
|
-
|
|
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':
|