@pinepaper.studio/mcp-server 1.6.18 → 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 +97 -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 +25 -0
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +107 -1
- 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.map +1 -1
- package/dist/index.js +20 -7
- 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/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 +422 -126
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +17 -2
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +102 -2
- package/dist/tools/engine-lists.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +14 -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 +49 -8
- package/dist/tools/engine-surface.js.map +1 -1
- 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 +21 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +240 -37
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +32 -25
- 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 +57 -3
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +1007 -253
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +1842 -890
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +229 -51
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +22 -2
- package/package.json +1 -1
|
@@ -25,7 +25,7 @@ GlobeEnableInputSchema, GlobeRotateToInputSchema, GlobeSpinInputSchema, WorldTou
|
|
|
25
25
|
// Custom relation/code types
|
|
26
26
|
RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
|
|
27
27
|
// Mask types
|
|
28
|
-
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, } from './schemas.js';
|
|
28
|
+
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, MaskTypeSchema, MaskPresetSchema, } from './schemas.js';
|
|
29
29
|
import { OntologyCompiler } from '../ontology/ontology-compiler.js';
|
|
30
30
|
// =============================================================================
|
|
31
31
|
// HELPER FUNCTIONS
|
|
@@ -480,6 +480,19 @@ const DIRECTION_KEYS = ['direction', 'textDirection', 'dir'];
|
|
|
480
480
|
// first STRONG character decides. Mirrored so a result can say which way
|
|
481
481
|
// 'auto' went.
|
|
482
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
|
+
}
|
|
483
496
|
function baseDirection(text) {
|
|
484
497
|
for (const ch of text) {
|
|
485
498
|
if (RTL_SCRIPT.test(ch))
|
|
@@ -1477,7 +1490,9 @@ function generateGetItemsCode(filter) {
|
|
|
1477
1490
|
if (filter?.source)
|
|
1478
1491
|
conditions.push(`entry.source === '${filter.source}'`);
|
|
1479
1492
|
if (filter?.hasAnimation !== undefined) {
|
|
1480
|
-
|
|
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}`);
|
|
1481
1496
|
}
|
|
1482
1497
|
if (filter?.hasRelation !== undefined) {
|
|
1483
1498
|
conditions.push(`(app.getRelations(entry.id || entry.itemId).length > 0) === ${filter.hasRelation}`);
|
|
@@ -1490,7 +1505,7 @@ const items = entries.map(entry => ({
|
|
|
1490
1505
|
id: entry.id || entry.itemId,
|
|
1491
1506
|
type: entry.type,
|
|
1492
1507
|
position: entry.item.position ? { x: entry.item.position.x, y: entry.item.position.y } : null,
|
|
1493
|
-
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)),
|
|
1494
1509
|
relations: app.getRelations(entry.id || entry.itemId).map(r => r.relation || r.type)
|
|
1495
1510
|
}));
|
|
1496
1511
|
|
|
@@ -1505,7 +1520,7 @@ const items = filtered.map(entry => ({
|
|
|
1505
1520
|
id: entry.id || entry.itemId,
|
|
1506
1521
|
type: entry.type,
|
|
1507
1522
|
position: entry.item.position ? { x: entry.item.position.x, y: entry.item.position.y } : null,
|
|
1508
|
-
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)),
|
|
1509
1524
|
relations: app.getRelations(entry.id || entry.itemId).map(r => r.relation || r.type)
|
|
1510
1525
|
}));
|
|
1511
1526
|
|
|
@@ -1679,7 +1694,7 @@ app.setBackgroundColor('${color}');
|
|
|
1679
1694
|
*/
|
|
1680
1695
|
function generateSetCanvasSizeCode(width, height, preset) {
|
|
1681
1696
|
const sizeArg = preset
|
|
1682
|
-
?
|
|
1697
|
+
? JSON.stringify(preset)
|
|
1683
1698
|
: `{ width: ${width}, height: ${height} }`;
|
|
1684
1699
|
return `
|
|
1685
1700
|
// Set canvas size
|
|
@@ -1697,9 +1712,10 @@ function generateSetCanvasSizeCode(width, height, preset) {
|
|
|
1697
1712
|
app.historyManager.saveState();
|
|
1698
1713
|
// Report what the canvas ACTUALLY took where the engine says so, rather than
|
|
1699
1714
|
// echoing the request back.
|
|
1700
|
-
|
|
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},
|
|
1701
1717
|
...(r && r.preset ? { preset: r.preset } : {}),
|
|
1702
|
-
...(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) ? `,
|
|
1703
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.'` : ''} };
|
|
1704
1720
|
})();
|
|
1705
1721
|
`.trim();
|
|
@@ -2210,8 +2226,11 @@ function world3dColor(value) {
|
|
|
2210
2226
|
return value;
|
|
2211
2227
|
const hex = value.trim().replace(/^#/, '');
|
|
2212
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).
|
|
2213
2232
|
if (!/^[0-9a-f]{6}$/i.test(full))
|
|
2214
|
-
|
|
2233
|
+
throw new Error(`"${value}" is not a colour: use hex ("#03050c" or "#abc") or [r, g, b]`);
|
|
2215
2234
|
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255);
|
|
2216
2235
|
}
|
|
2217
2236
|
/**
|
|
@@ -2313,6 +2332,52 @@ export const SHEET_RUBRIC = {
|
|
|
2313
2332
|
'a jump between the last and first frame of a loop (loopSeam)',
|
|
2314
2333
|
],
|
|
2315
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".';
|
|
2316
2381
|
export class PinePaperCodeGenerator {
|
|
2317
2382
|
/**
|
|
2318
2383
|
* Generate code for creating an item
|
|
@@ -2354,8 +2419,14 @@ export class PinePaperCodeGenerator {
|
|
|
2354
2419
|
/**
|
|
2355
2420
|
* Generate code for adding a relation
|
|
2356
2421
|
*/
|
|
2357
|
-
generateAddRelation(input) {
|
|
2358
|
-
|
|
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;
|
|
2359
2430
|
// A PRESET decides its own relationType and params, so this is a different
|
|
2360
2431
|
// call, not a defaulted one. applyRelationPreset refuses with
|
|
2361
2432
|
// `{ok:false, errors:[{field, reason}]}` — a third refusal shape on this
|
|
@@ -3068,9 +3139,12 @@ if (typeof _gen === 'function') {
|
|
|
3068
3139
|
codeParts.push(`
|
|
3069
3140
|
// Save history and return results
|
|
3070
3141
|
app.historyManager.saveState();
|
|
3071
|
-
results;
|
|
3142
|
+
return results;
|
|
3072
3143
|
`);
|
|
3073
|
-
|
|
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})();`;
|
|
3074
3148
|
}
|
|
3075
3149
|
// =============================================================================
|
|
3076
3150
|
// DIAGRAM TOOLS
|
|
@@ -3191,12 +3265,26 @@ if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}');
|
|
|
3191
3265
|
if (!targetItem) throw new Error('Target item not found: ${targetItemId}');
|
|
3192
3266
|
|
|
3193
3267
|
const config = ${configStr};
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
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;
|
|
3200
3288
|
app.historyManager.saveState();
|
|
3201
3289
|
|
|
3202
3290
|
({ connectorId, sourceItemId: '${sourceItemId}', sourcePort: '${sourcePort}', targetItemId: '${targetItemId}', targetPort: '${targetPort}' });
|
|
@@ -3291,61 +3379,50 @@ const shapes = category ? _lib.getByCategory(category) : _lib.getAll();
|
|
|
3291
3379
|
generateUpdateConnector(input) {
|
|
3292
3380
|
const validated = UpdateConnectorInputSchema.parse(input);
|
|
3293
3381
|
const { connectorId, style, label, labelPosition } = validated;
|
|
3294
|
-
const updates = [];
|
|
3295
|
-
if (style) {
|
|
3296
|
-
updates.push(`style: ${JSON.stringify(style)}`);
|
|
3297
|
-
}
|
|
3298
|
-
if (label !== undefined) {
|
|
3299
|
-
updates.push(`label: ${JSON.stringify(label)}`);
|
|
3300
|
-
}
|
|
3301
|
-
if (labelPosition !== undefined) {
|
|
3302
|
-
updates.push(`labelPosition: ${labelPosition}`);
|
|
3303
|
-
}
|
|
3304
|
-
const updatesStr = updates.length > 0 ? `{ ${updates.join(', ')} }` : '{}';
|
|
3305
3382
|
return `
|
|
3306
3383
|
// Update connector: ${connectorId}
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
const
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
//
|
|
3315
|
-
//
|
|
3316
|
-
//
|
|
3317
|
-
//
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
const
|
|
3326
|
-
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
|
+
}
|
|
3327
3417
|
app.historyManager.saveState();
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
else _unsupported.push(k);
|
|
3337
|
-
}
|
|
3338
|
-
if (_applied.length) { app.historyManager.saveState(); app._scheduleRepaint?.(); }
|
|
3339
|
-
if (_unsupported.length && !_applied.length) {
|
|
3340
|
-
throw new Error(
|
|
3341
|
-
'this build cannot update ' + _unsupported.join(', ') + ' on an existing connector — '
|
|
3342
|
-
+ 'this build has no per-connector update — app.diagramSystem.updateConnector arrived later. '
|
|
3343
|
-
+ 'Update the studio for it. Removing and recreating the connector works, but mints a NEW id '
|
|
3344
|
-
+ 'and orphans anything holding the old one, so it is a last resort rather than the fix.'
|
|
3345
|
-
);
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
({ 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
|
+
})();
|
|
3349
3426
|
`.trim();
|
|
3350
3427
|
}
|
|
3351
3428
|
/**
|
|
@@ -3356,8 +3433,10 @@ if (_unsupported.length && !_applied.length) {
|
|
|
3356
3433
|
const { connectorId } = validated;
|
|
3357
3434
|
return `
|
|
3358
3435
|
// Remove connector: ${connectorId}
|
|
3359
|
-
|
|
3360
|
-
|
|
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);
|
|
3361
3440
|
|
|
3362
3441
|
app.diagramSystem.connectorManager.removeConnector(connector);
|
|
3363
3442
|
app.historyManager.saveState();
|
|
@@ -3446,6 +3525,23 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3446
3525
|
app.setCanvasSize('${canvasPresetFor(canvasPreset)}');
|
|
3447
3526
|
// Allow canvas resize to take effect
|
|
3448
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
|
+
}
|
|
3449
3545
|
`;
|
|
3450
3546
|
}
|
|
3451
3547
|
if (wantOntology) {
|
|
@@ -3479,7 +3575,8 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3479
3575
|
screenshotPolicy: '${policyStr}',
|
|
3480
3576
|
canvasPreset: ${canvasPreset ? `'${canvasPreset}'` : 'null'},
|
|
3481
3577
|
canvasCleared: ${shouldClear},
|
|
3482
|
-
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 ? `,
|
|
3483
3580
|
ontology: _ontology,
|
|
3484
3581
|
ontologyUnavailable: _ontologyUnavailable || undefined` : ''}
|
|
3485
3582
|
};
|
|
@@ -3558,12 +3655,13 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3558
3655
|
|
|
3559
3656
|
if (entry.type === 'text' || item.className === 'PointText') analysis.hasText = true;
|
|
3560
3657
|
if (item.className === 'Raster') analysis.hasImages = true;
|
|
3561
|
-
|
|
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') {
|
|
3562
3660
|
analysis.hasAnimations = true;
|
|
3563
3661
|
animationSet.add(data.animationType);
|
|
3564
3662
|
}
|
|
3565
3663
|
// Detect keyframe animations (added via app.addAnimation with keyframes array)
|
|
3566
|
-
if (data.keyframes || data.animation || data.keyframeAnimation) {
|
|
3664
|
+
if ((Array.isArray(data.keyframes) && data.keyframes.length > 0) || data.animation || data.keyframeAnimation) {
|
|
3567
3665
|
analysis.hasAnimations = true;
|
|
3568
3666
|
animationSet.add('keyframe');
|
|
3569
3667
|
}
|
|
@@ -3915,7 +4013,7 @@ return { success: true, backgroundColor: '${bgColor}' };
|
|
|
3915
4013
|
case 'set_canvas_size': {
|
|
3916
4014
|
const w = op.width || 1080;
|
|
3917
4015
|
const h = op.height || 1080;
|
|
3918
|
-
const sizeArg = op.preset ?
|
|
4016
|
+
const sizeArg = op.preset ? JSON.stringify(op.preset) : `{ width: ${w}, height: ${h} }`;
|
|
3919
4017
|
return `
|
|
3920
4018
|
// Read the verdict, as the standalone tool now does: an unknown preset or an
|
|
3921
4019
|
// out-of-range dimension is refused by name, and reporting the size we ASKED
|
|
@@ -4403,7 +4501,11 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4403
4501
|
// A 3D world under the canvas IS the background: since FxTool #50
|
|
4404
4502
|
// (747315e3) jpg/webp stills composite it, so nothing turns white/black.
|
|
4405
4503
|
const hasWorld = !!app._world3d;
|
|
4406
|
-
|
|
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 [];
|
|
4407
4509
|
return [{ code: 'alpha_dropped', message: fmt === 'apng'
|
|
4408
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.'
|
|
4409
4511
|
: fmt === 'jpg'
|
|
@@ -4530,7 +4632,11 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4530
4632
|
camBase = { width: app.canvasSize.width, height: app.canvasSize.height };
|
|
4531
4633
|
}
|
|
4532
4634
|
if (!camBase) camBase = { width: dimensions.width, height: dimensions.height };
|
|
4533
|
-
|
|
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) };
|
|
4534
4640
|
}
|
|
4535
4641
|
|
|
4536
4642
|
// AN EXPORTER ANSWERS A BLOB OR A RECORD AROUND ONE.
|
|
@@ -4693,6 +4799,12 @@ ${stillTime !== undefined ? `
|
|
|
4693
4799
|
? { width: png.width, height: png.height }
|
|
4694
4800
|
: { width: dimensions.width, height: dimensions.height },
|
|
4695
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
|
+
}
|
|
4696
4808
|
} else {
|
|
4697
4809
|
const canvas = document.querySelector('canvas');
|
|
4698
4810
|
if (canvas) {
|
|
@@ -5369,6 +5481,21 @@ ${(() => {
|
|
|
5369
5481
|
})()}
|
|
5370
5482
|
${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) { /* the still is already rendered */ }
|
|
5371
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
|
+
}
|
|
5372
5499
|
|
|
5373
5500
|
return result;
|
|
5374
5501
|
})();
|
|
@@ -5436,12 +5563,13 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5436
5563
|
if (entry.type === 'text' || item.className === 'PointText') analysis.hasText = true;
|
|
5437
5564
|
if (item.className === 'Raster') analysis.hasImages = true;
|
|
5438
5565
|
|
|
5439
|
-
|
|
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') {
|
|
5440
5568
|
analysis.hasAnimations = true;
|
|
5441
5569
|
animationSet.add(data.animationType);
|
|
5442
5570
|
}
|
|
5443
5571
|
// Detect keyframe animations (added via app.addAnimation with keyframes array)
|
|
5444
|
-
if (data.keyframes || data.animation || data.keyframeAnimation) {
|
|
5572
|
+
if ((Array.isArray(data.keyframes) && data.keyframes.length > 0) || data.animation || data.keyframeAnimation) {
|
|
5445
5573
|
analysis.hasAnimations = true;
|
|
5446
5574
|
animationSet.add('keyframe');
|
|
5447
5575
|
}
|
|
@@ -5693,7 +5821,9 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5693
5821
|
return {
|
|
5694
5822
|
success: true,
|
|
5695
5823
|
mapId: result.mapId || '${mapId}',
|
|
5696
|
-
|
|
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),
|
|
5697
5827
|
bounds: result.bounds,
|
|
5698
5828
|
center: result.center,${warn}
|
|
5699
5829
|
};
|
|
@@ -5702,6 +5832,55 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5702
5832
|
}
|
|
5703
5833
|
})();
|
|
5704
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
|
+
}`;
|
|
5705
5884
|
}
|
|
5706
5885
|
/**
|
|
5707
5886
|
* Generate code to highlight map regions
|
|
@@ -5720,13 +5899,19 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5720
5899
|
const fill = ho.color ?? ho.fillColor ?? ho.fill;
|
|
5721
5900
|
if (fill !== undefined)
|
|
5722
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;
|
|
5723
5908
|
const stroke = ho.strokeColor ?? ho.stroke;
|
|
5724
5909
|
if (stroke !== undefined)
|
|
5725
5910
|
style.stroke = stroke;
|
|
5726
5911
|
if (ho.strokeWidth !== undefined)
|
|
5727
5912
|
style.strokeWidth = ho.strokeWidth;
|
|
5728
5913
|
for (const [k, v] of Object.entries(ho)) {
|
|
5729
|
-
if (!['color', 'fillColor', 'fill', 'strokeColor', 'stroke', 'strokeWidth'].includes(k) && v !== undefined) {
|
|
5914
|
+
if (!['color', 'fillColor', 'fill', 'strokeColor', 'stroke', 'strokeWidth', 'opacity'].includes(k) && v !== undefined) {
|
|
5730
5915
|
style[k] = v;
|
|
5731
5916
|
}
|
|
5732
5917
|
}
|
|
@@ -5738,9 +5923,68 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5738
5923
|
return { success: false, error: 'Map system not available' };
|
|
5739
5924
|
}
|
|
5740
5925
|
|
|
5741
|
-
try {
|
|
5742
|
-
|
|
5743
|
-
|
|
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;
|
|
5744
5988
|
} catch (error) {
|
|
5745
5989
|
return { success: false, error: error.message };
|
|
5746
5990
|
}
|
|
@@ -5821,9 +6065,20 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5821
6065
|
return { success: false, error: 'Map system not available' };
|
|
5822
6066
|
}
|
|
5823
6067
|
|
|
5824
|
-
try {
|
|
5825
|
-
|
|
5826
|
-
|
|
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;
|
|
5827
6082
|
} catch (error) {
|
|
5828
6083
|
return { success: false, error: error.message };
|
|
5829
6084
|
}
|
|
@@ -5857,8 +6112,19 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5857
6112
|
}
|
|
5858
6113
|
|
|
5859
6114
|
try {
|
|
5860
|
-
|
|
5861
|
-
|
|
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] };
|
|
5862
6128
|
} catch (error) {
|
|
5863
6129
|
return { success: false, error: error.message };
|
|
5864
6130
|
}
|
|
@@ -5908,26 +6174,13 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5908
6174
|
options.animate = validated.animate;
|
|
5909
6175
|
if (validated.duration !== undefined)
|
|
5910
6176
|
options.duration = validated.duration;
|
|
5911
|
-
|
|
5912
|
-
//
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
try {
|
|
5919
|
-
// panTo() has never existed on any studio, and there is no lat/lon pan for
|
|
5920
|
-
// a flat map to route this to. Refused by NAME with the calls that do work,
|
|
5921
|
-
// rather than letting the caller take an undefined-is-not-a-function.
|
|
5922
|
-
return { success: false, error: 'pan_map is not supported: the engine has no lat/lon pan. '
|
|
5923
|
-
+ 'On a globe, rotateGlobeTo brings a coordinate to the front; on a flat map, zoom to a REGION '
|
|
5924
|
-
+ '(zoomToRegion) or call resetView for the whole view. This tool accepted coordinates and did '
|
|
5925
|
-
+ 'nothing for as long as it existed.' };
|
|
5926
|
-
} catch (error) {
|
|
5927
|
-
return { success: false, error: error.message };
|
|
5928
|
-
}
|
|
5929
|
-
})();
|
|
5930
|
-
`.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'))} }; })();`;
|
|
5931
6184
|
}
|
|
5932
6185
|
/**
|
|
5933
6186
|
* Generate code to zoom the map
|
|
@@ -5939,24 +6192,8 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5939
6192
|
options.animate = validated.animate;
|
|
5940
6193
|
if (validated.duration !== undefined)
|
|
5941
6194
|
options.duration = validated.duration;
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
(function() {
|
|
5945
|
-
if (!app.mapSystem) {
|
|
5946
|
-
return { success: false, error: 'Map system not available' };
|
|
5947
|
-
}
|
|
5948
|
-
|
|
5949
|
-
try {
|
|
5950
|
-
// zoomTo(level) has never existed. The engine zooms to a REGION, not to a
|
|
5951
|
-
// numeric level, so there is nothing to convert a level into.
|
|
5952
|
-
return { success: false, error: 'zoom_map takes a numeric level and the engine has no such call: it zooms to a REGION. '
|
|
5953
|
-
+ 'Use zoomToRegion with a region id, or resetView for the full view. A level was accepted here and did '
|
|
5954
|
-
+ 'nothing for as long as this tool existed.' };
|
|
5955
|
-
} catch (error) {
|
|
5956
|
-
return { success: false, error: error.message };
|
|
5957
|
-
}
|
|
5958
|
-
})();
|
|
5959
|
-
`.trim();
|
|
6195
|
+
// Refused for the same reason as pan (see generatePanMap).
|
|
6196
|
+
return `(function() { return { success: false, error: ${JSON.stringify(MAP_VIEW_REFUSAL('zoom'))} }; })();`;
|
|
5960
6197
|
}
|
|
5961
6198
|
/**
|
|
5962
6199
|
* Generate code to export map configuration
|
|
@@ -6001,7 +6238,11 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6001
6238
|
|
|
6002
6239
|
try {
|
|
6003
6240
|
const result = await app.mapSystem.importCustomMap('${validated.url}', ${optionsStr});
|
|
6004
|
-
|
|
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) };
|
|
6005
6246
|
} catch (error) {
|
|
6006
6247
|
return { success: false, error: error.message };
|
|
6007
6248
|
}
|
|
@@ -6018,7 +6259,11 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6018
6259
|
|
|
6019
6260
|
try {
|
|
6020
6261
|
const result = await app.mapSystem.importCustomMap(${JSON.stringify(validated.geoJson)}, ${optionsStr});
|
|
6021
|
-
|
|
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) };
|
|
6022
6267
|
} catch (error) {
|
|
6023
6268
|
return { success: false, error: error.message };
|
|
6024
6269
|
}
|
|
@@ -6045,20 +6290,17 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6045
6290
|
}
|
|
6046
6291
|
|
|
6047
6292
|
try {
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
}
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
return { success: true, regionId: region.id, regionName: region.name, properties: region.properties };
|
|
6060
|
-
}
|
|
6061
|
-
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 };
|
|
6062
6304
|
} catch (error) {
|
|
6063
6305
|
return { success: false, error: error.message };
|
|
6064
6306
|
}
|
|
@@ -6111,7 +6353,8 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6111
6353
|
const result = app.mapSystem.animateRegionsWave({
|
|
6112
6354
|
duration: ${validated.duration || 10},
|
|
6113
6355
|
loop: ${validated.loop !== false},
|
|
6114
|
-
colors: ${colorsStr}
|
|
6356
|
+
colors: ${colorsStr},${validated.waveSpeed !== undefined ? `
|
|
6357
|
+
waveSpeed: ${validated.waveSpeed},` : ''}
|
|
6115
6358
|
waveDirection: '${validated.waveDirection || 'horizontal'}'
|
|
6116
6359
|
});
|
|
6117
6360
|
return { success: true, animatedRegions: result?.animatedRegions || [], totalRegions: result?.totalRegions || 0 };
|
|
@@ -6277,9 +6520,17 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6277
6520
|
return { success: false, error: 'Map system not available' };
|
|
6278
6521
|
}
|
|
6279
6522
|
|
|
6280
|
-
try {
|
|
6281
|
-
|
|
6282
|
-
|
|
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.' };
|
|
6283
6534
|
} catch (error) {
|
|
6284
6535
|
return { success: false, error: error.message };
|
|
6285
6536
|
}
|
|
@@ -6481,6 +6732,87 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6481
6732
|
/**
|
|
6482
6733
|
* Generate code to export the complete scene state
|
|
6483
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
|
+
}
|
|
6484
6816
|
generateExportScene() {
|
|
6485
6817
|
return `
|
|
6486
6818
|
// Export complete scene state
|
|
@@ -6592,7 +6924,11 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6592
6924
|
backgroundColor: backgroundColor,
|
|
6593
6925
|
canvasSize: canvasSize,
|
|
6594
6926
|
itemCount: items.length,
|
|
6595
|
-
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.'
|
|
6596
6932
|
};
|
|
6597
6933
|
} catch (error) {
|
|
6598
6934
|
return { success: false, error: error.message };
|
|
@@ -6693,29 +7029,32 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6693
7029
|
try {
|
|
6694
7030
|
// Import the Paper.js item from JSON
|
|
6695
7031
|
const item = paper.project.importJSON(${itemJsonStr});
|
|
6696
|
-
|
|
6697
|
-
|
|
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' };
|
|
6698
7050
|
}
|
|
6699
|
-
|
|
6700
|
-
// Register in item registry
|
|
6701
|
-
const itemId = app.itemRegistry ? 'item_' + (app.itemRegistry.size + 1) : 'item_1';
|
|
6702
|
-
item.data = item.data || {};
|
|
6703
|
-
item.data.registryId = itemId;
|
|
6704
|
-
item.data.type = ${JSON.stringify(input.itemType)};
|
|
6705
|
-
|
|
6706
|
-
// Merge custom properties
|
|
6707
7051
|
const customProps = ${propertiesStr};
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
app.itemRegistry.set(itemId, item);
|
|
6713
|
-
}
|
|
6714
|
-
|
|
6715
|
-
// Add to appropriate group
|
|
6716
|
-
if (app.textItemGroup) {
|
|
6717
|
-
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' };
|
|
6718
7056
|
}
|
|
7057
|
+
try { if (paper.view) paper.view.update(); } catch (e) {}
|
|
6719
7058
|
|
|
6720
7059
|
return {
|
|
6721
7060
|
success: true,
|
|
@@ -6997,7 +7336,9 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6997
7336
|
// Get available mask types
|
|
6998
7337
|
(function() {
|
|
6999
7338
|
return {
|
|
7000
|
-
|
|
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)}
|
|
7001
7342
|
};
|
|
7002
7343
|
})();
|
|
7003
7344
|
`.trim();
|
|
@@ -7007,14 +7348,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7007
7348
|
// Get available mask animation presets
|
|
7008
7349
|
(function() {
|
|
7009
7350
|
return {
|
|
7010
|
-
animations:
|
|
7011
|
-
'wipeLeft', 'wipeRight', 'wipeUp', 'wipeDown',
|
|
7012
|
-
'iris', 'irisOut',
|
|
7013
|
-
'star', 'heart',
|
|
7014
|
-
'curtainHorizontal', 'curtainVertical', 'cinematic',
|
|
7015
|
-
'diagonalWipe',
|
|
7016
|
-
'revealUp', 'revealDown'
|
|
7017
|
-
]
|
|
7351
|
+
animations: ${JSON.stringify(MaskPresetSchema.options)}
|
|
7018
7352
|
};
|
|
7019
7353
|
})();
|
|
7020
7354
|
`.trim();
|
|
@@ -7028,10 +7362,12 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7028
7362
|
const categoryFilter = category ? `'${category}'` : 'null';
|
|
7029
7363
|
return `
|
|
7030
7364
|
// List available templates
|
|
7031
|
-
(function() {
|
|
7365
|
+
(async function() {
|
|
7032
7366
|
if (!app.templateManager) {
|
|
7033
7367
|
return { error: 'Template manager not available. Make sure PinePaper Studio is loaded.' };
|
|
7034
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) {}
|
|
7035
7371
|
const allTemplates = app.templateManager.getAllTemplates();
|
|
7036
7372
|
const category = ${categoryFilter};
|
|
7037
7373
|
const filtered = category
|
|
@@ -7056,8 +7392,25 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7056
7392
|
if (!app.templateManager) {
|
|
7057
7393
|
return { error: 'Template manager not available. Make sure PinePaper Studio is loaded.' };
|
|
7058
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
|
+
}
|
|
7059
7412
|
try {
|
|
7060
|
-
await app.templateManager.loadTemplate(
|
|
7413
|
+
await app.templateManager.loadTemplate(__tid, true);
|
|
7061
7414
|
|
|
7062
7415
|
// DID THE CLIPPED CHARACTER PARTS SURVIVE?
|
|
7063
7416
|
//
|
|
@@ -7714,7 +8067,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7714
8067
|
for (const id of ids) {
|
|
7715
8068
|
const entry = app.itemRegistry.get(id);
|
|
7716
8069
|
if (entry && entry.item) {
|
|
7717
|
-
${mode === 'remove' ? 'entry.item.selected = false;' : 'app.select(entry.item);'}
|
|
8070
|
+
${mode === 'remove' ? 'entry.item.selected = false;' : 'app.select(entry.item, false);'}
|
|
7718
8071
|
selected.push(id);
|
|
7719
8072
|
}
|
|
7720
8073
|
}
|
|
@@ -7725,8 +8078,9 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7725
8078
|
return `
|
|
7726
8079
|
// Select all items
|
|
7727
8080
|
(function() {
|
|
8081
|
+
${REG_ID_JS}
|
|
7728
8082
|
app.selectAll();
|
|
7729
|
-
const items = app.getSelectedItems().map(i => i.
|
|
8083
|
+
const items = app.getSelectedItems().map(i => __ppRegId(i)).filter(Boolean);
|
|
7730
8084
|
return { success: true, action: 'select_all', count: items.length, items };
|
|
7731
8085
|
})();`.trim();
|
|
7732
8086
|
case 'deselect_all':
|
|
@@ -7740,8 +8094,9 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7740
8094
|
return `
|
|
7741
8095
|
// Get current selection
|
|
7742
8096
|
(function() {
|
|
8097
|
+
${REG_ID_JS}
|
|
7743
8098
|
const items = app.getSelectedItems().map(i => ({
|
|
7744
|
-
itemId: i
|
|
8099
|
+
itemId: __ppRegId(i),
|
|
7745
8100
|
type: i.data?.itemType || i.className,
|
|
7746
8101
|
bounds: i.bounds ? { x: i.bounds.x, y: i.bounds.y, width: i.bounds.width, height: i.bounds.height } : null
|
|
7747
8102
|
}));
|
|
@@ -7751,8 +8106,9 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7751
8106
|
return `
|
|
7752
8107
|
// Delete selected items
|
|
7753
8108
|
(function() {
|
|
8109
|
+
${REG_ID_JS}
|
|
7754
8110
|
const selection = app.getSelectedItems();
|
|
7755
|
-
const deleted = selection.map(i => i.
|
|
8111
|
+
const deleted = selection.map(i => __ppRegId(i)).filter(Boolean);
|
|
7756
8112
|
app.deleteSelected();
|
|
7757
8113
|
return { success: true, action: 'delete_selected', deleted, count: deleted.length };
|
|
7758
8114
|
})();`.trim();
|
|
@@ -7931,7 +8287,7 @@ ${loads}
|
|
|
7931
8287
|
case 'lineage': return this._facadeCall('getLineage', id, 'Provenance: lineage');
|
|
7932
8288
|
case 'dependents': return this._facadeCall('getDependents', id, 'Provenance: dependents');
|
|
7933
8289
|
case 'record':
|
|
7934
|
-
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');
|
|
7935
8291
|
}
|
|
7936
8292
|
}
|
|
7937
8293
|
generateCompose(input) {
|
|
@@ -7953,27 +8309,46 @@ ${loads}
|
|
|
7953
8309
|
// vector and audio halves of composeCollage were unreachable through
|
|
7954
8310
|
// MCP, so agents converged on the four static patterns the description
|
|
7955
8311
|
// happened to name. A capability gap reads as model bias from outside.
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
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
|
+
}
|
|
7977
8352
|
}
|
|
7978
8353
|
}
|
|
7979
8354
|
generateAudioBeats(input) {
|
|
@@ -8624,16 +8999,28 @@ ${resolve}
|
|
|
8624
8999
|
return `
|
|
8625
9000
|
// Clear background
|
|
8626
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.
|
|
8627
9005
|
app.clearBackground();
|
|
8628
|
-
|
|
9006
|
+
if (typeof app.setBackgroundColor === 'function') app.setBackgroundColor('transparent');
|
|
9007
|
+
return { success: true, action: 'clear', removed: ['pattern', 'drawing', 'image', 'color'] };
|
|
8629
9008
|
})();`.trim();
|
|
8630
9009
|
case 'get':
|
|
8631
9010
|
return `
|
|
8632
9011
|
// Get background info
|
|
8633
9012
|
(function() {
|
|
8634
|
-
//
|
|
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.
|
|
8635
9015
|
const mode = app.getBackgroundMode ? app.getBackgroundMode() : null;
|
|
8636
|
-
|
|
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) };
|
|
8637
9024
|
})();`.trim();
|
|
8638
9025
|
default:
|
|
8639
9026
|
return `(function() { return { error: 'Unknown background action: ${input.action}' }; })();`;
|
|
@@ -8669,13 +9056,14 @@ ${resolve}
|
|
|
8669
9056
|
return `
|
|
8670
9057
|
// Hit test at point
|
|
8671
9058
|
(function() {
|
|
9059
|
+
${REG_ID_JS}
|
|
8672
9060
|
const point = new paper.Point(${input.x ?? 0}, ${input.y ?? 0});
|
|
8673
9061
|
${all
|
|
8674
9062
|
? `const results = app.hitTestAll(point, { tolerance: ${tolerance} });
|
|
8675
9063
|
const hits = results.map(r => ({
|
|
8676
|
-
itemId: r.item
|
|
8677
|
-
type: r.item?.data?.
|
|
8678
|
-
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
|
|
8679
9067
|
}));
|
|
8680
9068
|
return { success: true, action: 'hit_test', x: ${input.x ?? 0}, y: ${input.y ?? 0}, hits, count: hits.length };`
|
|
8681
9069
|
: `const result = app.hitTest(point, { tolerance: ${tolerance} });
|
|
@@ -8683,7 +9071,7 @@ ${resolve}
|
|
|
8683
9071
|
return {
|
|
8684
9072
|
success: true, action: 'hit_test',
|
|
8685
9073
|
x: ${input.x ?? 0}, y: ${input.y ?? 0},
|
|
8686
|
-
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 }
|
|
8687
9075
|
};`}
|
|
8688
9076
|
})();`.trim();
|
|
8689
9077
|
}
|
|
@@ -9083,12 +9471,47 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9083
9471
|
${guard}
|
|
9084
9472
|
const entry = app.itemRegistry.get(${itemIdStr});
|
|
9085
9473
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
|
|
9086
|
-
|
|
9087
|
-
|
|
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)' }) };
|
|
9088
9496
|
})();`.trim();
|
|
9089
9497
|
}
|
|
9090
9498
|
case 'trigger_action': {
|
|
9091
|
-
|
|
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);
|
|
9092
9515
|
return `
|
|
9093
9516
|
// Trigger interaction action
|
|
9094
9517
|
(function() {
|
|
@@ -9460,8 +9883,10 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9460
9883
|
// Toggle grid
|
|
9461
9884
|
(function() {
|
|
9462
9885
|
${guard}
|
|
9463
|
-
//
|
|
9464
|
-
|
|
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']();
|
|
9465
9890
|
return { success: true, action: 'set_grid', enabled: ${enabled} };
|
|
9466
9891
|
})();`.trim();
|
|
9467
9892
|
}
|
|
@@ -9480,17 +9905,18 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9480
9905
|
})();`.trim();
|
|
9481
9906
|
}
|
|
9482
9907
|
case 'set_snap': {
|
|
9483
|
-
//
|
|
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.
|
|
9484
9912
|
return `
|
|
9485
|
-
// Snap to grid
|
|
9913
|
+
// Snap to unit grid (drag / resize in the studio)
|
|
9486
9914
|
(function() {
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
return { success: false, action: 'set_snap',
|
|
9493
|
-
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.' };
|
|
9494
9920
|
})();`.trim();
|
|
9495
9921
|
}
|
|
9496
9922
|
default:
|
|
@@ -9568,6 +9994,30 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9568
9994
|
* Extract the best-matching detected region from an image as a NEW item (FxTool
|
|
9569
9995
|
* ImageWorkflow.extractObject). Async (runs detection first).
|
|
9570
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
|
+
}
|
|
9571
10021
|
generateExtractObject(input) {
|
|
9572
10022
|
const args = JSON.stringify({
|
|
9573
10023
|
...(input.label !== undefined ? { label: input.label } : {}),
|
|
@@ -9584,6 +10034,16 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9584
10034
|
}
|
|
9585
10035
|
try {
|
|
9586
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
|
+
}
|
|
9587
10047
|
const out = Object.assign({ success: !!(res && res.ok) }, res || {});
|
|
9588
10048
|
if (!out.success && typeof out.error === 'string') out.error = (${PinePaperCodeGenerator.MODEL_FETCH_HINT})(out.error);
|
|
9589
10049
|
return out;
|
|
@@ -9971,12 +10431,26 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
9971
10431
|
// which get the box loop below and a note saying so.
|
|
9972
10432
|
const doc = ${docJson};
|
|
9973
10433
|
if (typeof app.instantiateOntology === 'function') {
|
|
9974
|
-
|
|
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);
|
|
9975
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]; });
|
|
9976
10448
|
return {
|
|
9977
10449
|
success: !diags.some(function(d) { return d && d.level === 'error'; }) && !r.imagesFailed,
|
|
9978
10450
|
engine: 'studio',
|
|
9979
|
-
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,
|
|
9980
10454
|
// Every count the engine returns, not a chosen three. imagesFailed is the
|
|
9981
10455
|
// one that matters most: a raster that never decoded leaves a scene that
|
|
9982
10456
|
// looks built and is not, and dropping the count is how that becomes
|
|
@@ -10464,7 +10938,9 @@ ${guard}
|
|
|
10464
10938
|
// item — so reading .data.id off the answer returned no id, every time. The
|
|
10465
10939
|
// group is registered only on one of its two branches; the other is
|
|
10466
10940
|
// registered here so the caller always gets an addressable id.
|
|
10467
|
-
|
|
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))});
|
|
10468
10944
|
const g = r && (r.group || (r.data ? r : null));
|
|
10469
10945
|
if (!g) { return { success: false, error: 'the cursive text produced no path' }; }
|
|
10470
10946
|
let itemId = g.data && (g.data.id || g.data.registryId);
|
|
@@ -10505,7 +10981,11 @@ ${guard}
|
|
|
10505
10981
|
// would otherwise read as "the conversion did nothing to your text".
|
|
10506
10982
|
const r = app.convertTextToCollage(item, ${S(input.text ?? null)} || item.content, ${S(input.collageOptions ?? {})});
|
|
10507
10983
|
if (!r) { return { success: false, error: 'the collage system is unavailable in this build, so the text was left as it was' }; }
|
|
10508
|
-
|
|
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 };
|
|
10509
10989
|
})();`.trim();
|
|
10510
10990
|
}
|
|
10511
10991
|
}
|
|
@@ -10520,6 +11000,117 @@ ${guard}
|
|
|
10520
11000
|
* only ids/count/duration are forwarded. `ids` is the whole handle set the
|
|
10521
11001
|
* caller now has, since the source item is gone.
|
|
10522
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
|
+
}
|
|
10523
11114
|
/**
|
|
10524
11115
|
* The medium axis. `list_media` and `resolve` are READS — they exist so an
|
|
10525
11116
|
* agent can find out what this build can honestly make before it promises a
|
|
@@ -10542,8 +11133,43 @@ ${guard}
|
|
|
10542
11133
|
if (!r || r.ok === false) {
|
|
10543
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 } : {}) };
|
|
10544
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
|
+
}
|
|
10545
11154
|
return { success: true, action: 'apply', medium: r.medium || ${S(input.medium)}, fidelity: r.fidelity, itemId: r.id, ...(r.note ? { note: r.note } : {}) };
|
|
10546
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
|
+
}
|
|
10547
11173
|
case 'list_media':
|
|
10548
11174
|
return wrap('Medium: list', ` if (typeof app.listDesignMedia !== 'function') { return { success: false, error: 'medium axis unavailable — update FxTool' }; }
|
|
10549
11175
|
const media = app.listDesignMedia();
|
|
@@ -10792,14 +11418,60 @@ ${guard}
|
|
|
10792
11418
|
// Import a layered character (decomposer output → role-bound parts; blink works out of the box)
|
|
10793
11419
|
(async function() {
|
|
10794
11420
|
if (typeof app.importLayeredCharacter !== 'function') { return { success: false, error: 'app.importLayeredCharacter unavailable — update FxTool to a layered-character build' }; }
|
|
10795
|
-
// The
|
|
10796
|
-
//
|
|
10797
|
-
|
|
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 };
|
|
10798
11463
|
const r = await app.importLayeredCharacter(bundle, ${opts});
|
|
10799
11464
|
if (!r || !r.groupId) { return { success: false, error: (r && r.error) || 'layered character import failed' }; }
|
|
10800
11465
|
// roles maps part roles → item ids; zero roles wired means the character
|
|
10801
11466
|
// renders but will NOT animate — surface the count so that cannot hide.
|
|
10802
|
-
|
|
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 };
|
|
10803
11475
|
})();`.trim();
|
|
10804
11476
|
}
|
|
10805
11477
|
/**
|
|
@@ -10902,7 +11574,9 @@ ${needWorld}
|
|
|
10902
11574
|
(function() {
|
|
10903
11575
|
if (typeof app.addWorldActor !== 'function') { return { success: false, error: 'app.addWorldActor unavailable — update FxTool' }; }
|
|
10904
11576
|
${needWorld}
|
|
10905
|
-
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;
|
|
10906
11580
|
return id ? { success: true, actorId: id } : { success: false, error: 'actor not added — is the sprite id a canvas item?' };
|
|
10907
11581
|
})();`.trim();
|
|
10908
11582
|
}
|
|
@@ -10940,7 +11614,9 @@ ${needWorld}
|
|
|
10940
11614
|
// World3D: place an object (y defaults to sitting on the terrain)
|
|
10941
11615
|
(function() {
|
|
10942
11616
|
${needWorld}
|
|
10943
|
-
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;
|
|
10944
11620
|
return id ? { success: true, objectId: id } : { success: false, error: 'object not added' };
|
|
10945
11621
|
})();`.trim();
|
|
10946
11622
|
case 'remove_object':
|
|
@@ -10955,7 +11631,10 @@ ${needWorld}
|
|
|
10955
11631
|
// World3D: tear the world down — Paper's canvas is untouched (it was always its own layer)
|
|
10956
11632
|
(function() {
|
|
10957
11633
|
if (typeof app.removeWorld3D !== 'function') { return { success: false, error: 'app.removeWorld3D unavailable — update FxTool' }; }
|
|
10958
|
-
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 };
|
|
10959
11638
|
})();`.trim();
|
|
10960
11639
|
// --- Mesh authoring ---------------------------------------------------
|
|
10961
11640
|
// extrudeToMesh / latheToMesh REFUSE by name when there is no world
|
|
@@ -11008,7 +11687,19 @@ ${needWorld}
|
|
|
11008
11687
|
// World3D: every authored mesh on the stage
|
|
11009
11688
|
(function() {
|
|
11010
11689
|
if (typeof app.listWorldMeshes !== 'function') { return { success: false, error: 'app.listWorldMeshes unavailable — update FxTool' }; }
|
|
11011
|
-
|
|
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); }) };
|
|
11012
11703
|
})();`.trim();
|
|
11013
11704
|
case 'remove_mesh':
|
|
11014
11705
|
return `
|
|
@@ -11178,7 +11869,10 @@ ${needWorld}
|
|
|
11178
11869
|
(function() {
|
|
11179
11870
|
if (typeof app.worldToCanvas !== 'function') { return { success: false, error: 'app.worldToCanvas unavailable — update FxTool' }; }
|
|
11180
11871
|
${needWorld}
|
|
11181
|
-
|
|
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' };
|
|
11182
11876
|
})();`.trim();
|
|
11183
11877
|
case 'canvas_to_ground':
|
|
11184
11878
|
return `
|
|
@@ -11285,14 +11979,20 @@ ${needWorld}
|
|
|
11285
11979
|
* already decided by a pure function, so nothing is left for the page to
|
|
11286
11980
|
* work out.
|
|
11287
11981
|
*/
|
|
11288
|
-
generateDesignCompose(scene, ops, style) {
|
|
11982
|
+
generateDesignCompose(scene, ops, style, sizeWarning) {
|
|
11289
11983
|
const S = (v) => JSON.stringify(v);
|
|
11290
11984
|
return `
|
|
11291
11985
|
// Design: compose a ${style} scene — ${ops.length} items, laid out server-side
|
|
11292
11986
|
(function() {
|
|
11293
11987
|
if (typeof app.create !== 'function') { return { success: false, error: 'app.create unavailable — update FxTool' }; }
|
|
11294
11988
|
const size = ${S({ width: scene.width, height: scene.height })};
|
|
11295
|
-
|
|
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;
|
|
11296
11996
|
${scene.backgroundHex ? `if (typeof app.setBackgroundColor === 'function') { try { app.setBackgroundColor(${S(scene.backgroundHex)}); } catch (_) { /* ditto */ } }` : '// this style paints no background of its own'}
|
|
11297
11997
|
const ops = ${S(ops)};
|
|
11298
11998
|
const ids = [];
|
|
@@ -11307,7 +12007,9 @@ ${needWorld}
|
|
|
11307
12007
|
if (app.historyManager) app.historyManager.saveState();
|
|
11308
12008
|
// A partial composition is reported as one. Half a poster that claims
|
|
11309
12009
|
// success is worse than a poster that says which four elements are missing.
|
|
11310
|
-
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 ?? '')} } : {}) };
|
|
11311
12013
|
})();`.trim();
|
|
11312
12014
|
}
|
|
11313
12015
|
/**
|
|
@@ -11448,9 +12150,13 @@ ${needWorld}
|
|
|
11448
12150
|
// Interchange: ${fn}
|
|
11449
12151
|
(async function() {
|
|
11450
12152
|
${guard(fn)}
|
|
12153
|
+
${TO_DATA_URL_JS}
|
|
11451
12154
|
const out = await app.${fn}(${opts});
|
|
11452
12155
|
if (!out) { return { success: false, error: '${fn} produced nothing — the scene may have no animatable content' }; }
|
|
11453
|
-
|
|
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' };`}
|
|
11454
12160
|
})();`.trim();
|
|
11455
12161
|
}
|
|
11456
12162
|
case 'import_lottie':
|
|
@@ -11460,9 +12166,23 @@ ${guard(fn)}
|
|
|
11460
12166
|
${guard('importLottie')}
|
|
11461
12167
|
// importLottie logs and returns falsy when the importer is absent — a
|
|
11462
12168
|
// stripped console makes that indistinguishable from an empty animation.
|
|
12169
|
+
${REG_ID_JS}
|
|
11463
12170
|
const r = await app.importLottie(${S(input.data)}, ${opts});
|
|
11464
|
-
|
|
11465
|
-
|
|
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 } : {}) };
|
|
11466
12186
|
})();`.trim();
|
|
11467
12187
|
case 'export_glb':
|
|
11468
12188
|
return `
|
|
@@ -11473,9 +12193,11 @@ ${guard('exportGLB')}
|
|
|
11473
12193
|
// stripped in production, so the precondition is checked here instead.
|
|
11474
12194
|
const has3d = app._initialized && app._initialized.has && app._initialized.has('threeD');
|
|
11475
12195
|
if (!has3d) { return { success: false, error: 'no perspective objects to export — create one with createObject3D first' }; }
|
|
12196
|
+
${TO_DATA_URL_JS}
|
|
11476
12197
|
const out = await app.exportGLB(${opts});
|
|
11477
12198
|
if (!out) { return { success: false, error: 'GLB export produced nothing' }; }
|
|
11478
|
-
|
|
12199
|
+
const __url = await __ppToDataUrl(out, 'model/gltf-binary');
|
|
12200
|
+
return __url ? { success: true, format: 'glb', data: __url } : { success: true, format: 'glb', data: out };
|
|
11479
12201
|
})();`.trim();
|
|
11480
12202
|
case 'export_bvh':
|
|
11481
12203
|
return `
|
|
@@ -11536,7 +12258,16 @@ ${guard('exportPNGSequence')}
|
|
|
11536
12258
|
generateSound(input) {
|
|
11537
12259
|
const S = (v) => JSON.stringify(v);
|
|
11538
12260
|
const guard = (fn) => ` if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool' }; }`;
|
|
11539
|
-
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() : `
|
|
11540
12271
|
// Sound: ${label}
|
|
11541
12272
|
(function() {
|
|
11542
12273
|
${guard(fn)}
|
|
@@ -11551,8 +12282,12 @@ ${guard(fn)}
|
|
|
11551
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}`);
|
|
11552
12283
|
case 'play_percussion': return simple('playPercussion', `${S(input.name)}, ${S(input.options ?? {})}`, 'played', `percussion ${input.name}`);
|
|
11553
12284
|
case 'play_sfx': return simple('playSfx', `${S(input.name)}, ${S(input.options ?? {})}`, 'played', `sfx ${input.name}`);
|
|
11554
|
-
|
|
11555
|
-
|
|
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');
|
|
11556
12291
|
case 'from_text':
|
|
11557
12292
|
return `
|
|
11558
12293
|
// Sound: resolve "${input.text}" to a spec WITHOUT playing it
|
|
@@ -11712,7 +12447,9 @@ ${guard('createSound')}
|
|
|
11712
12447
|
for (let i = 0; i < cues.length; i++) {
|
|
11713
12448
|
const c = cues[i];
|
|
11714
12449
|
try {
|
|
11715
|
-
|
|
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);
|
|
11716
12453
|
if (!spec) { failed.push({ index: i, t: c.t, error: 'neither spec nor preset' }); continue; }
|
|
11717
12454
|
const item = app.createSound(spec, c.visual || {});
|
|
11718
12455
|
if (!item) { failed.push({ index: i, t: c.t, error: 'the cue produced no waveform path' }); continue; }
|
|
@@ -11801,10 +12538,20 @@ ${guard('timbreFromPath')}
|
|
|
11801
12538
|
generatePathOp(input) {
|
|
11802
12539
|
const S = (v) => JSON.stringify(v);
|
|
11803
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).
|
|
11804
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); };
|
|
11805
12548
|
const r = ${expr};
|
|
11806
12549
|
if (!r || r.ok === false) { return { success: false, error: (r && r.reason) || 'the engine refused the operation' }; }
|
|
11807
|
-
|
|
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;`;
|
|
11808
12555
|
switch (input.action) {
|
|
11809
12556
|
case 'boolean':
|
|
11810
12557
|
return `
|
|
@@ -12146,7 +12893,14 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
12146
12893
|
return wrap('Rigging: bake animation to keyframes', ` const res = R.bakeAnimation(${S(input.skeletonId)}, ${S(input.options ?? {})});
|
|
12147
12894
|
if (res && app.historyManager) app.historyManager.saveState();
|
|
12148
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." }; }
|
|
12149
|
-
|
|
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.' } : {}) };`);
|
|
12150
12904
|
case 'list_shape_keys':
|
|
12151
12905
|
return wrap('Rigging: list shape keys', ` const keys = R.listShapeKeys(${S(input.skeletonId)});
|
|
12152
12906
|
return { success: true, action: 'list_shape_keys', shapeKeys: keys, count: keys.length };`);
|