@pinepaper.studio/mcp-server 1.6.17 → 1.6.19
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 +189 -10
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/agent/export-manager.js +2 -2
- package/dist/agent/session-manager.d.ts +8 -0
- package/dist/agent/session-manager.d.ts.map +1 -1
- package/dist/agent/session-manager.js +17 -2
- package/dist/agent/session-manager.js.map +1 -1
- package/dist/assets/asset-manager.d.ts +28 -1
- package/dist/assets/asset-manager.d.ts.map +1 -1
- package/dist/assets/asset-manager.js +65 -25
- package/dist/assets/asset-manager.js.map +1 -1
- package/dist/assets/index.d.ts +1 -3
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +1 -3
- package/dist/assets/index.js.map +1 -1
- package/dist/assets/repositories/iconify.js +1 -1
- package/dist/assets/repositories/iconify.js.map +1 -1
- package/dist/assets/repositories/openclipart.js +1 -1
- package/dist/assets/repositories/openclipart.js.map +1 -1
- package/dist/assets/repositories/svgrepo.js +2 -2
- package/dist/assets/repositories/svgrepo.js.map +1 -1
- package/dist/assets/types.d.ts +2 -2
- package/dist/assets/types.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.d.ts +86 -1
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +274 -6
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/design/design-systems.d.ts +2 -0
- package/dist/design/design-systems.d.ts.map +1 -1
- package/dist/design/design-systems.js +2 -0
- package/dist/design/design-systems.js.map +1 -1
- package/dist/execution/canvas-state.d.ts.map +1 -1
- package/dist/execution/canvas-state.js +19 -43
- package/dist/execution/canvas-state.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +94 -16
- package/dist/index.js.map +1 -1
- package/dist/ontology/design-graph.js +2 -2
- package/dist/ontology/design-graph.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/ontology/validator.js +1 -1
- package/dist/ontology/validator.js.map +1 -1
- package/dist/tools/compact-descriptions.d.ts.map +1 -1
- package/dist/tools/compact-descriptions.js +15 -14
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +556 -157
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +35 -0
- package/dist/tools/engine-lists.d.ts.map +1 -0
- package/dist/tools/engine-lists.js +186 -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 +18 -1
- 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 +66 -9
- 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/camera.js +8 -8
- package/dist/tools/handlers/camera.js.map +1 -1
- package/dist/tools/handlers/export.d.ts.map +1 -1
- package/dist/tools/handlers/export.js +77 -5
- package/dist/tools/handlers/export.js.map +1 -1
- package/dist/tools/handlers/font.js +10 -10
- package/dist/tools/handlers/font.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +12 -12
- package/dist/tools/handlers/maps.d.ts.map +1 -1
- package/dist/tools/handlers/maps.js +23 -3
- package/dist/tools/handlers/maps.js.map +1 -1
- package/dist/tools/handlers.d.ts +44 -11
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +462 -43
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +36 -29
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.d.ts.map +1 -1
- package/dist/tools/shape-params.js +5 -2
- package/dist/tools/shape-params.js.map +1 -1
- 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 +5 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +96 -4
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +1265 -277
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +4181 -1757
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +378 -84
- 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 +22 -2
- 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, MaskTypeSchema, MaskPresetSchema, } from './schemas.js';
|
|
28
29
|
import { OntologyCompiler } from '../ontology/ontology-compiler.js';
|
|
29
30
|
// =============================================================================
|
|
30
31
|
// HELPER FUNCTIONS
|
|
@@ -479,6 +480,19 @@ const DIRECTION_KEYS = ['direction', 'textDirection', 'dir'];
|
|
|
479
480
|
// first STRONG character decides. Mirrored so a result can say which way
|
|
480
481
|
// 'auto' went.
|
|
481
482
|
const RTL_SCRIPT = /[\p{Script=Hebrew}\p{Script=Arabic}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}\p{Script=Samaritan}\p{Script=Mandaic}\p{Script=Adlam}\p{Script=Hanifi_Rohingya}]/u;
|
|
483
|
+
/**
|
|
484
|
+
* cursiveOptions as createCursiveText reads them. The engine takes `nib` as a
|
|
485
|
+
* boolean or a tuning object; the tool also takes the words 'monoline' and
|
|
486
|
+
* 'broad', which the engine would read as truthy (= broad) both times.
|
|
487
|
+
*/
|
|
488
|
+
export function cursiveEngineOptions(o) {
|
|
489
|
+
if (!o)
|
|
490
|
+
return {};
|
|
491
|
+
const { nib, ...rest } = o;
|
|
492
|
+
if (nib === undefined)
|
|
493
|
+
return rest;
|
|
494
|
+
return { ...rest, nib: nib === 'monoline' ? false : nib === 'broad' ? true : nib };
|
|
495
|
+
}
|
|
482
496
|
function baseDirection(text) {
|
|
483
497
|
for (const ch of text) {
|
|
484
498
|
if (RTL_SCRIPT.test(ch))
|
|
@@ -1476,7 +1490,9 @@ function generateGetItemsCode(filter) {
|
|
|
1476
1490
|
if (filter?.source)
|
|
1477
1491
|
conditions.push(`entry.source === '${filter.source}'`);
|
|
1478
1492
|
if (filter?.hasAnimation !== undefined) {
|
|
1479
|
-
|
|
1493
|
+
// 'none' is what the engine stores on a still item, and !!'none' is true
|
|
1494
|
+
// (gate H5): animated means a real loop animation OR keyframes.
|
|
1495
|
+
conditions.push(`!!((entry.item.data?.animationType && entry.item.data.animationType !== 'none') || (Array.isArray(entry.item.data?.keyframes) && entry.item.data.keyframes.length > 0)) === ${filter.hasAnimation}`);
|
|
1480
1496
|
}
|
|
1481
1497
|
if (filter?.hasRelation !== undefined) {
|
|
1482
1498
|
conditions.push(`(app.getRelations(entry.id || entry.itemId).length > 0) === ${filter.hasRelation}`);
|
|
@@ -1489,7 +1505,7 @@ const items = entries.map(entry => ({
|
|
|
1489
1505
|
id: entry.id || entry.itemId,
|
|
1490
1506
|
type: entry.type,
|
|
1491
1507
|
position: entry.item.position ? { x: entry.item.position.x, y: entry.item.position.y } : null,
|
|
1492
|
-
hasAnimation: !!entry.item.data?.animationType,
|
|
1508
|
+
hasAnimation: !!((entry.item.data?.animationType && entry.item.data.animationType !== 'none') || (Array.isArray(entry.item.data?.keyframes) && entry.item.data.keyframes.length > 0)),
|
|
1493
1509
|
relations: app.getRelations(entry.id || entry.itemId).map(r => r.relation || r.type)
|
|
1494
1510
|
}));
|
|
1495
1511
|
|
|
@@ -1504,7 +1520,7 @@ const items = filtered.map(entry => ({
|
|
|
1504
1520
|
id: entry.id || entry.itemId,
|
|
1505
1521
|
type: entry.type,
|
|
1506
1522
|
position: entry.item.position ? { x: entry.item.position.x, y: entry.item.position.y } : null,
|
|
1507
|
-
hasAnimation: !!entry.item.data?.animationType,
|
|
1523
|
+
hasAnimation: !!((entry.item.data?.animationType && entry.item.data.animationType !== 'none') || (Array.isArray(entry.item.data?.keyframes) && entry.item.data.keyframes.length > 0)),
|
|
1508
1524
|
relations: app.getRelations(entry.id || entry.itemId).map(r => r.relation || r.type)
|
|
1509
1525
|
}));
|
|
1510
1526
|
|
|
@@ -1678,7 +1694,7 @@ app.setBackgroundColor('${color}');
|
|
|
1678
1694
|
*/
|
|
1679
1695
|
function generateSetCanvasSizeCode(width, height, preset) {
|
|
1680
1696
|
const sizeArg = preset
|
|
1681
|
-
?
|
|
1697
|
+
? JSON.stringify(preset)
|
|
1682
1698
|
: `{ width: ${width}, height: ${height} }`;
|
|
1683
1699
|
return `
|
|
1684
1700
|
// Set canvas size
|
|
@@ -1696,9 +1712,10 @@ function generateSetCanvasSizeCode(width, height, preset) {
|
|
|
1696
1712
|
app.historyManager.saveState();
|
|
1697
1713
|
// Report what the canvas ACTUALLY took where the engine says so, rather than
|
|
1698
1714
|
// echoing the request back.
|
|
1699
|
-
|
|
1715
|
+
const __sz = app.getCanvasSize ? app.getCanvasSize() : null;
|
|
1716
|
+
return { success: true, width: (r && r.width) || (__sz && __sz.width) || ${width ?? null}, height: (r && r.height) || (__sz && __sz.height) || ${height ?? null},
|
|
1700
1717
|
...(r && r.preset ? { preset: r.preset } : {}),
|
|
1701
|
-
...(r && r.unbounded !== undefined ? { unbounded: r.unbounded } : {})${!preset && (width > 8192 || height > 8192) ? `,
|
|
1718
|
+
...(r && r.unbounded !== undefined ? { unbounded: r.unbounded } : {})${!preset && ((width ?? 0) > 8192 || (height ?? 0) > 8192) ? `,
|
|
1702
1719
|
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.'` : ''} };
|
|
1703
1720
|
})();
|
|
1704
1721
|
`.trim();
|
|
@@ -2209,10 +2226,46 @@ function world3dColor(value) {
|
|
|
2209
2226
|
return value;
|
|
2210
2227
|
const hex = value.trim().replace(/^#/, '');
|
|
2211
2228
|
const full = hex.length === 3 ? hex.split('').map((c) => c + c).join('') : hex;
|
|
2229
|
+
// A string that is not hex is refused HERE, naming both forms: passed on,
|
|
2230
|
+
// the engine answered "must be [r,g,b]", which contradicted the docs
|
|
2231
|
+
// saying hex works (gate A, 1.6.19).
|
|
2212
2232
|
if (!/^[0-9a-f]{6}$/i.test(full))
|
|
2213
|
-
|
|
2233
|
+
throw new Error(`"${value}" is not a colour: use hex ("#03050c" or "#abc") or [r, g, b]`);
|
|
2214
2234
|
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255);
|
|
2215
2235
|
}
|
|
2236
|
+
/**
|
|
2237
|
+
* The world spec's colour fields: GENERATED from FxTool WORLD_SCHEMA's kind
|
|
2238
|
+
* 'color' entries (scripts/sync-engine-lists.mjs). None of them is named
|
|
2239
|
+
* `color`, so world3dColors never touched them and a hex `env.zenith` reached
|
|
2240
|
+
* the engine as a string, where the validator refused it.
|
|
2241
|
+
*/
|
|
2242
|
+
export { WORLD3D_COLOR_PATHS };
|
|
2243
|
+
/** A hex colour to [r, g, b] 0..1; anything else unchanged (arrays too: light colours may exceed 1). */
|
|
2244
|
+
export function world3dHex(value) {
|
|
2245
|
+
return typeof value === 'string' ? world3dColor(value) : value;
|
|
2246
|
+
}
|
|
2247
|
+
/** Convert hex strings at the world spec's colour paths (configure / create). */
|
|
2248
|
+
export function world3dSpecColors(spec) {
|
|
2249
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec))
|
|
2250
|
+
return spec;
|
|
2251
|
+
const out = JSON.parse(JSON.stringify(spec));
|
|
2252
|
+
for (const path of WORLD3D_COLOR_PATHS) {
|
|
2253
|
+
const [group, key] = path.split('.');
|
|
2254
|
+
const g = out[group];
|
|
2255
|
+
if (g && typeof g === 'object' && key in g)
|
|
2256
|
+
g[key] = world3dHex(g[key]);
|
|
2257
|
+
}
|
|
2258
|
+
return out;
|
|
2259
|
+
}
|
|
2260
|
+
/** A light or material spec with hex colours (color, emissive, sheenColor) converted; an array is left exactly as given. */
|
|
2261
|
+
function world3dHexColor(spec) {
|
|
2262
|
+
const out = { ...(spec ?? {}) };
|
|
2263
|
+
for (const key of ['color', 'emissive', 'sheenColor']) {
|
|
2264
|
+
if (typeof out[key] === 'string')
|
|
2265
|
+
out[key] = world3dColor(out[key]);
|
|
2266
|
+
}
|
|
2267
|
+
return out;
|
|
2268
|
+
}
|
|
2216
2269
|
/** Recursively convert every `color` field in a world3d payload. */
|
|
2217
2270
|
function world3dColors(value) {
|
|
2218
2271
|
if (Array.isArray(value))
|
|
@@ -2225,6 +2278,106 @@ function world3dColors(value) {
|
|
|
2225
2278
|
}
|
|
2226
2279
|
return out;
|
|
2227
2280
|
}
|
|
2281
|
+
/** At most this many tiles in one contact sheet: past it, the tiles are too small to judge. */
|
|
2282
|
+
export const SHEET_MAX_TILES = 48;
|
|
2283
|
+
/**
|
|
2284
|
+
* The times a contact sheet captures, and a label for each. Pure, so the
|
|
2285
|
+
* modes are testable without a browser. Explicit `times` win; otherwise
|
|
2286
|
+
* every / strip / loopSeam. Rounded to the millisecond, clamped at 0.
|
|
2287
|
+
*/
|
|
2288
|
+
export function sheetTimes(times, sheet) {
|
|
2289
|
+
const r = (t) => Math.max(0, Math.round(t * 1000) / 1000);
|
|
2290
|
+
const fmt = (t) => `${t.toFixed(2)}s`;
|
|
2291
|
+
let out = [];
|
|
2292
|
+
let labels = null;
|
|
2293
|
+
if (times && times.length) {
|
|
2294
|
+
out = times.map(r);
|
|
2295
|
+
}
|
|
2296
|
+
else if (sheet.strip) {
|
|
2297
|
+
const { at, count, step } = sheet.strip;
|
|
2298
|
+
const first = at - Math.floor(count / 2) * step;
|
|
2299
|
+
out = Array.from({ length: count }, (_, i) => r(first + i * step));
|
|
2300
|
+
}
|
|
2301
|
+
else if (sheet.loopSeam && sheet.duration) {
|
|
2302
|
+
// The frame BEFORE the loop wraps, then the first: a jump between them is the stutter.
|
|
2303
|
+
out = [r(sheet.duration - 1 / 30), 0];
|
|
2304
|
+
labels = [`end ${fmt(out[0])}`, 'start 0.00s'];
|
|
2305
|
+
}
|
|
2306
|
+
else if (sheet.every && sheet.duration) {
|
|
2307
|
+
for (let t = 0; t < sheet.duration - 1e-9 && out.length < SHEET_MAX_TILES; t += sheet.every)
|
|
2308
|
+
out.push(r(t));
|
|
2309
|
+
}
|
|
2310
|
+
out = out.slice(0, SHEET_MAX_TILES);
|
|
2311
|
+
return { times: out, labels: labels ?? out.map(fmt) };
|
|
2312
|
+
}
|
|
2313
|
+
/**
|
|
2314
|
+
* The critique an agent applies to a contact sheet. One scale, the same every
|
|
2315
|
+
* time, so two passes over one film can be compared.
|
|
2316
|
+
*/
|
|
2317
|
+
export const SHEET_RUBRIC = {
|
|
2318
|
+
scale: 'Score each 1-10. Under 7 on any line is a fix before export.',
|
|
2319
|
+
criteria: [
|
|
2320
|
+
'hook: does something worth watching happen in the first 2 s?',
|
|
2321
|
+
'phone readability: at 360 px wide, can every word be read?',
|
|
2322
|
+
'motion quality: do moves ease in and out, with nothing jumping or sliding at constant speed?',
|
|
2323
|
+
'variety: does something change every 2-4 s, with no stretch that looks the same?',
|
|
2324
|
+
'composition: is there a clear subject, with nothing crammed against an edge?',
|
|
2325
|
+
'brand accuracy: are the colours, fonts and logo the brand\'s own?',
|
|
2326
|
+
'sound sync: do cuts and hits land on the beat (check against pinepaper_beat_cuts)?',
|
|
2327
|
+
],
|
|
2328
|
+
checklist: [
|
|
2329
|
+
'text overlapping text or a subject',
|
|
2330
|
+
'dead beats: adjacent tiles that are the same picture',
|
|
2331
|
+
'labels or logos cut off in a corner',
|
|
2332
|
+
'a jump between the last and first frame of a loop (loopSeam)',
|
|
2333
|
+
],
|
|
2334
|
+
};
|
|
2335
|
+
/**
|
|
2336
|
+
* Page code: what a project holds, counted the same way before a save and
|
|
2337
|
+
* after a load, so a round trip can be checked (keyframes in = keyframes out).
|
|
2338
|
+
*/
|
|
2339
|
+
const SCENE_COUNTS_JS = `function __ppSceneCounts() {
|
|
2340
|
+
const all = (app.itemRegistry && typeof app.itemRegistry.getAll === 'function') ? app.itemRegistry.getAll() : [];
|
|
2341
|
+
let animated = 0, keyframes = 0;
|
|
2342
|
+
all.forEach(function (e) {
|
|
2343
|
+
const k = e && e.item && e.item.data && e.item.data.keyframes;
|
|
2344
|
+
if (Array.isArray(k) && k.length) { animated++; keyframes += k.length; }
|
|
2345
|
+
});
|
|
2346
|
+
const rel = (app.relationRegistry && typeof app.relationRegistry.exportForSave === 'function') ? app.relationRegistry.exportForSave().length : null;
|
|
2347
|
+
return { items: all.length, animatedItems: animated, keyframes: keyframes, relations: rel };
|
|
2348
|
+
}`;
|
|
2349
|
+
/**
|
|
2350
|
+
* Page code: the REGISTRY id for a Paper item, walking up from a child (a hit
|
|
2351
|
+
* test lands on a group's inner path). Results used to fall back to Paper's
|
|
2352
|
+
* internal numeric id (gate D9/D10, 1.6.19), which no other tool can address.
|
|
2353
|
+
*/
|
|
2354
|
+
const REG_ID_JS = `function __ppRegId(it) {
|
|
2355
|
+
for (let n = it; n; n = n.parent) {
|
|
2356
|
+
const id = n.data && (n.data.id || n.data.registryId);
|
|
2357
|
+
if (id && app.itemRegistry && app.itemRegistry.get(id)) return id;
|
|
2358
|
+
}
|
|
2359
|
+
const all = (app.itemRegistry && typeof app.itemRegistry.getAll === 'function') ? app.itemRegistry.getAll() : [];
|
|
2360
|
+
for (let n = it; n; n = n.parent) { const e = all.find(function (x) { return x && x.item === n; }); if (e) return e.itemId; }
|
|
2361
|
+
return null;
|
|
2362
|
+
}`;
|
|
2363
|
+
/**
|
|
2364
|
+
* Page code: any binary an engine export returns (Blob, ArrayBuffer, typed
|
|
2365
|
+
* array) as a data URL, so it crosses the bridge as bytes and the handler can
|
|
2366
|
+
* write a file. A Uint8Array crossed as a JSON object of byte indices
|
|
2367
|
+
* ({"0": 80, "1": 75, ...}): export_dotlottie was unusable (gate D7, 1.6.19).
|
|
2368
|
+
*/
|
|
2369
|
+
const TO_DATA_URL_JS = `async function __ppToDataUrl(out, mime) {
|
|
2370
|
+
if (typeof out === 'string') return out.indexOf('data:') === 0 ? out : null;
|
|
2371
|
+
let blob = null;
|
|
2372
|
+
if (out instanceof Blob) blob = out;
|
|
2373
|
+
else if (out instanceof ArrayBuffer || ArrayBuffer.isView(out)) blob = new Blob([out], { type: mime });
|
|
2374
|
+
if (!blob) return null;
|
|
2375
|
+
return await new Promise(function (res, rej) { const r = new FileReader(); r.onload = function () { res(r.result); }; r.onerror = rej; r.readAsDataURL(blob); });
|
|
2376
|
+
}`;
|
|
2377
|
+
/** Why map pan / zoom refuse, and what to do instead. */
|
|
2378
|
+
const MAP_VIEW_REFUSAL = (action) => `map ${action} is not supported: the map has no view transform, and moving the camera changes only the studio view, `
|
|
2379
|
+
+ 'which a canvas export ignores. To frame part of a map in an export: keyframe the camera with pinepaper_camera_animate '
|
|
2380
|
+
+ 'and export with framing:"camera".';
|
|
2228
2381
|
export class PinePaperCodeGenerator {
|
|
2229
2382
|
/**
|
|
2230
2383
|
* Generate code for creating an item
|
|
@@ -2266,8 +2419,14 @@ export class PinePaperCodeGenerator {
|
|
|
2266
2419
|
/**
|
|
2267
2420
|
* Generate code for adding a relation
|
|
2268
2421
|
*/
|
|
2269
|
-
generateAddRelation(input) {
|
|
2270
|
-
|
|
2422
|
+
generateAddRelation(input, customName = false) {
|
|
2423
|
+
// The handler has already parsed, and a session-registered custom
|
|
2424
|
+
// relation name is not in the enum (gate D2); the handler says so, and the
|
|
2425
|
+
// name is re-parsed with a built-in stand-in and put back.
|
|
2426
|
+
const rt = input.relationType;
|
|
2427
|
+
const validated = AddRelationInputSchema.parse(customName ? { ...input, relationType: 'orbits' } : input);
|
|
2428
|
+
if (customName)
|
|
2429
|
+
validated.relationType = rt;
|
|
2271
2430
|
// A PRESET decides its own relationType and params, so this is a different
|
|
2272
2431
|
// call, not a defaulted one. applyRelationPreset refuses with
|
|
2273
2432
|
// `{ok:false, errors:[{field, reason}]}` — a third refusal shape on this
|
|
@@ -2980,9 +3139,12 @@ if (typeof _gen === 'function') {
|
|
|
2980
3139
|
codeParts.push(`
|
|
2981
3140
|
// Save history and return results
|
|
2982
3141
|
app.historyManager.saveState();
|
|
2983
|
-
results;
|
|
3142
|
+
return results;
|
|
2984
3143
|
`);
|
|
2985
|
-
|
|
3144
|
+
// ONE (-led FUNCTION (gate C4, 1.6.19). This was a statement list ending
|
|
3145
|
+
// in a bare \`results;\`, and the governor keeps only a (-led snippet's
|
|
3146
|
+
// value, so every created id was collected and then dropped.
|
|
3147
|
+
return `(async function() {\n${codeParts.join('\n')}\n})();`;
|
|
2986
3148
|
}
|
|
2987
3149
|
// =============================================================================
|
|
2988
3150
|
// DIAGRAM TOOLS
|
|
@@ -3103,12 +3265,26 @@ if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}');
|
|
|
3103
3265
|
if (!targetItem) throw new Error('Target item not found: ${targetItemId}');
|
|
3104
3266
|
|
|
3105
3267
|
const config = ${configStr};
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
const
|
|
3268
|
+
// connectPorts(sourcePort, targetPort, config) takes PORT OBJECTS (gate C8,
|
|
3269
|
+
// 1.6.19). This passed (item, name, item, name, config): five arguments to a
|
|
3270
|
+
// three-argument method, so the engine got an item where a port belongs,
|
|
3271
|
+
// returned null, and connector.data threw. Ports are resolved by name or
|
|
3272
|
+
// position first; an item with none gets the standard diagram ports.
|
|
3273
|
+
const __pm = app.diagramSystem.portManager;
|
|
3274
|
+
const __port = function (item, name) {
|
|
3275
|
+
let p = __pm.getPort(item, name);
|
|
3276
|
+
if (!p && !(__pm.getPorts(item) || []).length) { app.diagramSystem.addPorts(item); p = __pm.getPort(item, name); }
|
|
3277
|
+
return p;
|
|
3278
|
+
};
|
|
3279
|
+
const __sp = __port(sourceItem, ${JSON.stringify(sourcePort)});
|
|
3280
|
+
const __tp = __port(targetItem, ${JSON.stringify(targetPort)});
|
|
3281
|
+
if (!__sp || !__tp) {
|
|
3282
|
+
const __names = function (item) { return (__pm.getPorts(item) || []).map(function (p) { return p.position || p.id; }).join(', '); };
|
|
3283
|
+
return { success: false, error: 'no such port: ' + (!__sp ? ${JSON.stringify(sourcePort)} + ' on ' + ${JSON.stringify(sourceItemId)} + ' (has: ' + __names(sourceItem) + ')' : ${JSON.stringify(targetPort)} + ' on ' + ${JSON.stringify(targetItemId)} + ' (has: ' + __names(targetItem) + ')') };
|
|
3284
|
+
}
|
|
3285
|
+
const connector = app.diagramSystem.connectPorts(__sp, __tp, config);
|
|
3286
|
+
if (!connector) { return { success: false, error: 'the diagram system did not create the connector' }; }
|
|
3287
|
+
const connectorId = connector.id || connector.data?.registryId;
|
|
3112
3288
|
app.historyManager.saveState();
|
|
3113
3289
|
|
|
3114
3290
|
({ connectorId, sourceItemId: '${sourceItemId}', sourcePort: '${sourcePort}', targetItemId: '${targetItemId}', targetPort: '${targetPort}' });
|
|
@@ -3203,61 +3379,50 @@ const shapes = category ? _lib.getByCategory(category) : _lib.getAll();
|
|
|
3203
3379
|
generateUpdateConnector(input) {
|
|
3204
3380
|
const validated = UpdateConnectorInputSchema.parse(input);
|
|
3205
3381
|
const { connectorId, style, label, labelPosition } = validated;
|
|
3206
|
-
const updates = [];
|
|
3207
|
-
if (style) {
|
|
3208
|
-
updates.push(`style: ${JSON.stringify(style)}`);
|
|
3209
|
-
}
|
|
3210
|
-
if (label !== undefined) {
|
|
3211
|
-
updates.push(`label: ${JSON.stringify(label)}`);
|
|
3212
|
-
}
|
|
3213
|
-
if (labelPosition !== undefined) {
|
|
3214
|
-
updates.push(`labelPosition: ${labelPosition}`);
|
|
3215
|
-
}
|
|
3216
|
-
const updatesStr = updates.length > 0 ? `{ ${updates.join(', ')} }` : '{}';
|
|
3217
3382
|
return `
|
|
3218
3383
|
// Update connector: ${connectorId}
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
const
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
//
|
|
3227
|
-
//
|
|
3228
|
-
//
|
|
3229
|
-
//
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
const
|
|
3238
|
-
if (
|
|
3384
|
+
(function() {
|
|
3385
|
+
// CONNECTORS LIVE IN THE DIAGRAM SYSTEM, NOT THE ITEM REGISTRY (gate D1,
|
|
3386
|
+
// 1.6.19): getItemById never resolves the id connect returned.
|
|
3387
|
+
const __cid = ${JSON.stringify(connectorId)};
|
|
3388
|
+
const ds = app.diagramSystem;
|
|
3389
|
+
const connector = ds && typeof ds.getConnector === 'function' ? ds.getConnector(__cid) : null;
|
|
3390
|
+
if (!connector) return { success: false, error: 'Connector not found: ' + __cid + '. Use the connectorId pinepaper_connect returned.' };
|
|
3391
|
+
// updateConnector(id, style) takes the STYLE BAG ITSELF. This passed
|
|
3392
|
+
// { style: {...}, label } (gate run 2, 1.6.19), so the engine merged a key
|
|
3393
|
+
// named "style" into the connector's style, changed nothing it draws, and
|
|
3394
|
+
// the tool reported updated:true over an unchanged connector.
|
|
3395
|
+
const style = ${JSON.stringify(style ?? {})};
|
|
3396
|
+
const label = ${label === undefined ? 'undefined' : JSON.stringify(label)};
|
|
3397
|
+
const labelPosition = ${labelPosition === undefined ? 'undefined' : labelPosition};
|
|
3398
|
+
const keys = Object.keys(style);
|
|
3399
|
+
if (!keys.length && label === undefined && labelPosition === undefined) {
|
|
3400
|
+
return { success: false, error: 'nothing to update: pass style, label or labelPosition' };
|
|
3401
|
+
}
|
|
3402
|
+
const applied = [];
|
|
3403
|
+
if (keys.length) {
|
|
3404
|
+
if (typeof ds.updateConnector !== 'function') {
|
|
3405
|
+
return { success: false, error: 'this studio has no per-connector update (app.diagramSystem.updateConnector); update the studio' };
|
|
3406
|
+
}
|
|
3407
|
+
const res = ds.updateConnector(__cid, style);
|
|
3408
|
+
if (!res || res.ok === false) return { success: false, error: (res && res.reason) || 'the connector could not be updated' };
|
|
3409
|
+
const unchanged = keys.filter(function (k) { return connector.style[k] !== style[k]; });
|
|
3410
|
+
if (unchanged.length) return { success: false, error: 'the studio did not take ' + unchanged.join(', ') };
|
|
3411
|
+
keys.forEach(function (k) { applied.push('style.' + k); });
|
|
3412
|
+
}
|
|
3413
|
+
if (label !== undefined || labelPosition !== undefined) {
|
|
3414
|
+
connector.setLabel(label !== undefined ? label : connector.label, labelPosition !== undefined ? labelPosition : connector.labelPosition);
|
|
3415
|
+
applied.push(label !== undefined ? 'label' : 'labelPosition');
|
|
3416
|
+
}
|
|
3239
3417
|
app.historyManager.saveState();
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
else _unsupported.push(k);
|
|
3249
|
-
}
|
|
3250
|
-
if (_applied.length) { app.historyManager.saveState(); app._scheduleRepaint?.(); }
|
|
3251
|
-
if (_unsupported.length && !_applied.length) {
|
|
3252
|
-
throw new Error(
|
|
3253
|
-
'this build cannot update ' + _unsupported.join(', ') + ' on an existing connector — '
|
|
3254
|
-
+ 'this build has no per-connector update — app.diagramSystem.updateConnector arrived later. '
|
|
3255
|
-
+ 'Update the studio for it. Removing and recreating the connector works, but mints a NEW id '
|
|
3256
|
-
+ 'and orphans anything holding the old one, so it is a last resort rather than the fix.'
|
|
3257
|
-
);
|
|
3258
|
-
}
|
|
3259
|
-
|
|
3260
|
-
({ connectorId: '${connectorId}', updated: _applied.length > 0, applied: _applied, unsupported: _unsupported });
|
|
3418
|
+
// What it now DRAWS, read off the path, so a caller can check rather than trust.
|
|
3419
|
+
const p = connector.path;
|
|
3420
|
+
return {
|
|
3421
|
+
success: true, connectorId: __cid, updated: true, applied: applied,
|
|
3422
|
+
drawn: p ? { stroke: p.strokeColor ? p.strokeColor.toCSS(true) : null, width: p.strokeWidth, dash: p.dashArray || [] } : null,
|
|
3423
|
+
label: connector.label || null,
|
|
3424
|
+
};
|
|
3425
|
+
})();
|
|
3261
3426
|
`.trim();
|
|
3262
3427
|
}
|
|
3263
3428
|
/**
|
|
@@ -3268,8 +3433,10 @@ if (_unsupported.length && !_applied.length) {
|
|
|
3268
3433
|
const { connectorId } = validated;
|
|
3269
3434
|
return `
|
|
3270
3435
|
// Remove connector: ${connectorId}
|
|
3271
|
-
|
|
3272
|
-
|
|
3436
|
+
// Resolved through the diagram system (gate D1, 1.6.19), as update_connector is.
|
|
3437
|
+
const __cid = ${JSON.stringify(connectorId)};
|
|
3438
|
+
const connector = (app.diagramSystem && app.diagramSystem.getConnector(__cid)) || app.getItemById(__cid);
|
|
3439
|
+
if (!connector) throw new Error('Connector not found: ' + __cid);
|
|
3273
3440
|
|
|
3274
3441
|
app.diagramSystem.connectorManager.removeConnector(connector);
|
|
3275
3442
|
app.historyManager.saveState();
|
|
@@ -3358,6 +3525,23 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3358
3525
|
app.setCanvasSize('${canvasPresetFor(canvasPreset)}');
|
|
3359
3526
|
// Allow canvas resize to take effect
|
|
3360
3527
|
await new Promise(r => setTimeout(r, 50));
|
|
3528
|
+
`;
|
|
3529
|
+
}
|
|
3530
|
+
else {
|
|
3531
|
+
code += `
|
|
3532
|
+
// BOUND THE BOARD IT REPORTS (gate D57). An UNBOUNDED canvas still answers
|
|
3533
|
+
// getCanvasSize with its 1920x1080 fallback, which is what this job reports
|
|
3534
|
+
// and what every sizing rule here is relative to; but its stills export the
|
|
3535
|
+
// artwork's bounds plus a margin (432x232 for one rectangle), while video
|
|
3536
|
+
// exports the board. Fixing it at the size reported makes the two agree.
|
|
3537
|
+
let _boundedFrom = null;
|
|
3538
|
+
if (app.canvasSize && app.canvasSize.unbounded && typeof app.setCanvasSize === 'function') {
|
|
3539
|
+
const _u = app.getCanvasSize ? app.getCanvasSize() : app.canvasSize;
|
|
3540
|
+
if (_u && _u.width > 0 && _u.height > 0) {
|
|
3541
|
+
const _r = app.setCanvasSize({ width: _u.width, height: _u.height });
|
|
3542
|
+
if (!(_r && _r.ok === false)) _boundedFrom = 'unbounded';
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3361
3545
|
`;
|
|
3362
3546
|
}
|
|
3363
3547
|
if (wantOntology) {
|
|
@@ -3391,7 +3575,8 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3391
3575
|
screenshotPolicy: '${policyStr}',
|
|
3392
3576
|
canvasPreset: ${canvasPreset ? `'${canvasPreset}'` : 'null'},
|
|
3393
3577
|
canvasCleared: ${shouldClear},
|
|
3394
|
-
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }${
|
|
3578
|
+
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }${canvasPreset ? '' : `,
|
|
3579
|
+
...(_boundedFrom ? { note: 'the canvas was unbounded (it exported the artwork, not the board); it is now fixed at ' + (_cs.width || 800) + 'x' + (_cs.height || 600) + ', so stills and video both export that size.' } : {})`}${wantOntology ? `,
|
|
3395
3580
|
ontology: _ontology,
|
|
3396
3581
|
ontologyUnavailable: _ontologyUnavailable || undefined` : ''}
|
|
3397
3582
|
};
|
|
@@ -3470,12 +3655,13 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3470
3655
|
|
|
3471
3656
|
if (entry.type === 'text' || item.className === 'PointText') analysis.hasText = true;
|
|
3472
3657
|
if (item.className === 'Raster') analysis.hasImages = true;
|
|
3473
|
-
|
|
3658
|
+
// 'none' is what a still item stores (gate C5, 1.6.19): it is not an animation.
|
|
3659
|
+
if (data.animationType && data.animationType !== 'none') {
|
|
3474
3660
|
analysis.hasAnimations = true;
|
|
3475
3661
|
animationSet.add(data.animationType);
|
|
3476
3662
|
}
|
|
3477
3663
|
// Detect keyframe animations (added via app.addAnimation with keyframes array)
|
|
3478
|
-
if (data.keyframes || data.animation || data.keyframeAnimation) {
|
|
3664
|
+
if ((Array.isArray(data.keyframes) && data.keyframes.length > 0) || data.animation || data.keyframeAnimation) {
|
|
3479
3665
|
analysis.hasAnimations = true;
|
|
3480
3666
|
animationSet.add('keyframe');
|
|
3481
3667
|
}
|
|
@@ -3827,7 +4013,7 @@ return { success: true, backgroundColor: '${bgColor}' };
|
|
|
3827
4013
|
case 'set_canvas_size': {
|
|
3828
4014
|
const w = op.width || 1080;
|
|
3829
4015
|
const h = op.height || 1080;
|
|
3830
|
-
const sizeArg = op.preset ?
|
|
4016
|
+
const sizeArg = op.preset ? JSON.stringify(op.preset) : `{ width: ${w}, height: ${h} }`;
|
|
3831
4017
|
return `
|
|
3832
4018
|
// Read the verdict, as the standalone tool now does: an unknown preset or an
|
|
3833
4019
|
// out-of-range dimension is refused by name, and reporting the size we ASKED
|
|
@@ -3997,7 +4183,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3997
4183
|
}
|
|
3998
4184
|
generateAgentExport(input) {
|
|
3999
4185
|
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;
|
|
4186
|
+
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
4187
|
// Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
|
|
4002
4188
|
const videoEncodeOpts = {};
|
|
4003
4189
|
if (broadcast)
|
|
@@ -4019,6 +4205,10 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4019
4205
|
// the only bytes that varied, and this pins them.
|
|
4020
4206
|
if (deterministic)
|
|
4021
4207
|
videoEncodeOpts.deterministic = true;
|
|
4208
|
+
// Motion blur (FxTool #55): the engine forwards it on every mp4/webm path
|
|
4209
|
+
// (quickExport, the agent paths, the export store, videoExporter.export).
|
|
4210
|
+
if (motionBlur)
|
|
4211
|
+
videoEncodeOpts.motionBlur = motionBlur;
|
|
4022
4212
|
// A transparent WebM (FxTool 1833b397) rides the store route too.
|
|
4023
4213
|
const webmAlpha = format === 'webm' && transparent === true;
|
|
4024
4214
|
if (webmAlpha) {
|
|
@@ -4308,7 +4498,14 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4308
4498
|
const bg = app.canvasEl && app.canvasEl.style && app.canvasEl.style.backgroundColor;
|
|
4309
4499
|
const hasColor = !!bg && !/^(transparent|rgba\\([^)]*,\\s*0\\))$/i.test(bg);
|
|
4310
4500
|
const hasBgItems = !!(app.patternGroup && app.patternGroup.children && app.patternGroup.children.length);
|
|
4311
|
-
|
|
4501
|
+
// A 3D world under the canvas IS the background: since FxTool #50
|
|
4502
|
+
// (747315e3) jpg/webp stills composite it, so nothing turns white/black.
|
|
4503
|
+
const hasWorld = !!app._world3d;
|
|
4504
|
+
// So does a styled scene: one raster sized to COVER the frame (gate run 3:
|
|
4505
|
+
// every export after styled_scene warned of black areas it does not have).
|
|
4506
|
+
const hasStyledScene = !!(app.textItemGroup && app.textItemGroup.children
|
|
4507
|
+
&& app.textItemGroup.children.some(function (it) { return it && it.data && it.data.styledScene && it.visible !== false; }));
|
|
4508
|
+
if (hasColor || hasBgItems || hasWorld || hasStyledScene) return [];
|
|
4312
4509
|
return [{ code: 'alpha_dropped', message: fmt === 'apng'
|
|
4313
4510
|
? '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
4511
|
: fmt === 'jpg'
|
|
@@ -4365,7 +4562,9 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4365
4562
|
fps: settings.fps,
|
|
4366
4563
|
quality,
|
|
4367
4564
|
width: dimensions.width,
|
|
4368
|
-
height: dimensions.height
|
|
4565
|
+
height: dimensions.height,${motionBlur ? `
|
|
4566
|
+
// The estimate reports renders (frames x subframes) and renderCost.
|
|
4567
|
+
motionBlur: ${JSON.stringify(motionBlur)},` : ''}
|
|
4369
4568
|
});
|
|
4370
4569
|
return {
|
|
4371
4570
|
success: true,
|
|
@@ -4433,7 +4632,11 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4433
4632
|
camBase = { width: app.canvasSize.width, height: app.canvasSize.height };
|
|
4434
4633
|
}
|
|
4435
4634
|
if (!camBase) camBase = { width: dimensions.width, height: dimensions.height };
|
|
4436
|
-
|
|
4635
|
+
// scale applies here too: it was dropped, so scale 0.5 rendered full
|
|
4636
|
+
// size and nothing said so (gate C7).
|
|
4637
|
+
const __cs = ${scale !== undefined ? scale : 1};
|
|
4638
|
+
const __ev = (n) => Math.max(2, Math.round((n * __cs) / 2) * 2);
|
|
4639
|
+
cameraDims = { width: __ev(camBase.width), height: __ev(camBase.height) };
|
|
4437
4640
|
}
|
|
4438
4641
|
|
|
4439
4642
|
// AN EXPORTER ANSWERS A BLOB OR A RECORD AROUND ONE.
|
|
@@ -4596,6 +4799,12 @@ ${stillTime !== undefined ? `
|
|
|
4596
4799
|
? { width: png.width, height: png.height }
|
|
4597
4800
|
: { width: dimensions.width, height: dimensions.height },
|
|
4598
4801
|
};
|
|
4802
|
+
// An unbounded canvas exports its ARTWORK (bounds plus a margin), not
|
|
4803
|
+
// the board getCanvasSize reports (gate D57). Say so rather than
|
|
4804
|
+
// hand back a size nobody asked for unexplained.
|
|
4805
|
+
if (app.canvasSize && app.canvasSize.unbounded) {
|
|
4806
|
+
result.warning = (result.warning ? result.warning + ' ' : '') + 'this canvas is unbounded, so the still is cropped to the artwork, not the ' + pngBoard.width + 'x' + pngBoard.height + ' board. pinepaper_set_canvas_size {width, height} fixes the board; video exports the board either way.';
|
|
4807
|
+
}
|
|
4599
4808
|
} else {
|
|
4600
4809
|
const canvas = document.querySelector('canvas');
|
|
4601
4810
|
if (canvas) {
|
|
@@ -5230,6 +5439,17 @@ ${Object.keys(videoEncodeOpts).length ? ` if (result && result.success) {
|
|
|
5230
5439
|
if (result.fidelity.note) delete result.fidelity.note;
|
|
5231
5440
|
}
|
|
5232
5441
|
}
|
|
5442
|
+
` : ''}${motionBlur ? ` // What the exporter actually did with motionBlur (FxTool #55 records it as
|
|
5443
|
+
// lastMotionBlur, with renders = frames x subframes). A studio that predates
|
|
5444
|
+
// it ignores the option silently, so that is said by name.
|
|
5445
|
+
if (result && result.success) {
|
|
5446
|
+
const __mb = __vx && __vx.lastMotionBlur;
|
|
5447
|
+
if (__mb) result.motionBlur = __mb;
|
|
5448
|
+
else {
|
|
5449
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5450
|
+
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.' }]);
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5233
5453
|
` : ''} const __ar = __vx && __vx.lastAudioReport;
|
|
5234
5454
|
if (result && result.success && __ar && typeof __ar === 'object') {
|
|
5235
5455
|
result.audio = __ar;
|
|
@@ -5261,6 +5481,21 @@ ${(() => {
|
|
|
5261
5481
|
})()}
|
|
5262
5482
|
${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) { /* the still is already rendered */ }
|
|
5263
5483
|
if (result && result.success) result.time = ${stillTime};` : ''}
|
|
5484
|
+
// RELIGHT (FxTool D5): applied to png / pdf / mp4 / webm, not gif / apng
|
|
5485
|
+
// or the vector formats; and it is what makes a video export slow.
|
|
5486
|
+
if (result && result.success && typeof app.getRelight === 'function') {
|
|
5487
|
+
try {
|
|
5488
|
+
const __rl = app.getRelight();
|
|
5489
|
+
if (__rl && __rl.enabled) {
|
|
5490
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5491
|
+
if (['png', 'jpg', 'webp', 'pdf', 'mp4', 'webm'].indexOf(${JSON.stringify(format)}) === -1) {
|
|
5492
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'relight_not_applied', message: 'a relight rig is set, but ${format} is not relit. Export png, pdf, mp4 or webm to keep the lighting.' }]);
|
|
5493
|
+
} else {
|
|
5494
|
+
result.relight = { applied: true, lights: (__rl.lights || []).length };
|
|
5495
|
+
}
|
|
5496
|
+
}
|
|
5497
|
+
} catch (_) { /* the export stands */ }
|
|
5498
|
+
}
|
|
5264
5499
|
|
|
5265
5500
|
return result;
|
|
5266
5501
|
})();
|
|
@@ -5328,12 +5563,13 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5328
5563
|
if (entry.type === 'text' || item.className === 'PointText') analysis.hasText = true;
|
|
5329
5564
|
if (item.className === 'Raster') analysis.hasImages = true;
|
|
5330
5565
|
|
|
5331
|
-
|
|
5566
|
+
// 'none' is what a still item stores (gate C5, 1.6.19): it is not an animation.
|
|
5567
|
+
if (data.animationType && data.animationType !== 'none') {
|
|
5332
5568
|
analysis.hasAnimations = true;
|
|
5333
5569
|
animationSet.add(data.animationType);
|
|
5334
5570
|
}
|
|
5335
5571
|
// Detect keyframe animations (added via app.addAnimation with keyframes array)
|
|
5336
|
-
if (data.keyframes || data.animation || data.keyframeAnimation) {
|
|
5572
|
+
if ((Array.isArray(data.keyframes) && data.keyframes.length > 0) || data.animation || data.keyframeAnimation) {
|
|
5337
5573
|
analysis.hasAnimations = true;
|
|
5338
5574
|
animationSet.add('keyframe');
|
|
5339
5575
|
}
|
|
@@ -5585,7 +5821,9 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5585
5821
|
return {
|
|
5586
5822
|
success: true,
|
|
5587
5823
|
mapId: result.mapId || '${mapId}',
|
|
5588
|
-
|
|
5824
|
+
// regionPaths is a Map: .length was undefined, so every load reported
|
|
5825
|
+
// regions: 0 (gate D3). size, with the feature count as a fallback.
|
|
5826
|
+
regions: (result.regions && (result.regions.size ?? result.regions.length)) || (result.geoData && result.geoData.features ? result.geoData.features.length : 0),
|
|
5589
5827
|
bounds: result.bounds,
|
|
5590
5828
|
center: result.center,${warn}
|
|
5591
5829
|
};
|
|
@@ -5594,6 +5832,55 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5594
5832
|
}
|
|
5595
5833
|
})();
|
|
5596
5834
|
`.trim();
|
|
5835
|
+
}
|
|
5836
|
+
/**
|
|
5837
|
+
* Emitted helpers for reading a map's region colours back.
|
|
5838
|
+
*
|
|
5839
|
+
* highlightRegions and applyDataColors both write ONE channel — a colour
|
|
5840
|
+
* override keyed on the feature's _mapId — and log a console.warn for an id
|
|
5841
|
+
* they cannot resolve, which production strips. getHighlightedRegions reads
|
|
5842
|
+
* path.data.highlighted over regionPaths, which a rasterised map leaves
|
|
5843
|
+
* empty, so it saw nothing either tool did (gate D51). The override, read
|
|
5844
|
+
* through the public getRegionOverride, is the record the engine leaves.
|
|
5845
|
+
* Ids resolve the way the engine resolves them: exact, then lower-case.
|
|
5846
|
+
*/
|
|
5847
|
+
mapRegionReadbackJs() {
|
|
5848
|
+
return `
|
|
5849
|
+
function __resolve(id) {
|
|
5850
|
+
const m = app.mapSystem.regionNameMappings;
|
|
5851
|
+
if (!m || typeof m.get !== 'function') return null;
|
|
5852
|
+
return m.get(id) || m.get(String(id).toLowerCase()) || null;
|
|
5853
|
+
}
|
|
5854
|
+
function __features() {
|
|
5855
|
+
const seen = new Map();
|
|
5856
|
+
app.mapSystem.regionNameMappings.forEach(function (f) { if (f && !seen.has(f._mapId)) seen.set(f._mapId, f); });
|
|
5857
|
+
return Array.from(seen.values());
|
|
5858
|
+
}
|
|
5859
|
+
function __row(f, asked) {
|
|
5860
|
+
const o = app.mapSystem.getRegionOverride(f._mapId);
|
|
5861
|
+
const r = { regionId: f._mapId, name: f._mapName, fill: o ? o.fill : null };
|
|
5862
|
+
if (asked !== undefined) r.asked = asked;
|
|
5863
|
+
return r;
|
|
5864
|
+
}
|
|
5865
|
+
function __overridden() {
|
|
5866
|
+
return __features().filter(function (f) { return !!app.mapSystem.getRegionOverride(f._mapId); })
|
|
5867
|
+
.map(function (f) { return __row(f); });
|
|
5868
|
+
}
|
|
5869
|
+
// No map loaded: every id is "not on this map", which blames the ids.
|
|
5870
|
+
const __noMap = { success: false, error: 'no map is loaded. Load one with pinepaper_map {action: "load"} first.' };
|
|
5871
|
+
function __sampleIds() {
|
|
5872
|
+
return __features().slice(0, 8).map(function (f) { return f._mapId + ' (' + f._mapName + ')'; }).join(', ');
|
|
5873
|
+
}
|
|
5874
|
+
function __check(asked) {
|
|
5875
|
+
const done = [], notFound = [], notApplied = [];
|
|
5876
|
+
asked.forEach(function (id) {
|
|
5877
|
+
const f = __resolve(id);
|
|
5878
|
+
if (!f) { notFound.push(id); return; }
|
|
5879
|
+
const r = __row(f, id);
|
|
5880
|
+
(r.fill ? done : notApplied).push(r);
|
|
5881
|
+
});
|
|
5882
|
+
return { done: done, notFound: notFound, notApplied: notApplied };
|
|
5883
|
+
}`;
|
|
5597
5884
|
}
|
|
5598
5885
|
/**
|
|
5599
5886
|
* Generate code to highlight map regions
|
|
@@ -5612,13 +5899,19 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5612
5899
|
const fill = ho.color ?? ho.fillColor ?? ho.fill;
|
|
5613
5900
|
if (fill !== undefined)
|
|
5614
5901
|
style.fill = fill;
|
|
5902
|
+
// OPACITY IS A TINT OVER THE LAND (gate D59). The override REPLACES a
|
|
5903
|
+
// region's land fill rather than drawing over it, and an rgba fill lost
|
|
5904
|
+
// its alpha on the way to the exported pixels, so the readback reported a
|
|
5905
|
+
// blend the PNG did not draw. The colour is blended over each region's own
|
|
5906
|
+
// land fill in the browser and sent opaque: export and readback agree.
|
|
5907
|
+
const opacity = typeof ho.opacity === 'number' ? ho.opacity : null;
|
|
5615
5908
|
const stroke = ho.strokeColor ?? ho.stroke;
|
|
5616
5909
|
if (stroke !== undefined)
|
|
5617
5910
|
style.stroke = stroke;
|
|
5618
5911
|
if (ho.strokeWidth !== undefined)
|
|
5619
5912
|
style.strokeWidth = ho.strokeWidth;
|
|
5620
5913
|
for (const [k, v] of Object.entries(ho)) {
|
|
5621
|
-
if (!['color', 'fillColor', 'fill', 'strokeColor', 'stroke', 'strokeWidth'].includes(k) && v !== undefined) {
|
|
5914
|
+
if (!['color', 'fillColor', 'fill', 'strokeColor', 'stroke', 'strokeWidth', 'opacity'].includes(k) && v !== undefined) {
|
|
5622
5915
|
style[k] = v;
|
|
5623
5916
|
}
|
|
5624
5917
|
}
|
|
@@ -5630,9 +5923,68 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5630
5923
|
return { success: false, error: 'Map system not available' };
|
|
5631
5924
|
}
|
|
5632
5925
|
|
|
5633
|
-
try {
|
|
5634
|
-
|
|
5635
|
-
|
|
5926
|
+
try {${this.mapRegionReadbackJs()}
|
|
5927
|
+
if (!__features().length) return __noMap;
|
|
5928
|
+
const __style = ${optionsStr};
|
|
5929
|
+
const __op = ${JSON.stringify(opacity)};
|
|
5930
|
+
const __expected = {};
|
|
5931
|
+
const __unblended = [];
|
|
5932
|
+
if (__op !== null && typeof __style.fill === 'string') {
|
|
5933
|
+
const __rgb = function (c) {
|
|
5934
|
+
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(c || ''));
|
|
5935
|
+
if (!m) return null;
|
|
5936
|
+
const x = m[1].length === 3 ? m[1].replace(/./g, function (ch) { return ch + ch; }) : m[1];
|
|
5937
|
+
return [0, 2, 4].map(function (i) { return parseInt(x.slice(i, i + 2), 16); });
|
|
5938
|
+
};
|
|
5939
|
+
const __src = __rgb(__style.fill);
|
|
5940
|
+
const __p = app.mapSystem._mapRenderParams;
|
|
5941
|
+
const __ms = (__p && __p.mergedStyles) || {};
|
|
5942
|
+
const __user = (app.mapSystem.currentMap && app.mapSystem.currentMap.geoData && app.mapSystem.currentMap.geoData.features) || [];
|
|
5943
|
+
${regionIds}.forEach(function (id) {
|
|
5944
|
+
const f = __resolve(id);
|
|
5945
|
+
if (!f) return;
|
|
5946
|
+
// The land the engine paints under this region: a user feature takes
|
|
5947
|
+
// the map's fill, a basemap country its basemapFill.
|
|
5948
|
+
const __land = __rgb(__user.indexOf(f) !== -1 ? __ms.fill
|
|
5949
|
+
: (__ms.basemapFill || (app.mapSystem.defaultStyles || {}).basemapFill));
|
|
5950
|
+
let __fill = __style.fill;
|
|
5951
|
+
if (__src && __land) {
|
|
5952
|
+
__fill = '#' + [0, 1, 2].map(function (i) {
|
|
5953
|
+
return ('0' + Math.round(__src[i] * __op + __land[i] * (1 - __op)).toString(16)).slice(-2);
|
|
5954
|
+
}).join('');
|
|
5955
|
+
} else {
|
|
5956
|
+
__unblended.push(id);
|
|
5957
|
+
}
|
|
5958
|
+
__expected[id] = __fill;
|
|
5959
|
+
app.mapSystem.highlightRegions([id], Object.assign({}, __style, { fill: __fill }));
|
|
5960
|
+
});
|
|
5961
|
+
} else {
|
|
5962
|
+
app.mapSystem.highlightRegions(${regionIds}, __style);
|
|
5963
|
+
}
|
|
5964
|
+
// READ BACK (gate D16, D51): every id is resolved and its colour read.
|
|
5965
|
+
const __r = __check(${regionIds});
|
|
5966
|
+
if (!__r.done.length) {
|
|
5967
|
+
return { success: false, notFound: __r.notFound, notApplied: __r.notApplied,
|
|
5968
|
+
error: 'no region was highlighted.'
|
|
5969
|
+
+ (__r.notFound.length ? ' Not on this map: ' + __r.notFound.join(', ') + '. Ids on this map include ' + __sampleIds() + '; names work too.' : '') };
|
|
5970
|
+
}
|
|
5971
|
+
// The colour asked for must be the colour drawn (gate D59: the readback
|
|
5972
|
+
// reported #3b82f6 for a #e11d48 request and still said success).
|
|
5973
|
+
const __want = ${JSON.stringify(style.fill ?? null)};
|
|
5974
|
+
if (__want) {
|
|
5975
|
+
const __exp = function (d) { return __expected[d.asked] || __want; };
|
|
5976
|
+
const __off = __r.done.filter(function (d) { return String(d.fill).toLowerCase() !== String(__exp(d)).toLowerCase(); });
|
|
5977
|
+
if (__off.length === __r.done.length) {
|
|
5978
|
+
return { success: false, highlighted: __r.done, error: 'the regions were highlighted in ' + __off[0].fill + ', not the ' + __exp(__off[0]) + ' asked for.' };
|
|
5979
|
+
}
|
|
5980
|
+
if (__off.length) __r.wrongFill = __off;
|
|
5981
|
+
}
|
|
5982
|
+
const __res = { success: true, highlighted: __r.done };
|
|
5983
|
+
if (__unblended.length) { __res.unblended = __unblended; __res.warning = 'opacity needs a hex colour and a hex land fill; drawn at full strength: ' + __unblended.join(', ') + '.'; }
|
|
5984
|
+
if (__r.wrongFill) { __res.wrongFill = __r.wrongFill; __res.warning = 'some regions were not drawn in ' + __want + ': ' + __r.wrongFill.map(function (d) { return d.regionId; }).join(', ') + '.'; }
|
|
5985
|
+
if (__r.notFound.length) { __res.notFound = __r.notFound; __res.warning = 'not on this map, so not highlighted: ' + __r.notFound.join(', ') + '. Ids on this map include ' + __sampleIds() + '.'; }
|
|
5986
|
+
if (__r.notApplied.length) __res.notApplied = __r.notApplied;
|
|
5987
|
+
return __res;
|
|
5636
5988
|
} catch (error) {
|
|
5637
5989
|
return { success: false, error: error.message };
|
|
5638
5990
|
}
|
|
@@ -5713,9 +6065,20 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5713
6065
|
return { success: false, error: 'Map system not available' };
|
|
5714
6066
|
}
|
|
5715
6067
|
|
|
5716
|
-
try {
|
|
5717
|
-
|
|
5718
|
-
|
|
6068
|
+
try {${this.mapRegionReadbackJs()}
|
|
6069
|
+
if (!__features().length) return __noMap;
|
|
6070
|
+
const __legend = app.mapSystem.applyDataColors(${dataStr}, ${optionsStr});
|
|
6071
|
+
// READ BACK (gate D51): the engine skips a key it cannot resolve with a
|
|
6072
|
+
// console warning, which production strips, so a misspelt key looked coloured.
|
|
6073
|
+
const __r = __check(${JSON.stringify(Object.keys(validated.data))});
|
|
6074
|
+
if (!__r.done.length) {
|
|
6075
|
+
return { success: false, unmatched: __r.notFound,
|
|
6076
|
+
error: 'no region was coloured: none of the keys is on this map. Ids on this map include ' + __sampleIds() + '; names work too.' };
|
|
6077
|
+
}
|
|
6078
|
+
const __res = { success: true, regionsColored: __r.done.length, colored: __r.done };
|
|
6079
|
+
if (__legend && __legend.id !== undefined) __res.legendId = __legend.id;
|
|
6080
|
+
if (__r.notFound.length) { __res.unmatched = __r.notFound; __res.warning = 'not on this map, so not coloured: ' + __r.notFound.join(', ') + '.'; }
|
|
6081
|
+
return __res;
|
|
5719
6082
|
} catch (error) {
|
|
5720
6083
|
return { success: false, error: error.message };
|
|
5721
6084
|
}
|
|
@@ -5749,8 +6112,19 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5749
6112
|
}
|
|
5750
6113
|
|
|
5751
6114
|
try {
|
|
5752
|
-
|
|
5753
|
-
|
|
6115
|
+
// addMarker(longitude, latitude, options). One object went in as the
|
|
6116
|
+
// longitude, projected to NaN, and the engine returned null while this
|
|
6117
|
+
// reported success with the made-up id 'marker_added' (gate D51).
|
|
6118
|
+
const __o = ${JSON.stringify(options)};
|
|
6119
|
+
const marker = app.mapSystem.addMarker(__o.lon, __o.lat, __o);
|
|
6120
|
+
if (!marker) {
|
|
6121
|
+
return { success: false, error: 'no marker was added: [' + __o.lat + ', ' + __o.lon + '] (lat, lon) is outside the visible map, or no map is loaded.' };
|
|
6122
|
+
}
|
|
6123
|
+
// A REGISTRY id, so modify / animate / delete can take it. register()
|
|
6124
|
+
// leaves the marker inside the map group; registerItem would move it out,
|
|
6125
|
+
// and it would stop panning and clearing with the map.
|
|
6126
|
+
const markerId = app.itemRegistry.register(marker, 'mapMarker', { lat: __o.lat, lon: __o.lon, label: __o.label || null }, 'user');
|
|
6127
|
+
return { success: true, markerId: markerId, position: [marker.position.x, marker.position.y] };
|
|
5754
6128
|
} catch (error) {
|
|
5755
6129
|
return { success: false, error: error.message };
|
|
5756
6130
|
}
|
|
@@ -5800,26 +6174,13 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5800
6174
|
options.animate = validated.animate;
|
|
5801
6175
|
if (validated.duration !== undefined)
|
|
5802
6176
|
options.duration = validated.duration;
|
|
5803
|
-
|
|
5804
|
-
//
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
try {
|
|
5811
|
-
// panTo() has never existed on any studio, and there is no lat/lon pan for
|
|
5812
|
-
// a flat map to route this to. Refused by NAME with the calls that do work,
|
|
5813
|
-
// rather than letting the caller take an undefined-is-not-a-function.
|
|
5814
|
-
return { success: false, error: 'pan_map is not supported: the engine has no lat/lon pan. '
|
|
5815
|
-
+ 'On a globe, rotateGlobeTo brings a coordinate to the front; on a flat map, zoom to a REGION '
|
|
5816
|
-
+ '(zoomToRegion) or call resetView for the whole view. This tool accepted coordinates and did '
|
|
5817
|
-
+ 'nothing for as long as it existed.' };
|
|
5818
|
-
} catch (error) {
|
|
5819
|
-
return { success: false, error: error.message };
|
|
5820
|
-
}
|
|
5821
|
-
})();
|
|
5822
|
-
`.trim();
|
|
6177
|
+
// REFUSED, HONESTLY (gate run 2, 1.6.19). Run 1 turned pan into a camera
|
|
6178
|
+
// pan, which moves the studio VIEW but not the map: a canvas-framed export
|
|
6179
|
+
// ignores the camera, so the call reported success and the export was
|
|
6180
|
+
// unchanged. The map itself cannot be moved without breaking geoToCanvas
|
|
6181
|
+
// (it projects against the untransformed map, so markers would land in the
|
|
6182
|
+
// wrong place). Until the engine has a map view transform, say so.
|
|
6183
|
+
return `(function() { return { success: false, error: ${JSON.stringify(MAP_VIEW_REFUSAL('pan'))} }; })();`;
|
|
5823
6184
|
}
|
|
5824
6185
|
/**
|
|
5825
6186
|
* Generate code to zoom the map
|
|
@@ -5831,24 +6192,8 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5831
6192
|
options.animate = validated.animate;
|
|
5832
6193
|
if (validated.duration !== undefined)
|
|
5833
6194
|
options.duration = validated.duration;
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
(function() {
|
|
5837
|
-
if (!app.mapSystem) {
|
|
5838
|
-
return { success: false, error: 'Map system not available' };
|
|
5839
|
-
}
|
|
5840
|
-
|
|
5841
|
-
try {
|
|
5842
|
-
// zoomTo(level) has never existed. The engine zooms to a REGION, not to a
|
|
5843
|
-
// numeric level, so there is nothing to convert a level into.
|
|
5844
|
-
return { success: false, error: 'zoom_map takes a numeric level and the engine has no such call: it zooms to a REGION. '
|
|
5845
|
-
+ 'Use zoomToRegion with a region id, or resetView for the full view. A level was accepted here and did '
|
|
5846
|
-
+ 'nothing for as long as this tool existed.' };
|
|
5847
|
-
} catch (error) {
|
|
5848
|
-
return { success: false, error: error.message };
|
|
5849
|
-
}
|
|
5850
|
-
})();
|
|
5851
|
-
`.trim();
|
|
6195
|
+
// Refused for the same reason as pan (see generatePanMap).
|
|
6196
|
+
return `(function() { return { success: false, error: ${JSON.stringify(MAP_VIEW_REFUSAL('zoom'))} }; })();`;
|
|
5852
6197
|
}
|
|
5853
6198
|
/**
|
|
5854
6199
|
* Generate code to export map configuration
|
|
@@ -5893,7 +6238,11 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5893
6238
|
|
|
5894
6239
|
try {
|
|
5895
6240
|
const result = await app.mapSystem.importCustomMap('${validated.url}', ${optionsStr});
|
|
5896
|
-
|
|
6241
|
+
// A summary, not the engine's return: that carries the Paper group and
|
|
6242
|
+
// the region Map, which serialised into a raw Paper.js dump (gate N1).
|
|
6243
|
+
const __r = result && result.regions;
|
|
6244
|
+
const __n = (__r && (__r.size ?? __r.length)) || (result && result.geoData && result.geoData.features ? result.geoData.features.length : 0);
|
|
6245
|
+
return { success: !!result, mapId: (result && result.mapId) || null, regions: __n, regionIds: (__r && __r.size ? Array.from(__r.keys()) : ((result && result.geoData && result.geoData.features) || []).map(function (f) { return (f.properties && f.properties.name) || f.id; })).slice(0, 50) };
|
|
5897
6246
|
} catch (error) {
|
|
5898
6247
|
return { success: false, error: error.message };
|
|
5899
6248
|
}
|
|
@@ -5910,7 +6259,11 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5910
6259
|
|
|
5911
6260
|
try {
|
|
5912
6261
|
const result = await app.mapSystem.importCustomMap(${JSON.stringify(validated.geoJson)}, ${optionsStr});
|
|
5913
|
-
|
|
6262
|
+
// A summary, not the engine's return: that carries the Paper group and
|
|
6263
|
+
// the region Map, which serialised into a raw Paper.js dump (gate N1).
|
|
6264
|
+
const __r = result && result.regions;
|
|
6265
|
+
const __n = (__r && (__r.size ?? __r.length)) || (result && result.geoData && result.geoData.features ? result.geoData.features.length : 0);
|
|
6266
|
+
return { success: !!result, mapId: (result && result.mapId) || null, regions: __n, regionIds: (__r && __r.size ? Array.from(__r.keys()) : ((result && result.geoData && result.geoData.features) || []).map(function (f) { return (f.properties && f.properties.name) || f.id; })).slice(0, 50) };
|
|
5914
6267
|
} catch (error) {
|
|
5915
6268
|
return { success: false, error: error.message };
|
|
5916
6269
|
}
|
|
@@ -5937,20 +6290,17 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5937
6290
|
}
|
|
5938
6291
|
|
|
5939
6292
|
try {
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
}
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
return { success: true, regionId: region.id, regionName: region.name, properties: region.properties };
|
|
5952
|
-
}
|
|
5953
|
-
return { success: true, regionId: null, message: 'No region at this point' };
|
|
6293
|
+
if (!app.mapSystem.currentMap) {
|
|
6294
|
+
return { success: false, error: 'no map is loaded. Load one with pinepaper_map {action: "load"} first.' };
|
|
6295
|
+
}
|
|
6296
|
+
// canvasToGeo takes ONE [x, y] argument; (x, y) handed it a bare number.
|
|
6297
|
+
const coord = typeof app.mapSystem.canvasToGeo === 'function' ? app.mapSystem.canvasToGeo([${validated.x}, ${validated.y}]) : null;
|
|
6298
|
+
// The hit test the studio's own hover uses (getRegionAtPoint never existed).
|
|
6299
|
+
const f = app.mapSystem._findFeatureAtCanvasPoint(${validated.x}, ${validated.y});
|
|
6300
|
+
if (!f) return { success: true, regionId: null, coordinate: coord, message: 'no region at this point (off the map, or ocean)' };
|
|
6301
|
+
const p = f.properties || {};
|
|
6302
|
+
return { success: true, regionId: f._mapId !== undefined ? f._mapId : (p.id || p.ISO_A3 || p.name || null),
|
|
6303
|
+
regionName: f._mapName || p.name || p.NAME || null, coordinate: coord, properties: p };
|
|
5954
6304
|
} catch (error) {
|
|
5955
6305
|
return { success: false, error: error.message };
|
|
5956
6306
|
}
|
|
@@ -6003,7 +6353,8 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6003
6353
|
const result = app.mapSystem.animateRegionsWave({
|
|
6004
6354
|
duration: ${validated.duration || 10},
|
|
6005
6355
|
loop: ${validated.loop !== false},
|
|
6006
|
-
colors: ${colorsStr}
|
|
6356
|
+
colors: ${colorsStr},${validated.waveSpeed !== undefined ? `
|
|
6357
|
+
waveSpeed: ${validated.waveSpeed},` : ''}
|
|
6007
6358
|
waveDirection: '${validated.waveDirection || 'horizontal'}'
|
|
6008
6359
|
});
|
|
6009
6360
|
return { success: true, animatedRegions: result?.animatedRegions || [], totalRegions: result?.totalRegions || 0 };
|
|
@@ -6169,9 +6520,17 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6169
6520
|
return { success: false, error: 'Map system not available' };
|
|
6170
6521
|
}
|
|
6171
6522
|
|
|
6172
|
-
try {
|
|
6173
|
-
|
|
6174
|
-
|
|
6523
|
+
try {${this.mapRegionReadbackJs()}
|
|
6524
|
+
// getHighlightedRegions returns an ARRAY of {regionId, name, path}; this
|
|
6525
|
+
// read result.highlighted off it and always answered [] (gate D51). It also
|
|
6526
|
+
// walks regionPaths, empty on a rasterised map, so the override channel is
|
|
6527
|
+
// read as well.
|
|
6528
|
+
const __byId = new Map();
|
|
6529
|
+
(app.mapSystem.getHighlightedRegions() || []).forEach(function (r) { __byId.set(r.regionId, { regionId: r.regionId, name: r.name }); });
|
|
6530
|
+
__overridden().forEach(function (r) { __byId.set(r.regionId, r); });
|
|
6531
|
+
const __list = Array.from(__byId.values());
|
|
6532
|
+
return { success: true, highlighted: __list, count: __list.length,
|
|
6533
|
+
note: 'highlight and apply_colors share one colour channel, so this lists every region either has coloured.' };
|
|
6175
6534
|
} catch (error) {
|
|
6176
6535
|
return { success: false, error: error.message };
|
|
6177
6536
|
}
|
|
@@ -6373,6 +6732,87 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6373
6732
|
/**
|
|
6374
6733
|
* Generate code to export the complete scene state
|
|
6375
6734
|
*/
|
|
6735
|
+
/**
|
|
6736
|
+
* pinepaper_place_on_surface (FxTool #56): app.placeOnSurface corner-pins
|
|
6737
|
+
* the UI into the photo's screen, mattes it so a hand stays in front, and
|
|
6738
|
+
* BAKES it into the photo's pixels. Async. Only plain fields come back.
|
|
6739
|
+
*/
|
|
6740
|
+
generatePlaceOnSurface(input) {
|
|
6741
|
+
// Named, not spread: a spread makes every field look consumed, which is
|
|
6742
|
+
// how an accepted-and-ignored option hides (the guard test reads these).
|
|
6743
|
+
const opts = {};
|
|
6744
|
+
if (input.quad !== undefined)
|
|
6745
|
+
opts.quad = input.quad;
|
|
6746
|
+
if (input.quadSpace !== undefined)
|
|
6747
|
+
opts.quadSpace = input.quadSpace;
|
|
6748
|
+
if (input.occlusion !== undefined)
|
|
6749
|
+
opts.occlusion = input.occlusion;
|
|
6750
|
+
if (input.key !== undefined)
|
|
6751
|
+
opts.key = input.key;
|
|
6752
|
+
if (input.erode !== undefined)
|
|
6753
|
+
opts.erode = input.erode;
|
|
6754
|
+
if (input.matteBlur !== undefined)
|
|
6755
|
+
opts.matteBlur = input.matteBlur;
|
|
6756
|
+
if (input.soften !== undefined)
|
|
6757
|
+
opts.soften = input.soften;
|
|
6758
|
+
if (input.glare !== undefined)
|
|
6759
|
+
opts.glare = input.glare;
|
|
6760
|
+
if (input.spill !== undefined)
|
|
6761
|
+
opts.spill = input.spill;
|
|
6762
|
+
if (input.spillRadius !== undefined)
|
|
6763
|
+
opts.spillRadius = input.spillRadius;
|
|
6764
|
+
if (input.tint !== undefined)
|
|
6765
|
+
opts.tint = input.tint;
|
|
6766
|
+
if (input.slot !== undefined)
|
|
6767
|
+
opts.slot = input.slot;
|
|
6768
|
+
if (input.hideSource !== undefined)
|
|
6769
|
+
opts.hideSource = input.hideSource;
|
|
6770
|
+
const { photoId, sourceId } = input;
|
|
6771
|
+
return `
|
|
6772
|
+
// Place a UI onto a screen in a photo
|
|
6773
|
+
(async function() {
|
|
6774
|
+
if (typeof app.placeOnSurface !== 'function') { return { success: false, error: 'app.placeOnSurface unavailable — update FxTool' }; }
|
|
6775
|
+
const r = await app.placeOnSurface(${JSON.stringify(photoId)}, ${JSON.stringify(sourceId)}, ${JSON.stringify(opts)});
|
|
6776
|
+
if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the UI could not be placed' }; }
|
|
6777
|
+
return { success: true, photoId: r.id, slot: r.slot, quad: r.quad, detected: r.detected || null,
|
|
6778
|
+
key: r.key, matteCoverage: r.matteCoverage, ...(r.lostSlots && r.lostSlots.length ? { lostSlots: r.lostSlots } : {}) };
|
|
6779
|
+
})();`.trim();
|
|
6780
|
+
}
|
|
6781
|
+
/**
|
|
6782
|
+
* A RESTORABLE project document (D28): app.captureProjectDocument, the
|
|
6783
|
+
* engine's own durable format (core/ProjectDocument.js), which carries
|
|
6784
|
+
* relations, rigging, connectors, scene chains and animation config.
|
|
6785
|
+
* NOT exportProjectJSON: the engine documents that one as a bare Paper tree
|
|
6786
|
+
* that drops roughly half of a non-trivial scene.
|
|
6787
|
+
*/
|
|
6788
|
+
generateCaptureProject(name) {
|
|
6789
|
+
return `
|
|
6790
|
+
// Capture a restorable project document
|
|
6791
|
+
(function() {
|
|
6792
|
+
if (typeof app.captureProjectDocument !== 'function') { return { success: false, error: 'app.captureProjectDocument unavailable — update FxTool' }; }
|
|
6793
|
+
${SCENE_COUNTS_JS}
|
|
6794
|
+
const doc = app.captureProjectDocument(${JSON.stringify(name ? { name } : {})});
|
|
6795
|
+
return { success: true, json: JSON.stringify(doc), counts: __ppSceneCounts() };
|
|
6796
|
+
})();`.trim();
|
|
6797
|
+
}
|
|
6798
|
+
/** Load a staged project document (window.__ppStage.projectDoc), replacing the scene. */
|
|
6799
|
+
generateLoadProject(strict) {
|
|
6800
|
+
return `
|
|
6801
|
+
// Load a project document, replacing the current scene
|
|
6802
|
+
(function() {
|
|
6803
|
+
if (typeof app.loadProjectDocument !== 'function') { return { success: false, error: 'app.loadProjectDocument unavailable — update FxTool' }; }
|
|
6804
|
+
${SCENE_COUNTS_JS}
|
|
6805
|
+
const text = window.__ppStage && window.__ppStage.projectDoc;
|
|
6806
|
+
if (typeof text !== 'string' || !text) { return { success: false, error: 'no project document was staged' }; }
|
|
6807
|
+
// Second guard, in the page: nothing but a project document reaches the loader.
|
|
6808
|
+
let __doc = null;
|
|
6809
|
+
try { __doc = JSON.parse(text); } catch (e) { return { success: false, error: 'not a PinePaper scene file (not JSON); nothing was changed' }; }
|
|
6810
|
+
if (!__doc || __doc.kind !== 'pinepaper.project') { return { success: false, error: 'not a PinePaper scene file (no kind "pinepaper.project"); nothing was changed' }; }
|
|
6811
|
+
const r = app.loadProjectDocument(__doc, { strict: ${strict ? 'true' : 'false'} });
|
|
6812
|
+
if (!r || r.ok === false) { return { success: false, error: 'the document was refused: ' + ((r && r.errors && r.errors.join('; ')) || 'invalid project document'), warnings: (r && r.warnings) || [] }; }
|
|
6813
|
+
return { success: true, counts: __ppSceneCounts(), warnings: r.warnings || [], meta: r.meta || null };
|
|
6814
|
+
})();`.trim();
|
|
6815
|
+
}
|
|
6376
6816
|
generateExportScene() {
|
|
6377
6817
|
return `
|
|
6378
6818
|
// Export complete scene state
|
|
@@ -6484,7 +6924,11 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6484
6924
|
backgroundColor: backgroundColor,
|
|
6485
6925
|
canvasSize: canvasSize,
|
|
6486
6926
|
itemCount: items.length,
|
|
6487
|
-
relationCount: relations.length
|
|
6927
|
+
relationCount: relations.length,
|
|
6928
|
+
// NOT A BACKUP (D28). This summary carries positions and bounds, not
|
|
6929
|
+
// keyframe tracks, rigging or scene chains: a scene saved this way and
|
|
6930
|
+
// rebuilt from it has no motion. Said in the result, not only in docs.
|
|
6931
|
+
note: 'summary for inspection: animation tracks, rigging and scene chains are not included and this cannot be restored. To save a restorable scene: pinepaper_export_scene with full: true, then pinepaper_import_scene.'
|
|
6488
6932
|
};
|
|
6489
6933
|
} catch (error) {
|
|
6490
6934
|
return { success: false, error: error.message };
|
|
@@ -6585,29 +7029,32 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6585
7029
|
try {
|
|
6586
7030
|
// Import the Paper.js item from JSON
|
|
6587
7031
|
const item = paper.project.importJSON(${itemJsonStr});
|
|
6588
|
-
|
|
6589
|
-
|
|
7032
|
+
// importJSON accepts anything: { bogus: true } came back as an object with
|
|
7033
|
+
// no bounds and was registered as an item (found by the 1.6.19 gate). Only
|
|
7034
|
+
// a real Paper item with geometry may be registered.
|
|
7035
|
+
if (!item || !(item instanceof paper.Item) || !item.bounds) {
|
|
7036
|
+
try { if (item && typeof item.remove === 'function') item.remove(); } catch (e) {}
|
|
7037
|
+
return { success: false, error: 'itemJson is not a Paper.js item (pass the output of item.exportJSON())' };
|
|
7038
|
+
}
|
|
7039
|
+
|
|
7040
|
+
// REGISTER THROUGH THE ENGINE (gate H1, 1.6.19). This used to call
|
|
7041
|
+
// app.itemRegistry.set(), which does not exist ("not a function", nothing
|
|
7042
|
+
// registered), with an id invented from .size (undefined, so 'item_NaN',
|
|
7043
|
+
// or a collision), and it moved every item into the TEXT group. The
|
|
7044
|
+
// registry's own register() mints the id, never steals a live one, treats
|
|
7045
|
+
// re-registration as an update, and returns the id it assigned.
|
|
7046
|
+
// importJSON already put the item in the active layer.
|
|
7047
|
+
if (!app.itemRegistry || typeof app.itemRegistry.register !== 'function') {
|
|
7048
|
+
try { item.remove(); } catch (e) {}
|
|
7049
|
+
return { success: false, error: 'app.itemRegistry.register unavailable — update FxTool' };
|
|
6590
7050
|
}
|
|
6591
|
-
|
|
6592
|
-
// Register in item registry
|
|
6593
|
-
const itemId = app.itemRegistry ? 'item_' + (app.itemRegistry.size + 1) : 'item_1';
|
|
6594
|
-
item.data = item.data || {};
|
|
6595
|
-
item.data.registryId = itemId;
|
|
6596
|
-
item.data.type = ${JSON.stringify(input.itemType)};
|
|
6597
|
-
|
|
6598
|
-
// Merge custom properties
|
|
6599
7051
|
const customProps = ${propertiesStr};
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
app.itemRegistry.set(itemId, item);
|
|
6605
|
-
}
|
|
6606
|
-
|
|
6607
|
-
// Add to appropriate group
|
|
6608
|
-
if (app.textItemGroup) {
|
|
6609
|
-
app.textItemGroup.addChild(item);
|
|
7052
|
+
const itemId = app.itemRegistry.register(item, ${JSON.stringify(input.itemType)}, customProps, 'import');
|
|
7053
|
+
if (!itemId) {
|
|
7054
|
+
try { item.remove(); } catch (e) {}
|
|
7055
|
+
return { success: false, error: 'the registry refused the item' };
|
|
6610
7056
|
}
|
|
7057
|
+
try { if (paper.view) paper.view.update(); } catch (e) {}
|
|
6611
7058
|
|
|
6612
7059
|
return {
|
|
6613
7060
|
success: true,
|
|
@@ -6861,19 +7308,25 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6861
7308
|
`.trim();
|
|
6862
7309
|
}
|
|
6863
7310
|
generateGetAvailableEasings() {
|
|
7311
|
+
// The engine's own list wins: listAnimatableProperties().easings is its
|
|
7312
|
+
// EASING_NAMES, read from the easing table itself. The static list is the
|
|
7313
|
+
// fallback for studios that predate it (fixture-pinned, easing-parity test).
|
|
6864
7314
|
return `
|
|
6865
7315
|
// Get available easing functions
|
|
6866
7316
|
(function() {
|
|
7317
|
+
const fallback = ${JSON.stringify(KEYFRAME_EASINGS)};
|
|
7318
|
+
const descriptions = ${JSON.stringify(EASING_DESCRIPTIONS)};
|
|
7319
|
+
let live = null;
|
|
7320
|
+
try {
|
|
7321
|
+
const r = typeof app !== 'undefined' && app && typeof app.listAnimatableProperties === 'function'
|
|
7322
|
+
? app.listAnimatableProperties() : null;
|
|
7323
|
+
if (r && Array.isArray(r.easings) && r.easings.length) live = r.easings;
|
|
7324
|
+
} catch (e) { live = null; }
|
|
6867
7325
|
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
|
-
}
|
|
7326
|
+
easings: live || fallback,
|
|
7327
|
+
source: live ? 'engine' : 'mcp-server fallback list',
|
|
7328
|
+
descriptions,
|
|
7329
|
+
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
7330
|
};
|
|
6878
7331
|
})();
|
|
6879
7332
|
`.trim();
|
|
@@ -6883,7 +7336,9 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6883
7336
|
// Get available mask types
|
|
6884
7337
|
(function() {
|
|
6885
7338
|
return {
|
|
6886
|
-
|
|
7339
|
+
// From the enum the mask tools validate against: this listed 'custom',
|
|
7340
|
+
// which both refuse (1.6.19 gate).
|
|
7341
|
+
maskTypes: ${JSON.stringify(MaskTypeSchema.options)}
|
|
6887
7342
|
};
|
|
6888
7343
|
})();
|
|
6889
7344
|
`.trim();
|
|
@@ -6893,14 +7348,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6893
7348
|
// Get available mask animation presets
|
|
6894
7349
|
(function() {
|
|
6895
7350
|
return {
|
|
6896
|
-
animations:
|
|
6897
|
-
'wipeLeft', 'wipeRight', 'wipeUp', 'wipeDown',
|
|
6898
|
-
'iris', 'irisOut',
|
|
6899
|
-
'star', 'heart',
|
|
6900
|
-
'curtainHorizontal', 'curtainVertical', 'cinematic',
|
|
6901
|
-
'diagonalWipe',
|
|
6902
|
-
'revealUp', 'revealDown'
|
|
6903
|
-
]
|
|
7351
|
+
animations: ${JSON.stringify(MaskPresetSchema.options)}
|
|
6904
7352
|
};
|
|
6905
7353
|
})();
|
|
6906
7354
|
`.trim();
|
|
@@ -6914,10 +7362,12 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6914
7362
|
const categoryFilter = category ? `'${category}'` : 'null';
|
|
6915
7363
|
return `
|
|
6916
7364
|
// List available templates
|
|
6917
|
-
(function() {
|
|
7365
|
+
(async function() {
|
|
6918
7366
|
if (!app.templateManager) {
|
|
6919
7367
|
return { error: 'Template manager not available. Make sure PinePaper Studio is loaded.' };
|
|
6920
7368
|
}
|
|
7369
|
+
// The built-in catalogue loads lazily; a fresh page listed none (1.6.19 gate).
|
|
7370
|
+
try { if (app.templateManager._ensureTemplateData) await app.templateManager._ensureTemplateData(); } catch (e) {}
|
|
6921
7371
|
const allTemplates = app.templateManager.getAllTemplates();
|
|
6922
7372
|
const category = ${categoryFilter};
|
|
6923
7373
|
const filtered = category
|
|
@@ -6942,8 +7392,25 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6942
7392
|
if (!app.templateManager) {
|
|
6943
7393
|
return { error: 'Template manager not available. Make sure PinePaper Studio is loaded.' };
|
|
6944
7394
|
}
|
|
7395
|
+
// AN UNKNOWN ID IS AN ERROR, NOT A SUCCESS (gate C1, 1.6.19). loadTemplate
|
|
7396
|
+
// on an id that does not exist changed nothing and this still reported
|
|
7397
|
+
// "Template applied successfully"; the description's own example id
|
|
7398
|
+
// ('solar-system') was one of them. The nearest real ids are offered.
|
|
7399
|
+
const __tid = ${JSON.stringify(templateId)};
|
|
7400
|
+
// The built-in catalogue loads LAZILY: on a fresh page getAllTemplates() is
|
|
7401
|
+
// [] until it does, which made every id look unknown. Load it first, as
|
|
7402
|
+
// loadTemplate itself does, and count the cloud templates it falls back to.
|
|
7403
|
+
try { if (app.templateManager._ensureTemplateData) await app.templateManager._ensureTemplateData(); } catch (e) {}
|
|
7404
|
+
const __cloud = (app.templateManager.cloudSync && app.templateManager.cloudSync.cloudTemplates) || [];
|
|
7405
|
+
const __all = app.templateManager.getAllTemplates().concat(__cloud.filter(Boolean));
|
|
7406
|
+
if (__all.length && !__all.some(function(t) { return t && t.id === __tid; })) {
|
|
7407
|
+
const __lev = function(a, b) { const m = a.length, n = b.length; const d = Array.from({ length: m + 1 }, function(_, i) { return [i]; }); for (let j = 1; j <= n; j++) d[0][j] = j; for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) d[i][j] = Math.min(d[i-1][j] + 1, d[i][j-1] + 1, d[i-1][j-1] + (a[i-1] === b[j-1] ? 0 : 1)); return d[m][n]; };
|
|
7408
|
+
const __near = __all.map(function(t) { const id = String(t.id); return { id: id, d: id.indexOf(__tid) >= 0 || __tid.indexOf(id) >= 0 ? 0 : __lev(__tid, id) }; })
|
|
7409
|
+
.sort(function(a, b) { return a.d - b.d; }).slice(0, 5).map(function(x) { return x.id; });
|
|
7410
|
+
return { success: false, error: 'no template "' + __tid + '". Nearest: ' + __near.join(', ') + '. List them with listOnly: true.' };
|
|
7411
|
+
}
|
|
6945
7412
|
try {
|
|
6946
|
-
await app.templateManager.loadTemplate(
|
|
7413
|
+
await app.templateManager.loadTemplate(__tid, true);
|
|
6947
7414
|
|
|
6948
7415
|
// DID THE CLIPPED CHARACTER PARTS SURVIVE?
|
|
6949
7416
|
//
|
|
@@ -7600,7 +8067,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7600
8067
|
for (const id of ids) {
|
|
7601
8068
|
const entry = app.itemRegistry.get(id);
|
|
7602
8069
|
if (entry && entry.item) {
|
|
7603
|
-
${mode === 'remove' ? 'entry.item.selected = false;' : 'app.select(entry.item);'}
|
|
8070
|
+
${mode === 'remove' ? 'entry.item.selected = false;' : 'app.select(entry.item, false);'}
|
|
7604
8071
|
selected.push(id);
|
|
7605
8072
|
}
|
|
7606
8073
|
}
|
|
@@ -7611,8 +8078,9 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7611
8078
|
return `
|
|
7612
8079
|
// Select all items
|
|
7613
8080
|
(function() {
|
|
8081
|
+
${REG_ID_JS}
|
|
7614
8082
|
app.selectAll();
|
|
7615
|
-
const items = app.getSelectedItems().map(i => i.
|
|
8083
|
+
const items = app.getSelectedItems().map(i => __ppRegId(i)).filter(Boolean);
|
|
7616
8084
|
return { success: true, action: 'select_all', count: items.length, items };
|
|
7617
8085
|
})();`.trim();
|
|
7618
8086
|
case 'deselect_all':
|
|
@@ -7626,8 +8094,9 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7626
8094
|
return `
|
|
7627
8095
|
// Get current selection
|
|
7628
8096
|
(function() {
|
|
8097
|
+
${REG_ID_JS}
|
|
7629
8098
|
const items = app.getSelectedItems().map(i => ({
|
|
7630
|
-
itemId: i
|
|
8099
|
+
itemId: __ppRegId(i),
|
|
7631
8100
|
type: i.data?.itemType || i.className,
|
|
7632
8101
|
bounds: i.bounds ? { x: i.bounds.x, y: i.bounds.y, width: i.bounds.width, height: i.bounds.height } : null
|
|
7633
8102
|
}));
|
|
@@ -7637,8 +8106,9 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7637
8106
|
return `
|
|
7638
8107
|
// Delete selected items
|
|
7639
8108
|
(function() {
|
|
8109
|
+
${REG_ID_JS}
|
|
7640
8110
|
const selection = app.getSelectedItems();
|
|
7641
|
-
const deleted = selection.map(i => i.
|
|
8111
|
+
const deleted = selection.map(i => __ppRegId(i)).filter(Boolean);
|
|
7642
8112
|
app.deleteSelected();
|
|
7643
8113
|
return { success: true, action: 'delete_selected', deleted, count: deleted.length };
|
|
7644
8114
|
})();`.trim();
|
|
@@ -7817,7 +8287,7 @@ ${loads}
|
|
|
7817
8287
|
case 'lineage': return this._facadeCall('getLineage', id, 'Provenance: lineage');
|
|
7818
8288
|
case 'dependents': return this._facadeCall('getDependents', id, 'Provenance: dependents');
|
|
7819
8289
|
case 'record':
|
|
7820
|
-
return this._facadeCall('recordLineage', `${id}, ${JSON.stringify(input.kind || '
|
|
8290
|
+
return this._facadeCall('recordLineage', `${id}, ${JSON.stringify(input.kind || 'derivedFrom')}, ${JSON.stringify(input.sourceRef || '')}, ${JSON.stringify(input.meta || {})}`, 'Provenance: record lineage');
|
|
7821
8291
|
}
|
|
7822
8292
|
}
|
|
7823
8293
|
generateCompose(input) {
|
|
@@ -7839,27 +8309,46 @@ ${loads}
|
|
|
7839
8309
|
// vector and audio halves of composeCollage were unreachable through
|
|
7840
8310
|
// MCP, so agents converged on the four static patterns the description
|
|
7841
8311
|
// happened to name. A capability gap reads as model bias from outside.
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
8312
|
+
{
|
|
8313
|
+
const apply = this._facadeCall('composeCollage', `${JSON.stringify(input.pattern || '')}, ${JSON.stringify(input.itemIds || [])}, ${JSON.stringify({
|
|
8314
|
+
...(input.treatment ? { treatment: input.treatment } : {}),
|
|
8315
|
+
...(input.applyCamera === false ? { applyCamera: false } : {}),
|
|
8316
|
+
...(input.loop ? { loop: true } : {}),
|
|
8317
|
+
...(input.craft ? { craft: input.craft } : {}),
|
|
8318
|
+
...(input.reveal !== undefined ? { reveal: input.reveal } : {}),
|
|
8319
|
+
...(input.revealOptions ? { revealOptions: input.revealOptions } : {}),
|
|
8320
|
+
...(input.style ? { style: input.style } : {}),
|
|
8321
|
+
...(input.text ? { text: input.text } : {}),
|
|
8322
|
+
...(input.assets ? { assets: input.assets } : {}),
|
|
8323
|
+
...(input.audio ? { audio: input.audio } : {}),
|
|
8324
|
+
...(input.grid ? { grid: true } : {}),
|
|
8325
|
+
// The design axes. A register resolves the craft (gutter, margin,
|
|
8326
|
+
// hue budget, type scale); a medium says what makes the marks and
|
|
8327
|
+
// is REFUSED when a composition cannot be rendered in it.
|
|
8328
|
+
...(input.register ? { register: input.register } : {}),
|
|
8329
|
+
...(input.level !== undefined ? { level: input.level } : {}),
|
|
8330
|
+
...(input.medium ? { medium: input.medium } : {}),
|
|
8331
|
+
...(input.stitch ? { stitch: input.stitch } : {}),
|
|
8332
|
+
...(input.stitchBudget !== undefined ? { stitchBudget: input.stitchBudget } : {}),
|
|
8333
|
+
})}`, 'Compose: apply pattern');
|
|
8334
|
+
;
|
|
8335
|
+
if (typeof input.reveal !== 'string' || !input.reveal)
|
|
8336
|
+
return apply;
|
|
8337
|
+
// CHECK THE REVEAL FIRST (gate C3). composeCollage lays the items out
|
|
8338
|
+
// and wires their relations BEFORE it reaches an unknown reveal, so a
|
|
8339
|
+
// typo errored with the grid already applied and nothing reverted.
|
|
8340
|
+
return `
|
|
8341
|
+
// Compose: check the reveal, then apply
|
|
8342
|
+
(async function() {
|
|
8343
|
+
if (typeof app.listReveals === 'function') {
|
|
8344
|
+
const __keys = (app.listReveals() || []).map(function (r) { return r && r.key; });
|
|
8345
|
+
if (__keys.length && __keys.indexOf(${JSON.stringify(input.reveal)}) === -1) {
|
|
8346
|
+
return { success: false, error: 'unknown reveal ' + ${JSON.stringify(JSON.stringify(input.reveal))} + ' — known: ' + __keys.join(', ') + '. Nothing was changed.' };
|
|
8347
|
+
}
|
|
8348
|
+
}
|
|
8349
|
+
return await ${apply.replace(/^\/\/[^\n]*\n/, '').replace(/;\s*$/, '')};
|
|
8350
|
+
})();`.trim();
|
|
8351
|
+
}
|
|
7863
8352
|
}
|
|
7864
8353
|
}
|
|
7865
8354
|
generateAudioBeats(input) {
|
|
@@ -8510,16 +8999,28 @@ ${resolve}
|
|
|
8510
8999
|
return `
|
|
8511
9000
|
// Clear background
|
|
8512
9001
|
(function() {
|
|
9002
|
+
// clearBackground() removes generated patterns, drawings and the image, but
|
|
9003
|
+
// not the colour; the tool promises "remove entirely" and left #1e293b
|
|
9004
|
+
// behind (gate C6, 1.6.19). The colour goes too, to transparent.
|
|
8513
9005
|
app.clearBackground();
|
|
8514
|
-
|
|
9006
|
+
if (typeof app.setBackgroundColor === 'function') app.setBackgroundColor('transparent');
|
|
9007
|
+
return { success: true, action: 'clear', removed: ['pattern', 'drawing', 'image', 'color'] };
|
|
8515
9008
|
})();`.trim();
|
|
8516
9009
|
case 'get':
|
|
8517
9010
|
return `
|
|
8518
9011
|
// Get background info
|
|
8519
9012
|
(function() {
|
|
8520
|
-
//
|
|
9013
|
+
// getBackgroundMode() returns only the mode, so 'color' came back with no
|
|
9014
|
+
// colour (gate C6, 1.6.19). The colour is read from the canvas, as hex.
|
|
8521
9015
|
const mode = app.getBackgroundMode ? app.getBackgroundMode() : null;
|
|
8522
|
-
|
|
9016
|
+
const css = (app.canvasEl && app.canvasEl.style && app.canvasEl.style.backgroundColor) || '';
|
|
9017
|
+
// No regex: this is a template literal, and \\( or \\s here reach the page
|
|
9018
|
+
// without their backslash (that was the #NaN293b).
|
|
9019
|
+
const open = css.indexOf('('), close = css.indexOf(')');
|
|
9020
|
+
let color = css || null;
|
|
9021
|
+
if (css.indexOf('rgb') === 0 && open > 0 && close > open) { const p = css.slice(open + 1, close).split(',').join(' ').split('/').join(' ').split(' ').filter(Boolean).map(function (x) { return parseFloat(x); }); color = (p.length > 3 && p[3] === 0) ? 'transparent' : '#' + p.slice(0, 3).map(function (v) { return Math.round(v).toString(16).padStart(2, '0'); }).join(''); }
|
|
9022
|
+
const patterns = app.patternGroup && app.patternGroup.children ? app.patternGroup.children.length : 0;
|
|
9023
|
+
return { success: true, action: 'get', mode, color, hasPattern: patterns > 0, hasImage: !!(app.config && app.config.backgroundImage) };
|
|
8523
9024
|
})();`.trim();
|
|
8524
9025
|
default:
|
|
8525
9026
|
return `(function() { return { error: 'Unknown background action: ${input.action}' }; })();`;
|
|
@@ -8555,13 +9056,14 @@ ${resolve}
|
|
|
8555
9056
|
return `
|
|
8556
9057
|
// Hit test at point
|
|
8557
9058
|
(function() {
|
|
9059
|
+
${REG_ID_JS}
|
|
8558
9060
|
const point = new paper.Point(${input.x ?? 0}, ${input.y ?? 0});
|
|
8559
9061
|
${all
|
|
8560
9062
|
? `const results = app.hitTestAll(point, { tolerance: ${tolerance} });
|
|
8561
9063
|
const hits = results.map(r => ({
|
|
8562
|
-
itemId: r.item
|
|
8563
|
-
type: r.item?.data?.
|
|
8564
|
-
point: { x: r.point
|
|
9064
|
+
itemId: __ppRegId(r.item),
|
|
9065
|
+
type: r.item?.data?.type || r.item?.className,
|
|
9066
|
+
point: r.point ? { x: r.point.x, y: r.point.y } : null
|
|
8565
9067
|
}));
|
|
8566
9068
|
return { success: true, action: 'hit_test', x: ${input.x ?? 0}, y: ${input.y ?? 0}, hits, count: hits.length };`
|
|
8567
9069
|
: `const result = app.hitTest(point, { tolerance: ${tolerance} });
|
|
@@ -8569,7 +9071,7 @@ ${resolve}
|
|
|
8569
9071
|
return {
|
|
8570
9072
|
success: true, action: 'hit_test',
|
|
8571
9073
|
x: ${input.x ?? 0}, y: ${input.y ?? 0},
|
|
8572
|
-
hit: { itemId: result.item
|
|
9074
|
+
hit: { itemId: __ppRegId(result.item), type: result.item?.data?.type || result.item?.className, point: result.point ? { x: result.point.x, y: result.point.y } : null }
|
|
8573
9075
|
};`}
|
|
8574
9076
|
})();`.trim();
|
|
8575
9077
|
}
|
|
@@ -8969,12 +9471,47 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
8969
9471
|
${guard}
|
|
8970
9472
|
const entry = app.itemRegistry.get(${itemIdStr});
|
|
8971
9473
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
|
|
8972
|
-
|
|
8973
|
-
|
|
9474
|
+
// The engine's remove returns nothing, and this used to echo its inputs, so
|
|
9475
|
+
// a wrong behaviorId still "succeeded" (gate run 2). Count what went.
|
|
9476
|
+
const __list = function () { const m = app.interactionSystem.continuousBehaviors; const l = m && m.get ? m.get(${itemIdStr}) : null; return (l || []).map(function (b) { return b.id; }); };
|
|
9477
|
+
const __before = __list();
|
|
9478
|
+
const __bid = ${behaviorIdStr};
|
|
9479
|
+
if (__bid && __before.indexOf(__bid) === -1) {
|
|
9480
|
+
return { success: false, error: 'no behavior ' + JSON.stringify(__bid) + ' on ' + ${itemIdStr} + (__before.length ? '; it has: ' + __before.join(', ') : '; it has none') + '. Use the behaviorId add_behavior returned, or omit it to remove all.' };
|
|
9481
|
+
}
|
|
9482
|
+
if (__bid) app.interactionSystem.removeContinuousBehavior(entry.item, __bid);
|
|
9483
|
+
else {
|
|
9484
|
+
// "Remove all" means the behaviors a caller ADDED. The engine's own
|
|
9485
|
+
// selectable / draggable go too with a bare remove, which left the item
|
|
9486
|
+
// impossible to select or drag (gate run 3, N4).
|
|
9487
|
+
const __sys = ['selectable', 'draggable'];
|
|
9488
|
+
const __m = app.interactionSystem.continuousBehaviors;
|
|
9489
|
+
((__m && __m.get ? __m.get(${itemIdStr}) : null) || []).slice()
|
|
9490
|
+
.filter(function (b) { return __sys.indexOf(b.type) === -1; })
|
|
9491
|
+
.forEach(function (b) { app.interactionSystem.removeContinuousBehavior(entry.item, b.id); });
|
|
9492
|
+
}
|
|
9493
|
+
const __after = __list();
|
|
9494
|
+
const removed = __before.filter(function (id) { return __after.indexOf(id) === -1; });
|
|
9495
|
+
return { success: removed.length > 0, action: 'remove_behavior', itemId: ${itemIdStr}, removed: removed, remaining: __after, ...(removed.length ? {} : { error: 'nothing was removed: the item has no behaviors added to it (the built-in selectable / draggable are kept)' }) };
|
|
8974
9496
|
})();`.trim();
|
|
8975
9497
|
}
|
|
8976
9498
|
case 'trigger_action': {
|
|
8977
|
-
|
|
9499
|
+
// The engine's handlers read params.items (ids) for show / hide /
|
|
9500
|
+
// animate / stopAnimation, and params.points for incrementScore. The
|
|
9501
|
+
// tool documented neither, so { itemId } hid nothing and { amount: 5 }
|
|
9502
|
+
// scored 1 (gate D13, 1.6.19). The natural names are accepted too.
|
|
9503
|
+
const raw = { ...(input.params || {}) };
|
|
9504
|
+
if (raw.items === undefined) {
|
|
9505
|
+
const one = raw.itemId ?? raw.targetItemId;
|
|
9506
|
+
const many = raw.itemIds;
|
|
9507
|
+
if (Array.isArray(many))
|
|
9508
|
+
raw.items = many;
|
|
9509
|
+
else if (typeof one === 'string')
|
|
9510
|
+
raw.items = [one];
|
|
9511
|
+
}
|
|
9512
|
+
if (input.actionType === 'incrementScore' && raw.points === undefined && typeof raw.amount === 'number')
|
|
9513
|
+
raw.points = raw.amount;
|
|
9514
|
+
const params = JSON.stringify(raw);
|
|
8978
9515
|
return `
|
|
8979
9516
|
// Trigger interaction action
|
|
8980
9517
|
(function() {
|
|
@@ -9346,8 +9883,10 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9346
9883
|
// Toggle grid
|
|
9347
9884
|
(function() {
|
|
9348
9885
|
${guard}
|
|
9349
|
-
//
|
|
9350
|
-
|
|
9886
|
+
// setUnitGridEnabled shows/hides the grid AND records the setting that
|
|
9887
|
+
// set_snap depends on; showGrid() alone left snapping inert (1.6.19 gate).
|
|
9888
|
+
if (typeof app.setUnitGridEnabled === 'function') app.setUnitGridEnabled(${enabled});
|
|
9889
|
+
else app.measurementSystem[${enabled} ? 'showGrid' : 'hideGrid']();
|
|
9351
9890
|
return { success: true, action: 'set_grid', enabled: ${enabled} };
|
|
9352
9891
|
})();`.trim();
|
|
9353
9892
|
}
|
|
@@ -9366,17 +9905,18 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9366
9905
|
})();`.trim();
|
|
9367
9906
|
}
|
|
9368
9907
|
case 'set_snap': {
|
|
9369
|
-
//
|
|
9908
|
+
// The engine has setSnapToUnitEnabled now; this refused, saying it had
|
|
9909
|
+
// never existed (1.6.19 gate). It is an EDITOR setting: it snaps a
|
|
9910
|
+
// drag or resize to the unit grid's ticks, and does nothing while that
|
|
9911
|
+
// grid is off. Tool-placed items are not snapped, and the result says so.
|
|
9370
9912
|
return `
|
|
9371
|
-
// Snap to grid
|
|
9913
|
+
// Snap to unit grid (drag / resize in the studio)
|
|
9372
9914
|
(function() {
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
return { success: false, action: 'set_snap',
|
|
9379
|
-
error: 'this build has no snap-to-unit MODE to switch: the measurement system snaps a coordinate on request rather than holding a setting. Snap positions yourself before creating or moving items, or set a grid with the grid action and place on its multiples.' };
|
|
9915
|
+
if (typeof app.setSnapToUnitEnabled !== 'function') { return { success: false, action: 'set_snap', error: 'this studio has no snap-to-unit setting; update FxTool' }; }
|
|
9916
|
+
const enabled = app.setSnapToUnitEnabled(${input.enabled !== false});
|
|
9917
|
+
const gridOn = typeof app.getUnitGridEnabled === 'function' ? app.getUnitGridEnabled() : null;
|
|
9918
|
+
return { success: true, action: 'set_snap', enabled: enabled, unitGrid: gridOn,
|
|
9919
|
+
note: 'snaps dragging and resizing in the studio to the unit grid' + (gridOn === false ? ', which is OFF, so nothing snaps until it is on (set_grid)' : '') + '. Items placed by tools are not snapped: give them grid multiples.' };
|
|
9380
9920
|
})();`.trim();
|
|
9381
9921
|
}
|
|
9382
9922
|
default:
|
|
@@ -9454,6 +9994,30 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9454
9994
|
* Extract the best-matching detected region from an image as a NEW item (FxTool
|
|
9455
9995
|
* ImageWorkflow.extractObject). Async (runs detection first).
|
|
9456
9996
|
*/
|
|
9997
|
+
/**
|
|
9998
|
+
* pinepaper_styled_scene (FxTool K2). app.styledScene answers {ok:false,
|
|
9999
|
+
* error} rather than rejecting, so the verdict is read, not assumed. The
|
|
10000
|
+
* result carries the duration to export for: the scene item lives that long,
|
|
10001
|
+
* and an export left at its own default would cut it short or pad it.
|
|
10002
|
+
*/
|
|
10003
|
+
generateStyledScene(input) {
|
|
10004
|
+
const opts = {
|
|
10005
|
+
...(input.style !== undefined ? { style: input.style } : {}),
|
|
10006
|
+
...(input.duration !== undefined ? { duration: input.duration } : {}),
|
|
10007
|
+
...(input.id !== undefined ? { id: input.id } : {}),
|
|
10008
|
+
};
|
|
10009
|
+
return `
|
|
10010
|
+
// Styled scene${input.style ? ` (${input.style.replace(/[\r\n]/g, ' ')})` : ''}
|
|
10011
|
+
(async function() {
|
|
10012
|
+
if (typeof app.styledScene !== 'function') { return { success: false, error: 'app.styledScene unavailable — update FxTool' }; }
|
|
10013
|
+
const r = await app.styledScene(${JSON.stringify(input.spec ?? {})}, ${JSON.stringify(opts)});
|
|
10014
|
+
if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the styled scene was refused' }; }
|
|
10015
|
+
const notes = ['export for ' + r.duration + ' s (agent_export duration: ' + r.duration + '): the scene runs that long'];
|
|
10016
|
+
if (r.style === 'dither' || r.style === 'ascii') notes.push(r.style + ' needs a Chrome renderer (WebKit ignores the canvas filter it uses)');
|
|
10017
|
+
notes.push('the scene redraws every exported frame; after a page reload it is a still until styled_scene is called again');
|
|
10018
|
+
return { success: true, itemId: r.id, style: r.style, width: r.width, height: r.height, duration: r.duration, firstFrameMs: r.firstFrameMs, notes: notes };
|
|
10019
|
+
})();`.trim();
|
|
10020
|
+
}
|
|
9457
10021
|
generateExtractObject(input) {
|
|
9458
10022
|
const args = JSON.stringify({
|
|
9459
10023
|
...(input.label !== undefined ? { label: input.label } : {}),
|
|
@@ -9470,6 +10034,16 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9470
10034
|
}
|
|
9471
10035
|
try {
|
|
9472
10036
|
const res = await app.extractObject(${args});
|
|
10037
|
+
// A numeric itemId is Paper's own id: the engine's register fallback did
|
|
10038
|
+
// not take, so the new item was unaddressable and uncounted (gate run 2:
|
|
10039
|
+
// itemId 226, itemsCreated 0). Register it here and hand back that id.
|
|
10040
|
+
if (res && res.ok && typeof res.itemId === 'number' && typeof paper !== 'undefined' && paper.project) {
|
|
10041
|
+
const __n = res.itemId;
|
|
10042
|
+
const __it = paper.project.getItem({ match: function (i) { return i.id === __n; } });
|
|
10043
|
+
const __rid = __it && app.itemRegistry && app.itemRegistry.register ? app.itemRegistry.register(__it, 'image', { source: 'extract' }, 'import') : null;
|
|
10044
|
+
if (typeof __rid === 'string') res.itemId = __rid;
|
|
10045
|
+
else return { success: false, error: 'the object was extracted but could not be registered, so it has no usable id (Paper id ' + __n + ')' };
|
|
10046
|
+
}
|
|
9473
10047
|
const out = Object.assign({ success: !!(res && res.ok) }, res || {});
|
|
9474
10048
|
if (!out.success && typeof out.error === 'string') out.error = (${PinePaperCodeGenerator.MODEL_FETCH_HINT})(out.error);
|
|
9475
10049
|
return out;
|
|
@@ -9704,7 +10278,7 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
9704
10278
|
})();`.trim();
|
|
9705
10279
|
}
|
|
9706
10280
|
generateCaptureFrames(input) {
|
|
9707
|
-
const timesJson = JSON.stringify(input.times);
|
|
10281
|
+
const timesJson = JSON.stringify(input.times ?? []);
|
|
9708
10282
|
const seed = input.seed !== undefined ? input.seed : 0;
|
|
9709
10283
|
const includeDataUrls = !!input.includeDataUrls;
|
|
9710
10284
|
return `
|
|
@@ -9763,6 +10337,69 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
9763
10337
|
...(__firstSeek ? { firstSeek: __firstSeek } : {}),
|
|
9764
10338
|
...(__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
10339
|
frames: frames };
|
|
10340
|
+
})();`.trim();
|
|
10341
|
+
}
|
|
10342
|
+
/**
|
|
10343
|
+
* A contact sheet (G3): capture at the planned times and tile them into one
|
|
10344
|
+
* image in the page. On a studio with sized capture (FxTool #42:
|
|
10345
|
+
* captureFramesAt(times, { width, format })) the engine renders each frame
|
|
10346
|
+
* small; on an older one the full frame is scaled while tiling.
|
|
10347
|
+
*/
|
|
10348
|
+
generateCaptureSheet(input) {
|
|
10349
|
+
const sheet = input.sheet;
|
|
10350
|
+
const { times, labels } = sheetTimes(input.times, sheet);
|
|
10351
|
+
const n = times.length;
|
|
10352
|
+
const columns = sheet.columns ?? Math.min(6, Math.max(1, Math.ceil(Math.sqrt(n))));
|
|
10353
|
+
const mime = sheet.format === 'png' ? 'image/png' : 'image/jpeg';
|
|
10354
|
+
const S = (v) => JSON.stringify(v);
|
|
10355
|
+
return `
|
|
10356
|
+
// Contact sheet of ${n} frame(s)
|
|
10357
|
+
(async function() {
|
|
10358
|
+
if (typeof app.captureFramesAt !== 'function') {
|
|
10359
|
+
return { success: false, error: 'app.captureFramesAt unavailable — update FxTool to a build with the deterministic capture entrypoint' };
|
|
10360
|
+
}
|
|
10361
|
+
const times = ${S(times)};
|
|
10362
|
+
const labels = ${S(labels)};
|
|
10363
|
+
const tileW = ${sheet.tileWidth};
|
|
10364
|
+
const columns = ${columns};
|
|
10365
|
+
const sized = typeof app._frameCapture === 'function';
|
|
10366
|
+
const cap = typeof app.captureFramesAtAsync === 'function' ? app.captureFramesAtAsync.bind(app) : app.captureFramesAt.bind(app);
|
|
10367
|
+
const urls = sized
|
|
10368
|
+
? await cap(times, { seed: ${input.seed ?? 0}, width: tileW, format: 'png' })
|
|
10369
|
+
: await cap(times, { seed: ${input.seed ?? 0}, capture: function(c) {
|
|
10370
|
+
return typeof app.captureFrameDataURL === 'function' ? app.captureFrameDataURL() : (c && c.toDataURL ? c.toDataURL() : '');
|
|
10371
|
+
} });
|
|
10372
|
+
const imgs = await Promise.all(urls.map(function(u) {
|
|
10373
|
+
return new Promise(function(res) { const im = new Image(); im.onload = function() { res(im); }; im.onerror = function() { res(null); }; im.src = u; });
|
|
10374
|
+
}));
|
|
10375
|
+
const first = imgs.find(function(im) { return im && im.naturalWidth; });
|
|
10376
|
+
if (!first) return { success: false, error: 'no frame could be captured' };
|
|
10377
|
+
const tileH = Math.max(1, Math.round(tileW * first.naturalHeight / first.naturalWidth));
|
|
10378
|
+
const rows = Math.ceil(imgs.length / columns);
|
|
10379
|
+
const gap = 4;
|
|
10380
|
+
const cv = document.createElement('canvas');
|
|
10381
|
+
cv.width = columns * tileW + (columns + 1) * gap;
|
|
10382
|
+
cv.height = rows * tileH + (rows + 1) * gap;
|
|
10383
|
+
const g = cv.getContext('2d');
|
|
10384
|
+
g.fillStyle = '#202020'; g.fillRect(0, 0, cv.width, cv.height);
|
|
10385
|
+
imgs.forEach(function(im, i) {
|
|
10386
|
+
const x = gap + (i % columns) * (tileW + gap);
|
|
10387
|
+
const y = gap + Math.floor(i / columns) * (tileH + gap);
|
|
10388
|
+
if (im) g.drawImage(im, x, y, tileW, tileH);
|
|
10389
|
+
else { g.fillStyle = '#802020'; g.fillRect(x, y, tileW, tileH); }
|
|
10390
|
+
if (${sheet.labels ? 'true' : 'false'}) {
|
|
10391
|
+
const text = labels[i];
|
|
10392
|
+
g.font = '600 13px system-ui, sans-serif';
|
|
10393
|
+
const w = g.measureText(text).width + 10;
|
|
10394
|
+
g.fillStyle = 'rgba(0,0,0,0.65)'; g.fillRect(x + 4, y + 4, w, 20);
|
|
10395
|
+
g.fillStyle = '#fff'; g.fillText(text, x + 9, y + 19);
|
|
10396
|
+
}
|
|
10397
|
+
});
|
|
10398
|
+
const dataUrl = cv.toDataURL(${S(mime)}${sheet.format === 'png' ? '' : ', 0.82'});
|
|
10399
|
+
return { success: true, dataUrl: dataUrl, tiles: imgs.length, columns: columns, rows: rows,
|
|
10400
|
+
width: cv.width, height: cv.height, tileWidth: tileW, times: times,
|
|
10401
|
+
missing: imgs.filter(function(im) { return !im; }).length,
|
|
10402
|
+
sizedCapture: sized };
|
|
9766
10403
|
})();`.trim();
|
|
9767
10404
|
}
|
|
9768
10405
|
/**
|
|
@@ -9794,12 +10431,26 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
9794
10431
|
// which get the box loop below and a note saying so.
|
|
9795
10432
|
const doc = ${docJson};
|
|
9796
10433
|
if (typeof app.instantiateOntology === 'function') {
|
|
9797
|
-
|
|
10434
|
+
// THE LIVE CANVAS, unless one was given: the engine lays out for 1080x1080
|
|
10435
|
+
// otherwise, which put items at (540,540) on a 640x360 canvas (gate run 3).
|
|
10436
|
+
const __opts = ${engineOptsJson};
|
|
10437
|
+
if (!__opts.canvas && app.getCanvasSize) { const __c = app.getCanvasSize(); if (__c && __c.width > 0) __opts.canvas = { width: __c.width, height: __c.height }; }
|
|
10438
|
+
// REGISTRY IDS. The engine returns the DOCUMENT's ids ("bar"), which no
|
|
10439
|
+
// other tool resolves; the new registry entries are found by diffing.
|
|
10440
|
+
const __all = function () { return (app.itemRegistry && app.itemRegistry.getAll) ? app.itemRegistry.getAll() : []; };
|
|
10441
|
+
const __before = new Set(__all().map(function (e) { return e.itemId; }));
|
|
10442
|
+
const r = await app.instantiateOntology(doc, __opts);
|
|
9798
10443
|
const diags = r.diagnostics || [];
|
|
10444
|
+
const __new = __all().filter(function (e) { return !__before.has(e.itemId) && e.item && e.item.data && e.item.data._origin === 'ontology'; }).map(function (e) { return e.itemId; });
|
|
10445
|
+
const docIds = r.itemIds || [];
|
|
10446
|
+
const idMap = {};
|
|
10447
|
+
if (__new.length === docIds.length) docIds.forEach(function (d, i) { idMap[d] = __new[i]; });
|
|
9799
10448
|
return {
|
|
9800
10449
|
success: !diags.some(function(d) { return d && d.level === 'error'; }) && !r.imagesFailed,
|
|
9801
10450
|
engine: 'studio',
|
|
9802
|
-
itemIds:
|
|
10451
|
+
itemIds: __new, itemCount: __new.length, docIds: docIds,
|
|
10452
|
+
...(Object.keys(idMap).length ? { idMap: idMap } : { note: 'document ids could not be paired with registry ids one to one; use itemIds' }),
|
|
10453
|
+
canvas: __opts.canvas || null,
|
|
9803
10454
|
// Every count the engine returns, not a chosen three. imagesFailed is the
|
|
9804
10455
|
// one that matters most: a raster that never decoded leaves a scene that
|
|
9805
10456
|
// looks built and is not, and dropping the count is how that becomes
|
|
@@ -10142,6 +10793,45 @@ ${guard}
|
|
|
10142
10793
|
// The facade throws on a missing raster/matte rather than returning {ok:false}.
|
|
10143
10794
|
return { success: false, error: e && e.message ? e.message : String(e) };
|
|
10144
10795
|
}
|
|
10796
|
+
})();`.trim();
|
|
10797
|
+
}
|
|
10798
|
+
case 'analyze_reference': {
|
|
10799
|
+
const S = (v) => JSON.stringify(v);
|
|
10800
|
+
return `
|
|
10801
|
+
// Measure a reference video's FORMAT: cuts, shot lengths, colours, pacing, beat
|
|
10802
|
+
(async function() {
|
|
10803
|
+
if (typeof app.analyzeReferenceVideo !== 'function') { return { success: false, error: 'app.analyzeReferenceVideo unavailable — update FxTool' }; }
|
|
10804
|
+
const r = await app.analyzeReferenceVideo(${S(input.id)}, { thumbnails: ${input.thumbnails ? 'true' : 'false'} });
|
|
10805
|
+
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'the reference could not be analysed' }; }
|
|
10806
|
+
// Kept in the page for remake_from_reference, so the analysis does not have
|
|
10807
|
+
// to travel back through the agent.
|
|
10808
|
+
window.__ppLastReference = { analysis: r, id: ${S(input.id)} };
|
|
10809
|
+
const shots = Array.isArray(r.shots) ? r.shots : [];
|
|
10810
|
+
return { success: true, referenceId: ${S(input.id)}, duration: r.duration, aspect: r.aspect, width: r.width, height: r.height,
|
|
10811
|
+
cuts: r.cuts, shots: shots, pacing: r.pacing, audio: r.audio, card: r.card,
|
|
10812
|
+
...(r.truncated ? { truncated: true } : {}),
|
|
10813
|
+
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.' };
|
|
10814
|
+
})();`.trim();
|
|
10815
|
+
}
|
|
10816
|
+
case 'remake_from_reference': {
|
|
10817
|
+
const S = (v) => JSON.stringify(v);
|
|
10818
|
+
const opts = {
|
|
10819
|
+
texts: input.texts ?? [],
|
|
10820
|
+
...(input.duration !== undefined ? { duration: input.duration } : {}),
|
|
10821
|
+
...(input.clear !== undefined ? { clear: input.clear } : {}),
|
|
10822
|
+
};
|
|
10823
|
+
return `
|
|
10824
|
+
// Build a new piece on the analysed reference's structure, in your own words
|
|
10825
|
+
(async function() {
|
|
10826
|
+
if (typeof app.remakeFromReference !== 'function') { return { success: false, error: 'app.remakeFromReference unavailable — update FxTool' }; }
|
|
10827
|
+
const last = window.__ppLastReference;
|
|
10828
|
+
if (!last || !last.analysis) { return { success: false, error: 'no analysed reference in this session: call pinepaper_media analyze_reference first' }; }
|
|
10829
|
+
// reference: the analysed clip stays in the project MUTED; without it the
|
|
10830
|
+
// reference's own sound is exported under the new cards.
|
|
10831
|
+
const r = await app.remakeFromReference(last.analysis, Object.assign(${S(opts)}, { reference: last.id }));
|
|
10832
|
+
return r && r.ok
|
|
10833
|
+
? { success: true, action: 'remake_from_reference', duration: r.duration, shots: r.shots, items: r.items, referenceMuted: last.id }
|
|
10834
|
+
: { success: false, error: (r && r.error) || 'the remake could not be built' };
|
|
10145
10835
|
})();`.trim();
|
|
10146
10836
|
}
|
|
10147
10837
|
case 'stop_live_matte': {
|
|
@@ -10248,7 +10938,9 @@ ${guard}
|
|
|
10248
10938
|
// item — so reading .data.id off the answer returned no id, every time. The
|
|
10249
10939
|
// group is registered only on one of its two branches; the other is
|
|
10250
10940
|
// registered here so the caller always gets an addressable id.
|
|
10251
|
-
|
|
10941
|
+
// nib goes to the engine as a boolean or a tuning object. A STRING is
|
|
10942
|
+
// truthy there, so 'monoline' would have selected the broad nib (D31).
|
|
10943
|
+
const r = app.createCursiveText(${S(input.text)}, ${S(cursiveEngineOptions(input.cursiveOptions))});
|
|
10252
10944
|
const g = r && (r.group || (r.data ? r : null));
|
|
10253
10945
|
if (!g) { return { success: false, error: 'the cursive text produced no path' }; }
|
|
10254
10946
|
let itemId = g.data && (g.data.id || g.data.registryId);
|
|
@@ -10289,7 +10981,11 @@ ${guard}
|
|
|
10289
10981
|
// would otherwise read as "the conversion did nothing to your text".
|
|
10290
10982
|
const r = app.convertTextToCollage(item, ${S(input.text ?? null)} || item.content, ${S(input.collageOptions ?? {})});
|
|
10291
10983
|
if (!r) { return { success: false, error: 'the collage system is unavailable in this build, so the text was left as it was' }; }
|
|
10292
|
-
|
|
10984
|
+
// A summary, not the collage's Paper group (that was a raw dump, gate run 3).
|
|
10985
|
+
${REG_ID_JS}
|
|
10986
|
+
const __g = (r && (r.group || r.item)) || (r && typeof r.className === 'string' ? r : null);
|
|
10987
|
+
return { success: true, itemId: __g ? __ppRegId(__g) : null, letters: __g && __g.children ? __g.children.length : undefined,
|
|
10988
|
+
bounds: __g && __g.bounds ? { x: __g.bounds.x, y: __g.bounds.y, width: __g.bounds.width, height: __g.bounds.height } : undefined };
|
|
10293
10989
|
})();`.trim();
|
|
10294
10990
|
}
|
|
10295
10991
|
}
|
|
@@ -10304,6 +11000,117 @@ ${guard}
|
|
|
10304
11000
|
* only ids/count/duration are forwarded. `ids` is the whole handle set the
|
|
10305
11001
|
* caller now has, since the source item is gone.
|
|
10306
11002
|
*/
|
|
11003
|
+
/**
|
|
11004
|
+
* pinepaper_relight — FxTool D5's relight post pass over the 3D world layer.
|
|
11005
|
+
* The rig is not saved with the project, GIF / APNG are not relit, and it
|
|
11006
|
+
* costs ~0.42 s per 1080p frame on the CPU, so a set says what an export
|
|
11007
|
+
* will pay.
|
|
11008
|
+
*/
|
|
11009
|
+
generateRelight(input) {
|
|
11010
|
+
const S = (v) => JSON.stringify(v);
|
|
11011
|
+
const missing = (fn) => `if (typeof app.${fn} !== 'function') { return { success: false, action: ${S(input.action)}, error: 'this studio has no relight pass (app.${fn}) — it predates FxTool D5.' }; }`;
|
|
11012
|
+
switch (input.action) {
|
|
11013
|
+
case 'set': {
|
|
11014
|
+
const spec = S({
|
|
11015
|
+
...(input.lights !== undefined ? { lights: input.lights } : {}),
|
|
11016
|
+
...(input.ambient !== undefined ? { ambient: input.ambient } : {}),
|
|
11017
|
+
...(input.strength !== undefined ? { strength: input.strength } : {}),
|
|
11018
|
+
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
|
|
11019
|
+
...(input.preview !== undefined ? { preview: input.preview } : {}),
|
|
11020
|
+
});
|
|
11021
|
+
return `
|
|
11022
|
+
// Relight: set the rig
|
|
11023
|
+
(function() {
|
|
11024
|
+
${missing('setRelight')}
|
|
11025
|
+
const r = app.setRelight(${spec});
|
|
11026
|
+
if (!r || r.ok === false) { return { success: false, action: 'set', error: (r && ((r.errors && r.errors.join('; ')) || r.reason)) || 'the relight rig was refused' }; }
|
|
11027
|
+
// What an export will pay: ~418 ms per 1920x1080 frame, scaled by pixels.
|
|
11028
|
+
const __f = (typeof app.exportFrameRect === 'function' && app.exportFrameRect()) || (app.getCanvasSize && app.getCanvasSize()) || { width: 1920, height: 1080 };
|
|
11029
|
+
const __ms = Math.round(418 * (__f.width * __f.height) / (1920 * 1080));
|
|
11030
|
+
const res = { success: true, action: 'set', active: r.active, relight: r.relight,
|
|
11031
|
+
cost: { msPerFrame: __ms, frame: { width: __f.width, height: __f.height },
|
|
11032
|
+
tenSecondsAt30fps: Math.round(__ms * 300 / 1000) + ' s extra' },
|
|
11033
|
+
note: 'applied in the preview and in PNG, PDF, MP4 and WebM exports; GIF and APNG are not relit. The rig is NOT saved with the project — set it again after a reload.' };
|
|
11034
|
+
if (r.reason) res.reason = r.reason;
|
|
11035
|
+
if (r.active === false) res.warning = 'the rig is set but not active' + (r.reason ? ': ' + r.reason : '') + ' — relight needs a 3D world (pinepaper_world3d create).';
|
|
11036
|
+
return res;
|
|
11037
|
+
})();`.trim();
|
|
11038
|
+
}
|
|
11039
|
+
case 'get':
|
|
11040
|
+
return `
|
|
11041
|
+
// Relight: the current rig
|
|
11042
|
+
(function() {
|
|
11043
|
+
${missing('getRelight')}
|
|
11044
|
+
return Object.assign({ success: true, action: 'get' }, app.getRelight());
|
|
11045
|
+
})();`.trim();
|
|
11046
|
+
case 'clear':
|
|
11047
|
+
return `
|
|
11048
|
+
// Relight: remove the rig
|
|
11049
|
+
(function() {
|
|
11050
|
+
${missing('clearRelight')}
|
|
11051
|
+
const r = app.clearRelight();
|
|
11052
|
+
return { success: true, action: 'clear', cleared: !!(r && r.cleared) };
|
|
11053
|
+
})();`.trim();
|
|
11054
|
+
case 'list_lights':
|
|
11055
|
+
return `
|
|
11056
|
+
// Relight: light types
|
|
11057
|
+
(function() {
|
|
11058
|
+
${missing('listRelightLights')}
|
|
11059
|
+
return { success: true, action: 'list_lights', lights: app.listRelightLights() };
|
|
11060
|
+
})();`.trim();
|
|
11061
|
+
}
|
|
11062
|
+
}
|
|
11063
|
+
/**
|
|
11064
|
+
* pinepaper_shader_graph — FxTool D5's JSON pixel graphs. Applying to an
|
|
11065
|
+
* item makes a NEW image item and hides the source (one undo step);
|
|
11066
|
+
* applying to 'scene' returns the processed frame and changes nothing.
|
|
11067
|
+
*/
|
|
11068
|
+
generateShaderGraph(input) {
|
|
11069
|
+
const S = (v) => JSON.stringify(v);
|
|
11070
|
+
const ref = input.graph ?? input.graphId;
|
|
11071
|
+
const head = ` if (!app.shaderGraph || typeof app.shaderGraph.create !== 'function') { return { success: false, action: ${S(input.action)}, error: 'this studio has no shader graph (app.shaderGraph) — it predates FxTool D5.' }; }`;
|
|
11072
|
+
const fail = `(r && ((r.errors && r.errors.join('; ')) || r.reason)) || 'the shader graph call failed'`;
|
|
11073
|
+
const body = (() => {
|
|
11074
|
+
switch (input.action) {
|
|
11075
|
+
case 'node_types': return ` return { success: true, action: 'node_types', nodeTypes: app.shaderGraph.nodeTypes() };`;
|
|
11076
|
+
case 'list': return ` const r = app.shaderGraph.list();\n return { success: true, action: 'list', graphs: (r && r.graphs) || [] };`;
|
|
11077
|
+
case 'validate': return ` const r = app.shaderGraph.validate(${S(input.graph)});\n if (!r || !r.ok) return { success: false, action: 'validate', errors: (r && r.errors) || [], error: ${fail} };\n return { success: true, action: 'validate', order: r.order };`;
|
|
11078
|
+
case 'create': return ` const r = app.shaderGraph.create(${S(input.graph)});\n if (!r || !r.ok) return { success: false, action: 'create', errors: (r && r.errors) || [], error: ${fail} };\n return { success: true, action: 'create', graphId: r.id, order: r.order, nodes: r.nodes, replaced: r.replaced };`;
|
|
11079
|
+
case 'get': return ` const r = app.shaderGraph.get(${S(input.graphId)});\n if (!r || !r.ok) return { success: false, action: 'get', error: ${fail} };\n return { success: true, action: 'get', graph: r.graph };`;
|
|
11080
|
+
case 'remove': return ` const r = app.shaderGraph.remove(${S(input.graphId)});\n if (!r || !r.ok) return { success: false, action: 'remove', error: ${fail} };\n return { success: true, action: 'remove', removed: r.removed };`;
|
|
11081
|
+
case 'apply': {
|
|
11082
|
+
const opts = S({ ...(input.keepSource !== undefined ? { keepSource: input.keepSource } : {}), ...(input.asItem !== undefined ? { asItem: input.asItem } : {}) });
|
|
11083
|
+
return ` if (typeof app.applyShaderGraph !== 'function') { return { success: false, action: 'apply', error: 'this studio has no app.applyShaderGraph — it predates FxTool D5.' }; }
|
|
11084
|
+
const r = await app.applyShaderGraph(${S(input.target)}, ${S(ref)}, ${opts});
|
|
11085
|
+
if (!r || !r.ok) return { success: false, action: 'apply', errors: (r && r.errors) || undefined, error: ${fail} };
|
|
11086
|
+
if (r.target === 'item') {
|
|
11087
|
+
if (!r.id) return { success: false, action: 'apply', error: 'the graph ran but made no item' };
|
|
11088
|
+
// The new item's OWN bounds: a bloom with reach (FxTool D60) is larger
|
|
11089
|
+
// than its source, centred on it, so the source's bounds are not these.
|
|
11090
|
+
const __ne = app.itemRegistry && typeof app.itemRegistry.get === 'function' ? app.itemRegistry.get(r.id) : null;
|
|
11091
|
+
const __ni = __ne && __ne.item;
|
|
11092
|
+
// An image item made from a data URL decodes ASYNCHRONOUSLY and has 0x0
|
|
11093
|
+
// bounds until it does (prod read {width:0, height:0}); wait for it.
|
|
11094
|
+
if (__ni && __ni.className === 'Raster' && !__ni.loaded && typeof __ni.once === 'function') {
|
|
11095
|
+
await new Promise(function (resolve) { const t = setTimeout(resolve, 5000); __ni.once('load', function () { clearTimeout(t); resolve(); }); });
|
|
11096
|
+
}
|
|
11097
|
+
const __nb = __ni && __ni.bounds && __ni.bounds.width > 0 ? __ni.bounds : null;
|
|
11098
|
+
return { success: true, action: 'apply', target: 'item', itemId: r.id, sourceId: r.sourceId, sourceHidden: r.sourceHidden, graphId: r.graphId, width: r.width, height: r.height,
|
|
11099
|
+
...(__nb ? { bounds: { x: __nb.x, y: __nb.y, width: __nb.width, height: __nb.height } } : {}) };
|
|
11100
|
+
}
|
|
11101
|
+
const res = { success: true, action: 'apply', target: 'scene', graphId: r.graphId, width: r.width, height: r.height };
|
|
11102
|
+
if (r.id) { res.itemId = r.id; } else { res.data = r.dataURL; res.mimeType = 'image/png'; res.note = 'the processed frame; the scene is unchanged. asItem: true adds it as an image item instead.'; }
|
|
11103
|
+
return res;`;
|
|
11104
|
+
}
|
|
11105
|
+
}
|
|
11106
|
+
})();
|
|
11107
|
+
return `
|
|
11108
|
+
// Shader graph: ${input.action}
|
|
11109
|
+
(async function() {
|
|
11110
|
+
${head}
|
|
11111
|
+
${body}
|
|
11112
|
+
})();`.trim();
|
|
11113
|
+
}
|
|
10307
11114
|
/**
|
|
10308
11115
|
* The medium axis. `list_media` and `resolve` are READS — they exist so an
|
|
10309
11116
|
* agent can find out what this build can honestly make before it promises a
|
|
@@ -10313,6 +11120,56 @@ ${guard}
|
|
|
10313
11120
|
const S = (v) => JSON.stringify(v);
|
|
10314
11121
|
const wrap = (comment, body) => `\n// ${comment}\n(function() {\n${body}\n})();`.trim();
|
|
10315
11122
|
switch (input.action) {
|
|
11123
|
+
case 'apply':
|
|
11124
|
+
// One engine entry point (FxTool #54): app.applyMedium dispatches to the
|
|
11125
|
+
// medium's own method (DesignMedia MEDIA[key].apply) and refuses an absent
|
|
11126
|
+
// or brush-only medium with the reason. The Paper item it returns is not
|
|
11127
|
+
// passed back (it cannot cross the bridge); its id is.
|
|
11128
|
+
return `
|
|
11129
|
+
// Medium: apply ${input.medium}
|
|
11130
|
+
(async function() {
|
|
11131
|
+
if (typeof app.applyMedium !== 'function') { return { success: false, error: 'app.applyMedium unavailable — update FxTool' }; }
|
|
11132
|
+
const r = await app.applyMedium(${S(input.itemId)}, ${S(input.medium)}, ${S(input.options ?? {})});
|
|
11133
|
+
if (!r || r.ok === false) {
|
|
11134
|
+
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 } : {}) };
|
|
11135
|
+
}
|
|
11136
|
+
// ink / charcoal restyle IN PLACE (FxTool D32): the same item, marked with
|
|
11137
|
+
// data.brushStyle on its paths and texts. Read the mark back, so an item
|
|
11138
|
+
// the engine passed over is a failure rather than a success (gate D32).
|
|
11139
|
+
if (${S(input.medium)} === 'ink' || ${S(input.medium)} === 'charcoal') {
|
|
11140
|
+
const __e = app.itemRegistry && app.itemRegistry.get(r.id || ${S(input.itemId)});
|
|
11141
|
+
const __it = (__e && __e.item) || r.item;
|
|
11142
|
+
let __styled = 0;
|
|
11143
|
+
const __walk = function (n) {
|
|
11144
|
+
if (!n) return;
|
|
11145
|
+
if (n.data && n.data.brushStyle) __styled++;
|
|
11146
|
+
(n.children || []).forEach(__walk);
|
|
11147
|
+
};
|
|
11148
|
+
__walk(__it);
|
|
11149
|
+
if (!__styled) {
|
|
11150
|
+
return { success: false, action: 'apply', medium: ${S(input.medium)}, error: 'the studio reported ' + ${S(input.medium)} + ' applied, but no path or text in ' + ${S(input.itemId)} + ' carries it. Ink and charcoal restyle paths and text; a primitive shape is skipped.' };
|
|
11151
|
+
}
|
|
11152
|
+
return { success: true, action: 'apply', medium: ${S(input.medium)}, fidelity: r.fidelity, itemId: r.id || ${S(input.itemId)}, inPlace: true, styled: __styled, ...(r.note ? { note: r.note } : {}) };
|
|
11153
|
+
}
|
|
11154
|
+
return { success: true, action: 'apply', medium: r.medium || ${S(input.medium)}, fidelity: r.fidelity, itemId: r.id, ...(r.note ? { note: r.note } : {}) };
|
|
11155
|
+
})();`.trim();
|
|
11156
|
+
case 'sdf_stroke': {
|
|
11157
|
+
// FxTool D5: an SDF capsule stroke rendered to a bitmap and created as a
|
|
11158
|
+
// registered, undoable image item. CPU-backed.
|
|
11159
|
+
const opts = S({
|
|
11160
|
+
...(input.width !== undefined ? { width: input.width } : {}),
|
|
11161
|
+
...(input.softness !== undefined ? { softness: input.softness } : {}),
|
|
11162
|
+
...(input.color !== undefined ? { color: input.color } : {}),
|
|
11163
|
+
...(input.opacity !== undefined ? { opacity: input.opacity } : {}),
|
|
11164
|
+
...(input.resolution !== undefined ? { resolution: input.resolution } : {}),
|
|
11165
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
11166
|
+
});
|
|
11167
|
+
return wrap('Medium: SDF brush stroke', ` if (typeof app.sdfBrushStroke !== 'function') { return { success: false, action: 'sdf_stroke', error: 'this studio has no SDF brush (app.sdfBrushStroke) — it predates FxTool D5.' }; }
|
|
11168
|
+
const r = app.sdfBrushStroke(${S(input.points)}, ${opts});
|
|
11169
|
+
if (!r || r.ok === false) { return { success: false, action: 'sdf_stroke', error: (r && ((r.errors && r.errors.join('; ')) || r.reason)) || 'the stroke was not drawn' }; }
|
|
11170
|
+
if (!r.id) { return { success: false, action: 'sdf_stroke', error: 'the stroke was drawn but returned no item id' }; }
|
|
11171
|
+
return { success: true, action: 'sdf_stroke', itemId: r.id, bounds: r.bounds, pixelSize: r.pixelSize, backend: r.backend };`);
|
|
11172
|
+
}
|
|
10316
11173
|
case 'list_media':
|
|
10317
11174
|
return wrap('Medium: list', ` if (typeof app.listDesignMedia !== 'function') { return { success: false, error: 'medium axis unavailable — update FxTool' }; }
|
|
10318
11175
|
const media = app.listDesignMedia();
|
|
@@ -10561,14 +11418,60 @@ ${guard}
|
|
|
10561
11418
|
// Import a layered character (decomposer output → role-bound parts; blink works out of the box)
|
|
10562
11419
|
(async function() {
|
|
10563
11420
|
if (typeof app.importLayeredCharacter !== 'function') { return { success: false, error: 'app.importLayeredCharacter unavailable — update FxTool to a layered-character build' }; }
|
|
10564
|
-
// The
|
|
10565
|
-
//
|
|
10566
|
-
|
|
11421
|
+
// The engine looks layers up by NORMALISED tag (lowercase, no dir/extension,
|
|
11422
|
+
// '_'/'-' collapsed to a space): parseSeeThroughManifest asks
|
|
11423
|
+
// images.has(normalizePartTag(tag)), then images.get(layer.tag). Its own
|
|
11424
|
+
// folder reader keys the Map that way; a wire object keyed 'eye_left' is never
|
|
11425
|
+
// found and every multi-word layer is skipped as "no image found".
|
|
11426
|
+
// Mirrors FxTool js/ontology/CharacterPartTaxonomy.js normalizePartTag.
|
|
11427
|
+
const norm = (t) => String(t).trim().toLowerCase().replace(/^.*[\\\\/]/, '').replace(/\\.(png|webp|tga|psd)$/i, '').replace(/[_\\-\\s]+/g, ' ').trim();
|
|
11428
|
+
// The taxonomy resolves DECOMPOSER tags (eyel, iridesr, eyebrowl, hairf) — not
|
|
11429
|
+
// the role tokens it emits. A layer tagged with a role token (eye_left,
|
|
11430
|
+
// pupil_right, hair_front) resolves to no role, so alias role tokens to the
|
|
11431
|
+
// decomposer tag that produces them.
|
|
11432
|
+
const SIDED = { eye: 'eye', pupil: 'irides', brow: 'eyebrow', eyelash: 'eyelash', ear: 'ears', hand: 'handwear' };
|
|
11433
|
+
const FLAT = { 'hair front': 'hairf', 'hair back': 'hairb', torso: 'topwear', hips: 'bottomwear', legs: 'legwear', feet: 'footwear', prop: 'objects' };
|
|
11434
|
+
const canon = (t) => {
|
|
11435
|
+
const k = norm(t);
|
|
11436
|
+
let m = /^(.+) (left|right|l|r)$/.exec(k);
|
|
11437
|
+
if (m && SIDED[m[1]] !== undefined) return SIDED[m[1]] + m[2][0];
|
|
11438
|
+
m = /^(left|right) (.+)$/.exec(k);
|
|
11439
|
+
if (m && SIDED[m[2]] !== undefined) return SIDED[m[2]] + m[1][0];
|
|
11440
|
+
return FLAT[k] || k;
|
|
11441
|
+
};
|
|
11442
|
+
const rawInfo = ${S(input.info)};
|
|
11443
|
+
const rawImages = ${S(input.images)};
|
|
11444
|
+
const partsKey = rawInfo.parts ? 'parts' : (rawInfo.tag2pinfo ? 'tag2pinfo' : null);
|
|
11445
|
+
const info = { ...rawInfo };
|
|
11446
|
+
const notes = [];
|
|
11447
|
+
if (partsKey && rawInfo[partsKey] && typeof rawInfo[partsKey] === 'object' && !Array.isArray(rawInfo[partsKey])) {
|
|
11448
|
+
info[partsKey] = {};
|
|
11449
|
+
for (const [t, v] of Object.entries(rawInfo[partsKey])) {
|
|
11450
|
+
const c = canon(t);
|
|
11451
|
+
if (info[partsKey][c]) { notes.push('layer "' + t + '" collides with another layer as "' + c + '" — kept the first'); continue; }
|
|
11452
|
+
info[partsKey][c] = v;
|
|
11453
|
+
}
|
|
11454
|
+
}
|
|
11455
|
+
const images = new Map();
|
|
11456
|
+
const suppliedBy = {}; // canonical tag → the caller's own key, for naming
|
|
11457
|
+
for (const [t, v] of Object.entries(rawImages)) {
|
|
11458
|
+
const c = canon(t);
|
|
11459
|
+
if (images.has(c)) { notes.push('image "' + t + '" collides with "' + suppliedBy[c] + '" — kept the first'); continue; }
|
|
11460
|
+
images.set(c, v); suppliedBy[c] = t;
|
|
11461
|
+
}
|
|
11462
|
+
const bundle = { info, images };
|
|
10567
11463
|
const r = await app.importLayeredCharacter(bundle, ${opts});
|
|
10568
11464
|
if (!r || !r.groupId) { return { success: false, error: (r && r.error) || 'layered character import failed' }; }
|
|
10569
11465
|
// roles maps part roles → item ids; zero roles wired means the character
|
|
10570
11466
|
// renders but will NOT animate — surface the count so that cannot hide.
|
|
10571
|
-
|
|
11467
|
+
const imported = new Set((r.parts || []).map((p) => p.tag));
|
|
11468
|
+
const skipped = Object.keys(suppliedBy).filter((c) => !imported.has(c)).map((c) => suppliedBy[c]);
|
|
11469
|
+
const unroled = (r.parts || []).filter((p) => !p.role).map((p) => suppliedBy[p.tag] || p.tag);
|
|
11470
|
+
const result = { groupId: r.groupId, parts: (r.parts || []).length, roles: r.roles || {}, rolesWired: Object.keys(r.roles || {}).length, skipped, unroled, warnings: [...notes, ...(r.warnings || [])] };
|
|
11471
|
+
// A supplied image that did not land is the caller's input dropped — not a
|
|
11472
|
+
// success. The group IS on the canvas, so hand back its id to fix or delete.
|
|
11473
|
+
if (skipped.length) { return { success: false, ...result, error: 'supplied layer(s) not imported: ' + skipped.join(', ') + ' — each needs a matching key under info.parts with a usable xyxy (see warnings)' }; }
|
|
11474
|
+
return { success: true, ...result };
|
|
10572
11475
|
})();`.trim();
|
|
10573
11476
|
}
|
|
10574
11477
|
/**
|
|
@@ -10629,12 +11532,15 @@ ${guard}
|
|
|
10629
11532
|
// World3D: create — terrain, sky, shadows, walkable character, under the Paper canvas
|
|
10630
11533
|
(async function() {
|
|
10631
11534
|
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});
|
|
11535
|
+
await app.createWorld3D(${S(world3dSpecColors(world3dColors(input.spec ?? 'forest')))}, ${opts});
|
|
10633
11536
|
// describe() is the world's own parameter schema — return the preset list +
|
|
10634
11537
|
// top-level keys so the agent knows what configure can touch, without the
|
|
10635
11538
|
// full multi-KB schema on every create.
|
|
10636
11539
|
const d = typeof app.describeWorld3D === 'function' ? await app.describeWorld3D() : null;
|
|
10637
|
-
|
|
11540
|
+
// params is WORLD_SCHEMA: an ARRAY of {path, kind, doc}. Object.keys on it
|
|
11541
|
+
// returned "0", "1", "2"… — the indexes, not a single usable key.
|
|
11542
|
+
const keys = !d ? [] : Array.isArray(d.params) ? d.params.map(function (e) { return e && e.path; }).filter(Boolean) : Object.keys(d.params || d);
|
|
11543
|
+
return { success: true, action: 'create', spec: ${S(input.spec ?? 'forest')}, configurableKeys: keys };
|
|
10638
11544
|
})();`.trim();
|
|
10639
11545
|
}
|
|
10640
11546
|
case 'describe':
|
|
@@ -10650,9 +11556,9 @@ ${guard}
|
|
|
10650
11556
|
(function() {
|
|
10651
11557
|
if (typeof app.configureWorld3D !== 'function') { return { success: false, error: 'app.configureWorld3D unavailable — update FxTool' }; }
|
|
10652
11558
|
${needWorld}
|
|
10653
|
-
const r = app.configureWorld3D(${S(world3dColors(input.patch))});
|
|
11559
|
+
const r = app.configureWorld3D(${S(world3dSpecColors(world3dColors(input.patch)))});
|
|
10654
11560
|
// 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' };
|
|
11561
|
+
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
11562
|
})();`.trim();
|
|
10657
11563
|
case 'add_actor': {
|
|
10658
11564
|
const actor = S({
|
|
@@ -10668,7 +11574,9 @@ ${needWorld}
|
|
|
10668
11574
|
(function() {
|
|
10669
11575
|
if (typeof app.addWorldActor !== 'function') { return { success: false, error: 'app.addWorldActor unavailable — update FxTool' }; }
|
|
10670
11576
|
${needWorld}
|
|
10671
|
-
const
|
|
11577
|
+
const __r = app.addWorldActor(${actor});
|
|
11578
|
+
// The engine may return the actor itself; the id is what later calls take (gate D5).
|
|
11579
|
+
const id = (__r && typeof __r === 'object') ? (__r.id || null) : __r;
|
|
10672
11580
|
return id ? { success: true, actorId: id } : { success: false, error: 'actor not added — is the sprite id a canvas item?' };
|
|
10673
11581
|
})();`.trim();
|
|
10674
11582
|
}
|
|
@@ -10706,7 +11614,9 @@ ${needWorld}
|
|
|
10706
11614
|
// World3D: place an object (y defaults to sitting on the terrain)
|
|
10707
11615
|
(function() {
|
|
10708
11616
|
${needWorld}
|
|
10709
|
-
const
|
|
11617
|
+
const __o = app.addWorldObject(${S(world3dColors(input.object))});
|
|
11618
|
+
// addWorldObject returns the OBJECT; its id is what set/remove calls take (gate D5).
|
|
11619
|
+
const id = (__o && typeof __o === 'object') ? (__o.id || null) : __o;
|
|
10710
11620
|
return id ? { success: true, objectId: id } : { success: false, error: 'object not added' };
|
|
10711
11621
|
})();`.trim();
|
|
10712
11622
|
case 'remove_object':
|
|
@@ -10721,7 +11631,10 @@ ${needWorld}
|
|
|
10721
11631
|
// World3D: tear the world down — Paper's canvas is untouched (it was always its own layer)
|
|
10722
11632
|
(function() {
|
|
10723
11633
|
if (typeof app.removeWorld3D !== 'function') { return { success: false, error: 'app.removeWorld3D unavailable — update FxTool' }; }
|
|
10724
|
-
return
|
|
11634
|
+
// removeWorld3D's return value is not a success flag; whether the world is
|
|
11635
|
+
// gone is (gate D5: it worked and reported failure).
|
|
11636
|
+
app.removeWorld3D();
|
|
11637
|
+
return { success: !app._world3d };
|
|
10725
11638
|
})();`.trim();
|
|
10726
11639
|
// --- Mesh authoring ---------------------------------------------------
|
|
10727
11640
|
// extrudeToMesh / latheToMesh REFUSE by name when there is no world
|
|
@@ -10765,7 +11678,7 @@ ${needWorld}
|
|
|
10765
11678
|
// @engine-methods extrudeToMesh latheToMesh importOBJToWorld importGLTFToWorld
|
|
10766
11679
|
if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool to a world3d-capable build' }; }
|
|
10767
11680
|
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' }; }
|
|
11681
|
+
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
11682
|
return { success: true, meshId: r.id, op: ${S(input.action === 'extrude_path' ? 'extrude' : 'lathe')}, sourceId: ${S(input.pathId)} };
|
|
10770
11683
|
})();`.trim();
|
|
10771
11684
|
}
|
|
@@ -10774,7 +11687,19 @@ ${needWorld}
|
|
|
10774
11687
|
// World3D: every authored mesh on the stage
|
|
10775
11688
|
(function() {
|
|
10776
11689
|
if (typeof app.listWorldMeshes !== 'function') { return { success: false, error: 'app.listWorldMeshes unavailable — update FxTool' }; }
|
|
10777
|
-
|
|
11690
|
+
// Summarised (gate A, 1.6.19): one star returned over 10 KB, nearly all of it
|
|
11691
|
+
// collision and vertex arrays nobody asked for. Arrays longer than 16 numbers
|
|
11692
|
+
// become their length; everything else passes as is.
|
|
11693
|
+
const __slim = function (v, depth) {
|
|
11694
|
+
// Typed arrays too: collision positions are a Float32Array, which
|
|
11695
|
+
// Array.isArray misses, so a star still came back as 12 KB (gate run 2).
|
|
11696
|
+
if (ArrayBuffer.isView(v)) return v.length > 16 ? { length: v.length } : Array.prototype.slice.call(v);
|
|
11697
|
+
if (Array.isArray(v)) return v.length > 16 ? { length: v.length } : v.map(function (x) { return __slim(x, depth + 1); });
|
|
11698
|
+
if (v && typeof v === 'object' && depth < 4) { const o = {}; for (const k in v) { if (typeof v[k] !== 'function') o[k] = __slim(v[k], depth + 1); } return o; }
|
|
11699
|
+
return v;
|
|
11700
|
+
};
|
|
11701
|
+
const __m = app.listWorldMeshes() || [];
|
|
11702
|
+
return { success: true, count: __m.length, meshes: __m.map(function (m) { return __slim(m, 0); }) };
|
|
10778
11703
|
})();`.trim();
|
|
10779
11704
|
case 'remove_mesh':
|
|
10780
11705
|
return `
|
|
@@ -10792,8 +11717,8 @@ ${needWorld}
|
|
|
10792
11717
|
(function() {
|
|
10793
11718
|
if (typeof app.addWorldLight !== 'function') { return { success: false, error: 'app.addWorldLight unavailable — update FxTool' }; }
|
|
10794
11719
|
${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' }; }
|
|
11720
|
+
const r = app.addWorldLight(${S(world3dHexColor(input.light))});
|
|
11721
|
+
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
11722
|
return { success: true, lightId: r.id, light: r.light };
|
|
10798
11723
|
})();`.trim();
|
|
10799
11724
|
case 'set_light':
|
|
@@ -10801,7 +11726,7 @@ ${needWorld}
|
|
|
10801
11726
|
// World3D: edit a light in place
|
|
10802
11727
|
(function() {
|
|
10803
11728
|
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
|
|
11729
|
+
return { success: !!app.setWorldLight(${S(input.lightId)}, ${S(world3dHexColor(input.light))}) };
|
|
10805
11730
|
})();`.trim();
|
|
10806
11731
|
case 'remove_light':
|
|
10807
11732
|
return `
|
|
@@ -10824,8 +11749,8 @@ ${needWorld}
|
|
|
10824
11749
|
(function() {
|
|
10825
11750
|
if (typeof app.addWorldMaterial !== 'function') { return { success: false, error: 'app.addWorldMaterial unavailable — update FxTool' }; }
|
|
10826
11751
|
${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' }; }
|
|
11752
|
+
const r = app.addWorldMaterial(${S(world3dHexColor(input.material))});
|
|
11753
|
+
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
11754
|
return { success: true, materialId: r.id, material: r.material };
|
|
10830
11755
|
})();`.trim();
|
|
10831
11756
|
case 'set_material':
|
|
@@ -10833,7 +11758,7 @@ ${needWorld}
|
|
|
10833
11758
|
// World3D: patch a material — every object referencing it changes next frame
|
|
10834
11759
|
(function() {
|
|
10835
11760
|
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
|
|
11761
|
+
return { success: !!app.setWorldMaterial(${S(input.materialId)}, ${S(world3dHexColor(input.material))}) };
|
|
10837
11762
|
})();`.trim();
|
|
10838
11763
|
case 'remove_material':
|
|
10839
11764
|
return `
|
|
@@ -10892,7 +11817,7 @@ ${needWorld}
|
|
|
10892
11817
|
(function() {
|
|
10893
11818
|
if (typeof app.setWorldNavTarget !== 'function') { return { success: false, error: 'app.setWorldNavTarget unavailable — update FxTool' }; }
|
|
10894
11819
|
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' }; }
|
|
11820
|
+
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
11821
|
return { success: true, navTarget: ${S(input.navTarget)} };
|
|
10897
11822
|
})();`.trim();
|
|
10898
11823
|
case 'get_nav_target':
|
|
@@ -10944,7 +11869,10 @@ ${needWorld}
|
|
|
10944
11869
|
(function() {
|
|
10945
11870
|
if (typeof app.worldToCanvas !== 'function') { return { success: false, error: 'app.worldToCanvas unavailable — update FxTool' }; }
|
|
10946
11871
|
${needWorld}
|
|
10947
|
-
|
|
11872
|
+
// projectToCanvas destructures [x, y, z]; an {x, y, z} object threw "is not iterable" (gate D5).
|
|
11873
|
+
const __p = ${S(input.point)};
|
|
11874
|
+
const __pt = app.worldToCanvas(Array.isArray(__p) ? __p : [__p.x, __p.y, __p.z ?? 0]);
|
|
11875
|
+
return __pt ? { success: true, point: Array.isArray(__pt) ? { x: __pt[0], y: __pt[1] } : __pt } : { success: false, error: 'the point is not in front of the camera, or there is no world' };
|
|
10948
11876
|
})();`.trim();
|
|
10949
11877
|
case 'canvas_to_ground':
|
|
10950
11878
|
return `
|
|
@@ -11051,14 +11979,20 @@ ${needWorld}
|
|
|
11051
11979
|
* already decided by a pure function, so nothing is left for the page to
|
|
11052
11980
|
* work out.
|
|
11053
11981
|
*/
|
|
11054
|
-
generateDesignCompose(scene, ops, style) {
|
|
11982
|
+
generateDesignCompose(scene, ops, style, sizeWarning) {
|
|
11055
11983
|
const S = (v) => JSON.stringify(v);
|
|
11056
11984
|
return `
|
|
11057
11985
|
// Design: compose a ${style} scene — ${ops.length} items, laid out server-side
|
|
11058
11986
|
(function() {
|
|
11059
11987
|
if (typeof app.create !== 'function') { return { success: false, error: 'app.create unavailable — update FxTool' }; }
|
|
11060
11988
|
const size = ${S({ width: scene.width, height: scene.height })};
|
|
11061
|
-
|
|
11989
|
+
// The layout was computed for this size, so the canvas is set to it — and
|
|
11990
|
+
// the result SAYS so: a 1080x1080 canvas became 1280x720 silently (gate C).
|
|
11991
|
+
const __before = app.getCanvasSize ? app.getCanvasSize() : null;
|
|
11992
|
+
if (typeof app.setCanvasSize === 'function') { try { app.setCanvasSize({ width: size.width, height: size.height }); } catch (_) { /* a fixed canvas is not a failure */ } }
|
|
11993
|
+
const __after = app.getCanvasSize ? app.getCanvasSize() : null;
|
|
11994
|
+
const canvasResized = __before && __after && (__before.width !== __after.width || __before.height !== __after.height)
|
|
11995
|
+
? { from: { width: __before.width, height: __before.height }, to: { width: __after.width, height: __after.height } } : null;
|
|
11062
11996
|
${scene.backgroundHex ? `if (typeof app.setBackgroundColor === 'function') { try { app.setBackgroundColor(${S(scene.backgroundHex)}); } catch (_) { /* ditto */ } }` : '// this style paints no background of its own'}
|
|
11063
11997
|
const ops = ${S(ops)};
|
|
11064
11998
|
const ids = [];
|
|
@@ -11073,7 +12007,9 @@ ${needWorld}
|
|
|
11073
12007
|
if (app.historyManager) app.historyManager.saveState();
|
|
11074
12008
|
// A partial composition is reported as one. Half a poster that claims
|
|
11075
12009
|
// success is worse than a poster that says which four elements are missing.
|
|
11076
|
-
return { success: failed.length === 0, style: ${S(style)}, itemIds: ids, itemCount: ids.length, failed: failed
|
|
12010
|
+
return { success: failed.length === 0, style: ${S(style)}, itemIds: ids, itemCount: ids.length, failed: failed,
|
|
12011
|
+
...(canvasResized ? { canvasResized: canvasResized, note: 'the canvas was resized to the composition' + (${S(sizeWarning ?? '')} ? '' : '; pass width and height to compose at your canvas size') } : {}),
|
|
12012
|
+
...(${S(sizeWarning ?? '')} ? { warning: ${S(sizeWarning ?? '')} } : {}) };
|
|
11077
12013
|
})();`.trim();
|
|
11078
12014
|
}
|
|
11079
12015
|
/**
|
|
@@ -11214,9 +12150,13 @@ ${needWorld}
|
|
|
11214
12150
|
// Interchange: ${fn}
|
|
11215
12151
|
(async function() {
|
|
11216
12152
|
${guard(fn)}
|
|
12153
|
+
${TO_DATA_URL_JS}
|
|
11217
12154
|
const out = await app.${fn}(${opts});
|
|
11218
12155
|
if (!out) { return { success: false, error: '${fn} produced nothing — the scene may have no animatable content' }; }
|
|
11219
|
-
|
|
12156
|
+
${input.action === 'export_lottie'
|
|
12157
|
+
? `return { success: true, format: 'lottie', data: out };`
|
|
12158
|
+
: `const __url = await __ppToDataUrl(out, 'application/zip');
|
|
12159
|
+
return __url ? { success: true, format: 'dotlottie', data: __url } : { success: false, error: 'exportDotLottie returned something that is not binary' };`}
|
|
11220
12160
|
})();`.trim();
|
|
11221
12161
|
}
|
|
11222
12162
|
case 'import_lottie':
|
|
@@ -11226,9 +12166,23 @@ ${guard(fn)}
|
|
|
11226
12166
|
${guard('importLottie')}
|
|
11227
12167
|
// importLottie logs and returns falsy when the importer is absent — a
|
|
11228
12168
|
// stripped console makes that indistinguishable from an empty animation.
|
|
12169
|
+
${REG_ID_JS}
|
|
11229
12170
|
const r = await app.importLottie(${S(input.data)}, ${opts});
|
|
11230
|
-
|
|
11231
|
-
|
|
12171
|
+
// importLottie ALWAYS answers { success, items, duration, warnings, errors };
|
|
12172
|
+
// only !r was checked, so a failed import read as a success, and the ids
|
|
12173
|
+
// were looked for on fields it does not have (gate N2: itemId null).
|
|
12174
|
+
if (!r || r.success === false) {
|
|
12175
|
+
const why = r && Array.isArray(r.errors) && r.errors.length ? r.errors.join('; ') : 'the Lottie did not import — it may be malformed';
|
|
12176
|
+
return { success: false, error: why, ...(r && r.warnings && r.warnings.length ? { warnings: r.warnings } : {}) };
|
|
12177
|
+
}
|
|
12178
|
+
const __items = Array.isArray(r.items) ? r.items : [];
|
|
12179
|
+
const itemIds = __items.map(function (it) { return __ppRegId(it); }).filter(Boolean);
|
|
12180
|
+
if (!__items.length) return { success: false, error: 'the Lottie imported no items' };
|
|
12181
|
+
let __b = null;
|
|
12182
|
+
__items.forEach(function (it) { if (it && it.bounds) __b = __b ? __b.unite(it.bounds) : it.bounds; });
|
|
12183
|
+
return { success: true, itemId: itemIds[0] || null, itemIds: itemIds, items: __items.length, duration: r.duration,
|
|
12184
|
+
bounds: __b ? { x: __b.x, y: __b.y, width: __b.width, height: __b.height } : undefined,
|
|
12185
|
+
...(r.warnings && r.warnings.length ? { warnings: r.warnings } : {}) };
|
|
11232
12186
|
})();`.trim();
|
|
11233
12187
|
case 'export_glb':
|
|
11234
12188
|
return `
|
|
@@ -11239,9 +12193,11 @@ ${guard('exportGLB')}
|
|
|
11239
12193
|
// stripped in production, so the precondition is checked here instead.
|
|
11240
12194
|
const has3d = app._initialized && app._initialized.has && app._initialized.has('threeD');
|
|
11241
12195
|
if (!has3d) { return { success: false, error: 'no perspective objects to export — create one with createObject3D first' }; }
|
|
12196
|
+
${TO_DATA_URL_JS}
|
|
11242
12197
|
const out = await app.exportGLB(${opts});
|
|
11243
12198
|
if (!out) { return { success: false, error: 'GLB export produced nothing' }; }
|
|
11244
|
-
|
|
12199
|
+
const __url = await __ppToDataUrl(out, 'model/gltf-binary');
|
|
12200
|
+
return __url ? { success: true, format: 'glb', data: __url } : { success: true, format: 'glb', data: out };
|
|
11245
12201
|
})();`.trim();
|
|
11246
12202
|
case 'export_bvh':
|
|
11247
12203
|
return `
|
|
@@ -11302,7 +12258,16 @@ ${guard('exportPNGSequence')}
|
|
|
11302
12258
|
generateSound(input) {
|
|
11303
12259
|
const S = (v) => JSON.stringify(v);
|
|
11304
12260
|
const guard = (fn) => ` if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool' }; }`;
|
|
11305
|
-
const simple = (fn, args, key, label) => `
|
|
12261
|
+
const simple = (fn, args, key, label) => key === 'played' ? `
|
|
12262
|
+
// Sound: ${label}
|
|
12263
|
+
(function() {
|
|
12264
|
+
${guard(fn)}
|
|
12265
|
+
// A play that returns no voice played nothing, and reported success (gate
|
|
12266
|
+
// sweep D, 1.6.19). A headless studio may have no audio output at all.
|
|
12267
|
+
const __v = app.${fn}(${args});
|
|
12268
|
+
if (__v == null) { return { success: false, error: 'nothing played: the studio returned no voice (it may have no audio output). Placed sounds (create, sequence) and render_soundtrack work without one.' }; }
|
|
12269
|
+
return { success: true, played: __v };
|
|
12270
|
+
})();`.trim() : `
|
|
11306
12271
|
// Sound: ${label}
|
|
11307
12272
|
(function() {
|
|
11308
12273
|
${guard(fn)}
|
|
@@ -11317,8 +12282,12 @@ ${guard(fn)}
|
|
|
11317
12282
|
case 'chord_frequencies': return simple('chordFrequencies', `${S(input.root)}, ${S(input.chord)}, ${S(input.options ?? {})}`, 'frequencies', `the Hz of ${input.chord} on ${input.root}`);
|
|
11318
12283
|
case 'play_percussion': return simple('playPercussion', `${S(input.name)}, ${S(input.options ?? {})}`, 'played', `percussion ${input.name}`);
|
|
11319
12284
|
case 'play_sfx': return simple('playSfx', `${S(input.name)}, ${S(input.options ?? {})}`, 'played', `sfx ${input.name}`);
|
|
11320
|
-
|
|
11321
|
-
|
|
12285
|
+
// options are merged into the spec, as play_tone's are; play_spec and
|
|
12286
|
+
// play_from_text dropped them (gate sweep D).
|
|
12287
|
+
case 'play_spec': return simple('playSound', `Object.assign({}, ${S(input.spec)}, ${S(input.options ?? {})})`, 'played', 'a raw spec');
|
|
12288
|
+
case 'play_from_text': return input.options
|
|
12289
|
+
? simple('playSound', `Object.assign({}, app.buildSoundFromText(${S(input.text)}) || {}, ${S(input.options)})`, 'played', 'a described sound')
|
|
12290
|
+
: simple('playSoundFromText', S(input.text), 'played', 'a described sound');
|
|
11322
12291
|
case 'from_text':
|
|
11323
12292
|
return `
|
|
11324
12293
|
// Sound: resolve "${input.text}" to a spec WITHOUT playing it
|
|
@@ -11478,7 +12447,9 @@ ${guard('createSound')}
|
|
|
11478
12447
|
for (let i = 0; i < cues.length; i++) {
|
|
11479
12448
|
const c = cues[i];
|
|
11480
12449
|
try {
|
|
11481
|
-
|
|
12450
|
+
// The engine reads spec.instrument and never preset, so a preset cue
|
|
12451
|
+
// played a plain sine (gate sweep D).
|
|
12452
|
+
const spec = c.spec || (c.preset ? { instrument: c.preset, note: c.note } : null);
|
|
11482
12453
|
if (!spec) { failed.push({ index: i, t: c.t, error: 'neither spec nor preset' }); continue; }
|
|
11483
12454
|
const item = app.createSound(spec, c.visual || {});
|
|
11484
12455
|
if (!item) { failed.push({ index: i, t: c.t, error: 'the cue produced no waveform path' }); continue; }
|
|
@@ -11567,10 +12538,20 @@ ${guard('timbreFromPath')}
|
|
|
11567
12538
|
generatePathOp(input) {
|
|
11568
12539
|
const S = (v) => JSON.stringify(v);
|
|
11569
12540
|
const guard = (fn) => ` if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool' }; }`;
|
|
12541
|
+
// Paper items in the engine's answer become their registry ids: spread
|
|
12542
|
+
// as-is they serialised to raw Paper dumps (1.6.19 gate: boolean,
|
|
12543
|
+
// outline_stroke and pattern).
|
|
11570
12544
|
const pass = (expr) => `
|
|
12545
|
+
${REG_ID_JS}
|
|
12546
|
+
const __isItem = function (v) { return v && typeof v === 'object' && typeof v.className === 'string' && 'bounds' in v; };
|
|
12547
|
+
const __clean = function (v) { return __isItem(v) ? (__ppRegId(v) || ('paper:' + v.id)) : (Array.isArray(v) && v.some(__isItem) ? v.map(__clean) : v); };
|
|
11571
12548
|
const r = ${expr};
|
|
11572
12549
|
if (!r || r.ok === false) { return { success: false, error: (r && r.reason) || 'the engine refused the operation' }; }
|
|
11573
|
-
|
|
12550
|
+
const out = { success: true };
|
|
12551
|
+
Object.keys(r).forEach(function (k) { out[k] = __clean(r[k]); });
|
|
12552
|
+
// itemId, as every other tool names it; the engine says id (gate run 3).
|
|
12553
|
+
if (out.itemId === undefined && typeof out.id === 'string') out.itemId = out.id;
|
|
12554
|
+
return out;`;
|
|
11574
12555
|
switch (input.action) {
|
|
11575
12556
|
case 'boolean':
|
|
11576
12557
|
return `
|
|
@@ -11912,7 +12893,14 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
11912
12893
|
return wrap('Rigging: bake animation to keyframes', ` const res = R.bakeAnimation(${S(input.skeletonId)}, ${S(input.options ?? {})});
|
|
11913
12894
|
if (res && app.historyManager) app.historyManager.saveState();
|
|
11914
12895
|
if (!res) { return { success: false, action: 'bake_animation', error: "nothing to bake — no such skeleton, or no items are ATTACHED to its bones. Baking writes keyframes onto the attached items, so a bare skeleton has no output." }; }
|
|
11915
|
-
|
|
12896
|
+
// bakeAnimation returns a MAP (itemId -> keyframes), which serialised to {},
|
|
12897
|
+
// and its report is a non-enumerable bakeReport (gate run 3). Read both.
|
|
12898
|
+
const rep = res.bakeReport || {};
|
|
12899
|
+
const detached = rep.detached || 0;
|
|
12900
|
+
return { success: (rep.written || 0) > 0 || ${input.options && input.options.write === false ? 'true' : 'false'}, action: 'bake_animation',
|
|
12901
|
+
items: res.size, written: rep.written || 0, keyframes: rep.keyframes || 0, cleared: rep.cleared || 0, detached: detached,
|
|
12902
|
+
wrote: rep.wrote || [], ...(rep.skipped && rep.skipped.length ? { skipped: rep.skipped } : {}),
|
|
12903
|
+
...(detached ? { note: detached + ' item(s) were DETACHED from the rig: their motion is now keyframes, and the bones no longer drive them. Pass options.detach:false to keep the rig driving.' } : {}) };`);
|
|
11916
12904
|
case 'list_shape_keys':
|
|
11917
12905
|
return wrap('Rigging: list shape keys', ` const keys = R.listShapeKeys(${S(input.skeletonId)});
|
|
11918
12906
|
return { success: true, action: 'list_shape_keys', shapeKeys: keys, count: keys.length };`);
|