@pinepaper.studio/mcp-server 1.6.18 → 1.6.20
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 +179 -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 +3 -3
- 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 +27 -11
- 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/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +8 -0
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/tools/choreograph.d.ts +64 -0
- package/dist/tools/choreograph.d.ts.map +1 -0
- package/dist/tools/choreograph.js +336 -0
- package/dist/tools/choreograph.js.map +1 -0
- package/dist/tools/compact-descriptions.d.ts.map +1 -1
- package/dist/tools/compact-descriptions.js +16 -15
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts +10 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +705 -150
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/effect-check.d.ts +26 -0
- package/dist/tools/effect-check.d.ts.map +1 -0
- package/dist/tools/effect-check.js +129 -0
- package/dist/tools/effect-check.js.map +1 -0
- package/dist/tools/effect-class.d.ts +31 -0
- package/dist/tools/effect-class.d.ts.map +1 -0
- package/dist/tools/effect-class.js +170 -0
- package/dist/tools/effect-class.js.map +1 -0
- package/dist/tools/engine-lists.d.ts +20 -5
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +106 -4
- 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 +22 -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 +232 -13
- 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 +84 -5
- package/dist/tools/handlers/export.js.map +1 -1
- package/dist/tools/handlers/font.js +10 -10
- package/dist/tools/handlers/font.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +12 -12
- package/dist/tools/handlers/maps.d.ts.map +1 -1
- package/dist/tools/handlers/maps.js +23 -3
- package/dist/tools/handlers/maps.js.map +1 -1
- package/dist/tools/handlers.d.ts +44 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +413 -39
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +39 -26
- 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 +27 -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 +11 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +91 -3
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +1544 -276
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +3203 -907
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +393 -53
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +46 -2
- package/package.json +4 -3
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* Converts MCP tool inputs to executable PinePaper JavaScript code.
|
|
5
5
|
* This code is designed to run in the browser context where window.PinePaper is available.
|
|
6
6
|
*/
|
|
7
|
+
import { compileChoreography } from '../tools/choreograph.js';
|
|
7
8
|
import { normalizeSvgSize } from '../utils/svg-size.js';
|
|
8
9
|
import { WORLD3D_COLOR_PATHS } from '../tools/engine-lists.js';
|
|
9
10
|
import { REQUIRED_ENGINE_METHODS, OPTIONAL_ENGINE_METHODS } from '../tools/engine-methods.js';
|
|
10
11
|
import { ACCEPTED_CREATE_PARAMS, NORMALIZE_PARAM_READS, MODIFY_CHANGE_READS } from '../tools/shape-params.js';
|
|
11
12
|
import { generateP5DrawCode } from '../tools/p5-compat/p5-helpers.js';
|
|
12
|
-
import { CreateItemInputSchema, ImportMotionCaptureInputSchema, ModifyItemInputSchema, AddRelationInputSchema, AnimateItemInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, CreateSceneInputSchema,
|
|
13
|
+
import { CreateItemInputSchema, ScreenSpaceSchema, StepTimingSchema, CounterSchema, ImportMotionCaptureInputSchema, ModifyItemInputSchema, AddRelationInputSchema, AnimateItemInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, CreateSceneInputSchema,
|
|
13
14
|
// Diagram types
|
|
14
15
|
CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
|
|
15
16
|
// Agent flow mode types
|
|
@@ -25,7 +26,7 @@ GlobeEnableInputSchema, GlobeRotateToInputSchema, GlobeSpinInputSchema, WorldTou
|
|
|
25
26
|
// Custom relation/code types
|
|
26
27
|
RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
|
|
27
28
|
// Mask types
|
|
28
|
-
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, } from './schemas.js';
|
|
29
|
+
ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema, QueryCapabilitiesInputSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, MaskTypeSchema, MaskPresetSchema, } from './schemas.js';
|
|
29
30
|
import { OntologyCompiler } from '../ontology/ontology-compiler.js';
|
|
30
31
|
// =============================================================================
|
|
31
32
|
// HELPER FUNCTIONS
|
|
@@ -480,6 +481,19 @@ const DIRECTION_KEYS = ['direction', 'textDirection', 'dir'];
|
|
|
480
481
|
// first STRONG character decides. Mirrored so a result can say which way
|
|
481
482
|
// 'auto' went.
|
|
482
483
|
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;
|
|
484
|
+
/**
|
|
485
|
+
* cursiveOptions as createCursiveText reads them. The engine takes `nib` as a
|
|
486
|
+
* boolean or a tuning object; the tool also takes the words 'monoline' and
|
|
487
|
+
* 'broad', which the engine would read as truthy (= broad) both times.
|
|
488
|
+
*/
|
|
489
|
+
export function cursiveEngineOptions(o) {
|
|
490
|
+
if (!o)
|
|
491
|
+
return {};
|
|
492
|
+
const { nib, ...rest } = o;
|
|
493
|
+
if (nib === undefined)
|
|
494
|
+
return rest;
|
|
495
|
+
return { ...rest, nib: nib === 'monoline' ? false : nib === 'broad' ? true : nib };
|
|
496
|
+
}
|
|
483
497
|
function baseDirection(text) {
|
|
484
498
|
for (const ch of text) {
|
|
485
499
|
if (RTL_SCRIPT.test(ch))
|
|
@@ -1477,7 +1491,9 @@ function generateGetItemsCode(filter) {
|
|
|
1477
1491
|
if (filter?.source)
|
|
1478
1492
|
conditions.push(`entry.source === '${filter.source}'`);
|
|
1479
1493
|
if (filter?.hasAnimation !== undefined) {
|
|
1480
|
-
|
|
1494
|
+
// 'none' is what the engine stores on a still item, and !!'none' is true
|
|
1495
|
+
// (gate H5): animated means a real loop animation OR keyframes.
|
|
1496
|
+
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
1497
|
}
|
|
1482
1498
|
if (filter?.hasRelation !== undefined) {
|
|
1483
1499
|
conditions.push(`(app.getRelations(entry.id || entry.itemId).length > 0) === ${filter.hasRelation}`);
|
|
@@ -1490,7 +1506,7 @@ const items = entries.map(entry => ({
|
|
|
1490
1506
|
id: entry.id || entry.itemId,
|
|
1491
1507
|
type: entry.type,
|
|
1492
1508
|
position: entry.item.position ? { x: entry.item.position.x, y: entry.item.position.y } : null,
|
|
1493
|
-
hasAnimation: !!entry.item.data?.animationType,
|
|
1509
|
+
hasAnimation: !!((entry.item.data?.animationType && entry.item.data.animationType !== 'none') || (Array.isArray(entry.item.data?.keyframes) && entry.item.data.keyframes.length > 0)),
|
|
1494
1510
|
relations: app.getRelations(entry.id || entry.itemId).map(r => r.relation || r.type)
|
|
1495
1511
|
}));
|
|
1496
1512
|
|
|
@@ -1505,7 +1521,7 @@ const items = filtered.map(entry => ({
|
|
|
1505
1521
|
id: entry.id || entry.itemId,
|
|
1506
1522
|
type: entry.type,
|
|
1507
1523
|
position: entry.item.position ? { x: entry.item.position.x, y: entry.item.position.y } : null,
|
|
1508
|
-
hasAnimation: !!entry.item.data?.animationType,
|
|
1524
|
+
hasAnimation: !!((entry.item.data?.animationType && entry.item.data.animationType !== 'none') || (Array.isArray(entry.item.data?.keyframes) && entry.item.data.keyframes.length > 0)),
|
|
1509
1525
|
relations: app.getRelations(entry.id || entry.itemId).map(r => r.relation || r.type)
|
|
1510
1526
|
}));
|
|
1511
1527
|
|
|
@@ -1679,7 +1695,7 @@ app.setBackgroundColor('${color}');
|
|
|
1679
1695
|
*/
|
|
1680
1696
|
function generateSetCanvasSizeCode(width, height, preset) {
|
|
1681
1697
|
const sizeArg = preset
|
|
1682
|
-
?
|
|
1698
|
+
? JSON.stringify(preset)
|
|
1683
1699
|
: `{ width: ${width}, height: ${height} }`;
|
|
1684
1700
|
return `
|
|
1685
1701
|
// Set canvas size
|
|
@@ -1697,9 +1713,10 @@ function generateSetCanvasSizeCode(width, height, preset) {
|
|
|
1697
1713
|
app.historyManager.saveState();
|
|
1698
1714
|
// Report what the canvas ACTUALLY took where the engine says so, rather than
|
|
1699
1715
|
// echoing the request back.
|
|
1700
|
-
|
|
1716
|
+
const __sz = app.getCanvasSize ? app.getCanvasSize() : null;
|
|
1717
|
+
return { success: true, width: (r && r.width) || (__sz && __sz.width) || ${width ?? null}, height: (r && r.height) || (__sz && __sz.height) || ${height ?? null},
|
|
1701
1718
|
...(r && r.preset ? { preset: r.preset } : {}),
|
|
1702
|
-
...(r && r.unbounded !== undefined ? { unbounded: r.unbounded } : {})${!preset && (width > 8192 || height > 8192) ? `,
|
|
1719
|
+
...(r && r.unbounded !== undefined ? { unbounded: r.unbounded } : {})${!preset && ((width ?? 0) > 8192 || (height ?? 0) > 8192) ? `,
|
|
1703
1720
|
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
1721
|
})();
|
|
1705
1722
|
`.trim();
|
|
@@ -1939,6 +1956,108 @@ export function planKeyframeMerges(operations) {
|
|
|
1939
1956
|
* Such a snippet's value is its last top-level (-led statement; the
|
|
1940
1957
|
* font-loading form is an async IIFE, which is awaited instead.
|
|
1941
1958
|
*/
|
|
1959
|
+
/**
|
|
1960
|
+
* The active brand's defaults for a new item (D69): only what the caller did
|
|
1961
|
+
* not name. Closed shapes take colors.primary; text takes colors.text (or
|
|
1962
|
+
* primary) and the heading font from 32 px up, else the body font. Applied
|
|
1963
|
+
* through modifyItem after create, so the engine's own bookkeeping runs.
|
|
1964
|
+
*/
|
|
1965
|
+
const BRAND_FILL_TYPES = new Set(['circle', 'star', 'rectangle', 'triangle', 'polygon', 'ellipse', 'pentagon', 'hexagon', 'diamond', 'arrow', 'heart']);
|
|
1966
|
+
function withBrandDefaults(code, itemType, props) {
|
|
1967
|
+
if (props.noBrand === true)
|
|
1968
|
+
return code;
|
|
1969
|
+
const isText = itemType === 'text';
|
|
1970
|
+
if (!isText && !BRAND_FILL_TYPES.has(itemType))
|
|
1971
|
+
return code;
|
|
1972
|
+
const hasColor = ['color', 'fillColor', 'fill'].some((k) => props[k] !== undefined);
|
|
1973
|
+
const hasFont = props.fontFamily !== undefined;
|
|
1974
|
+
if (hasColor && (!isText || hasFont))
|
|
1975
|
+
return code;
|
|
1976
|
+
const size = typeof props.fontSize === 'number' ? props.fontSize : 36;
|
|
1977
|
+
const anchor = 'const itemId = item.data.registryId;';
|
|
1978
|
+
if (!code.includes(anchor))
|
|
1979
|
+
return code;
|
|
1980
|
+
const patch = `
|
|
1981
|
+
// Active brand (D69): fill in what this call did not name.
|
|
1982
|
+
const __bk = (typeof window !== 'undefined' && window.__ppActiveBrand) || null;
|
|
1983
|
+
let __brand = null;
|
|
1984
|
+
if (__bk && __bk.colors && app.modifyItem) {
|
|
1985
|
+
const __bm = {};
|
|
1986
|
+
${hasColor ? '' : `__bm.color = ${isText ? '__bk.colors.text || __bk.colors.primary' : '__bk.colors.primary'};`}
|
|
1987
|
+
${isText && !hasFont ? `const __f = __bk.fonts && (${size >= 32 ? '__bk.fonts.heading || __bk.fonts.body' : '__bk.fonts.body || __bk.fonts.heading'}); if (__f) __bm.fontFamily = __f;` : ''}
|
|
1988
|
+
if (Object.keys(__bm).length) { app.modifyItem(itemId, __bm); __brand = __bm; }
|
|
1989
|
+
}`;
|
|
1990
|
+
return code.replace(anchor, anchor + patch).replace(/\(\{ itemId, type:/, '({ itemId, ...(__brand ? { brand: __brand } : {}), type:');
|
|
1991
|
+
}
|
|
1992
|
+
const ITEM_SETTERS = [
|
|
1993
|
+
{ key: 'screenSpace', fn: 'setScreenSpace', what: 'screen-space (HUD) items', since: 'D20' },
|
|
1994
|
+
{ key: 'stepTiming', fn: 'setStepTiming', what: 'step timing', since: 'D11' },
|
|
1995
|
+
{ key: 'counter', fn: 'setCounter', what: 'counters', since: 'D70' },
|
|
1996
|
+
];
|
|
1997
|
+
/** Lift screenSpace / hud / stepTiming out of a properties bag (mutated), explicit fields winning. */
|
|
1998
|
+
function takeItemSetters(props, explicit = {}) {
|
|
1999
|
+
const out = {};
|
|
2000
|
+
// From properties a caller may write hud: 1; it means on. The top-level
|
|
2001
|
+
// field is still a strict boolean.
|
|
2002
|
+
const ss = explicit.screenSpace ?? (props.screenSpace ?? props.hud);
|
|
2003
|
+
if (ss !== undefined)
|
|
2004
|
+
out.screenSpace = explicit.screenSpace !== undefined ? ScreenSpaceSchema.parse(ss) : !!ss;
|
|
2005
|
+
const st = explicit.stepTiming !== undefined ? explicit.stepTiming : props.stepTiming;
|
|
2006
|
+
if (st !== undefined)
|
|
2007
|
+
out.stepTiming = StepTimingSchema.parse(st);
|
|
2008
|
+
// A counter from properties is lifted only when it is a valid spec; anything
|
|
2009
|
+
// else stays where it was, for the engine to read and refuse by name.
|
|
2010
|
+
let liftCounter = explicit.counter !== undefined;
|
|
2011
|
+
if (liftCounter)
|
|
2012
|
+
out.counter = CounterSchema.parse(explicit.counter);
|
|
2013
|
+
else if (props.counter !== undefined && CounterSchema.safeParse(props.counter).success) {
|
|
2014
|
+
out.counter = props.counter;
|
|
2015
|
+
liftCounter = true;
|
|
2016
|
+
}
|
|
2017
|
+
delete props.screenSpace;
|
|
2018
|
+
delete props.hud;
|
|
2019
|
+
delete props.stepTiming;
|
|
2020
|
+
if (liftCounter)
|
|
2021
|
+
delete props.counter;
|
|
2022
|
+
return out;
|
|
2023
|
+
}
|
|
2024
|
+
function hasItemSetters(s) {
|
|
2025
|
+
return s.screenSpace !== undefined || s.stepTiming !== undefined || s.counter !== undefined;
|
|
2026
|
+
}
|
|
2027
|
+
/** Statements: refuse, before anything changes, when a requested setter is missing. */
|
|
2028
|
+
function itemSettersPreflightJs(s) {
|
|
2029
|
+
return ITEM_SETTERS.filter((d) => s[d.key] !== undefined).map((d) => ` if (typeof app.${d.fn} !== 'function') { return { success: false, error: 'this studio has no ${d.what} (app.${d.fn}) — it predates FxTool ${d.since}; nothing was changed.' }; }`).join('\n');
|
|
2030
|
+
}
|
|
2031
|
+
/** Statements: apply each setter to idExpr, folding the verdict into resVar. */
|
|
2032
|
+
function itemSettersApplyJs(s, idExpr, resVar) {
|
|
2033
|
+
return ITEM_SETTERS.filter((d) => s[d.key] !== undefined).map((d) => {
|
|
2034
|
+
const arg = JSON.stringify(s[d.key]);
|
|
2035
|
+
return ` {
|
|
2036
|
+
const __sr = app.${d.fn}(${idExpr}, ${arg});
|
|
2037
|
+
if (!__sr || __sr.ok === false) {
|
|
2038
|
+
return Object.assign({}, ${resVar}, { success: false, error: '${d.key} was not applied to ' + ${idExpr} + ': ' + ((__sr && (__sr.reason || __sr.error)) || 'refused by the studio') });
|
|
2039
|
+
}
|
|
2040
|
+
${resVar}.${d.key} = ${d.key === 'screenSpace' ? arg : `(__sr.${d.key} !== undefined ? __sr.${d.key} : ${arg})`};${d.key === 'counter' ? `\n if (__sr.text !== undefined) ${resVar}.counterText = __sr.text;` : ''}
|
|
2041
|
+
}`;
|
|
2042
|
+
}).join('\n');
|
|
2043
|
+
}
|
|
2044
|
+
/** Wrap a create / modify snippet so the setters run after it, on its item. */
|
|
2045
|
+
function withItemSetters(code, s, idExpr) {
|
|
2046
|
+
if (!hasItemSetters(s))
|
|
2047
|
+
return code;
|
|
2048
|
+
return `(async function() {
|
|
2049
|
+
${itemSettersPreflightJs(s)}
|
|
2050
|
+
const __res = await (async function() {
|
|
2051
|
+
${asReturningBody(code)}
|
|
2052
|
+
})();
|
|
2053
|
+
if (!__res || __res.success === false) return __res;
|
|
2054
|
+
const __sid = ${idExpr};
|
|
2055
|
+
if (!__sid) return __res;
|
|
2056
|
+
${itemSettersApplyJs(s, '__sid', '__res')}
|
|
2057
|
+
if (__res.success === undefined) __res.success = true;
|
|
2058
|
+
return __res;
|
|
2059
|
+
})();`;
|
|
2060
|
+
}
|
|
1942
2061
|
function asReturningBody(snippet) {
|
|
1943
2062
|
const single = snippet.trim();
|
|
1944
2063
|
if (single.startsWith('(async function()'))
|
|
@@ -2210,8 +2329,11 @@ function world3dColor(value) {
|
|
|
2210
2329
|
return value;
|
|
2211
2330
|
const hex = value.trim().replace(/^#/, '');
|
|
2212
2331
|
const full = hex.length === 3 ? hex.split('').map((c) => c + c).join('') : hex;
|
|
2332
|
+
// A string that is not hex is refused HERE, naming both forms: passed on,
|
|
2333
|
+
// the engine answered "must be [r,g,b]", which contradicted the docs
|
|
2334
|
+
// saying hex works (gate A, 1.6.19).
|
|
2213
2335
|
if (!/^[0-9a-f]{6}$/i.test(full))
|
|
2214
|
-
|
|
2336
|
+
throw new Error(`"${value}" is not a colour: use hex ("#03050c" or "#abc") or [r, g, b]`);
|
|
2215
2337
|
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255);
|
|
2216
2338
|
}
|
|
2217
2339
|
/**
|
|
@@ -2313,6 +2435,52 @@ export const SHEET_RUBRIC = {
|
|
|
2313
2435
|
'a jump between the last and first frame of a loop (loopSeam)',
|
|
2314
2436
|
],
|
|
2315
2437
|
};
|
|
2438
|
+
/**
|
|
2439
|
+
* Page code: what a project holds, counted the same way before a save and
|
|
2440
|
+
* after a load, so a round trip can be checked (keyframes in = keyframes out).
|
|
2441
|
+
*/
|
|
2442
|
+
const SCENE_COUNTS_JS = `function __ppSceneCounts() {
|
|
2443
|
+
const all = (app.itemRegistry && typeof app.itemRegistry.getAll === 'function') ? app.itemRegistry.getAll() : [];
|
|
2444
|
+
let animated = 0, keyframes = 0;
|
|
2445
|
+
all.forEach(function (e) {
|
|
2446
|
+
const k = e && e.item && e.item.data && e.item.data.keyframes;
|
|
2447
|
+
if (Array.isArray(k) && k.length) { animated++; keyframes += k.length; }
|
|
2448
|
+
});
|
|
2449
|
+
const rel = (app.relationRegistry && typeof app.relationRegistry.exportForSave === 'function') ? app.relationRegistry.exportForSave().length : null;
|
|
2450
|
+
return { items: all.length, animatedItems: animated, keyframes: keyframes, relations: rel };
|
|
2451
|
+
}`;
|
|
2452
|
+
/**
|
|
2453
|
+
* Page code: the REGISTRY id for a Paper item, walking up from a child (a hit
|
|
2454
|
+
* test lands on a group's inner path). Results used to fall back to Paper's
|
|
2455
|
+
* internal numeric id (gate D9/D10, 1.6.19), which no other tool can address.
|
|
2456
|
+
*/
|
|
2457
|
+
const REG_ID_JS = `function __ppRegId(it) {
|
|
2458
|
+
for (let n = it; n; n = n.parent) {
|
|
2459
|
+
const id = n.data && (n.data.id || n.data.registryId);
|
|
2460
|
+
if (id && app.itemRegistry && app.itemRegistry.get(id)) return id;
|
|
2461
|
+
}
|
|
2462
|
+
const all = (app.itemRegistry && typeof app.itemRegistry.getAll === 'function') ? app.itemRegistry.getAll() : [];
|
|
2463
|
+
for (let n = it; n; n = n.parent) { const e = all.find(function (x) { return x && x.item === n; }); if (e) return e.itemId; }
|
|
2464
|
+
return null;
|
|
2465
|
+
}`;
|
|
2466
|
+
/**
|
|
2467
|
+
* Page code: any binary an engine export returns (Blob, ArrayBuffer, typed
|
|
2468
|
+
* array) as a data URL, so it crosses the bridge as bytes and the handler can
|
|
2469
|
+
* write a file. A Uint8Array crossed as a JSON object of byte indices
|
|
2470
|
+
* ({"0": 80, "1": 75, ...}): export_dotlottie was unusable (gate D7, 1.6.19).
|
|
2471
|
+
*/
|
|
2472
|
+
const TO_DATA_URL_JS = `async function __ppToDataUrl(out, mime) {
|
|
2473
|
+
if (typeof out === 'string') return out.indexOf('data:') === 0 ? out : null;
|
|
2474
|
+
let blob = null;
|
|
2475
|
+
if (out instanceof Blob) blob = out;
|
|
2476
|
+
else if (out instanceof ArrayBuffer || ArrayBuffer.isView(out)) blob = new Blob([out], { type: mime });
|
|
2477
|
+
if (!blob) return null;
|
|
2478
|
+
return await new Promise(function (res, rej) { const r = new FileReader(); r.onload = function () { res(r.result); }; r.onerror = rej; r.readAsDataURL(blob); });
|
|
2479
|
+
}`;
|
|
2480
|
+
/** Why map pan / zoom refuse, and what to do instead. */
|
|
2481
|
+
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, `
|
|
2482
|
+
+ 'which a canvas export ignores. To frame part of a map in an export: keyframe the camera with pinepaper_camera_animate '
|
|
2483
|
+
+ 'and export with framing:"camera".';
|
|
2316
2484
|
export class PinePaperCodeGenerator {
|
|
2317
2485
|
/**
|
|
2318
2486
|
* Generate code for creating an item
|
|
@@ -2333,17 +2501,27 @@ export class PinePaperCodeGenerator {
|
|
|
2333
2501
|
properties.keyframes = validated.keyframes;
|
|
2334
2502
|
if (validated.anchor !== undefined && properties.anchor === undefined && properties.origin === undefined)
|
|
2335
2503
|
properties.anchor = validated.anchor;
|
|
2336
|
-
|
|
2504
|
+
const setters = takeItemSetters(properties, { screenSpace: validated.screenSpace, stepTiming: validated.stepTiming, counter: validated.counter });
|
|
2505
|
+
const brandProps = { ...properties };
|
|
2506
|
+
delete properties.noBrand;
|
|
2507
|
+
return withItemSetters(withBrandDefaults(generateCreateItemCode(validated.itemType, validated.position, properties, validated.data, positionGiven), validated.itemType, brandProps), setters, '__res.itemId');
|
|
2337
2508
|
}
|
|
2338
2509
|
/**
|
|
2339
2510
|
* Generate code for modifying an item
|
|
2340
2511
|
*/
|
|
2341
2512
|
generateModifyItem(input) {
|
|
2342
2513
|
const validated = ModifyItemInputSchema.parse(input);
|
|
2514
|
+
const props = { ...validated.properties };
|
|
2515
|
+
const setters = takeItemSetters(props, { screenSpace: validated.screenSpace, stepTiming: validated.stepTiming, counter: validated.counter });
|
|
2516
|
+
const idExpr = JSON.stringify(validated.itemId);
|
|
2343
2517
|
if (validated.atTime !== undefined) {
|
|
2344
|
-
return generateAutoKeyCode(validated.itemId,
|
|
2518
|
+
return withItemSetters(generateAutoKeyCode(validated.itemId, props, validated.atTime, validated.easing), setters, idExpr);
|
|
2345
2519
|
}
|
|
2346
|
-
|
|
2520
|
+
// Only a setter asked for: no property edit to run.
|
|
2521
|
+
if (hasItemSetters(setters) && Object.keys(props).length === 0 && !validated.data) {
|
|
2522
|
+
return withItemSetters(`({ success: true, itemId: ${idExpr} });`, setters, idExpr);
|
|
2523
|
+
}
|
|
2524
|
+
return withItemSetters(generateModifyItemCode(validated.itemId, props, validated.data), setters, idExpr);
|
|
2347
2525
|
}
|
|
2348
2526
|
/**
|
|
2349
2527
|
* Generate code for deleting an item
|
|
@@ -2354,8 +2532,14 @@ export class PinePaperCodeGenerator {
|
|
|
2354
2532
|
/**
|
|
2355
2533
|
* Generate code for adding a relation
|
|
2356
2534
|
*/
|
|
2357
|
-
generateAddRelation(input) {
|
|
2358
|
-
|
|
2535
|
+
generateAddRelation(input, customName = false) {
|
|
2536
|
+
// The handler has already parsed, and a session-registered custom
|
|
2537
|
+
// relation name is not in the enum (gate D2); the handler says so, and the
|
|
2538
|
+
// name is re-parsed with a built-in stand-in and put back.
|
|
2539
|
+
const rt = input.relationType;
|
|
2540
|
+
const validated = AddRelationInputSchema.parse(customName ? { ...input, relationType: 'orbits' } : input);
|
|
2541
|
+
if (customName)
|
|
2542
|
+
validated.relationType = rt;
|
|
2359
2543
|
// A PRESET decides its own relationType and params, so this is a different
|
|
2360
2544
|
// call, not a defaulted one. applyRelationPreset refuses with
|
|
2361
2545
|
// `{ok:false, errors:[{field, reason}]}` — a third refusal shape on this
|
|
@@ -2381,7 +2565,15 @@ export class PinePaperCodeGenerator {
|
|
|
2381
2565
|
if (validated.relationType === 'moves_along_path') {
|
|
2382
2566
|
return generateMovesAlongPathCode(validated.sourceId, validated.targetId ?? undefined, (validated.params ?? {}));
|
|
2383
2567
|
}
|
|
2384
|
-
|
|
2568
|
+
const code = generateAddRelationCode(validated.sourceId, validated.targetId || validated.sourceId, validated.relationType, validated.params);
|
|
2569
|
+
// NO TARGET (event reactions like on_event_play_sound, self-relations): the
|
|
2570
|
+
// engine is handed the source as the target, but the reply must not say
|
|
2571
|
+
// "item_2 → item_2" — a model reads that as the event wired to itself.
|
|
2572
|
+
if (validated.targetId)
|
|
2573
|
+
return code;
|
|
2574
|
+
return code
|
|
2575
|
+
.replace(`return { success: true, sourceId: '${validated.sourceId}', targetId: '${validated.sourceId}',`, `return { success: true, sourceId: '${validated.sourceId}', targetId: null,`)
|
|
2576
|
+
.replace(`relation between ${validated.sourceId} and ${validated.sourceId}.`, `relation on ${validated.sourceId} (no target).`);
|
|
2385
2577
|
}
|
|
2386
2578
|
/**
|
|
2387
2579
|
* Generate code for removing a relation
|
|
@@ -2407,7 +2599,19 @@ export class PinePaperCodeGenerator {
|
|
|
2407
2599
|
*/
|
|
2408
2600
|
generateKeyframeAnimate(input) {
|
|
2409
2601
|
const validated = KeyframeAnimateInputSchema.parse(input);
|
|
2410
|
-
|
|
2602
|
+
// Hold keys (FxTool D11) on an engine without them would interpolate in
|
|
2603
|
+
// silence; refuse before anything is written. setStepTiming ships in the
|
|
2604
|
+
// same change, so it is the probe.
|
|
2605
|
+
const holds = validated.keyframes.some((k) => k.interpolation === 'hold' || k.hold === true);
|
|
2606
|
+
const code = generateKeyframeAnimateCode(validated.itemId, validated.keyframes, validated.duration, validated.loop, validated.timeOffset, validated.clipInPoint, validated.clipOutPoint, validated.timeUnits, validated.append);
|
|
2607
|
+
if (!holds)
|
|
2608
|
+
return code;
|
|
2609
|
+
return `(async function() {
|
|
2610
|
+
if (!(typeof app.setStepTiming === 'function')) { return { success: false, error: 'this studio has no hold keys (interpolation: \\'hold\\') — it predates FxTool D11; nothing was written. Use two keys a frame apart for a cut.' }; }
|
|
2611
|
+
return await (async function() {
|
|
2612
|
+
${asReturningBody(code)}
|
|
2613
|
+
})();
|
|
2614
|
+
})();`;
|
|
2411
2615
|
}
|
|
2412
2616
|
/**
|
|
2413
2617
|
* Generate code for importing / retargeting a motion-capture clip
|
|
@@ -2655,6 +2859,15 @@ throw new Error('Either svgString or url must be provided');
|
|
|
2655
2859
|
success: !!result.success,
|
|
2656
2860
|
nodeCount: nodes.length,
|
|
2657
2861
|
edgeCount: edges.length,
|
|
2862
|
+
// CANVAS ids, the ones auto_layout / animate / add_relation take. The
|
|
2863
|
+
// engine's own nodeIds are the MERMAID ids (A, B…), which resolve to
|
|
2864
|
+
// nothing on the canvas (gate D45). Newer engines return nodeItemIds;
|
|
2865
|
+
// otherwise they are read off the registered nodes.
|
|
2866
|
+
nodeItemIds: Array.isArray(result.nodeItemIds) && result.nodeItemIds.length
|
|
2867
|
+
? result.nodeItemIds
|
|
2868
|
+
: nodes.map((n) => n.id).filter((id) => typeof id === 'string' && /^item_/.test(id)),
|
|
2869
|
+
...(result.nodeIdMap && typeof result.nodeIdMap === 'object' ? { nodeIdMap: result.nodeIdMap } : {}),
|
|
2870
|
+
...(Array.isArray(result.edgeIds) && result.edgeIds.length ? { edgeIds: result.edgeIds } : {}),
|
|
2658
2871
|
nodes,
|
|
2659
2872
|
edges,
|
|
2660
2873
|
errors: result.errors || [],
|
|
@@ -3068,9 +3281,12 @@ if (typeof _gen === 'function') {
|
|
|
3068
3281
|
codeParts.push(`
|
|
3069
3282
|
// Save history and return results
|
|
3070
3283
|
app.historyManager.saveState();
|
|
3071
|
-
results;
|
|
3284
|
+
return results;
|
|
3072
3285
|
`);
|
|
3073
|
-
|
|
3286
|
+
// ONE (-led FUNCTION (gate C4, 1.6.19). This was a statement list ending
|
|
3287
|
+
// in a bare \`results;\`, and the governor keeps only a (-led snippet's
|
|
3288
|
+
// value, so every created id was collected and then dropped.
|
|
3289
|
+
return `(async function() {\n${codeParts.join('\n')}\n})();`;
|
|
3074
3290
|
}
|
|
3075
3291
|
// =============================================================================
|
|
3076
3292
|
// DIAGRAM TOOLS
|
|
@@ -3191,12 +3407,26 @@ if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}');
|
|
|
3191
3407
|
if (!targetItem) throw new Error('Target item not found: ${targetItemId}');
|
|
3192
3408
|
|
|
3193
3409
|
const config = ${configStr};
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
const
|
|
3410
|
+
// connectPorts(sourcePort, targetPort, config) takes PORT OBJECTS (gate C8,
|
|
3411
|
+
// 1.6.19). This passed (item, name, item, name, config): five arguments to a
|
|
3412
|
+
// three-argument method, so the engine got an item where a port belongs,
|
|
3413
|
+
// returned null, and connector.data threw. Ports are resolved by name or
|
|
3414
|
+
// position first; an item with none gets the standard diagram ports.
|
|
3415
|
+
const __pm = app.diagramSystem.portManager;
|
|
3416
|
+
const __port = function (item, name) {
|
|
3417
|
+
let p = __pm.getPort(item, name);
|
|
3418
|
+
if (!p && !(__pm.getPorts(item) || []).length) { app.diagramSystem.addPorts(item); p = __pm.getPort(item, name); }
|
|
3419
|
+
return p;
|
|
3420
|
+
};
|
|
3421
|
+
const __sp = __port(sourceItem, ${JSON.stringify(sourcePort)});
|
|
3422
|
+
const __tp = __port(targetItem, ${JSON.stringify(targetPort)});
|
|
3423
|
+
if (!__sp || !__tp) {
|
|
3424
|
+
const __names = function (item) { return (__pm.getPorts(item) || []).map(function (p) { return p.position || p.id; }).join(', '); };
|
|
3425
|
+
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) + ')') };
|
|
3426
|
+
}
|
|
3427
|
+
const connector = app.diagramSystem.connectPorts(__sp, __tp, config);
|
|
3428
|
+
if (!connector) { return { success: false, error: 'the diagram system did not create the connector' }; }
|
|
3429
|
+
const connectorId = connector.id || connector.data?.registryId;
|
|
3200
3430
|
app.historyManager.saveState();
|
|
3201
3431
|
|
|
3202
3432
|
({ connectorId, sourceItemId: '${sourceItemId}', sourcePort: '${sourcePort}', targetItemId: '${targetItemId}', targetPort: '${targetPort}' });
|
|
@@ -3291,61 +3521,50 @@ const shapes = category ? _lib.getByCategory(category) : _lib.getAll();
|
|
|
3291
3521
|
generateUpdateConnector(input) {
|
|
3292
3522
|
const validated = UpdateConnectorInputSchema.parse(input);
|
|
3293
3523
|
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
3524
|
return `
|
|
3306
3525
|
// 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 (
|
|
3526
|
+
(function() {
|
|
3527
|
+
// CONNECTORS LIVE IN THE DIAGRAM SYSTEM, NOT THE ITEM REGISTRY (gate D1,
|
|
3528
|
+
// 1.6.19): getItemById never resolves the id connect returned.
|
|
3529
|
+
const __cid = ${JSON.stringify(connectorId)};
|
|
3530
|
+
const ds = app.diagramSystem;
|
|
3531
|
+
const connector = ds && typeof ds.getConnector === 'function' ? ds.getConnector(__cid) : null;
|
|
3532
|
+
if (!connector) return { success: false, error: 'Connector not found: ' + __cid + '. Use the connectorId pinepaper_connect returned.' };
|
|
3533
|
+
// updateConnector(id, style) takes the STYLE BAG ITSELF. This passed
|
|
3534
|
+
// { style: {...}, label } (gate run 2, 1.6.19), so the engine merged a key
|
|
3535
|
+
// named "style" into the connector's style, changed nothing it draws, and
|
|
3536
|
+
// the tool reported updated:true over an unchanged connector.
|
|
3537
|
+
const style = ${JSON.stringify(style ?? {})};
|
|
3538
|
+
const label = ${label === undefined ? 'undefined' : JSON.stringify(label)};
|
|
3539
|
+
const labelPosition = ${labelPosition === undefined ? 'undefined' : labelPosition};
|
|
3540
|
+
const keys = Object.keys(style);
|
|
3541
|
+
if (!keys.length && label === undefined && labelPosition === undefined) {
|
|
3542
|
+
return { success: false, error: 'nothing to update: pass style, label or labelPosition' };
|
|
3543
|
+
}
|
|
3544
|
+
const applied = [];
|
|
3545
|
+
if (keys.length) {
|
|
3546
|
+
if (typeof ds.updateConnector !== 'function') {
|
|
3547
|
+
return { success: false, error: 'this studio has no per-connector update (app.diagramSystem.updateConnector); update the studio' };
|
|
3548
|
+
}
|
|
3549
|
+
const res = ds.updateConnector(__cid, style);
|
|
3550
|
+
if (!res || res.ok === false) return { success: false, error: (res && res.reason) || 'the connector could not be updated' };
|
|
3551
|
+
const unchanged = keys.filter(function (k) { return connector.style[k] !== style[k]; });
|
|
3552
|
+
if (unchanged.length) return { success: false, error: 'the studio did not take ' + unchanged.join(', ') };
|
|
3553
|
+
keys.forEach(function (k) { applied.push('style.' + k); });
|
|
3554
|
+
}
|
|
3555
|
+
if (label !== undefined || labelPosition !== undefined) {
|
|
3556
|
+
connector.setLabel(label !== undefined ? label : connector.label, labelPosition !== undefined ? labelPosition : connector.labelPosition);
|
|
3557
|
+
applied.push(label !== undefined ? 'label' : 'labelPosition');
|
|
3558
|
+
}
|
|
3327
3559
|
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 });
|
|
3560
|
+
// What it now DRAWS, read off the path, so a caller can check rather than trust.
|
|
3561
|
+
const p = connector.path;
|
|
3562
|
+
return {
|
|
3563
|
+
success: true, connectorId: __cid, updated: true, applied: applied,
|
|
3564
|
+
drawn: p ? { stroke: p.strokeColor ? p.strokeColor.toCSS(true) : null, width: p.strokeWidth, dash: p.dashArray || [] } : null,
|
|
3565
|
+
label: connector.label || null,
|
|
3566
|
+
};
|
|
3567
|
+
})();
|
|
3349
3568
|
`.trim();
|
|
3350
3569
|
}
|
|
3351
3570
|
/**
|
|
@@ -3356,8 +3575,10 @@ if (_unsupported.length && !_applied.length) {
|
|
|
3356
3575
|
const { connectorId } = validated;
|
|
3357
3576
|
return `
|
|
3358
3577
|
// Remove connector: ${connectorId}
|
|
3359
|
-
|
|
3360
|
-
|
|
3578
|
+
// Resolved through the diagram system (gate D1, 1.6.19), as update_connector is.
|
|
3579
|
+
const __cid = ${JSON.stringify(connectorId)};
|
|
3580
|
+
const connector = (app.diagramSystem && app.diagramSystem.getConnector(__cid)) || app.getItemById(__cid);
|
|
3581
|
+
if (!connector) throw new Error('Connector not found: ' + __cid);
|
|
3361
3582
|
|
|
3362
3583
|
app.diagramSystem.connectorManager.removeConnector(connector);
|
|
3363
3584
|
app.historyManager.saveState();
|
|
@@ -3446,6 +3667,23 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3446
3667
|
app.setCanvasSize('${canvasPresetFor(canvasPreset)}');
|
|
3447
3668
|
// Allow canvas resize to take effect
|
|
3448
3669
|
await new Promise(r => setTimeout(r, 50));
|
|
3670
|
+
`;
|
|
3671
|
+
}
|
|
3672
|
+
else {
|
|
3673
|
+
code += `
|
|
3674
|
+
// BOUND THE BOARD IT REPORTS (gate D57). An UNBOUNDED canvas still answers
|
|
3675
|
+
// getCanvasSize with its 1920x1080 fallback, which is what this job reports
|
|
3676
|
+
// and what every sizing rule here is relative to; but its stills export the
|
|
3677
|
+
// artwork's bounds plus a margin (432x232 for one rectangle), while video
|
|
3678
|
+
// exports the board. Fixing it at the size reported makes the two agree.
|
|
3679
|
+
let _boundedFrom = null;
|
|
3680
|
+
if (app.canvasSize && app.canvasSize.unbounded && typeof app.setCanvasSize === 'function') {
|
|
3681
|
+
const _u = app.getCanvasSize ? app.getCanvasSize() : app.canvasSize;
|
|
3682
|
+
if (_u && _u.width > 0 && _u.height > 0) {
|
|
3683
|
+
const _r = app.setCanvasSize({ width: _u.width, height: _u.height });
|
|
3684
|
+
if (!(_r && _r.ok === false)) _boundedFrom = 'unbounded';
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3449
3687
|
`;
|
|
3450
3688
|
}
|
|
3451
3689
|
if (wantOntology) {
|
|
@@ -3479,7 +3717,8 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3479
3717
|
screenshotPolicy: '${policyStr}',
|
|
3480
3718
|
canvasPreset: ${canvasPreset ? `'${canvasPreset}'` : 'null'},
|
|
3481
3719
|
canvasCleared: ${shouldClear},
|
|
3482
|
-
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }${
|
|
3720
|
+
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }${canvasPreset ? '' : `,
|
|
3721
|
+
...(_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
3722
|
ontology: _ontology,
|
|
3484
3723
|
ontologyUnavailable: _ontologyUnavailable || undefined` : ''}
|
|
3485
3724
|
};
|
|
@@ -3499,6 +3738,8 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3499
3738
|
let code = `
|
|
3500
3739
|
// End agent job
|
|
3501
3740
|
(async function() {
|
|
3741
|
+
// Close the studio's job (D92). It never reloads.
|
|
3742
|
+
try { if (typeof window !== 'undefined' && window.PinePaperAgent && window.PinePaperAgent.endJob) window.PinePaperAgent.endJob(); } catch (_) { /* older studio */ }
|
|
3502
3743
|
const result = {
|
|
3503
3744
|
success: true,
|
|
3504
3745
|
jobEnded: true
|
|
@@ -3558,12 +3799,13 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3558
3799
|
|
|
3559
3800
|
if (entry.type === 'text' || item.className === 'PointText') analysis.hasText = true;
|
|
3560
3801
|
if (item.className === 'Raster') analysis.hasImages = true;
|
|
3561
|
-
|
|
3802
|
+
// 'none' is what a still item stores (gate C5, 1.6.19): it is not an animation.
|
|
3803
|
+
if (data.animationType && data.animationType !== 'none') {
|
|
3562
3804
|
analysis.hasAnimations = true;
|
|
3563
3805
|
animationSet.add(data.animationType);
|
|
3564
3806
|
}
|
|
3565
3807
|
// Detect keyframe animations (added via app.addAnimation with keyframes array)
|
|
3566
|
-
if (data.keyframes || data.animation || data.keyframeAnimation) {
|
|
3808
|
+
if ((Array.isArray(data.keyframes) && data.keyframes.length > 0) || data.animation || data.keyframeAnimation) {
|
|
3567
3809
|
analysis.hasAnimations = true;
|
|
3568
3810
|
animationSet.add('keyframe');
|
|
3569
3811
|
}
|
|
@@ -3802,12 +4044,19 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3802
4044
|
// the exact emitter create_item uses, adapted to return its result
|
|
3803
4045
|
// inside this op's function.
|
|
3804
4046
|
const pos = op.position || { x: 400, y: 300 };
|
|
3805
|
-
const
|
|
3806
|
-
const
|
|
3807
|
-
|
|
4047
|
+
const opProps = { ...(op.properties || {}) };
|
|
4048
|
+
const opSetters = takeItemSetters(opProps, { screenSpace: op.screenSpace, stepTiming: op.stepTiming, counter: op.counter });
|
|
4049
|
+
const createProps = withRadiusAxes(opProps);
|
|
4050
|
+
const opBrand = { ...opProps };
|
|
4051
|
+
delete opProps.noBrand;
|
|
4052
|
+
const returning = asReturningBody(withBrandDefaults(generateCreateItemCode(op.itemType, pos, { ...opProps }, undefined, op.position !== undefined), op.itemType, opBrand));
|
|
4053
|
+
let createCode = `${hasItemSetters(opSetters) ? `\n${itemSettersPreflightJs(opSetters)}` : ''}
|
|
3808
4054
|
const __created = await (async function() {
|
|
3809
4055
|
${returning}
|
|
3810
|
-
})()
|
|
4056
|
+
})();${hasItemSetters(opSetters) ? `
|
|
4057
|
+
if (__created && __created.success !== false && __created.itemId) {
|
|
4058
|
+
${itemSettersApplyJs(opSetters, '__created.itemId', '__created')}
|
|
4059
|
+
}` : ''}`;
|
|
3811
4060
|
// Coordinate-built items (path from segments/pathData, line/arc from
|
|
3812
4061
|
// from/through/to) derive their geometry from those coordinates and
|
|
3813
4062
|
// IGNORE params.position — so `create` at a point silently produced an
|
|
@@ -3915,7 +4164,7 @@ return { success: true, backgroundColor: '${bgColor}' };
|
|
|
3915
4164
|
case 'set_canvas_size': {
|
|
3916
4165
|
const w = op.width || 1080;
|
|
3917
4166
|
const h = op.height || 1080;
|
|
3918
|
-
const sizeArg = op.preset ?
|
|
4167
|
+
const sizeArg = op.preset ? JSON.stringify(op.preset) : `{ width: ${w}, height: ${h} }`;
|
|
3919
4168
|
return `
|
|
3920
4169
|
// Read the verdict, as the standalone tool now does: an unknown preset or an
|
|
3921
4170
|
// out-of-range dimension is refused by name, and reporting the size we ASKED
|
|
@@ -4403,7 +4652,11 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4403
4652
|
// A 3D world under the canvas IS the background: since FxTool #50
|
|
4404
4653
|
// (747315e3) jpg/webp stills composite it, so nothing turns white/black.
|
|
4405
4654
|
const hasWorld = !!app._world3d;
|
|
4406
|
-
|
|
4655
|
+
// So does a styled scene: one raster sized to COVER the frame (gate run 3:
|
|
4656
|
+
// every export after styled_scene warned of black areas it does not have).
|
|
4657
|
+
const hasStyledScene = !!(app.textItemGroup && app.textItemGroup.children
|
|
4658
|
+
&& app.textItemGroup.children.some(function (it) { return it && it.data && it.data.styledScene && it.visible !== false; }));
|
|
4659
|
+
if (hasColor || hasBgItems || hasWorld || hasStyledScene) return [];
|
|
4407
4660
|
return [{ code: 'alpha_dropped', message: fmt === 'apng'
|
|
4408
4661
|
? '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
4662
|
: fmt === 'jpg'
|
|
@@ -4530,7 +4783,11 @@ ${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as
|
|
|
4530
4783
|
camBase = { width: app.canvasSize.width, height: app.canvasSize.height };
|
|
4531
4784
|
}
|
|
4532
4785
|
if (!camBase) camBase = { width: dimensions.width, height: dimensions.height };
|
|
4533
|
-
|
|
4786
|
+
// scale applies here too: it was dropped, so scale 0.5 rendered full
|
|
4787
|
+
// size and nothing said so (gate C7).
|
|
4788
|
+
const __cs = ${scale !== undefined ? scale : 1};
|
|
4789
|
+
const __ev = (n) => Math.max(2, Math.round((n * __cs) / 2) * 2);
|
|
4790
|
+
cameraDims = { width: __ev(camBase.width), height: __ev(camBase.height) };
|
|
4534
4791
|
}
|
|
4535
4792
|
|
|
4536
4793
|
// AN EXPORTER ANSWERS A BLOB OR A RECORD AROUND ONE.
|
|
@@ -4693,6 +4950,12 @@ ${stillTime !== undefined ? `
|
|
|
4693
4950
|
? { width: png.width, height: png.height }
|
|
4694
4951
|
: { width: dimensions.width, height: dimensions.height },
|
|
4695
4952
|
};
|
|
4953
|
+
// An unbounded canvas exports its ARTWORK (bounds plus a margin), not
|
|
4954
|
+
// the board getCanvasSize reports (gate D57). Say so rather than
|
|
4955
|
+
// hand back a size nobody asked for unexplained.
|
|
4956
|
+
if (app.canvasSize && app.canvasSize.unbounded) {
|
|
4957
|
+
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.';
|
|
4958
|
+
}
|
|
4696
4959
|
} else {
|
|
4697
4960
|
const canvas = document.querySelector('canvas');
|
|
4698
4961
|
if (canvas) {
|
|
@@ -5369,6 +5632,49 @@ ${(() => {
|
|
|
5369
5632
|
})()}
|
|
5370
5633
|
${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) { /* the still is already rendered */ }
|
|
5371
5634
|
if (result && result.success) result.time = ${stillTime};` : ''}
|
|
5635
|
+
// SCREEN-SPACE ITEMS (FxTool D20): SVG and Lottie write geometry, not the
|
|
5636
|
+
// camera's frame map, so a HUD item is placed as if the camera had not moved.
|
|
5637
|
+
if (result && result.success && typeof app.screenSpaceItems === 'function' && ['svg', 'lottie', 'dotlottie'].indexOf(${JSON.stringify(format)}) !== -1) {
|
|
5638
|
+
try {
|
|
5639
|
+
const __hud = app.screenSpaceItems() || [];
|
|
5640
|
+
if (__hud.length) {
|
|
5641
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5642
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'screen_space_ignored', message: __hud.length + ' screen-space (HUD) item(s) are written at their frame coordinates in world space: ${format} has no camera frame, so they do not stay fixed under the camera move. Export mp4, webm, gif or png to keep them fixed.' }]);
|
|
5643
|
+
}
|
|
5644
|
+
} catch (_) { /* the export stands */ }
|
|
5645
|
+
}
|
|
5646
|
+
// WHICH STUDIO MADE THIS FILE (D92): a service worker can serve a stale
|
|
5647
|
+
// build right after a deploy, and a retest then judges old code. The
|
|
5648
|
+
// engine's own identity, version + build sha, rides on every export reply.
|
|
5649
|
+
try { if (result && typeof result === 'object' && app.buildId) result.studioBuild = app.buildId; } catch (_) { /* older studio */ }
|
|
5650
|
+
// A STALE STUDIO (D92): the file was made by an old build. Said in the
|
|
5651
|
+
// fidelity warnings the reply shows, not only as a field.
|
|
5652
|
+
try {
|
|
5653
|
+
const __bs = app.buildStatus ? app.buildStatus() : null;
|
|
5654
|
+
if (result && typeof result === 'object' && __bs) {
|
|
5655
|
+
result.liveBuild = __bs.liveBuildId;
|
|
5656
|
+
if (__bs.stale) {
|
|
5657
|
+
result.stale = true;
|
|
5658
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5659
|
+
result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'stale_build', message: 'this studio is running an old build (' + __bs.buildId + '; the live build is ' + __bs.liveBuildId + '): this file was made by the old code. Start a new job (pinepaper_agent_start_job, which reloads it) and export again.' }]);
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
} catch (_) { /* older studio */ }
|
|
5663
|
+
// RELIGHT (FxTool D5): applied to png / pdf / mp4 / webm, not gif / apng
|
|
5664
|
+
// or the vector formats; and it is what makes a video export slow.
|
|
5665
|
+
if (result && result.success && typeof app.getRelight === 'function') {
|
|
5666
|
+
try {
|
|
5667
|
+
const __rl = app.getRelight();
|
|
5668
|
+
if (__rl && __rl.enabled) {
|
|
5669
|
+
result.fidelity = result.fidelity || { warnings: [] };
|
|
5670
|
+
if (['png', 'jpg', 'webp', 'pdf', 'mp4', 'webm'].indexOf(${JSON.stringify(format)}) === -1) {
|
|
5671
|
+
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.' }]);
|
|
5672
|
+
} else {
|
|
5673
|
+
result.relight = { applied: true, lights: (__rl.lights || []).length };
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
5676
|
+
} catch (_) { /* the export stands */ }
|
|
5677
|
+
}
|
|
5372
5678
|
|
|
5373
5679
|
return result;
|
|
5374
5680
|
})();
|
|
@@ -5436,12 +5742,13 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5436
5742
|
if (entry.type === 'text' || item.className === 'PointText') analysis.hasText = true;
|
|
5437
5743
|
if (item.className === 'Raster') analysis.hasImages = true;
|
|
5438
5744
|
|
|
5439
|
-
|
|
5745
|
+
// 'none' is what a still item stores (gate C5, 1.6.19): it is not an animation.
|
|
5746
|
+
if (data.animationType && data.animationType !== 'none') {
|
|
5440
5747
|
analysis.hasAnimations = true;
|
|
5441
5748
|
animationSet.add(data.animationType);
|
|
5442
5749
|
}
|
|
5443
5750
|
// Detect keyframe animations (added via app.addAnimation with keyframes array)
|
|
5444
|
-
if (data.keyframes || data.animation || data.keyframeAnimation) {
|
|
5751
|
+
if ((Array.isArray(data.keyframes) && data.keyframes.length > 0) || data.animation || data.keyframeAnimation) {
|
|
5445
5752
|
analysis.hasAnimations = true;
|
|
5446
5753
|
animationSet.add('keyframe');
|
|
5447
5754
|
}
|
|
@@ -5693,7 +6000,9 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5693
6000
|
return {
|
|
5694
6001
|
success: true,
|
|
5695
6002
|
mapId: result.mapId || '${mapId}',
|
|
5696
|
-
|
|
6003
|
+
// regionPaths is a Map: .length was undefined, so every load reported
|
|
6004
|
+
// regions: 0 (gate D3). size, with the feature count as a fallback.
|
|
6005
|
+
regions: (result.regions && (result.regions.size ?? result.regions.length)) || (result.geoData && result.geoData.features ? result.geoData.features.length : 0),
|
|
5697
6006
|
bounds: result.bounds,
|
|
5698
6007
|
center: result.center,${warn}
|
|
5699
6008
|
};
|
|
@@ -5702,6 +6011,55 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5702
6011
|
}
|
|
5703
6012
|
})();
|
|
5704
6013
|
`.trim();
|
|
6014
|
+
}
|
|
6015
|
+
/**
|
|
6016
|
+
* Emitted helpers for reading a map's region colours back.
|
|
6017
|
+
*
|
|
6018
|
+
* highlightRegions and applyDataColors both write ONE channel — a colour
|
|
6019
|
+
* override keyed on the feature's _mapId — and log a console.warn for an id
|
|
6020
|
+
* they cannot resolve, which production strips. getHighlightedRegions reads
|
|
6021
|
+
* path.data.highlighted over regionPaths, which a rasterised map leaves
|
|
6022
|
+
* empty, so it saw nothing either tool did (gate D51). The override, read
|
|
6023
|
+
* through the public getRegionOverride, is the record the engine leaves.
|
|
6024
|
+
* Ids resolve the way the engine resolves them: exact, then lower-case.
|
|
6025
|
+
*/
|
|
6026
|
+
mapRegionReadbackJs() {
|
|
6027
|
+
return `
|
|
6028
|
+
function __resolve(id) {
|
|
6029
|
+
const m = app.mapSystem.regionNameMappings;
|
|
6030
|
+
if (!m || typeof m.get !== 'function') return null;
|
|
6031
|
+
return m.get(id) || m.get(String(id).toLowerCase()) || null;
|
|
6032
|
+
}
|
|
6033
|
+
function __features() {
|
|
6034
|
+
const seen = new Map();
|
|
6035
|
+
app.mapSystem.regionNameMappings.forEach(function (f) { if (f && !seen.has(f._mapId)) seen.set(f._mapId, f); });
|
|
6036
|
+
return Array.from(seen.values());
|
|
6037
|
+
}
|
|
6038
|
+
function __row(f, asked) {
|
|
6039
|
+
const o = app.mapSystem.getRegionOverride(f._mapId);
|
|
6040
|
+
const r = { regionId: f._mapId, name: f._mapName, fill: o ? o.fill : null };
|
|
6041
|
+
if (asked !== undefined) r.asked = asked;
|
|
6042
|
+
return r;
|
|
6043
|
+
}
|
|
6044
|
+
function __overridden() {
|
|
6045
|
+
return __features().filter(function (f) { return !!app.mapSystem.getRegionOverride(f._mapId); })
|
|
6046
|
+
.map(function (f) { return __row(f); });
|
|
6047
|
+
}
|
|
6048
|
+
// No map loaded: every id is "not on this map", which blames the ids.
|
|
6049
|
+
const __noMap = { success: false, error: 'no map is loaded. Load one with pinepaper_map {action: "load"} first.' };
|
|
6050
|
+
function __sampleIds() {
|
|
6051
|
+
return __features().slice(0, 8).map(function (f) { return f._mapId + ' (' + f._mapName + ')'; }).join(', ');
|
|
6052
|
+
}
|
|
6053
|
+
function __check(asked) {
|
|
6054
|
+
const done = [], notFound = [], notApplied = [];
|
|
6055
|
+
asked.forEach(function (id) {
|
|
6056
|
+
const f = __resolve(id);
|
|
6057
|
+
if (!f) { notFound.push(id); return; }
|
|
6058
|
+
const r = __row(f, id);
|
|
6059
|
+
(r.fill ? done : notApplied).push(r);
|
|
6060
|
+
});
|
|
6061
|
+
return { done: done, notFound: notFound, notApplied: notApplied };
|
|
6062
|
+
}`;
|
|
5705
6063
|
}
|
|
5706
6064
|
/**
|
|
5707
6065
|
* Generate code to highlight map regions
|
|
@@ -5720,13 +6078,19 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5720
6078
|
const fill = ho.color ?? ho.fillColor ?? ho.fill;
|
|
5721
6079
|
if (fill !== undefined)
|
|
5722
6080
|
style.fill = fill;
|
|
6081
|
+
// OPACITY IS A TINT OVER THE LAND (gate D59). The override REPLACES a
|
|
6082
|
+
// region's land fill rather than drawing over it, and an rgba fill lost
|
|
6083
|
+
// its alpha on the way to the exported pixels, so the readback reported a
|
|
6084
|
+
// blend the PNG did not draw. The colour is blended over each region's own
|
|
6085
|
+
// land fill in the browser and sent opaque: export and readback agree.
|
|
6086
|
+
const opacity = typeof ho.opacity === 'number' ? ho.opacity : null;
|
|
5723
6087
|
const stroke = ho.strokeColor ?? ho.stroke;
|
|
5724
6088
|
if (stroke !== undefined)
|
|
5725
6089
|
style.stroke = stroke;
|
|
5726
6090
|
if (ho.strokeWidth !== undefined)
|
|
5727
6091
|
style.strokeWidth = ho.strokeWidth;
|
|
5728
6092
|
for (const [k, v] of Object.entries(ho)) {
|
|
5729
|
-
if (!['color', 'fillColor', 'fill', 'strokeColor', 'stroke', 'strokeWidth'].includes(k) && v !== undefined) {
|
|
6093
|
+
if (!['color', 'fillColor', 'fill', 'strokeColor', 'stroke', 'strokeWidth', 'opacity'].includes(k) && v !== undefined) {
|
|
5730
6094
|
style[k] = v;
|
|
5731
6095
|
}
|
|
5732
6096
|
}
|
|
@@ -5738,9 +6102,68 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5738
6102
|
return { success: false, error: 'Map system not available' };
|
|
5739
6103
|
}
|
|
5740
6104
|
|
|
5741
|
-
try {
|
|
5742
|
-
|
|
5743
|
-
|
|
6105
|
+
try {${this.mapRegionReadbackJs()}
|
|
6106
|
+
if (!__features().length) return __noMap;
|
|
6107
|
+
const __style = ${optionsStr};
|
|
6108
|
+
const __op = ${JSON.stringify(opacity)};
|
|
6109
|
+
const __expected = {};
|
|
6110
|
+
const __unblended = [];
|
|
6111
|
+
if (__op !== null && typeof __style.fill === 'string') {
|
|
6112
|
+
const __rgb = function (c) {
|
|
6113
|
+
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(c || ''));
|
|
6114
|
+
if (!m) return null;
|
|
6115
|
+
const x = m[1].length === 3 ? m[1].replace(/./g, function (ch) { return ch + ch; }) : m[1];
|
|
6116
|
+
return [0, 2, 4].map(function (i) { return parseInt(x.slice(i, i + 2), 16); });
|
|
6117
|
+
};
|
|
6118
|
+
const __src = __rgb(__style.fill);
|
|
6119
|
+
const __p = app.mapSystem._mapRenderParams;
|
|
6120
|
+
const __ms = (__p && __p.mergedStyles) || {};
|
|
6121
|
+
const __user = (app.mapSystem.currentMap && app.mapSystem.currentMap.geoData && app.mapSystem.currentMap.geoData.features) || [];
|
|
6122
|
+
${regionIds}.forEach(function (id) {
|
|
6123
|
+
const f = __resolve(id);
|
|
6124
|
+
if (!f) return;
|
|
6125
|
+
// The land the engine paints under this region: a user feature takes
|
|
6126
|
+
// the map's fill, a basemap country its basemapFill.
|
|
6127
|
+
const __land = __rgb(__user.indexOf(f) !== -1 ? __ms.fill
|
|
6128
|
+
: (__ms.basemapFill || (app.mapSystem.defaultStyles || {}).basemapFill));
|
|
6129
|
+
let __fill = __style.fill;
|
|
6130
|
+
if (__src && __land) {
|
|
6131
|
+
__fill = '#' + [0, 1, 2].map(function (i) {
|
|
6132
|
+
return ('0' + Math.round(__src[i] * __op + __land[i] * (1 - __op)).toString(16)).slice(-2);
|
|
6133
|
+
}).join('');
|
|
6134
|
+
} else {
|
|
6135
|
+
__unblended.push(id);
|
|
6136
|
+
}
|
|
6137
|
+
__expected[id] = __fill;
|
|
6138
|
+
app.mapSystem.highlightRegions([id], Object.assign({}, __style, { fill: __fill }));
|
|
6139
|
+
});
|
|
6140
|
+
} else {
|
|
6141
|
+
app.mapSystem.highlightRegions(${regionIds}, __style);
|
|
6142
|
+
}
|
|
6143
|
+
// READ BACK (gate D16, D51): every id is resolved and its colour read.
|
|
6144
|
+
const __r = __check(${regionIds});
|
|
6145
|
+
if (!__r.done.length) {
|
|
6146
|
+
return { success: false, notFound: __r.notFound, notApplied: __r.notApplied,
|
|
6147
|
+
error: 'no region was highlighted.'
|
|
6148
|
+
+ (__r.notFound.length ? ' Not on this map: ' + __r.notFound.join(', ') + '. Ids on this map include ' + __sampleIds() + '; names work too.' : '') };
|
|
6149
|
+
}
|
|
6150
|
+
// The colour asked for must be the colour drawn (gate D59: the readback
|
|
6151
|
+
// reported #3b82f6 for a #e11d48 request and still said success).
|
|
6152
|
+
const __want = ${JSON.stringify(style.fill ?? null)};
|
|
6153
|
+
if (__want) {
|
|
6154
|
+
const __exp = function (d) { return __expected[d.asked] || __want; };
|
|
6155
|
+
const __off = __r.done.filter(function (d) { return String(d.fill).toLowerCase() !== String(__exp(d)).toLowerCase(); });
|
|
6156
|
+
if (__off.length === __r.done.length) {
|
|
6157
|
+
return { success: false, highlighted: __r.done, error: 'the regions were highlighted in ' + __off[0].fill + ', not the ' + __exp(__off[0]) + ' asked for.' };
|
|
6158
|
+
}
|
|
6159
|
+
if (__off.length) __r.wrongFill = __off;
|
|
6160
|
+
}
|
|
6161
|
+
const __res = { success: true, highlighted: __r.done };
|
|
6162
|
+
if (__unblended.length) { __res.unblended = __unblended; __res.warning = 'opacity needs a hex colour and a hex land fill; drawn at full strength: ' + __unblended.join(', ') + '.'; }
|
|
6163
|
+
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(', ') + '.'; }
|
|
6164
|
+
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() + '.'; }
|
|
6165
|
+
if (__r.notApplied.length) __res.notApplied = __r.notApplied;
|
|
6166
|
+
return __res;
|
|
5744
6167
|
} catch (error) {
|
|
5745
6168
|
return { success: false, error: error.message };
|
|
5746
6169
|
}
|
|
@@ -5821,9 +6244,20 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5821
6244
|
return { success: false, error: 'Map system not available' };
|
|
5822
6245
|
}
|
|
5823
6246
|
|
|
5824
|
-
try {
|
|
5825
|
-
|
|
5826
|
-
|
|
6247
|
+
try {${this.mapRegionReadbackJs()}
|
|
6248
|
+
if (!__features().length) return __noMap;
|
|
6249
|
+
const __legend = app.mapSystem.applyDataColors(${dataStr}, ${optionsStr});
|
|
6250
|
+
// READ BACK (gate D51): the engine skips a key it cannot resolve with a
|
|
6251
|
+
// console warning, which production strips, so a misspelt key looked coloured.
|
|
6252
|
+
const __r = __check(${JSON.stringify(Object.keys(validated.data))});
|
|
6253
|
+
if (!__r.done.length) {
|
|
6254
|
+
return { success: false, unmatched: __r.notFound,
|
|
6255
|
+
error: 'no region was coloured: none of the keys is on this map. Ids on this map include ' + __sampleIds() + '; names work too.' };
|
|
6256
|
+
}
|
|
6257
|
+
const __res = { success: true, regionsColored: __r.done.length, colored: __r.done };
|
|
6258
|
+
if (__legend && __legend.id !== undefined) __res.legendId = __legend.id;
|
|
6259
|
+
if (__r.notFound.length) { __res.unmatched = __r.notFound; __res.warning = 'not on this map, so not coloured: ' + __r.notFound.join(', ') + '.'; }
|
|
6260
|
+
return __res;
|
|
5827
6261
|
} catch (error) {
|
|
5828
6262
|
return { success: false, error: error.message };
|
|
5829
6263
|
}
|
|
@@ -5857,8 +6291,19 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5857
6291
|
}
|
|
5858
6292
|
|
|
5859
6293
|
try {
|
|
5860
|
-
|
|
5861
|
-
|
|
6294
|
+
// addMarker(longitude, latitude, options). One object went in as the
|
|
6295
|
+
// longitude, projected to NaN, and the engine returned null while this
|
|
6296
|
+
// reported success with the made-up id 'marker_added' (gate D51).
|
|
6297
|
+
const __o = ${JSON.stringify(options)};
|
|
6298
|
+
const marker = app.mapSystem.addMarker(__o.lon, __o.lat, __o);
|
|
6299
|
+
if (!marker) {
|
|
6300
|
+
return { success: false, error: 'no marker was added: [' + __o.lat + ', ' + __o.lon + '] (lat, lon) is outside the visible map, or no map is loaded.' };
|
|
6301
|
+
}
|
|
6302
|
+
// A REGISTRY id, so modify / animate / delete can take it. register()
|
|
6303
|
+
// leaves the marker inside the map group; registerItem would move it out,
|
|
6304
|
+
// and it would stop panning and clearing with the map.
|
|
6305
|
+
const markerId = app.itemRegistry.register(marker, 'mapMarker', { lat: __o.lat, lon: __o.lon, label: __o.label || null }, 'user');
|
|
6306
|
+
return { success: true, markerId: markerId, position: [marker.position.x, marker.position.y] };
|
|
5862
6307
|
} catch (error) {
|
|
5863
6308
|
return { success: false, error: error.message };
|
|
5864
6309
|
}
|
|
@@ -5908,26 +6353,13 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5908
6353
|
options.animate = validated.animate;
|
|
5909
6354
|
if (validated.duration !== undefined)
|
|
5910
6355
|
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();
|
|
6356
|
+
// REFUSED, HONESTLY (gate run 2, 1.6.19). Run 1 turned pan into a camera
|
|
6357
|
+
// pan, which moves the studio VIEW but not the map: a canvas-framed export
|
|
6358
|
+
// ignores the camera, so the call reported success and the export was
|
|
6359
|
+
// unchanged. The map itself cannot be moved without breaking geoToCanvas
|
|
6360
|
+
// (it projects against the untransformed map, so markers would land in the
|
|
6361
|
+
// wrong place). Until the engine has a map view transform, say so.
|
|
6362
|
+
return `(function() { return { success: false, error: ${JSON.stringify(MAP_VIEW_REFUSAL('pan'))} }; })();`;
|
|
5931
6363
|
}
|
|
5932
6364
|
/**
|
|
5933
6365
|
* Generate code to zoom the map
|
|
@@ -5939,24 +6371,8 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
5939
6371
|
options.animate = validated.animate;
|
|
5940
6372
|
if (validated.duration !== undefined)
|
|
5941
6373
|
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();
|
|
6374
|
+
// Refused for the same reason as pan (see generatePanMap).
|
|
6375
|
+
return `(function() { return { success: false, error: ${JSON.stringify(MAP_VIEW_REFUSAL('zoom'))} }; })();`;
|
|
5960
6376
|
}
|
|
5961
6377
|
/**
|
|
5962
6378
|
* Generate code to export map configuration
|
|
@@ -6001,7 +6417,11 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6001
6417
|
|
|
6002
6418
|
try {
|
|
6003
6419
|
const result = await app.mapSystem.importCustomMap('${validated.url}', ${optionsStr});
|
|
6004
|
-
|
|
6420
|
+
// A summary, not the engine's return: that carries the Paper group and
|
|
6421
|
+
// the region Map, which serialised into a raw Paper.js dump (gate N1).
|
|
6422
|
+
const __r = result && result.regions;
|
|
6423
|
+
const __n = (__r && (__r.size ?? __r.length)) || (result && result.geoData && result.geoData.features ? result.geoData.features.length : 0);
|
|
6424
|
+
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
6425
|
} catch (error) {
|
|
6006
6426
|
return { success: false, error: error.message };
|
|
6007
6427
|
}
|
|
@@ -6018,7 +6438,11 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6018
6438
|
|
|
6019
6439
|
try {
|
|
6020
6440
|
const result = await app.mapSystem.importCustomMap(${JSON.stringify(validated.geoJson)}, ${optionsStr});
|
|
6021
|
-
|
|
6441
|
+
// A summary, not the engine's return: that carries the Paper group and
|
|
6442
|
+
// the region Map, which serialised into a raw Paper.js dump (gate N1).
|
|
6443
|
+
const __r = result && result.regions;
|
|
6444
|
+
const __n = (__r && (__r.size ?? __r.length)) || (result && result.geoData && result.geoData.features ? result.geoData.features.length : 0);
|
|
6445
|
+
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
6446
|
} catch (error) {
|
|
6023
6447
|
return { success: false, error: error.message };
|
|
6024
6448
|
}
|
|
@@ -6045,20 +6469,17 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6045
6469
|
}
|
|
6046
6470
|
|
|
6047
6471
|
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' };
|
|
6472
|
+
if (!app.mapSystem.currentMap) {
|
|
6473
|
+
return { success: false, error: 'no map is loaded. Load one with pinepaper_map {action: "load"} first.' };
|
|
6474
|
+
}
|
|
6475
|
+
// canvasToGeo takes ONE [x, y] argument; (x, y) handed it a bare number.
|
|
6476
|
+
const coord = typeof app.mapSystem.canvasToGeo === 'function' ? app.mapSystem.canvasToGeo([${validated.x}, ${validated.y}]) : null;
|
|
6477
|
+
// The hit test the studio's own hover uses (getRegionAtPoint never existed).
|
|
6478
|
+
const f = app.mapSystem._findFeatureAtCanvasPoint(${validated.x}, ${validated.y});
|
|
6479
|
+
if (!f) return { success: true, regionId: null, coordinate: coord, message: 'no region at this point (off the map, or ocean)' };
|
|
6480
|
+
const p = f.properties || {};
|
|
6481
|
+
return { success: true, regionId: f._mapId !== undefined ? f._mapId : (p.id || p.ISO_A3 || p.name || null),
|
|
6482
|
+
regionName: f._mapName || p.name || p.NAME || null, coordinate: coord, properties: p };
|
|
6062
6483
|
} catch (error) {
|
|
6063
6484
|
return { success: false, error: error.message };
|
|
6064
6485
|
}
|
|
@@ -6111,7 +6532,8 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6111
6532
|
const result = app.mapSystem.animateRegionsWave({
|
|
6112
6533
|
duration: ${validated.duration || 10},
|
|
6113
6534
|
loop: ${validated.loop !== false},
|
|
6114
|
-
colors: ${colorsStr}
|
|
6535
|
+
colors: ${colorsStr},${validated.waveSpeed !== undefined ? `
|
|
6536
|
+
waveSpeed: ${validated.waveSpeed},` : ''}
|
|
6115
6537
|
waveDirection: '${validated.waveDirection || 'horizontal'}'
|
|
6116
6538
|
});
|
|
6117
6539
|
return { success: true, animatedRegions: result?.animatedRegions || [], totalRegions: result?.totalRegions || 0 };
|
|
@@ -6277,9 +6699,17 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6277
6699
|
return { success: false, error: 'Map system not available' };
|
|
6278
6700
|
}
|
|
6279
6701
|
|
|
6280
|
-
try {
|
|
6281
|
-
|
|
6282
|
-
|
|
6702
|
+
try {${this.mapRegionReadbackJs()}
|
|
6703
|
+
// getHighlightedRegions returns an ARRAY of {regionId, name, path}; this
|
|
6704
|
+
// read result.highlighted off it and always answered [] (gate D51). It also
|
|
6705
|
+
// walks regionPaths, empty on a rasterised map, so the override channel is
|
|
6706
|
+
// read as well.
|
|
6707
|
+
const __byId = new Map();
|
|
6708
|
+
(app.mapSystem.getHighlightedRegions() || []).forEach(function (r) { __byId.set(r.regionId, { regionId: r.regionId, name: r.name }); });
|
|
6709
|
+
__overridden().forEach(function (r) { __byId.set(r.regionId, r); });
|
|
6710
|
+
const __list = Array.from(__byId.values());
|
|
6711
|
+
return { success: true, highlighted: __list, count: __list.length,
|
|
6712
|
+
note: 'highlight and apply_colors share one colour channel, so this lists every region either has coloured.' };
|
|
6283
6713
|
} catch (error) {
|
|
6284
6714
|
return { success: false, error: error.message };
|
|
6285
6715
|
}
|
|
@@ -6481,6 +6911,91 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6481
6911
|
/**
|
|
6482
6912
|
* Generate code to export the complete scene state
|
|
6483
6913
|
*/
|
|
6914
|
+
/**
|
|
6915
|
+
* pinepaper_place_on_surface (FxTool #56): app.placeOnSurface corner-pins
|
|
6916
|
+
* the UI into the photo's screen, mattes it so a hand stays in front, and
|
|
6917
|
+
* BAKES it into the photo's pixels. Async. Only plain fields come back.
|
|
6918
|
+
*/
|
|
6919
|
+
generatePlaceOnSurface(input) {
|
|
6920
|
+
// Named, not spread: a spread makes every field look consumed, which is
|
|
6921
|
+
// how an accepted-and-ignored option hides (the guard test reads these).
|
|
6922
|
+
const opts = {};
|
|
6923
|
+
if (input.quad !== undefined)
|
|
6924
|
+
opts.quad = input.quad;
|
|
6925
|
+
if (input.quadSpace !== undefined)
|
|
6926
|
+
opts.quadSpace = input.quadSpace;
|
|
6927
|
+
if (input.occlusion !== undefined)
|
|
6928
|
+
opts.occlusion = input.occlusion;
|
|
6929
|
+
if (input.key !== undefined)
|
|
6930
|
+
opts.key = input.key;
|
|
6931
|
+
if (input.erode !== undefined)
|
|
6932
|
+
opts.erode = input.erode;
|
|
6933
|
+
if (input.matteBlur !== undefined)
|
|
6934
|
+
opts.matteBlur = input.matteBlur;
|
|
6935
|
+
if (input.soften !== undefined)
|
|
6936
|
+
opts.soften = input.soften;
|
|
6937
|
+
if (input.glare !== undefined)
|
|
6938
|
+
opts.glare = input.glare;
|
|
6939
|
+
if (input.spill !== undefined)
|
|
6940
|
+
opts.spill = input.spill;
|
|
6941
|
+
if (input.spillRadius !== undefined)
|
|
6942
|
+
opts.spillRadius = input.spillRadius;
|
|
6943
|
+
if (input.tint !== undefined)
|
|
6944
|
+
opts.tint = input.tint;
|
|
6945
|
+
if (input.slot !== undefined)
|
|
6946
|
+
opts.slot = input.slot;
|
|
6947
|
+
if (input.hideSource !== undefined)
|
|
6948
|
+
opts.hideSource = input.hideSource;
|
|
6949
|
+
const { photoId, sourceId } = input;
|
|
6950
|
+
return `
|
|
6951
|
+
// Place a UI onto a screen in a photo
|
|
6952
|
+
(async function() {
|
|
6953
|
+
if (typeof app.placeOnSurface !== 'function') { return { success: false, error: 'app.placeOnSurface unavailable — update FxTool' }; }
|
|
6954
|
+
const r = await app.placeOnSurface(${JSON.stringify(photoId)}, ${JSON.stringify(sourceId)}, ${JSON.stringify(opts)});
|
|
6955
|
+
if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the UI could not be placed' }; }
|
|
6956
|
+
return { success: true, photoId: r.id, slot: r.slot, quad: r.quad, detected: r.detected || null,
|
|
6957
|
+
key: r.key, matteCoverage: r.matteCoverage, ...(r.lostSlots && r.lostSlots.length ? { lostSlots: r.lostSlots } : {}) };
|
|
6958
|
+
})();`.trim();
|
|
6959
|
+
}
|
|
6960
|
+
/**
|
|
6961
|
+
* A RESTORABLE project document (D28): app.captureProjectDocument, the
|
|
6962
|
+
* engine's own durable format (core/ProjectDocument.js), which carries
|
|
6963
|
+
* relations, rigging, connectors, scene chains and animation config.
|
|
6964
|
+
* NOT exportProjectJSON: the engine documents that one as a bare Paper tree
|
|
6965
|
+
* that drops roughly half of a non-trivial scene.
|
|
6966
|
+
*/
|
|
6967
|
+
generateCaptureProject(name) {
|
|
6968
|
+
return `
|
|
6969
|
+
// Capture a restorable project document
|
|
6970
|
+
(function() {
|
|
6971
|
+
if (typeof app.captureProjectDocument !== 'function') { return { success: false, error: 'app.captureProjectDocument unavailable — update FxTool' }; }
|
|
6972
|
+
${SCENE_COUNTS_JS}
|
|
6973
|
+
const doc = app.captureProjectDocument(${JSON.stringify(name ? { name } : {})});
|
|
6974
|
+
// What the document CANNOT carry (D29): render hooks added as raw functions
|
|
6975
|
+
// and frame callbacks from generated code. Named, never dropped in silence.
|
|
6976
|
+
const __un = (doc && Array.isArray(doc.unserialized)) ? doc.unserialized
|
|
6977
|
+
: (app.listUnserializedCallbacks ? app.listUnserializedCallbacks() : []);
|
|
6978
|
+
return { success: true, json: JSON.stringify(doc), counts: __ppSceneCounts(), unserialized: __un };
|
|
6979
|
+
})();`.trim();
|
|
6980
|
+
}
|
|
6981
|
+
/** Load a staged project document (window.__ppStage.projectDoc), replacing the scene. */
|
|
6982
|
+
generateLoadProject(strict) {
|
|
6983
|
+
return `
|
|
6984
|
+
// Load a project document, replacing the current scene
|
|
6985
|
+
(function() {
|
|
6986
|
+
if (typeof app.loadProjectDocument !== 'function') { return { success: false, error: 'app.loadProjectDocument unavailable — update FxTool' }; }
|
|
6987
|
+
${SCENE_COUNTS_JS}
|
|
6988
|
+
const text = window.__ppStage && window.__ppStage.projectDoc;
|
|
6989
|
+
if (typeof text !== 'string' || !text) { return { success: false, error: 'no project document was staged' }; }
|
|
6990
|
+
// Second guard, in the page: nothing but a project document reaches the loader.
|
|
6991
|
+
let __doc = null;
|
|
6992
|
+
try { __doc = JSON.parse(text); } catch (e) { return { success: false, error: 'not a PinePaper scene file (not JSON); nothing was changed' }; }
|
|
6993
|
+
if (!__doc || __doc.kind !== 'pinepaper.project') { return { success: false, error: 'not a PinePaper scene file (no kind "pinepaper.project"); nothing was changed' }; }
|
|
6994
|
+
const r = app.loadProjectDocument(__doc, { strict: ${strict ? 'true' : 'false'} });
|
|
6995
|
+
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) || [] }; }
|
|
6996
|
+
return { success: true, counts: __ppSceneCounts(), warnings: r.warnings || [], meta: r.meta || null };
|
|
6997
|
+
})();`.trim();
|
|
6998
|
+
}
|
|
6484
6999
|
generateExportScene() {
|
|
6485
7000
|
return `
|
|
6486
7001
|
// Export complete scene state
|
|
@@ -6592,7 +7107,11 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6592
7107
|
backgroundColor: backgroundColor,
|
|
6593
7108
|
canvasSize: canvasSize,
|
|
6594
7109
|
itemCount: items.length,
|
|
6595
|
-
relationCount: relations.length
|
|
7110
|
+
relationCount: relations.length,
|
|
7111
|
+
// NOT A BACKUP (D28). This summary carries positions and bounds, not
|
|
7112
|
+
// keyframe tracks, rigging or scene chains: a scene saved this way and
|
|
7113
|
+
// rebuilt from it has no motion. Said in the result, not only in docs.
|
|
7114
|
+
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
7115
|
};
|
|
6597
7116
|
} catch (error) {
|
|
6598
7117
|
return { success: false, error: error.message };
|
|
@@ -6693,29 +7212,32 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6693
7212
|
try {
|
|
6694
7213
|
// Import the Paper.js item from JSON
|
|
6695
7214
|
const item = paper.project.importJSON(${itemJsonStr});
|
|
6696
|
-
|
|
6697
|
-
|
|
7215
|
+
// importJSON accepts anything: { bogus: true } came back as an object with
|
|
7216
|
+
// no bounds and was registered as an item (found by the 1.6.19 gate). Only
|
|
7217
|
+
// a real Paper item with geometry may be registered.
|
|
7218
|
+
if (!item || !(item instanceof paper.Item) || !item.bounds) {
|
|
7219
|
+
try { if (item && typeof item.remove === 'function') item.remove(); } catch (e) {}
|
|
7220
|
+
return { success: false, error: 'itemJson is not a Paper.js item (pass the output of item.exportJSON())' };
|
|
7221
|
+
}
|
|
7222
|
+
|
|
7223
|
+
// REGISTER THROUGH THE ENGINE (gate H1, 1.6.19). This used to call
|
|
7224
|
+
// app.itemRegistry.set(), which does not exist ("not a function", nothing
|
|
7225
|
+
// registered), with an id invented from .size (undefined, so 'item_NaN',
|
|
7226
|
+
// or a collision), and it moved every item into the TEXT group. The
|
|
7227
|
+
// registry's own register() mints the id, never steals a live one, treats
|
|
7228
|
+
// re-registration as an update, and returns the id it assigned.
|
|
7229
|
+
// importJSON already put the item in the active layer.
|
|
7230
|
+
if (!app.itemRegistry || typeof app.itemRegistry.register !== 'function') {
|
|
7231
|
+
try { item.remove(); } catch (e) {}
|
|
7232
|
+
return { success: false, error: 'app.itemRegistry.register unavailable — update FxTool' };
|
|
6698
7233
|
}
|
|
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
7234
|
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);
|
|
7235
|
+
const itemId = app.itemRegistry.register(item, ${JSON.stringify(input.itemType)}, customProps, 'import');
|
|
7236
|
+
if (!itemId) {
|
|
7237
|
+
try { item.remove(); } catch (e) {}
|
|
7238
|
+
return { success: false, error: 'the registry refused the item' };
|
|
6718
7239
|
}
|
|
7240
|
+
try { if (paper.view) paper.view.update(); } catch (e) {}
|
|
6719
7241
|
|
|
6720
7242
|
return {
|
|
6721
7243
|
success: true,
|
|
@@ -6997,7 +7519,9 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6997
7519
|
// Get available mask types
|
|
6998
7520
|
(function() {
|
|
6999
7521
|
return {
|
|
7000
|
-
|
|
7522
|
+
// From the enum the mask tools validate against: this listed 'custom',
|
|
7523
|
+
// which both refuse (1.6.19 gate).
|
|
7524
|
+
maskTypes: ${JSON.stringify(MaskTypeSchema.options)}
|
|
7001
7525
|
};
|
|
7002
7526
|
})();
|
|
7003
7527
|
`.trim();
|
|
@@ -7007,14 +7531,7 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7007
7531
|
// Get available mask animation presets
|
|
7008
7532
|
(function() {
|
|
7009
7533
|
return {
|
|
7010
|
-
animations:
|
|
7011
|
-
'wipeLeft', 'wipeRight', 'wipeUp', 'wipeDown',
|
|
7012
|
-
'iris', 'irisOut',
|
|
7013
|
-
'star', 'heart',
|
|
7014
|
-
'curtainHorizontal', 'curtainVertical', 'cinematic',
|
|
7015
|
-
'diagonalWipe',
|
|
7016
|
-
'revealUp', 'revealDown'
|
|
7017
|
-
]
|
|
7534
|
+
animations: ${JSON.stringify(MaskPresetSchema.options)}
|
|
7018
7535
|
};
|
|
7019
7536
|
})();
|
|
7020
7537
|
`.trim();
|
|
@@ -7028,10 +7545,12 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7028
7545
|
const categoryFilter = category ? `'${category}'` : 'null';
|
|
7029
7546
|
return `
|
|
7030
7547
|
// List available templates
|
|
7031
|
-
(function() {
|
|
7548
|
+
(async function() {
|
|
7032
7549
|
if (!app.templateManager) {
|
|
7033
7550
|
return { error: 'Template manager not available. Make sure PinePaper Studio is loaded.' };
|
|
7034
7551
|
}
|
|
7552
|
+
// The built-in catalogue loads lazily; a fresh page listed none (1.6.19 gate).
|
|
7553
|
+
try { if (app.templateManager._ensureTemplateData) await app.templateManager._ensureTemplateData(); } catch (e) {}
|
|
7035
7554
|
const allTemplates = app.templateManager.getAllTemplates();
|
|
7036
7555
|
const category = ${categoryFilter};
|
|
7037
7556
|
const filtered = category
|
|
@@ -7056,8 +7575,25 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
7056
7575
|
if (!app.templateManager) {
|
|
7057
7576
|
return { error: 'Template manager not available. Make sure PinePaper Studio is loaded.' };
|
|
7058
7577
|
}
|
|
7578
|
+
// AN UNKNOWN ID IS AN ERROR, NOT A SUCCESS (gate C1, 1.6.19). loadTemplate
|
|
7579
|
+
// on an id that does not exist changed nothing and this still reported
|
|
7580
|
+
// "Template applied successfully"; the description's own example id
|
|
7581
|
+
// ('solar-system') was one of them. The nearest real ids are offered.
|
|
7582
|
+
const __tid = ${JSON.stringify(templateId)};
|
|
7583
|
+
// The built-in catalogue loads LAZILY: on a fresh page getAllTemplates() is
|
|
7584
|
+
// [] until it does, which made every id look unknown. Load it first, as
|
|
7585
|
+
// loadTemplate itself does, and count the cloud templates it falls back to.
|
|
7586
|
+
try { if (app.templateManager._ensureTemplateData) await app.templateManager._ensureTemplateData(); } catch (e) {}
|
|
7587
|
+
const __cloud = (app.templateManager.cloudSync && app.templateManager.cloudSync.cloudTemplates) || [];
|
|
7588
|
+
const __all = app.templateManager.getAllTemplates().concat(__cloud.filter(Boolean));
|
|
7589
|
+
if (__all.length && !__all.some(function(t) { return t && t.id === __tid; })) {
|
|
7590
|
+
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]; };
|
|
7591
|
+
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) }; })
|
|
7592
|
+
.sort(function(a, b) { return a.d - b.d; }).slice(0, 5).map(function(x) { return x.id; });
|
|
7593
|
+
return { success: false, error: 'no template "' + __tid + '". Nearest: ' + __near.join(', ') + '. List them with listOnly: true.' };
|
|
7594
|
+
}
|
|
7059
7595
|
try {
|
|
7060
|
-
await app.templateManager.loadTemplate(
|
|
7596
|
+
await app.templateManager.loadTemplate(__tid, true);
|
|
7061
7597
|
|
|
7062
7598
|
// DID THE CLIPPED CHARACTER PARTS SURVIVE?
|
|
7063
7599
|
//
|
|
@@ -7356,13 +7892,22 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7356
7892
|
opts.push(`defaultTransition: ${JSON.stringify(input.defaultTransition)}`);
|
|
7357
7893
|
if (input.transitionDuration !== undefined)
|
|
7358
7894
|
opts.push(`transitionDuration: ${input.transitionDuration * 1000}`);
|
|
7895
|
+
if (input.cutSound !== undefined)
|
|
7896
|
+
opts.push(`cutSound: ${JSON.stringify(input.cutSound)}`);
|
|
7359
7897
|
const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : '';
|
|
7360
7898
|
return `
|
|
7361
7899
|
// Create scene chain for sequential playback
|
|
7362
7900
|
(function() {
|
|
7363
7901
|
${guard}
|
|
7364
|
-
app.sceneManager.createChain(${JSON.stringify(input.sceneIds)}${optsStr});
|
|
7365
|
-
|
|
7902
|
+
const __cr = app.sceneManager.createChain(${JSON.stringify(input.sceneIds)}${optsStr});
|
|
7903
|
+
// An unknown cut sound comes back {ok:false, reason} (FxTool D85); a
|
|
7904
|
+
// studio without cut sounds ignores the option, which the cuts readback shows.
|
|
7905
|
+
if (__cr && __cr.ok === false) { return { success: false, action: 'create_chain', error: __cr.reason || 'the chain was refused' }; }
|
|
7906
|
+
const __res = { success: true, action: 'create_chain', sceneIds: ${JSON.stringify(input.sceneIds)}${input.loop !== undefined ? `, loop: ${input.loop}` : ''}${input.defaultDuration !== undefined ? `, defaultDuration: ${input.defaultDuration}` : ''} };
|
|
7907
|
+
${input.cutSound !== undefined ? ` const __cuts = (__cr && __cr.cuts) || (app.sceneManager.getChainCuts ? app.sceneManager.getChainCuts() : null);
|
|
7908
|
+
if (__cuts) __res.cuts = __cuts;
|
|
7909
|
+
else __res.warning = 'this studio placed no cut sounds: it predates them (FxTool D85). The chain plays without them.';
|
|
7910
|
+
` : ''} return __res;
|
|
7366
7911
|
})();
|
|
7367
7912
|
`.trim();
|
|
7368
7913
|
}
|
|
@@ -7714,7 +8259,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7714
8259
|
for (const id of ids) {
|
|
7715
8260
|
const entry = app.itemRegistry.get(id);
|
|
7716
8261
|
if (entry && entry.item) {
|
|
7717
|
-
${mode === 'remove' ? 'entry.item.selected = false;' : 'app.select(entry.item);'}
|
|
8262
|
+
${mode === 'remove' ? 'entry.item.selected = false;' : 'app.select(entry.item, false);'}
|
|
7718
8263
|
selected.push(id);
|
|
7719
8264
|
}
|
|
7720
8265
|
}
|
|
@@ -7725,8 +8270,9 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7725
8270
|
return `
|
|
7726
8271
|
// Select all items
|
|
7727
8272
|
(function() {
|
|
8273
|
+
${REG_ID_JS}
|
|
7728
8274
|
app.selectAll();
|
|
7729
|
-
const items = app.getSelectedItems().map(i => i.
|
|
8275
|
+
const items = app.getSelectedItems().map(i => __ppRegId(i)).filter(Boolean);
|
|
7730
8276
|
return { success: true, action: 'select_all', count: items.length, items };
|
|
7731
8277
|
})();`.trim();
|
|
7732
8278
|
case 'deselect_all':
|
|
@@ -7740,8 +8286,9 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7740
8286
|
return `
|
|
7741
8287
|
// Get current selection
|
|
7742
8288
|
(function() {
|
|
8289
|
+
${REG_ID_JS}
|
|
7743
8290
|
const items = app.getSelectedItems().map(i => ({
|
|
7744
|
-
itemId: i
|
|
8291
|
+
itemId: __ppRegId(i),
|
|
7745
8292
|
type: i.data?.itemType || i.className,
|
|
7746
8293
|
bounds: i.bounds ? { x: i.bounds.x, y: i.bounds.y, width: i.bounds.width, height: i.bounds.height } : null
|
|
7747
8294
|
}));
|
|
@@ -7751,8 +8298,9 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7751
8298
|
return `
|
|
7752
8299
|
// Delete selected items
|
|
7753
8300
|
(function() {
|
|
8301
|
+
${REG_ID_JS}
|
|
7754
8302
|
const selection = app.getSelectedItems();
|
|
7755
|
-
const deleted = selection.map(i => i.
|
|
8303
|
+
const deleted = selection.map(i => __ppRegId(i)).filter(Boolean);
|
|
7756
8304
|
app.deleteSelected();
|
|
7757
8305
|
return { success: true, action: 'delete_selected', deleted, count: deleted.length };
|
|
7758
8306
|
})();`.trim();
|
|
@@ -7787,6 +8335,32 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7787
8335
|
})();`.trim();
|
|
7788
8336
|
}
|
|
7789
8337
|
generateBrandKit(input) {
|
|
8338
|
+
// THE ACTIVE BRAND (D69). apply recolours what is on the canvas once; a
|
|
8339
|
+
// later beat's new items knew nothing of it. 'set' applies AND makes the
|
|
8340
|
+
// kit the studio's active brand, which every create reads (withBrandDefaults).
|
|
8341
|
+
// It lives in the PAGE, not this process: one cloud server serves many
|
|
8342
|
+
// studios, and a brand must not cross between them.
|
|
8343
|
+
if (input.action === 'get' || input.action === 'clear') {
|
|
8344
|
+
return `
|
|
8345
|
+
// Brand kit: ${input.action} the active brand
|
|
8346
|
+
(function() {
|
|
8347
|
+
const b = (typeof window !== 'undefined' && window.__ppActiveBrand) || null;
|
|
8348
|
+
${input.action === 'clear' ? 'if (typeof window !== "undefined") window.__ppActiveBrand = null;' : ''}
|
|
8349
|
+
return { success: true, action: ${JSON.stringify(input.action)}, ${input.action === 'clear' ? 'cleared: !!b' : 'brand: b'} };
|
|
8350
|
+
})();`.trim();
|
|
8351
|
+
}
|
|
8352
|
+
if (input.action === 'set') {
|
|
8353
|
+
const applied = this.generateBrandKit({ ...input, action: 'apply' });
|
|
8354
|
+
return `
|
|
8355
|
+
// Brand kit: apply, then make it the active brand every new item inherits
|
|
8356
|
+
(async function() {
|
|
8357
|
+
const out = await ${applied.replace(/^\/\/[^\n]*\n/, '').replace(/;\s*$/, '')};
|
|
8358
|
+
if (out && (out.success === false || out.ok === false)) return out;
|
|
8359
|
+
if (typeof window !== 'undefined') window.__ppActiveBrand = ${JSON.stringify(input.kit)};
|
|
8360
|
+
return Object.assign({}, out, { success: true, action: 'set', active: true,
|
|
8361
|
+
note: 'new shapes take colors.primary and new text colors.text with the heading (32 px and up) or body font, wherever the call names none; noBrand: true opts an item out. The active brand lives with this studio session: set it again after a reload.' });
|
|
8362
|
+
})();`.trim();
|
|
8363
|
+
}
|
|
7790
8364
|
const opts = JSON.stringify({ ...(input.selectionOnly ? { selectionOnly: true } : {}) });
|
|
7791
8365
|
const method = input.action === 'plan' ? 'planBrandKit' : 'applyBrandKit';
|
|
7792
8366
|
const facade = this._facadeCall(method, `${JSON.stringify(input.kit)}, ${opts}`, `Brand kit: ${input.action}`);
|
|
@@ -7931,7 +8505,7 @@ ${loads}
|
|
|
7931
8505
|
case 'lineage': return this._facadeCall('getLineage', id, 'Provenance: lineage');
|
|
7932
8506
|
case 'dependents': return this._facadeCall('getDependents', id, 'Provenance: dependents');
|
|
7933
8507
|
case 'record':
|
|
7934
|
-
return this._facadeCall('recordLineage', `${id}, ${JSON.stringify(input.kind || '
|
|
8508
|
+
return this._facadeCall('recordLineage', `${id}, ${JSON.stringify(input.kind || 'derivedFrom')}, ${JSON.stringify(input.sourceRef || '')}, ${JSON.stringify(input.meta || {})}`, 'Provenance: record lineage');
|
|
7935
8509
|
}
|
|
7936
8510
|
}
|
|
7937
8511
|
generateCompose(input) {
|
|
@@ -7953,27 +8527,46 @@ ${loads}
|
|
|
7953
8527
|
// vector and audio halves of composeCollage were unreachable through
|
|
7954
8528
|
// MCP, so agents converged on the four static patterns the description
|
|
7955
8529
|
// 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
|
-
|
|
8530
|
+
{
|
|
8531
|
+
const apply = this._facadeCall('composeCollage', `${JSON.stringify(input.pattern || '')}, ${JSON.stringify(input.itemIds || [])}, ${JSON.stringify({
|
|
8532
|
+
...(input.treatment ? { treatment: input.treatment } : {}),
|
|
8533
|
+
...(input.applyCamera === false ? { applyCamera: false } : {}),
|
|
8534
|
+
...(input.loop ? { loop: true } : {}),
|
|
8535
|
+
...(input.craft ? { craft: input.craft } : {}),
|
|
8536
|
+
...(input.reveal !== undefined ? { reveal: input.reveal } : {}),
|
|
8537
|
+
...(input.revealOptions ? { revealOptions: input.revealOptions } : {}),
|
|
8538
|
+
...(input.style ? { style: input.style } : {}),
|
|
8539
|
+
...(input.text ? { text: input.text } : {}),
|
|
8540
|
+
...(input.assets ? { assets: input.assets } : {}),
|
|
8541
|
+
...(input.audio ? { audio: input.audio } : {}),
|
|
8542
|
+
...(input.grid ? { grid: true } : {}),
|
|
8543
|
+
// The design axes. A register resolves the craft (gutter, margin,
|
|
8544
|
+
// hue budget, type scale); a medium says what makes the marks and
|
|
8545
|
+
// is REFUSED when a composition cannot be rendered in it.
|
|
8546
|
+
...(input.register ? { register: input.register } : {}),
|
|
8547
|
+
...(input.level !== undefined ? { level: input.level } : {}),
|
|
8548
|
+
...(input.medium ? { medium: input.medium } : {}),
|
|
8549
|
+
...(input.stitch ? { stitch: input.stitch } : {}),
|
|
8550
|
+
...(input.stitchBudget !== undefined ? { stitchBudget: input.stitchBudget } : {}),
|
|
8551
|
+
})}`, 'Compose: apply pattern');
|
|
8552
|
+
;
|
|
8553
|
+
if (typeof input.reveal !== 'string' || !input.reveal)
|
|
8554
|
+
return apply;
|
|
8555
|
+
// CHECK THE REVEAL FIRST (gate C3). composeCollage lays the items out
|
|
8556
|
+
// and wires their relations BEFORE it reaches an unknown reveal, so a
|
|
8557
|
+
// typo errored with the grid already applied and nothing reverted.
|
|
8558
|
+
return `
|
|
8559
|
+
// Compose: check the reveal, then apply
|
|
8560
|
+
(async function() {
|
|
8561
|
+
if (typeof app.listReveals === 'function') {
|
|
8562
|
+
const __keys = (app.listReveals() || []).map(function (r) { return r && r.key; });
|
|
8563
|
+
if (__keys.length && __keys.indexOf(${JSON.stringify(input.reveal)}) === -1) {
|
|
8564
|
+
return { success: false, error: 'unknown reveal ' + ${JSON.stringify(JSON.stringify(input.reveal))} + ' — known: ' + __keys.join(', ') + '. Nothing was changed.' };
|
|
8565
|
+
}
|
|
8566
|
+
}
|
|
8567
|
+
return await ${apply.replace(/^\/\/[^\n]*\n/, '').replace(/;\s*$/, '')};
|
|
8568
|
+
})();`.trim();
|
|
8569
|
+
}
|
|
7977
8570
|
}
|
|
7978
8571
|
}
|
|
7979
8572
|
generateAudioBeats(input) {
|
|
@@ -8292,8 +8885,8 @@ ${resolveSource}
|
|
|
8292
8885
|
const resolve = ` const item = (typeof app._resolveRaster === 'function')
|
|
8293
8886
|
? app._resolveRaster(${JSON.stringify(input.itemId)})
|
|
8294
8887
|
: (app.itemRegistry ? app.itemRegistry.getItem(${JSON.stringify(input.itemId)}) : null);
|
|
8295
|
-
if (!item) return { error: 'Item not found or not a raster: ' + ${JSON.stringify(input.itemId)} };
|
|
8296
|
-
if (typeof app.applyImageFilter !== 'function') return { error: 'GPU image filters unavailable — update FxTool' };`;
|
|
8888
|
+
if (!item) return { success: false, error: 'Item not found or not a raster: ' + ${JSON.stringify(input.itemId)} };
|
|
8889
|
+
if (typeof app.applyImageFilter !== 'function') return { success: false, error: 'GPU image filters unavailable — update FxTool' };`;
|
|
8297
8890
|
switch (input.action) {
|
|
8298
8891
|
case 'apply':
|
|
8299
8892
|
return `
|
|
@@ -8310,7 +8903,7 @@ ${resolve}
|
|
|
8310
8903
|
}
|
|
8311
8904
|
return { success: true, action: 'apply', itemId: ${JSON.stringify(input.itemId)}, filter: ${JSON.stringify(input.filterName || '')} };
|
|
8312
8905
|
} catch (e) {
|
|
8313
|
-
return { error: 'Failed to apply filter: ' + e.message };
|
|
8906
|
+
return { success: false, action: 'apply', error: 'Failed to apply filter: ' + e.message };
|
|
8314
8907
|
}
|
|
8315
8908
|
})();`.trim();
|
|
8316
8909
|
case 'chain':
|
|
@@ -8323,7 +8916,7 @@ ${resolve}
|
|
|
8323
8916
|
await app.applyImageFilterChain(item, filters);
|
|
8324
8917
|
return { success: true, action: 'chain', itemId: ${JSON.stringify(input.itemId)}, filterCount: filters.length };
|
|
8325
8918
|
} catch (e) {
|
|
8326
|
-
return { error: 'Failed to apply filter chain: ' + e.message };
|
|
8919
|
+
return { success: false, action: 'chain', error: 'Failed to apply filter chain: ' + e.message };
|
|
8327
8920
|
}
|
|
8328
8921
|
})();`.trim();
|
|
8329
8922
|
default:
|
|
@@ -8624,16 +9217,28 @@ ${resolve}
|
|
|
8624
9217
|
return `
|
|
8625
9218
|
// Clear background
|
|
8626
9219
|
(function() {
|
|
9220
|
+
// clearBackground() removes generated patterns, drawings and the image, but
|
|
9221
|
+
// not the colour; the tool promises "remove entirely" and left #1e293b
|
|
9222
|
+
// behind (gate C6, 1.6.19). The colour goes too, to transparent.
|
|
8627
9223
|
app.clearBackground();
|
|
8628
|
-
|
|
9224
|
+
if (typeof app.setBackgroundColor === 'function') app.setBackgroundColor('transparent');
|
|
9225
|
+
return { success: true, action: 'clear', removed: ['pattern', 'drawing', 'image', 'color'] };
|
|
8629
9226
|
})();`.trim();
|
|
8630
9227
|
case 'get':
|
|
8631
9228
|
return `
|
|
8632
9229
|
// Get background info
|
|
8633
9230
|
(function() {
|
|
8634
|
-
//
|
|
9231
|
+
// getBackgroundMode() returns only the mode, so 'color' came back with no
|
|
9232
|
+
// colour (gate C6, 1.6.19). The colour is read from the canvas, as hex.
|
|
8635
9233
|
const mode = app.getBackgroundMode ? app.getBackgroundMode() : null;
|
|
8636
|
-
|
|
9234
|
+
const css = (app.canvasEl && app.canvasEl.style && app.canvasEl.style.backgroundColor) || '';
|
|
9235
|
+
// No regex: this is a template literal, and \\( or \\s here reach the page
|
|
9236
|
+
// without their backslash (that was the #NaN293b).
|
|
9237
|
+
const open = css.indexOf('('), close = css.indexOf(')');
|
|
9238
|
+
let color = css || null;
|
|
9239
|
+
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(''); }
|
|
9240
|
+
const patterns = app.patternGroup && app.patternGroup.children ? app.patternGroup.children.length : 0;
|
|
9241
|
+
return { success: true, action: 'get', mode, color, hasPattern: patterns > 0, hasImage: !!(app.config && app.config.backgroundImage) };
|
|
8637
9242
|
})();`.trim();
|
|
8638
9243
|
default:
|
|
8639
9244
|
return `(function() { return { error: 'Unknown background action: ${input.action}' }; })();`;
|
|
@@ -8669,13 +9274,14 @@ ${resolve}
|
|
|
8669
9274
|
return `
|
|
8670
9275
|
// Hit test at point
|
|
8671
9276
|
(function() {
|
|
9277
|
+
${REG_ID_JS}
|
|
8672
9278
|
const point = new paper.Point(${input.x ?? 0}, ${input.y ?? 0});
|
|
8673
9279
|
${all
|
|
8674
9280
|
? `const results = app.hitTestAll(point, { tolerance: ${tolerance} });
|
|
8675
9281
|
const hits = results.map(r => ({
|
|
8676
|
-
itemId: r.item
|
|
8677
|
-
type: r.item?.data?.
|
|
8678
|
-
point: { x: r.point
|
|
9282
|
+
itemId: __ppRegId(r.item),
|
|
9283
|
+
type: r.item?.data?.type || r.item?.className,
|
|
9284
|
+
point: r.point ? { x: r.point.x, y: r.point.y } : null
|
|
8679
9285
|
}));
|
|
8680
9286
|
return { success: true, action: 'hit_test', x: ${input.x ?? 0}, y: ${input.y ?? 0}, hits, count: hits.length };`
|
|
8681
9287
|
: `const result = app.hitTest(point, { tolerance: ${tolerance} });
|
|
@@ -8683,7 +9289,7 @@ ${resolve}
|
|
|
8683
9289
|
return {
|
|
8684
9290
|
success: true, action: 'hit_test',
|
|
8685
9291
|
x: ${input.x ?? 0}, y: ${input.y ?? 0},
|
|
8686
|
-
hit: { itemId: result.item
|
|
9292
|
+
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
9293
|
};`}
|
|
8688
9294
|
})();`.trim();
|
|
8689
9295
|
}
|
|
@@ -9083,12 +9689,47 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9083
9689
|
${guard}
|
|
9084
9690
|
const entry = app.itemRegistry.get(${itemIdStr});
|
|
9085
9691
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
|
|
9086
|
-
|
|
9087
|
-
|
|
9692
|
+
// The engine's remove returns nothing, and this used to echo its inputs, so
|
|
9693
|
+
// a wrong behaviorId still "succeeded" (gate run 2). Count what went.
|
|
9694
|
+
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; }); };
|
|
9695
|
+
const __before = __list();
|
|
9696
|
+
const __bid = ${behaviorIdStr};
|
|
9697
|
+
if (__bid && __before.indexOf(__bid) === -1) {
|
|
9698
|
+
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.' };
|
|
9699
|
+
}
|
|
9700
|
+
if (__bid) app.interactionSystem.removeContinuousBehavior(entry.item, __bid);
|
|
9701
|
+
else {
|
|
9702
|
+
// "Remove all" means the behaviors a caller ADDED. The engine's own
|
|
9703
|
+
// selectable / draggable go too with a bare remove, which left the item
|
|
9704
|
+
// impossible to select or drag (gate run 3, N4).
|
|
9705
|
+
const __sys = ['selectable', 'draggable'];
|
|
9706
|
+
const __m = app.interactionSystem.continuousBehaviors;
|
|
9707
|
+
((__m && __m.get ? __m.get(${itemIdStr}) : null) || []).slice()
|
|
9708
|
+
.filter(function (b) { return __sys.indexOf(b.type) === -1; })
|
|
9709
|
+
.forEach(function (b) { app.interactionSystem.removeContinuousBehavior(entry.item, b.id); });
|
|
9710
|
+
}
|
|
9711
|
+
const __after = __list();
|
|
9712
|
+
const removed = __before.filter(function (id) { return __after.indexOf(id) === -1; });
|
|
9713
|
+
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
9714
|
})();`.trim();
|
|
9089
9715
|
}
|
|
9090
9716
|
case 'trigger_action': {
|
|
9091
|
-
|
|
9717
|
+
// The engine's handlers read params.items (ids) for show / hide /
|
|
9718
|
+
// animate / stopAnimation, and params.points for incrementScore. The
|
|
9719
|
+
// tool documented neither, so { itemId } hid nothing and { amount: 5 }
|
|
9720
|
+
// scored 1 (gate D13, 1.6.19). The natural names are accepted too.
|
|
9721
|
+
const raw = { ...(input.params || {}) };
|
|
9722
|
+
if (raw.items === undefined) {
|
|
9723
|
+
const one = raw.itemId ?? raw.targetItemId;
|
|
9724
|
+
const many = raw.itemIds;
|
|
9725
|
+
if (Array.isArray(many))
|
|
9726
|
+
raw.items = many;
|
|
9727
|
+
else if (typeof one === 'string')
|
|
9728
|
+
raw.items = [one];
|
|
9729
|
+
}
|
|
9730
|
+
if (input.actionType === 'incrementScore' && raw.points === undefined && typeof raw.amount === 'number')
|
|
9731
|
+
raw.points = raw.amount;
|
|
9732
|
+
const params = JSON.stringify(raw);
|
|
9092
9733
|
return `
|
|
9093
9734
|
// Trigger interaction action
|
|
9094
9735
|
(function() {
|
|
@@ -9460,8 +10101,10 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9460
10101
|
// Toggle grid
|
|
9461
10102
|
(function() {
|
|
9462
10103
|
${guard}
|
|
9463
|
-
//
|
|
9464
|
-
|
|
10104
|
+
// setUnitGridEnabled shows/hides the grid AND records the setting that
|
|
10105
|
+
// set_snap depends on; showGrid() alone left snapping inert (1.6.19 gate).
|
|
10106
|
+
if (typeof app.setUnitGridEnabled === 'function') app.setUnitGridEnabled(${enabled});
|
|
10107
|
+
else app.measurementSystem[${enabled} ? 'showGrid' : 'hideGrid']();
|
|
9465
10108
|
return { success: true, action: 'set_grid', enabled: ${enabled} };
|
|
9466
10109
|
})();`.trim();
|
|
9467
10110
|
}
|
|
@@ -9480,17 +10123,18 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9480
10123
|
})();`.trim();
|
|
9481
10124
|
}
|
|
9482
10125
|
case 'set_snap': {
|
|
9483
|
-
//
|
|
10126
|
+
// The engine has setSnapToUnitEnabled now; this refused, saying it had
|
|
10127
|
+
// never existed (1.6.19 gate). It is an EDITOR setting: it snaps a
|
|
10128
|
+
// drag or resize to the unit grid's ticks, and does nothing while that
|
|
10129
|
+
// grid is off. Tool-placed items are not snapped, and the result says so.
|
|
9484
10130
|
return `
|
|
9485
|
-
// Snap to grid
|
|
10131
|
+
// Snap to unit grid (drag / resize in the studio)
|
|
9486
10132
|
(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.' };
|
|
10133
|
+
if (typeof app.setSnapToUnitEnabled !== 'function') { return { success: false, action: 'set_snap', error: 'this studio has no snap-to-unit setting; update FxTool' }; }
|
|
10134
|
+
const enabled = app.setSnapToUnitEnabled(${input.enabled !== false});
|
|
10135
|
+
const gridOn = typeof app.getUnitGridEnabled === 'function' ? app.getUnitGridEnabled() : null;
|
|
10136
|
+
return { success: true, action: 'set_snap', enabled: enabled, unitGrid: gridOn,
|
|
10137
|
+
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
10138
|
})();`.trim();
|
|
9495
10139
|
}
|
|
9496
10140
|
default:
|
|
@@ -9568,6 +10212,30 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9568
10212
|
* Extract the best-matching detected region from an image as a NEW item (FxTool
|
|
9569
10213
|
* ImageWorkflow.extractObject). Async (runs detection first).
|
|
9570
10214
|
*/
|
|
10215
|
+
/**
|
|
10216
|
+
* pinepaper_styled_scene (FxTool K2). app.styledScene answers {ok:false,
|
|
10217
|
+
* error} rather than rejecting, so the verdict is read, not assumed. The
|
|
10218
|
+
* result carries the duration to export for: the scene item lives that long,
|
|
10219
|
+
* and an export left at its own default would cut it short or pad it.
|
|
10220
|
+
*/
|
|
10221
|
+
generateStyledScene(input) {
|
|
10222
|
+
const opts = {
|
|
10223
|
+
...(input.style !== undefined ? { style: input.style } : {}),
|
|
10224
|
+
...(input.duration !== undefined ? { duration: input.duration } : {}),
|
|
10225
|
+
...(input.id !== undefined ? { id: input.id } : {}),
|
|
10226
|
+
};
|
|
10227
|
+
return `
|
|
10228
|
+
// Styled scene${input.style ? ` (${input.style.replace(/[\r\n]/g, ' ')})` : ''}
|
|
10229
|
+
(async function() {
|
|
10230
|
+
if (typeof app.styledScene !== 'function') { return { success: false, error: 'app.styledScene unavailable — update FxTool' }; }
|
|
10231
|
+
const r = await app.styledScene(${JSON.stringify(input.spec ?? {})}, ${JSON.stringify(opts)});
|
|
10232
|
+
if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the styled scene was refused' }; }
|
|
10233
|
+
const notes = ['export for ' + r.duration + ' s (agent_export duration: ' + r.duration + '): the scene runs that long'];
|
|
10234
|
+
if (r.style === 'dither' || r.style === 'ascii') notes.push(r.style + ' needs a Chrome renderer (WebKit ignores the canvas filter it uses)');
|
|
10235
|
+
notes.push('the scene redraws every exported frame; after a page reload it is a still until styled_scene is called again');
|
|
10236
|
+
return { success: true, itemId: r.id, style: r.style, width: r.width, height: r.height, duration: r.duration, firstFrameMs: r.firstFrameMs, notes: notes };
|
|
10237
|
+
})();`.trim();
|
|
10238
|
+
}
|
|
9571
10239
|
generateExtractObject(input) {
|
|
9572
10240
|
const args = JSON.stringify({
|
|
9573
10241
|
...(input.label !== undefined ? { label: input.label } : {}),
|
|
@@ -9584,6 +10252,16 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
9584
10252
|
}
|
|
9585
10253
|
try {
|
|
9586
10254
|
const res = await app.extractObject(${args});
|
|
10255
|
+
// A numeric itemId is Paper's own id: the engine's register fallback did
|
|
10256
|
+
// not take, so the new item was unaddressable and uncounted (gate run 2:
|
|
10257
|
+
// itemId 226, itemsCreated 0). Register it here and hand back that id.
|
|
10258
|
+
if (res && res.ok && typeof res.itemId === 'number' && typeof paper !== 'undefined' && paper.project) {
|
|
10259
|
+
const __n = res.itemId;
|
|
10260
|
+
const __it = paper.project.getItem({ match: function (i) { return i.id === __n; } });
|
|
10261
|
+
const __rid = __it && app.itemRegistry && app.itemRegistry.register ? app.itemRegistry.register(__it, 'image', { source: 'extract' }, 'import') : null;
|
|
10262
|
+
if (typeof __rid === 'string') res.itemId = __rid;
|
|
10263
|
+
else return { success: false, error: 'the object was extracted but could not be registered, so it has no usable id (Paper id ' + __n + ')' };
|
|
10264
|
+
}
|
|
9587
10265
|
const out = Object.assign({ success: !!(res && res.ok) }, res || {});
|
|
9588
10266
|
if (!out.success && typeof out.error === 'string') out.error = (${PinePaperCodeGenerator.MODEL_FETCH_HINT})(out.error);
|
|
9589
10267
|
return out;
|
|
@@ -9971,12 +10649,26 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
|
|
|
9971
10649
|
// which get the box loop below and a note saying so.
|
|
9972
10650
|
const doc = ${docJson};
|
|
9973
10651
|
if (typeof app.instantiateOntology === 'function') {
|
|
9974
|
-
|
|
10652
|
+
// THE LIVE CANVAS, unless one was given: the engine lays out for 1080x1080
|
|
10653
|
+
// otherwise, which put items at (540,540) on a 640x360 canvas (gate run 3).
|
|
10654
|
+
const __opts = ${engineOptsJson};
|
|
10655
|
+
if (!__opts.canvas && app.getCanvasSize) { const __c = app.getCanvasSize(); if (__c && __c.width > 0) __opts.canvas = { width: __c.width, height: __c.height }; }
|
|
10656
|
+
// REGISTRY IDS. The engine returns the DOCUMENT's ids ("bar"), which no
|
|
10657
|
+
// other tool resolves; the new registry entries are found by diffing.
|
|
10658
|
+
const __all = function () { return (app.itemRegistry && app.itemRegistry.getAll) ? app.itemRegistry.getAll() : []; };
|
|
10659
|
+
const __before = new Set(__all().map(function (e) { return e.itemId; }));
|
|
10660
|
+
const r = await app.instantiateOntology(doc, __opts);
|
|
9975
10661
|
const diags = r.diagnostics || [];
|
|
10662
|
+
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; });
|
|
10663
|
+
const docIds = r.itemIds || [];
|
|
10664
|
+
const idMap = {};
|
|
10665
|
+
if (__new.length === docIds.length) docIds.forEach(function (d, i) { idMap[d] = __new[i]; });
|
|
9976
10666
|
return {
|
|
9977
10667
|
success: !diags.some(function(d) { return d && d.level === 'error'; }) && !r.imagesFailed,
|
|
9978
10668
|
engine: 'studio',
|
|
9979
|
-
itemIds:
|
|
10669
|
+
itemIds: __new, itemCount: __new.length, docIds: docIds,
|
|
10670
|
+
...(Object.keys(idMap).length ? { idMap: idMap } : { note: 'document ids could not be paired with registry ids one to one; use itemIds' }),
|
|
10671
|
+
canvas: __opts.canvas || null,
|
|
9980
10672
|
// Every count the engine returns, not a chosen three. imagesFailed is the
|
|
9981
10673
|
// one that matters most: a raster that never decoded leaves a scene that
|
|
9982
10674
|
// looks built and is not, and dropping the count is how that becomes
|
|
@@ -10464,7 +11156,9 @@ ${guard}
|
|
|
10464
11156
|
// item — so reading .data.id off the answer returned no id, every time. The
|
|
10465
11157
|
// group is registered only on one of its two branches; the other is
|
|
10466
11158
|
// registered here so the caller always gets an addressable id.
|
|
10467
|
-
|
|
11159
|
+
// nib goes to the engine as a boolean or a tuning object. A STRING is
|
|
11160
|
+
// truthy there, so 'monoline' would have selected the broad nib (D31).
|
|
11161
|
+
const r = app.createCursiveText(${S(input.text)}, ${S(cursiveEngineOptions(input.cursiveOptions))});
|
|
10468
11162
|
const g = r && (r.group || (r.data ? r : null));
|
|
10469
11163
|
if (!g) { return { success: false, error: 'the cursive text produced no path' }; }
|
|
10470
11164
|
let itemId = g.data && (g.data.id || g.data.registryId);
|
|
@@ -10505,7 +11199,11 @@ ${guard}
|
|
|
10505
11199
|
// would otherwise read as "the conversion did nothing to your text".
|
|
10506
11200
|
const r = app.convertTextToCollage(item, ${S(input.text ?? null)} || item.content, ${S(input.collageOptions ?? {})});
|
|
10507
11201
|
if (!r) { return { success: false, error: 'the collage system is unavailable in this build, so the text was left as it was' }; }
|
|
10508
|
-
|
|
11202
|
+
// A summary, not the collage's Paper group (that was a raw dump, gate run 3).
|
|
11203
|
+
${REG_ID_JS}
|
|
11204
|
+
const __g = (r && (r.group || r.item)) || (r && typeof r.className === 'string' ? r : null);
|
|
11205
|
+
return { success: true, itemId: __g ? __ppRegId(__g) : null, letters: __g && __g.children ? __g.children.length : undefined,
|
|
11206
|
+
bounds: __g && __g.bounds ? { x: __g.bounds.x, y: __g.bounds.y, width: __g.bounds.width, height: __g.bounds.height } : undefined };
|
|
10509
11207
|
})();`.trim();
|
|
10510
11208
|
}
|
|
10511
11209
|
}
|
|
@@ -10520,6 +11218,404 @@ ${guard}
|
|
|
10520
11218
|
* only ids/count/duration are forwarded. `ids` is the whole handle set the
|
|
10521
11219
|
* caller now has, since the source item is gone.
|
|
10522
11220
|
*/
|
|
11221
|
+
/**
|
|
11222
|
+
* pinepaper_relight — FxTool D5's relight post pass over the 3D world layer.
|
|
11223
|
+
* The rig is not saved with the project, GIF / APNG are not relit, and it
|
|
11224
|
+
* costs ~0.42 s per 1080p frame on the CPU, so a set says what an export
|
|
11225
|
+
* will pay.
|
|
11226
|
+
*/
|
|
11227
|
+
/**
|
|
11228
|
+
* pinepaper_choreograph (D67). The compiler runs IN THE STUDIO, serialised
|
|
11229
|
+
* by toString, because the track depends on the actor's live size, scale,
|
|
11230
|
+
* rotation and the export frame. Merged into the actor's track, read back
|
|
11231
|
+
* the way keyframe_animate reads its own.
|
|
11232
|
+
*/
|
|
11233
|
+
generateChoreograph(input) {
|
|
11234
|
+
const S = (v) => JSON.stringify(v);
|
|
11235
|
+
const id = S(input.itemId);
|
|
11236
|
+
return `
|
|
11237
|
+
// Choreograph ${input.beats.length} beat(s) for ${input.itemId}
|
|
11238
|
+
(function() {
|
|
11239
|
+
const __it = (app.getItemById && app.getItemById(${id}))
|
|
11240
|
+
|| (app.itemRegistry && app.itemRegistry.get && (app.itemRegistry.get(${id}) || {}).item);
|
|
11241
|
+
if (!__it || !__it.bounds) { return { success: false, error: ${id} + ' is not on the canvas — check the id with pinepaper_get_items.' }; }
|
|
11242
|
+
const __sc = __it.scaling || { x: 1, y: 1 };
|
|
11243
|
+
const __b = __it.bounds;
|
|
11244
|
+
const __baked = (__it.data && typeof __it.data._bakedRotation === 'number') ? __it.data._bakedRotation : 0;
|
|
11245
|
+
const __fr = (typeof app.exportFrameRect === 'function' && app.exportFrameRect()) || (app.getCanvasSize && app.getCanvasSize()) || { width: 1920, height: 1080 };
|
|
11246
|
+
const __frame = { x: __fr.x || 0, y: __fr.y || 0, width: __fr.width, height: __fr.height };
|
|
11247
|
+
const __actor = { x: __it.position.x, y: __it.position.y,
|
|
11248
|
+
w: __b.width / (Math.abs(__sc.x) || 1), h: __b.height / (Math.abs(__sc.y) || 1),
|
|
11249
|
+
sx: __sc.x || 1, sy: __sc.y || 1, rot: (__it.rotation || 0) + __baked };
|
|
11250
|
+
const __r = (${compileChoreography.toString()})(__actor, __frame, ${S(input.beats)});
|
|
11251
|
+
if (__r.error) { return { success: false, itemId: ${id}, error: __r.error }; }
|
|
11252
|
+
const __prev = (__it.data && Array.isArray(__it.data.keyframes)) ? __it.data.keyframes : [];
|
|
11253
|
+
const __prevTimes = __prev.map(function (k) { return k && k.time; });
|
|
11254
|
+
const __ours = ['x', 'y', 'scaleX', 'scaleY', 'rotation', 'opacity'];
|
|
11255
|
+
const __other = __prev.reduce(function (acc, k) { Object.keys((k && k.properties) || {}).forEach(function (p) { if (__ours.indexOf(p) < 0 && acc.indexOf(p) < 0) acc.push(p); }); return acc; }, []);
|
|
11256
|
+
app.addAnimation(${id}, __r.keyframes, { duration: Math.max(__r.end, __prev.reduce(function (m, k) { return Math.max(m, (k && k.time) || 0); }, 0)), loop: false, mode: 'merge' });
|
|
11257
|
+
const __now = (__it.data && Array.isArray(__it.data.keyframes)) ? __it.data.keyframes.map(function (k) { return k && k.time; }) : [];
|
|
11258
|
+
const __warnings = (__r.warnings || []).slice();
|
|
11259
|
+
if (__prevTimes.length && !__prevTimes.every(function (t) { return __now.indexOf(t) >= 0; })) {
|
|
11260
|
+
__warnings.push('this studio replaced the existing track of the actor (' + __prevTimes.length + ' key(s)) instead of merging: put the other keys in after choreographing.');
|
|
11261
|
+
}
|
|
11262
|
+
if (__other.length) {
|
|
11263
|
+
__warnings.push('the actor already animates ' + __other.join(', ') + '; the beat keys leave those out, which the engine can break at the beat keys. Re-key ' + __other.join(', ') + ' with every key carrying them, after choreographing.');
|
|
11264
|
+
}
|
|
11265
|
+
if (__r.final && !__r.final.onScreen) __warnings.push('after the last beat the actor is off screen at [' + __r.final.x + ', ' + __r.final.y + '].');
|
|
11266
|
+
const __res = { success: true, itemId: ${id}, beats: ${input.beats.length}, keyframes: __r.keyframes.length, duration: __r.end, final: __r.final };
|
|
11267
|
+
${input.sound ? ` // SOUND ON THE ACTION (D68): one placed sound per moment the motion makes —
|
|
11268
|
+
// pop, launch, contact, shake — loud as the moment is strong. A sound that
|
|
11269
|
+
// does not place is reported; it never undoes the motion.
|
|
11270
|
+
const __map = Object.assign({ pop: 'pop', whoosh: 'whoosh', contact: 'tom', shake: 'hat' }, ${S(typeof input.sound === 'object' ? Object.fromEntries(Object.entries(input.sound).filter(([k]) => k !== 'gain')) : {})});
|
|
11271
|
+
const __gain = ${S(typeof input.sound === 'object' && input.sound.gain !== undefined ? input.sound.gain : 0.8)};
|
|
11272
|
+
const __sounds = [];
|
|
11273
|
+
const __silent = [];
|
|
11274
|
+
if (typeof app.createSound !== 'function' || (typeof app.sfxSpec !== 'function' && typeof app.percussionSpec !== 'function')) {
|
|
11275
|
+
__warnings.push('no sound was placed: this studio cannot place synthesized sounds (app.createSound / sfxSpec). The motion is written.');
|
|
11276
|
+
} else {
|
|
11277
|
+
(__r.events || []).forEach(function (e) {
|
|
11278
|
+
const name = __map[e.kind];
|
|
11279
|
+
if (name === false || name === undefined) return;
|
|
11280
|
+
const base = (typeof app.sfxSpec === 'function' && app.sfxSpec(name)) || (typeof app.percussionSpec === 'function' && app.percussionSpec(name));
|
|
11281
|
+
if (!base) { if (__silent.indexOf(name) < 0) __silent.push(name); return; }
|
|
11282
|
+
const spec = Object.assign({}, base, { gain: Math.round(((base.gain || 0.8) * __gain * (0.35 + 0.65 * e.strength)) * 1000) / 1000 });
|
|
11283
|
+
// MP4 (AAC) encoder priming swallows the first ~50 ms of audio, and this
|
|
11284
|
+
// muxer writes no edit list (the engine reports audio_lead_trimmed): a pop
|
|
11285
|
+
// at 0 s exported as silence (prod retest of D68). 60 ms later is in sync
|
|
11286
|
+
// to the eye and audible in every format.
|
|
11287
|
+
const at = e.time < 0.06 ? 0.06 : e.time;
|
|
11288
|
+
const it = app.createSound(spec, { startTime: at, duration: spec.duration || 0.5 });
|
|
11289
|
+
if (it && it.data) __sounds.push(Object.assign({ t: at, kind: e.kind, sound: name, itemId: it.data.id }, at !== e.time ? { movedFrom: e.time } : {}));
|
|
11290
|
+
});
|
|
11291
|
+
if (__silent.length) __warnings.push('not in the sound catalogue, so silent: ' + __silent.join(', ') + ' (pinepaper_sound list_sfx / list_percussion for the names).');
|
|
11292
|
+
}
|
|
11293
|
+
__res.sounds = __sounds;
|
|
11294
|
+
` : ''}
|
|
11295
|
+
if (__warnings.length) __res.warnings = __warnings;
|
|
11296
|
+
return __res;
|
|
11297
|
+
})();`.trim();
|
|
11298
|
+
}
|
|
11299
|
+
/**
|
|
11300
|
+
* pinepaper_morph_sequence (D73): ONE element through N states — R8's pill →
|
|
11301
|
+
* input → dot → grid → composer → orb. The engine already chains morphs_to
|
|
11302
|
+
* on one shape (per-relation state since gap 2.27); what a model could not
|
|
11303
|
+
* do was time the chain, make the reference shapes, and ride the camera on
|
|
11304
|
+
* the same curve. Compiled here to that chain, a camera track and sounds.
|
|
11305
|
+
*/
|
|
11306
|
+
generateMorphSequence(input) {
|
|
11307
|
+
const S = (v) => JSON.stringify(v);
|
|
11308
|
+
const states = [...input.states].sort((a, b) => a.at - b.at).map((st) => ({ at: st.at, duration: st.duration ?? 0.8, to: st.to ?? null, shape: st.shape ?? null }));
|
|
11309
|
+
const cam = input.camera === true ? { fill: 0.45 } : (input.camera || null);
|
|
11310
|
+
const sound = input.sound === true ? 'whoosh' : (input.sound || null);
|
|
11311
|
+
return `
|
|
11312
|
+
// Morph sequence: ${states.length} state(s)
|
|
11313
|
+
(function() {
|
|
11314
|
+
const src = app.getItemById && app.getItemById(${S(input.itemId)});
|
|
11315
|
+
if (!src) { return { success: false, error: ${S(input.itemId)} + ' is not on the canvas.' }; }
|
|
11316
|
+
if (typeof app.addRelation !== 'function') { return { success: false, error: 'this studio cannot add relations.' }; }
|
|
11317
|
+
const states = ${S(states)};
|
|
11318
|
+
const done = [];
|
|
11319
|
+
for (let i = 0; i < states.length; i++) {
|
|
11320
|
+
const st = states[i];
|
|
11321
|
+
let tid = st.to;
|
|
11322
|
+
if (!tid) {
|
|
11323
|
+
const p = Object.assign({}, st.shape.properties || {}, { x: st.shape.position.x, y: st.shape.position.y });
|
|
11324
|
+
const made = app.create(st.shape.itemType, p);
|
|
11325
|
+
tid = made && made.data && made.data.registryId;
|
|
11326
|
+
if (!tid) { return { success: false, error: 'state ' + i + ': could not make the ' + st.shape.itemType + ' reference shape', states: done }; }
|
|
11327
|
+
} else if (!(app.getItemById && app.getItemById(tid))) {
|
|
11328
|
+
return { success: false, error: 'state ' + i + ': ' + tid + ' is not on the canvas', states: done };
|
|
11329
|
+
}
|
|
11330
|
+
const ok = app.addRelation(${S(input.itemId)}, tid, 'morphs_to', { duration: st.duration, delay: st.at, easing: ${S(input.easing ?? 'easeInOut')}, hideTarget: true });
|
|
11331
|
+
if (ok === false) { return { success: false, error: 'state ' + i + ': the studio refused the morph into ' + tid, states: done }; }
|
|
11332
|
+
done.push({ at: st.at, duration: st.duration, targetId: tid });
|
|
11333
|
+
}
|
|
11334
|
+
const res = { success: true, itemId: ${S(input.itemId)}, states: done, end: done[done.length - 1].at + done[done.length - 1].duration };
|
|
11335
|
+
const n = app.getRelations ? app.getRelations(${S(input.itemId)}, 'morphs_to').length : null;
|
|
11336
|
+
if (n !== null && n < done.length) res.warning = 'only ' + n + ' of ' + done.length + ' morphs are on the element.';
|
|
11337
|
+
${cam ? ` // The camera rides the SAME timing: framing each state as its morph lands.
|
|
11338
|
+
{
|
|
11339
|
+
const fr = (typeof app.exportFrameRect === 'function' && app.exportFrameRect()) || (app.getCanvasSize && app.getCanvasSize()) || { width: 1920, height: 1080 };
|
|
11340
|
+
const fill = ${S(cam.fill ?? 0.45)};
|
|
11341
|
+
const frameOf = function (it) { const b = it.bounds; return { focus: [b.center.x, b.center.y], zoom: Math.max(0.2, Math.min(8, fill * Math.min(fr.width / Math.max(1, b.width), fr.height / Math.max(1, b.height)))) }; };
|
|
11342
|
+
const f0 = frameOf(src);
|
|
11343
|
+
const kfs = [{ time: 0, zoom: f0.zoom, focus: f0.focus }];
|
|
11344
|
+
done.forEach(function (d) {
|
|
11345
|
+
const t = app.getItemById(d.targetId);
|
|
11346
|
+
const f = frameOf(t);
|
|
11347
|
+
const prev = kfs[kfs.length - 1];
|
|
11348
|
+
kfs.push({ time: d.at, zoom: prev.zoom, focus: prev.focus });
|
|
11349
|
+
kfs.push({ time: d.at + d.duration, zoom: f.zoom, focus: f.focus, easing: ${S(cam.easing ?? 'easeInOut')} });
|
|
11350
|
+
});
|
|
11351
|
+
// The SAME door pinepaper_camera_animate uses: app.camera.animate where the
|
|
11352
|
+
// build has it, else the camera_animates relation — prod has only the
|
|
11353
|
+
// relation, and probing app.camera.animate alone reported "no camera" there.
|
|
11354
|
+
const r = app.camera && app.camera.animate
|
|
11355
|
+
? app.camera.animate(kfs, res.end, false, 0, {})
|
|
11356
|
+
: app.addRelation('camera', 'camera', 'camera_animates', { keyframes: kfs, duration: res.end, loop: false, delay: 0 });
|
|
11357
|
+
// Read the track back where the engine keeps it.
|
|
11358
|
+
const rr = app.relationRegistry;
|
|
11359
|
+
const written = rr && rr.getCameraAnimationParams ? rr.getCameraAnimationParams() : null;
|
|
11360
|
+
if (r === false || (rr && rr.getCameraAnimationParams && !written)) {
|
|
11361
|
+
res.warning = (res.warning ? res.warning + ' ' : '') + 'the camera track was not written; the morphs run without it.';
|
|
11362
|
+
} else {
|
|
11363
|
+
res.camera = { keyframes: kfs.length };
|
|
11364
|
+
}
|
|
11365
|
+
}
|
|
11366
|
+
` : ''}${sound ? ` // A sound on each morph (D68's placement, with its MP4-lead nudge).
|
|
11367
|
+
const base = (app.sfxSpec && app.sfxSpec(${S(sound)})) || (app.percussionSpec && app.percussionSpec(${S(sound)}));
|
|
11368
|
+
if (base && app.createSound) {
|
|
11369
|
+
res.sounds = done.map(function (d) { const at = d.at < 0.06 ? 0.06 : d.at; const it = app.createSound(Object.assign({}, base), { startTime: at, duration: base.duration || 0.5 }); return { t: at, itemId: it && it.data && it.data.id }; });
|
|
11370
|
+
} else { res.warning = (res.warning ? res.warning + ' ' : '') + ${S(`no sound placed: ${sound} is not in the catalogue, or this studio cannot place sounds.`)}; }
|
|
11371
|
+
` : ''} return res;
|
|
11372
|
+
})();`.trim();
|
|
11373
|
+
}
|
|
11374
|
+
/**
|
|
11375
|
+
* pinepaper_original_character (D66): app.createCharacter and its schema
|
|
11376
|
+
* helpers. The helpers read the SAME table the validator uses, so a model
|
|
11377
|
+
* reads the legal values (describe) before it guesses; a wrong one is
|
|
11378
|
+
* refused by name and nothing is drawn.
|
|
11379
|
+
*/
|
|
11380
|
+
generateOriginalCharacter(input) {
|
|
11381
|
+
const S = (v) => JSON.stringify(v);
|
|
11382
|
+
const guard = `if (typeof app.createCharacter !== 'function') { return { success: false, error: 'this studio has no original characters (app.createCharacter) — it predates FxTool D66.' }; }`;
|
|
11383
|
+
switch (input.action) {
|
|
11384
|
+
case 'bases':
|
|
11385
|
+
return `(function() {\n ${guard}\n return { success: true, action: 'bases', bases: app.listCharacterBases() };\n})();`;
|
|
11386
|
+
case 'describe':
|
|
11387
|
+
return `(function() {\n ${guard}\n return { success: true, action: 'describe', schema: app.describeCharacterVariantSchema(${input.base ? S(input.base) : ''}) };\n})();`;
|
|
11388
|
+
case 'random':
|
|
11389
|
+
return `(function() {\n ${guard}\n const r = app.randomCharacterVariant(${S(input.base)}, ${S(input.seed)}${input.fixed ? `, { fixed: ${S(input.fixed)} }` : ''});\n if (!r || r.ok === false) return { success: false, action: 'random', error: (r && r.errors && r.errors.join('; ')) || 'no variant' };\n return { success: true, action: 'random', base: ${S(input.base)}, seed: ${S(input.seed)}, variant: r.variant };\n})();`;
|
|
11390
|
+
default: {
|
|
11391
|
+
const spec = { base: input.base, ...(input.variant ? { variant: input.variant } : {}), ...(input.at ? { x: input.at.x, y: input.at.y } : {}),
|
|
11392
|
+
...(input.height !== undefined ? { height: input.height } : {}), ...(input.scale !== undefined ? { scale: input.scale } : {}), ...(input.rig !== undefined ? { rig: input.rig } : {}) };
|
|
11393
|
+
return `
|
|
11394
|
+
// Original character: create
|
|
11395
|
+
(async function() {
|
|
11396
|
+
${guard}
|
|
11397
|
+
const r = await app.createCharacter(${S(spec)});
|
|
11398
|
+
if (!r || r.ok === false) return { success: false, action: 'create', errors: (r && r.errors) || [], error: (r && r.errors && r.errors.join('; ')) || 'the character was refused' };
|
|
11399
|
+
return { success: true, action: 'create', itemId: r.id, rigId: r.rigId, parts: r.parts, name: r.name, base: r.base, look: r.look, variant: r.variant };
|
|
11400
|
+
})();`.trim();
|
|
11401
|
+
}
|
|
11402
|
+
}
|
|
11403
|
+
}
|
|
11404
|
+
/**
|
|
11405
|
+
* pinepaper_look (D64): one look over an item, a subtree or the whole scene
|
|
11406
|
+
* — ids, keys and relations kept, removable pixel for pixel. Every apply
|
|
11407
|
+
* carries the look's own FIDELITY grade, and anything below 'convincing'
|
|
11408
|
+
* says so in the reply, so a weak approximation is never handed over as
|
|
11409
|
+
* the finished style.
|
|
11410
|
+
*/
|
|
11411
|
+
generateLook(input) {
|
|
11412
|
+
const S = (v) => JSON.stringify(v);
|
|
11413
|
+
const guard = `if (typeof app.applyLook !== 'function') { return { success: false, error: 'this studio has no looks (app.applyLook) — it predates FxTool D64.' }; }`;
|
|
11414
|
+
const target = S(input.target ?? 'scene');
|
|
11415
|
+
switch (input.action) {
|
|
11416
|
+
case 'list':
|
|
11417
|
+
return `(function() {\n ${guard}\n return { success: true, action: 'list', looks: app.listLooks(), note: 'fidelity is the engine grade: native (the medium itself), faithful (close, approximations named), impression (an evocation, not the real style). Describe one before promising it.' };\n})();`;
|
|
11418
|
+
case 'describe':
|
|
11419
|
+
return `(function() {\n ${guard}\n const r = app.describeLook(${S(input.look)});\n if (!r || r.ok === false) return { success: false, action: 'describe', error: (r && r.error) || 'unknown look', known: r && r.known };\n return { success: true, action: 'describe', fidelity: r.look && r.look.fidelity, approximations: (r.look && r.look.approximations) || [], recipe: r.look, enginePaths: r.enginePaths, sceneOnly: r.sceneOnly };\n})();`;
|
|
11420
|
+
case 'get':
|
|
11421
|
+
return `(function() {\n ${guard}\n return { success: true, action: 'get', target: ${target}, look: app.getLook(${target}) || null };\n})();`;
|
|
11422
|
+
case 'remove':
|
|
11423
|
+
return `
|
|
11424
|
+
// Look: remove (restores what was there)
|
|
11425
|
+
(async function() {
|
|
11426
|
+
${guard}
|
|
11427
|
+
const r = await app.removeLook(${target});
|
|
11428
|
+
if (!r || r.ok === false) return { success: false, action: 'remove', error: (r && r.error) || 'nothing to remove' };
|
|
11429
|
+
return { success: true, action: 'remove', target: ${target}, removed: r.removed, scene: r.scene };
|
|
11430
|
+
})();`.trim();
|
|
11431
|
+
case 'grid':
|
|
11432
|
+
return `
|
|
11433
|
+
// Look: a contact sheet of looks over the current scene
|
|
11434
|
+
(async function() {
|
|
11435
|
+
${guard}
|
|
11436
|
+
const r = await app.renderLookGrid(${input.looks ? S(input.looks) : 'undefined'}, ${S({ ...(input.cols ? { cols: input.cols } : {}), ...(input.cellWidth ? { cellWidth: input.cellWidth } : {}), ...(input.t !== undefined ? { t: input.t } : {}) })});
|
|
11437
|
+
if (!r || r.ok === false || !r.dataURL) return { success: false, action: 'grid', error: (r && r.error) || 'the grid did not render' };
|
|
11438
|
+
return { success: true, action: 'grid', data: r.dataURL, mimeType: 'image/png', cells: (r.cells || []).map(function (c) { return { id: c.id, name: c.name, fidelity: c.fidelity }; }), cols: r.cols, rows: r.rows };
|
|
11439
|
+
})();`.trim();
|
|
11440
|
+
default: {
|
|
11441
|
+
const opts = input.scope ? { scope: input.scope } : {};
|
|
11442
|
+
return `
|
|
11443
|
+
// Look: apply
|
|
11444
|
+
(async function() {
|
|
11445
|
+
${guard}
|
|
11446
|
+
const d = app.describeLook(${S(input.look)});
|
|
11447
|
+
const r = await app.applyLook(${target}, ${S(input.look)}, ${S(opts)});
|
|
11448
|
+
if (!r || r.ok === false) return { success: false, action: 'apply', error: (r && r.error) || 'the look was refused', known: r && r.known };
|
|
11449
|
+
const fidelity = (d && d.look && d.look.fidelity) || (r.look && r.look.fidelity) || null;
|
|
11450
|
+
const res = { success: true, action: 'apply', target: ${target}, look: r.look && (r.look.id || r.look.name), fidelity: fidelity,
|
|
11451
|
+
restyled: r.restyled, texts: r.texts, images: r.images, brushed: r.brushed, keyframes: r.keyframes, filters: r.filters, ids: r.ids,
|
|
11452
|
+
approximations: r.approximations || [], warnings: r.warnings || [] };
|
|
11453
|
+
// The engine's grades: native (the medium itself), faithful (close, with
|
|
11454
|
+
// named approximations), impression (an evocation), custom (an ad-hoc spec).
|
|
11455
|
+
const __approx = (res.approximations && res.approximations.length) ? res.approximations.join('; ') : '';
|
|
11456
|
+
if (fidelity === 'impression') {
|
|
11457
|
+
res.warning = 'this look is an IMPRESSION of the style (the engine\\'s grade)' + (__approx ? ' — it approximates: ' + __approx : '') + '. Present it as an approximation of the style, not the finished style.';
|
|
11458
|
+
} else if (fidelity === 'faithful' && __approx) {
|
|
11459
|
+
res.note = 'a faithful look; what it approximates: ' + __approx + '.';
|
|
11460
|
+
} else if (fidelity === 'custom') {
|
|
11461
|
+
res.note = 'a custom look spec: the engine has no fidelity grade for it.';
|
|
11462
|
+
}
|
|
11463
|
+
return res;
|
|
11464
|
+
})();`.trim();
|
|
11465
|
+
}
|
|
11466
|
+
}
|
|
11467
|
+
}
|
|
11468
|
+
/** pinepaper_emitter (D29): create('emitter', spec) / setEmitter(id, patch). */
|
|
11469
|
+
generateEmitter(input) {
|
|
11470
|
+
const S = (v) => JSON.stringify(v);
|
|
11471
|
+
if (input.action === 'set') {
|
|
11472
|
+
return `
|
|
11473
|
+
// Emitter: change a spec
|
|
11474
|
+
(function() {
|
|
11475
|
+
if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }
|
|
11476
|
+
const r = app.setEmitter(${S(input.itemId)}, ${S(input.spec)});
|
|
11477
|
+
if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the emitter change was refused' }; }
|
|
11478
|
+
return { success: true, action: 'set', itemId: ${S(input.itemId)}, spec: r.spec };
|
|
11479
|
+
})();`.trim();
|
|
11480
|
+
}
|
|
11481
|
+
return `
|
|
11482
|
+
// Emitter: create
|
|
11483
|
+
(function() {
|
|
11484
|
+
if (typeof app.setEmitter !== 'function') { return { success: false, error: 'this studio has no emitter item (app.setEmitter) — it predates FxTool D29.' }; }
|
|
11485
|
+
const it = app.create('emitter', ${S(input.spec)});
|
|
11486
|
+
// A bad spec comes back null with an emitter-invalid warning; the reason
|
|
11487
|
+
// reaches this reply through the governor's warnings.
|
|
11488
|
+
if (!it) { return { success: false, error: 'the emitter spec was refused' }; }
|
|
11489
|
+
const id = it.data && (it.data.registryId || it.data.id);
|
|
11490
|
+
if (!id) { return { success: false, error: 'the emitter was made but not registered — no usable id' }; }
|
|
11491
|
+
return { success: true, action: 'create', itemId: id };
|
|
11492
|
+
})();`.trim();
|
|
11493
|
+
}
|
|
11494
|
+
/** pinepaper_render_hook (D29): drawing saved as source, re-installed on load. */
|
|
11495
|
+
generateRenderHook(input) {
|
|
11496
|
+
const S = (v) => JSON.stringify(v);
|
|
11497
|
+
const guard = `if (typeof app.registerRenderHook !== 'function') { return { success: false, error: 'this studio has no saved render hooks (app.registerRenderHook) — it predates FxTool D29.' }; }`;
|
|
11498
|
+
if (input.action === 'list') {
|
|
11499
|
+
return `(function() {\n ${guard}\n return { success: true, action: 'list', hooks: app.listRenderHooks() };\n})();`;
|
|
11500
|
+
}
|
|
11501
|
+
if (input.action === 'unregister') {
|
|
11502
|
+
return `(function() {\n ${guard}\n const had = app.unregisterRenderHook(${S(input.id)});\n if (!had) return { success: false, error: 'no render hook ' + ${S(input.id)} };\n return { success: true, action: 'unregister', id: ${S(input.id)} };\n})();`;
|
|
11503
|
+
}
|
|
11504
|
+
const spec = { id: input.id, source: input.source, ...(input.layer ? { layer: input.layer } : {}), ...(input.deterministic !== undefined ? { deterministic: input.deterministic } : {}), ...(input.seed !== undefined ? { seed: input.seed } : {}) };
|
|
11505
|
+
return `
|
|
11506
|
+
// Render hook: register (saved as source)
|
|
11507
|
+
(function() {
|
|
11508
|
+
${guard}
|
|
11509
|
+
const r = app.registerRenderHook(${S(spec)});
|
|
11510
|
+
if (!r || r.ok === false) { return { success: false, error: (r && r.error) || 'the render hook was refused' }; }
|
|
11511
|
+
return { success: true, action: 'register', id: r.id, layer: r.layer, deterministic: r.deterministic, seed: r.seed };
|
|
11512
|
+
})();`.trim();
|
|
11513
|
+
}
|
|
11514
|
+
generateRelight(input) {
|
|
11515
|
+
const S = (v) => JSON.stringify(v);
|
|
11516
|
+
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.' }; }`;
|
|
11517
|
+
switch (input.action) {
|
|
11518
|
+
case 'set': {
|
|
11519
|
+
const spec = S({
|
|
11520
|
+
...(input.lights !== undefined ? { lights: input.lights } : {}),
|
|
11521
|
+
...(input.ambient !== undefined ? { ambient: input.ambient } : {}),
|
|
11522
|
+
...(input.strength !== undefined ? { strength: input.strength } : {}),
|
|
11523
|
+
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
|
|
11524
|
+
...(input.preview !== undefined ? { preview: input.preview } : {}),
|
|
11525
|
+
});
|
|
11526
|
+
return `
|
|
11527
|
+
// Relight: set the rig
|
|
11528
|
+
(function() {
|
|
11529
|
+
${missing('setRelight')}
|
|
11530
|
+
const r = app.setRelight(${spec});
|
|
11531
|
+
if (!r || r.ok === false) { return { success: false, action: 'set', error: (r && ((r.errors && r.errors.join('; ')) || r.reason)) || 'the relight rig was refused' }; }
|
|
11532
|
+
// What an export will pay: ~418 ms per 1920x1080 frame, scaled by pixels.
|
|
11533
|
+
const __f = (typeof app.exportFrameRect === 'function' && app.exportFrameRect()) || (app.getCanvasSize && app.getCanvasSize()) || { width: 1920, height: 1080 };
|
|
11534
|
+
const __ms = Math.round(418 * (__f.width * __f.height) / (1920 * 1080));
|
|
11535
|
+
const res = { success: true, action: 'set', active: r.active, relight: r.relight,
|
|
11536
|
+
cost: { msPerFrame: __ms, frame: { width: __f.width, height: __f.height },
|
|
11537
|
+
tenSecondsAt30fps: Math.round(__ms * 300 / 1000) + ' s extra' },
|
|
11538
|
+
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.' };
|
|
11539
|
+
if (r.reason) res.reason = r.reason;
|
|
11540
|
+
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).';
|
|
11541
|
+
return res;
|
|
11542
|
+
})();`.trim();
|
|
11543
|
+
}
|
|
11544
|
+
case 'get':
|
|
11545
|
+
return `
|
|
11546
|
+
// Relight: the current rig
|
|
11547
|
+
(function() {
|
|
11548
|
+
${missing('getRelight')}
|
|
11549
|
+
return Object.assign({ success: true, action: 'get' }, app.getRelight());
|
|
11550
|
+
})();`.trim();
|
|
11551
|
+
case 'clear':
|
|
11552
|
+
return `
|
|
11553
|
+
// Relight: remove the rig
|
|
11554
|
+
(function() {
|
|
11555
|
+
${missing('clearRelight')}
|
|
11556
|
+
const r = app.clearRelight();
|
|
11557
|
+
return { success: true, action: 'clear', cleared: !!(r && r.cleared) };
|
|
11558
|
+
})();`.trim();
|
|
11559
|
+
case 'list_lights':
|
|
11560
|
+
return `
|
|
11561
|
+
// Relight: light types
|
|
11562
|
+
(function() {
|
|
11563
|
+
${missing('listRelightLights')}
|
|
11564
|
+
return { success: true, action: 'list_lights', lights: app.listRelightLights() };
|
|
11565
|
+
})();`.trim();
|
|
11566
|
+
}
|
|
11567
|
+
}
|
|
11568
|
+
/**
|
|
11569
|
+
* pinepaper_shader_graph — FxTool D5's JSON pixel graphs. Applying to an
|
|
11570
|
+
* item makes a NEW image item and hides the source (one undo step);
|
|
11571
|
+
* applying to 'scene' returns the processed frame and changes nothing.
|
|
11572
|
+
*/
|
|
11573
|
+
generateShaderGraph(input) {
|
|
11574
|
+
const S = (v) => JSON.stringify(v);
|
|
11575
|
+
const ref = input.graph ?? input.graphId;
|
|
11576
|
+
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.' }; }`;
|
|
11577
|
+
const fail = `(r && ((r.errors && r.errors.join('; ')) || r.reason)) || 'the shader graph call failed'`;
|
|
11578
|
+
const body = (() => {
|
|
11579
|
+
switch (input.action) {
|
|
11580
|
+
case 'node_types': return ` return { success: true, action: 'node_types', nodeTypes: app.shaderGraph.nodeTypes() };`;
|
|
11581
|
+
case 'list': return ` const r = app.shaderGraph.list();\n return { success: true, action: 'list', graphs: (r && r.graphs) || [] };`;
|
|
11582
|
+
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 };`;
|
|
11583
|
+
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 };`;
|
|
11584
|
+
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 };`;
|
|
11585
|
+
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 };`;
|
|
11586
|
+
case 'apply': {
|
|
11587
|
+
const opts = S({ ...(input.keepSource !== undefined ? { keepSource: input.keepSource } : {}), ...(input.asItem !== undefined ? { asItem: input.asItem } : {}) });
|
|
11588
|
+
return ` if (typeof app.applyShaderGraph !== 'function') { return { success: false, action: 'apply', error: 'this studio has no app.applyShaderGraph — it predates FxTool D5.' }; }
|
|
11589
|
+
const r = await app.applyShaderGraph(${S(input.target)}, ${S(ref)}, ${opts});
|
|
11590
|
+
if (!r || !r.ok) return { success: false, action: 'apply', errors: (r && r.errors) || undefined, error: ${fail} };
|
|
11591
|
+
if (r.target === 'item') {
|
|
11592
|
+
if (!r.id) return { success: false, action: 'apply', error: 'the graph ran but made no item' };
|
|
11593
|
+
// The new item's OWN bounds: a bloom with reach (FxTool D60) is larger
|
|
11594
|
+
// than its source, centred on it, so the source's bounds are not these.
|
|
11595
|
+
const __ne = app.itemRegistry && typeof app.itemRegistry.get === 'function' ? app.itemRegistry.get(r.id) : null;
|
|
11596
|
+
const __ni = __ne && __ne.item;
|
|
11597
|
+
// An image item made from a data URL decodes ASYNCHRONOUSLY and has 0x0
|
|
11598
|
+
// bounds until it does (prod read {width:0, height:0}); wait for it.
|
|
11599
|
+
if (__ni && __ni.className === 'Raster' && !__ni.loaded && typeof __ni.once === 'function') {
|
|
11600
|
+
await new Promise(function (resolve) { const t = setTimeout(resolve, 5000); __ni.once('load', function () { clearTimeout(t); resolve(); }); });
|
|
11601
|
+
}
|
|
11602
|
+
const __nb = __ni && __ni.bounds && __ni.bounds.width > 0 ? __ni.bounds : null;
|
|
11603
|
+
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,
|
|
11604
|
+
...(__nb ? { bounds: { x: __nb.x, y: __nb.y, width: __nb.width, height: __nb.height } } : {}) };
|
|
11605
|
+
}
|
|
11606
|
+
const res = { success: true, action: 'apply', target: 'scene', graphId: r.graphId, width: r.width, height: r.height };
|
|
11607
|
+
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.'; }
|
|
11608
|
+
return res;`;
|
|
11609
|
+
}
|
|
11610
|
+
}
|
|
11611
|
+
})();
|
|
11612
|
+
return `
|
|
11613
|
+
// Shader graph: ${input.action}
|
|
11614
|
+
(async function() {
|
|
11615
|
+
${head}
|
|
11616
|
+
${body}
|
|
11617
|
+
})();`.trim();
|
|
11618
|
+
}
|
|
10523
11619
|
/**
|
|
10524
11620
|
* The medium axis. `list_media` and `resolve` are READS — they exist so an
|
|
10525
11621
|
* agent can find out what this build can honestly make before it promises a
|
|
@@ -10542,8 +11638,43 @@ ${guard}
|
|
|
10542
11638
|
if (!r || r.ok === false) {
|
|
10543
11639
|
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
11640
|
}
|
|
10545
|
-
|
|
11641
|
+
// ink / charcoal restyle IN PLACE (FxTool D32): the same item, marked with
|
|
11642
|
+
// data.brushStyle on its paths and texts. Read the mark back, so an item
|
|
11643
|
+
// the engine passed over is a failure rather than a success (gate D32).
|
|
11644
|
+
if (${S(input.medium)} === 'ink' || ${S(input.medium)} === 'charcoal') {
|
|
11645
|
+
const __e = app.itemRegistry && app.itemRegistry.get(r.id || ${S(input.itemId)});
|
|
11646
|
+
const __it = (__e && __e.item) || r.item;
|
|
11647
|
+
let __styled = 0;
|
|
11648
|
+
const __walk = function (n) {
|
|
11649
|
+
if (!n) return;
|
|
11650
|
+
if (n.data && n.data.brushStyle) __styled++;
|
|
11651
|
+
(n.children || []).forEach(__walk);
|
|
11652
|
+
};
|
|
11653
|
+
__walk(__it);
|
|
11654
|
+
if (!__styled) {
|
|
11655
|
+
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.' };
|
|
11656
|
+
}
|
|
11657
|
+
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 } : {}) };
|
|
11658
|
+
}
|
|
11659
|
+
return { success: true, action: 'apply', medium: r.medium || ${S(input.medium)}, fidelity: r.fidelity, itemId: r.id, ...(r.filter ? { filter: r.filter } : {}), ...(r.note ? { note: r.note } : {}) };
|
|
10546
11660
|
})();`.trim();
|
|
11661
|
+
case 'sdf_stroke': {
|
|
11662
|
+
// FxTool D5: an SDF capsule stroke rendered to a bitmap and created as a
|
|
11663
|
+
// registered, undoable image item. CPU-backed.
|
|
11664
|
+
const opts = S({
|
|
11665
|
+
...(input.width !== undefined ? { width: input.width } : {}),
|
|
11666
|
+
...(input.softness !== undefined ? { softness: input.softness } : {}),
|
|
11667
|
+
...(input.color !== undefined ? { color: input.color } : {}),
|
|
11668
|
+
...(input.opacity !== undefined ? { opacity: input.opacity } : {}),
|
|
11669
|
+
...(input.resolution !== undefined ? { resolution: input.resolution } : {}),
|
|
11670
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
11671
|
+
});
|
|
11672
|
+
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.' }; }
|
|
11673
|
+
const r = app.sdfBrushStroke(${S(input.points)}, ${opts});
|
|
11674
|
+
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' }; }
|
|
11675
|
+
if (!r.id) { return { success: false, action: 'sdf_stroke', error: 'the stroke was drawn but returned no item id' }; }
|
|
11676
|
+
return { success: true, action: 'sdf_stroke', itemId: r.id, bounds: r.bounds, pixelSize: r.pixelSize, backend: r.backend };`);
|
|
11677
|
+
}
|
|
10547
11678
|
case 'list_media':
|
|
10548
11679
|
return wrap('Medium: list', ` if (typeof app.listDesignMedia !== 'function') { return { success: false, error: 'medium axis unavailable — update FxTool' }; }
|
|
10549
11680
|
const media = app.listDesignMedia();
|
|
@@ -10792,14 +11923,60 @@ ${guard}
|
|
|
10792
11923
|
// Import a layered character (decomposer output → role-bound parts; blink works out of the box)
|
|
10793
11924
|
(async function() {
|
|
10794
11925
|
if (typeof app.importLayeredCharacter !== 'function') { return { success: false, error: 'app.importLayeredCharacter unavailable — update FxTool to a layered-character build' }; }
|
|
10795
|
-
// The
|
|
10796
|
-
//
|
|
10797
|
-
|
|
11926
|
+
// The engine looks layers up by NORMALISED tag (lowercase, no dir/extension,
|
|
11927
|
+
// '_'/'-' collapsed to a space): parseSeeThroughManifest asks
|
|
11928
|
+
// images.has(normalizePartTag(tag)), then images.get(layer.tag). Its own
|
|
11929
|
+
// folder reader keys the Map that way; a wire object keyed 'eye_left' is never
|
|
11930
|
+
// found and every multi-word layer is skipped as "no image found".
|
|
11931
|
+
// Mirrors FxTool js/ontology/CharacterPartTaxonomy.js normalizePartTag.
|
|
11932
|
+
const norm = (t) => String(t).trim().toLowerCase().replace(/^.*[\\\\/]/, '').replace(/\\.(png|webp|tga|psd)$/i, '').replace(/[_\\-\\s]+/g, ' ').trim();
|
|
11933
|
+
// The taxonomy resolves DECOMPOSER tags (eyel, iridesr, eyebrowl, hairf) — not
|
|
11934
|
+
// the role tokens it emits. A layer tagged with a role token (eye_left,
|
|
11935
|
+
// pupil_right, hair_front) resolves to no role, so alias role tokens to the
|
|
11936
|
+
// decomposer tag that produces them.
|
|
11937
|
+
const SIDED = { eye: 'eye', pupil: 'irides', brow: 'eyebrow', eyelash: 'eyelash', ear: 'ears', hand: 'handwear' };
|
|
11938
|
+
const FLAT = { 'hair front': 'hairf', 'hair back': 'hairb', torso: 'topwear', hips: 'bottomwear', legs: 'legwear', feet: 'footwear', prop: 'objects' };
|
|
11939
|
+
const canon = (t) => {
|
|
11940
|
+
const k = norm(t);
|
|
11941
|
+
let m = /^(.+) (left|right|l|r)$/.exec(k);
|
|
11942
|
+
if (m && SIDED[m[1]] !== undefined) return SIDED[m[1]] + m[2][0];
|
|
11943
|
+
m = /^(left|right) (.+)$/.exec(k);
|
|
11944
|
+
if (m && SIDED[m[2]] !== undefined) return SIDED[m[2]] + m[1][0];
|
|
11945
|
+
return FLAT[k] || k;
|
|
11946
|
+
};
|
|
11947
|
+
const rawInfo = ${S(input.info)};
|
|
11948
|
+
const rawImages = ${S(input.images)};
|
|
11949
|
+
const partsKey = rawInfo.parts ? 'parts' : (rawInfo.tag2pinfo ? 'tag2pinfo' : null);
|
|
11950
|
+
const info = { ...rawInfo };
|
|
11951
|
+
const notes = [];
|
|
11952
|
+
if (partsKey && rawInfo[partsKey] && typeof rawInfo[partsKey] === 'object' && !Array.isArray(rawInfo[partsKey])) {
|
|
11953
|
+
info[partsKey] = {};
|
|
11954
|
+
for (const [t, v] of Object.entries(rawInfo[partsKey])) {
|
|
11955
|
+
const c = canon(t);
|
|
11956
|
+
if (info[partsKey][c]) { notes.push('layer "' + t + '" collides with another layer as "' + c + '" — kept the first'); continue; }
|
|
11957
|
+
info[partsKey][c] = v;
|
|
11958
|
+
}
|
|
11959
|
+
}
|
|
11960
|
+
const images = new Map();
|
|
11961
|
+
const suppliedBy = {}; // canonical tag → the caller's own key, for naming
|
|
11962
|
+
for (const [t, v] of Object.entries(rawImages)) {
|
|
11963
|
+
const c = canon(t);
|
|
11964
|
+
if (images.has(c)) { notes.push('image "' + t + '" collides with "' + suppliedBy[c] + '" — kept the first'); continue; }
|
|
11965
|
+
images.set(c, v); suppliedBy[c] = t;
|
|
11966
|
+
}
|
|
11967
|
+
const bundle = { info, images };
|
|
10798
11968
|
const r = await app.importLayeredCharacter(bundle, ${opts});
|
|
10799
11969
|
if (!r || !r.groupId) { return { success: false, error: (r && r.error) || 'layered character import failed' }; }
|
|
10800
11970
|
// roles maps part roles → item ids; zero roles wired means the character
|
|
10801
11971
|
// renders but will NOT animate — surface the count so that cannot hide.
|
|
10802
|
-
|
|
11972
|
+
const imported = new Set((r.parts || []).map((p) => p.tag));
|
|
11973
|
+
const skipped = Object.keys(suppliedBy).filter((c) => !imported.has(c)).map((c) => suppliedBy[c]);
|
|
11974
|
+
const unroled = (r.parts || []).filter((p) => !p.role).map((p) => suppliedBy[p.tag] || p.tag);
|
|
11975
|
+
const result = { groupId: r.groupId, parts: (r.parts || []).length, roles: r.roles || {}, rolesWired: Object.keys(r.roles || {}).length, skipped, unroled, warnings: [...notes, ...(r.warnings || [])] };
|
|
11976
|
+
// A supplied image that did not land is the caller's input dropped — not a
|
|
11977
|
+
// success. The group IS on the canvas, so hand back its id to fix or delete.
|
|
11978
|
+
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)' }; }
|
|
11979
|
+
return { success: true, ...result };
|
|
10803
11980
|
})();`.trim();
|
|
10804
11981
|
}
|
|
10805
11982
|
/**
|
|
@@ -10902,7 +12079,9 @@ ${needWorld}
|
|
|
10902
12079
|
(function() {
|
|
10903
12080
|
if (typeof app.addWorldActor !== 'function') { return { success: false, error: 'app.addWorldActor unavailable — update FxTool' }; }
|
|
10904
12081
|
${needWorld}
|
|
10905
|
-
const
|
|
12082
|
+
const __r = app.addWorldActor(${actor});
|
|
12083
|
+
// The engine may return the actor itself; the id is what later calls take (gate D5).
|
|
12084
|
+
const id = (__r && typeof __r === 'object') ? (__r.id || null) : __r;
|
|
10906
12085
|
return id ? { success: true, actorId: id } : { success: false, error: 'actor not added — is the sprite id a canvas item?' };
|
|
10907
12086
|
})();`.trim();
|
|
10908
12087
|
}
|
|
@@ -10940,7 +12119,9 @@ ${needWorld}
|
|
|
10940
12119
|
// World3D: place an object (y defaults to sitting on the terrain)
|
|
10941
12120
|
(function() {
|
|
10942
12121
|
${needWorld}
|
|
10943
|
-
const
|
|
12122
|
+
const __o = app.addWorldObject(${S(world3dColors(input.object))});
|
|
12123
|
+
// addWorldObject returns the OBJECT; its id is what set/remove calls take (gate D5).
|
|
12124
|
+
const id = (__o && typeof __o === 'object') ? (__o.id || null) : __o;
|
|
10944
12125
|
return id ? { success: true, objectId: id } : { success: false, error: 'object not added' };
|
|
10945
12126
|
})();`.trim();
|
|
10946
12127
|
case 'remove_object':
|
|
@@ -10955,7 +12136,10 @@ ${needWorld}
|
|
|
10955
12136
|
// World3D: tear the world down — Paper's canvas is untouched (it was always its own layer)
|
|
10956
12137
|
(function() {
|
|
10957
12138
|
if (typeof app.removeWorld3D !== 'function') { return { success: false, error: 'app.removeWorld3D unavailable — update FxTool' }; }
|
|
10958
|
-
return
|
|
12139
|
+
// removeWorld3D's return value is not a success flag; whether the world is
|
|
12140
|
+
// gone is (gate D5: it worked and reported failure).
|
|
12141
|
+
app.removeWorld3D();
|
|
12142
|
+
return { success: !app._world3d };
|
|
10959
12143
|
})();`.trim();
|
|
10960
12144
|
// --- Mesh authoring ---------------------------------------------------
|
|
10961
12145
|
// extrudeToMesh / latheToMesh REFUSE by name when there is no world
|
|
@@ -11008,7 +12192,19 @@ ${needWorld}
|
|
|
11008
12192
|
// World3D: every authored mesh on the stage
|
|
11009
12193
|
(function() {
|
|
11010
12194
|
if (typeof app.listWorldMeshes !== 'function') { return { success: false, error: 'app.listWorldMeshes unavailable — update FxTool' }; }
|
|
11011
|
-
|
|
12195
|
+
// Summarised (gate A, 1.6.19): one star returned over 10 KB, nearly all of it
|
|
12196
|
+
// collision and vertex arrays nobody asked for. Arrays longer than 16 numbers
|
|
12197
|
+
// become their length; everything else passes as is.
|
|
12198
|
+
const __slim = function (v, depth) {
|
|
12199
|
+
// Typed arrays too: collision positions are a Float32Array, which
|
|
12200
|
+
// Array.isArray misses, so a star still came back as 12 KB (gate run 2).
|
|
12201
|
+
if (ArrayBuffer.isView(v)) return v.length > 16 ? { length: v.length } : Array.prototype.slice.call(v);
|
|
12202
|
+
if (Array.isArray(v)) return v.length > 16 ? { length: v.length } : v.map(function (x) { return __slim(x, depth + 1); });
|
|
12203
|
+
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; }
|
|
12204
|
+
return v;
|
|
12205
|
+
};
|
|
12206
|
+
const __m = app.listWorldMeshes() || [];
|
|
12207
|
+
return { success: true, count: __m.length, meshes: __m.map(function (m) { return __slim(m, 0); }) };
|
|
11012
12208
|
})();`.trim();
|
|
11013
12209
|
case 'remove_mesh':
|
|
11014
12210
|
return `
|
|
@@ -11178,7 +12374,10 @@ ${needWorld}
|
|
|
11178
12374
|
(function() {
|
|
11179
12375
|
if (typeof app.worldToCanvas !== 'function') { return { success: false, error: 'app.worldToCanvas unavailable — update FxTool' }; }
|
|
11180
12376
|
${needWorld}
|
|
11181
|
-
|
|
12377
|
+
// projectToCanvas destructures [x, y, z]; an {x, y, z} object threw "is not iterable" (gate D5).
|
|
12378
|
+
const __p = ${S(input.point)};
|
|
12379
|
+
const __pt = app.worldToCanvas(Array.isArray(__p) ? __p : [__p.x, __p.y, __p.z ?? 0]);
|
|
12380
|
+
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
12381
|
})();`.trim();
|
|
11183
12382
|
case 'canvas_to_ground':
|
|
11184
12383
|
return `
|
|
@@ -11285,14 +12484,20 @@ ${needWorld}
|
|
|
11285
12484
|
* already decided by a pure function, so nothing is left for the page to
|
|
11286
12485
|
* work out.
|
|
11287
12486
|
*/
|
|
11288
|
-
generateDesignCompose(scene, ops, style) {
|
|
12487
|
+
generateDesignCompose(scene, ops, style, sizeWarning) {
|
|
11289
12488
|
const S = (v) => JSON.stringify(v);
|
|
11290
12489
|
return `
|
|
11291
12490
|
// Design: compose a ${style} scene — ${ops.length} items, laid out server-side
|
|
11292
12491
|
(function() {
|
|
11293
12492
|
if (typeof app.create !== 'function') { return { success: false, error: 'app.create unavailable — update FxTool' }; }
|
|
11294
12493
|
const size = ${S({ width: scene.width, height: scene.height })};
|
|
11295
|
-
|
|
12494
|
+
// The layout was computed for this size, so the canvas is set to it — and
|
|
12495
|
+
// the result SAYS so: a 1080x1080 canvas became 1280x720 silently (gate C).
|
|
12496
|
+
const __before = app.getCanvasSize ? app.getCanvasSize() : null;
|
|
12497
|
+
if (typeof app.setCanvasSize === 'function') { try { app.setCanvasSize({ width: size.width, height: size.height }); } catch (_) { /* a fixed canvas is not a failure */ } }
|
|
12498
|
+
const __after = app.getCanvasSize ? app.getCanvasSize() : null;
|
|
12499
|
+
const canvasResized = __before && __after && (__before.width !== __after.width || __before.height !== __after.height)
|
|
12500
|
+
? { from: { width: __before.width, height: __before.height }, to: { width: __after.width, height: __after.height } } : null;
|
|
11296
12501
|
${scene.backgroundHex ? `if (typeof app.setBackgroundColor === 'function') { try { app.setBackgroundColor(${S(scene.backgroundHex)}); } catch (_) { /* ditto */ } }` : '// this style paints no background of its own'}
|
|
11297
12502
|
const ops = ${S(ops)};
|
|
11298
12503
|
const ids = [];
|
|
@@ -11307,7 +12512,9 @@ ${needWorld}
|
|
|
11307
12512
|
if (app.historyManager) app.historyManager.saveState();
|
|
11308
12513
|
// A partial composition is reported as one. Half a poster that claims
|
|
11309
12514
|
// 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
|
|
12515
|
+
return { success: failed.length === 0, style: ${S(style)}, itemIds: ids, itemCount: ids.length, failed: failed,
|
|
12516
|
+
...(canvasResized ? { canvasResized: canvasResized, note: 'the canvas was resized to the composition' + (${S(sizeWarning ?? '')} ? '' : '; pass width and height to compose at your canvas size') } : {}),
|
|
12517
|
+
...(${S(sizeWarning ?? '')} ? { warning: ${S(sizeWarning ?? '')} } : {}) };
|
|
11311
12518
|
})();`.trim();
|
|
11312
12519
|
}
|
|
11313
12520
|
/**
|
|
@@ -11378,7 +12585,14 @@ ${needWorld}
|
|
|
11378
12585
|
*/
|
|
11379
12586
|
generateStory(input) {
|
|
11380
12587
|
const S = (v) => JSON.stringify(v);
|
|
11381
|
-
|
|
12588
|
+
// nav decides the story's FORM (D109): 'scenes' = a scene chain (video),
|
|
12589
|
+
// 'panels' = tabs (widget). storyFromText reads options.nav; a spec carries
|
|
12590
|
+
// its own nav, which a top-level nav fills in when the spec has none.
|
|
12591
|
+
const opts = S({ ...(input.options ?? {}), ...(input.nav ? { nav: input.nav } : {}) });
|
|
12592
|
+
const spec = input.spec && input.nav && input.spec.nav === undefined ? { ...input.spec, nav: input.nav } : input.spec;
|
|
12593
|
+
const cuts = input.nav === 'scenes'
|
|
12594
|
+
? `\n // The video form's cut times (s), for checking the edit.\n try { if (app.sceneManager && app.sceneManager.getChainCuts) __out.cuts = app.sceneManager.getChainCuts(); } catch (_) {}`
|
|
12595
|
+
: '';
|
|
11382
12596
|
switch (input.action) {
|
|
11383
12597
|
case 'distill':
|
|
11384
12598
|
return `
|
|
@@ -11404,16 +12618,18 @@ ${needWorld}
|
|
|
11404
12618
|
// fine in-page. Not a cycle; JSON.stringify succeeded on this the whole time.
|
|
11405
12619
|
// The groups have to stay for in-page callers, so the wrapper takes what
|
|
11406
12620
|
// travels: the spec, the ids, the count.
|
|
11407
|
-
|
|
12621
|
+
const __out = { success: true, nav: (r.spec && r.spec.nav) || ${S(input.nav ?? 'panels')}, spec: r.spec, partIds: r.partIds, partCount: r.partCount, beats: r.beats };${cuts}
|
|
12622
|
+
return __out;
|
|
11408
12623
|
})();`.trim();
|
|
11409
12624
|
case 'apply_spec':
|
|
11410
12625
|
return `
|
|
11411
12626
|
// Story: assemble a spec you already have
|
|
11412
12627
|
(async function() {
|
|
11413
12628
|
if (typeof app.applyStorySpec !== 'function') { return { success: false, error: 'app.applyStorySpec unavailable — update FxTool' }; }
|
|
11414
|
-
const r = await app.applyStorySpec(${S(
|
|
12629
|
+
const r = await app.applyStorySpec(${S(spec)}, ${opts});
|
|
11415
12630
|
if (!r || r.ok === false) { return { success: false, error: (r && (r.reason || r.error)) || 'the spec did not assemble' }; }
|
|
11416
|
-
|
|
12631
|
+
const __out = { success: true, ...r };${cuts}
|
|
12632
|
+
return __out;
|
|
11417
12633
|
})();`.trim();
|
|
11418
12634
|
case 'plan_book':
|
|
11419
12635
|
return `
|
|
@@ -11448,9 +12664,13 @@ ${needWorld}
|
|
|
11448
12664
|
// Interchange: ${fn}
|
|
11449
12665
|
(async function() {
|
|
11450
12666
|
${guard(fn)}
|
|
12667
|
+
${TO_DATA_URL_JS}
|
|
11451
12668
|
const out = await app.${fn}(${opts});
|
|
11452
12669
|
if (!out) { return { success: false, error: '${fn} produced nothing — the scene may have no animatable content' }; }
|
|
11453
|
-
|
|
12670
|
+
${input.action === 'export_lottie'
|
|
12671
|
+
? `return { success: true, format: 'lottie', data: out };`
|
|
12672
|
+
: `const __url = await __ppToDataUrl(out, 'application/zip');
|
|
12673
|
+
return __url ? { success: true, format: 'dotlottie', data: __url } : { success: false, error: 'exportDotLottie returned something that is not binary' };`}
|
|
11454
12674
|
})();`.trim();
|
|
11455
12675
|
}
|
|
11456
12676
|
case 'import_lottie':
|
|
@@ -11460,9 +12680,23 @@ ${guard(fn)}
|
|
|
11460
12680
|
${guard('importLottie')}
|
|
11461
12681
|
// importLottie logs and returns falsy when the importer is absent — a
|
|
11462
12682
|
// stripped console makes that indistinguishable from an empty animation.
|
|
12683
|
+
${REG_ID_JS}
|
|
11463
12684
|
const r = await app.importLottie(${S(input.data)}, ${opts});
|
|
11464
|
-
|
|
11465
|
-
|
|
12685
|
+
// importLottie ALWAYS answers { success, items, duration, warnings, errors };
|
|
12686
|
+
// only !r was checked, so a failed import read as a success, and the ids
|
|
12687
|
+
// were looked for on fields it does not have (gate N2: itemId null).
|
|
12688
|
+
if (!r || r.success === false) {
|
|
12689
|
+
const why = r && Array.isArray(r.errors) && r.errors.length ? r.errors.join('; ') : 'the Lottie did not import — it may be malformed';
|
|
12690
|
+
return { success: false, error: why, ...(r && r.warnings && r.warnings.length ? { warnings: r.warnings } : {}) };
|
|
12691
|
+
}
|
|
12692
|
+
const __items = Array.isArray(r.items) ? r.items : [];
|
|
12693
|
+
const itemIds = __items.map(function (it) { return __ppRegId(it); }).filter(Boolean);
|
|
12694
|
+
if (!__items.length) return { success: false, error: 'the Lottie imported no items' };
|
|
12695
|
+
let __b = null;
|
|
12696
|
+
__items.forEach(function (it) { if (it && it.bounds) __b = __b ? __b.unite(it.bounds) : it.bounds; });
|
|
12697
|
+
return { success: true, itemId: itemIds[0] || null, itemIds: itemIds, items: __items.length, duration: r.duration,
|
|
12698
|
+
bounds: __b ? { x: __b.x, y: __b.y, width: __b.width, height: __b.height } : undefined,
|
|
12699
|
+
...(r.warnings && r.warnings.length ? { warnings: r.warnings } : {}) };
|
|
11466
12700
|
})();`.trim();
|
|
11467
12701
|
case 'export_glb':
|
|
11468
12702
|
return `
|
|
@@ -11473,9 +12707,11 @@ ${guard('exportGLB')}
|
|
|
11473
12707
|
// stripped in production, so the precondition is checked here instead.
|
|
11474
12708
|
const has3d = app._initialized && app._initialized.has && app._initialized.has('threeD');
|
|
11475
12709
|
if (!has3d) { return { success: false, error: 'no perspective objects to export — create one with createObject3D first' }; }
|
|
12710
|
+
${TO_DATA_URL_JS}
|
|
11476
12711
|
const out = await app.exportGLB(${opts});
|
|
11477
12712
|
if (!out) { return { success: false, error: 'GLB export produced nothing' }; }
|
|
11478
|
-
|
|
12713
|
+
const __url = await __ppToDataUrl(out, 'model/gltf-binary');
|
|
12714
|
+
return __url ? { success: true, format: 'glb', data: __url } : { success: true, format: 'glb', data: out };
|
|
11479
12715
|
})();`.trim();
|
|
11480
12716
|
case 'export_bvh':
|
|
11481
12717
|
return `
|
|
@@ -11536,7 +12772,16 @@ ${guard('exportPNGSequence')}
|
|
|
11536
12772
|
generateSound(input) {
|
|
11537
12773
|
const S = (v) => JSON.stringify(v);
|
|
11538
12774
|
const guard = (fn) => ` if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool' }; }`;
|
|
11539
|
-
const simple = (fn, args, key, label) => `
|
|
12775
|
+
const simple = (fn, args, key, label) => key === 'played' ? `
|
|
12776
|
+
// Sound: ${label}
|
|
12777
|
+
(function() {
|
|
12778
|
+
${guard(fn)}
|
|
12779
|
+
// A play that returns no voice played nothing, and reported success (gate
|
|
12780
|
+
// sweep D, 1.6.19). A headless studio may have no audio output at all.
|
|
12781
|
+
const __v = app.${fn}(${args});
|
|
12782
|
+
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.' }; }
|
|
12783
|
+
return { success: true, played: __v };
|
|
12784
|
+
})();`.trim() : `
|
|
11540
12785
|
// Sound: ${label}
|
|
11541
12786
|
(function() {
|
|
11542
12787
|
${guard(fn)}
|
|
@@ -11551,8 +12796,12 @@ ${guard(fn)}
|
|
|
11551
12796
|
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
12797
|
case 'play_percussion': return simple('playPercussion', `${S(input.name)}, ${S(input.options ?? {})}`, 'played', `percussion ${input.name}`);
|
|
11553
12798
|
case 'play_sfx': return simple('playSfx', `${S(input.name)}, ${S(input.options ?? {})}`, 'played', `sfx ${input.name}`);
|
|
11554
|
-
|
|
11555
|
-
|
|
12799
|
+
// options are merged into the spec, as play_tone's are; play_spec and
|
|
12800
|
+
// play_from_text dropped them (gate sweep D).
|
|
12801
|
+
case 'play_spec': return simple('playSound', `Object.assign({}, ${S(input.spec)}, ${S(input.options ?? {})})`, 'played', 'a raw spec');
|
|
12802
|
+
case 'play_from_text': return input.options
|
|
12803
|
+
? simple('playSound', `Object.assign({}, app.buildSoundFromText(${S(input.text)}) || {}, ${S(input.options)})`, 'played', 'a described sound')
|
|
12804
|
+
: simple('playSoundFromText', S(input.text), 'played', 'a described sound');
|
|
11556
12805
|
case 'from_text':
|
|
11557
12806
|
return `
|
|
11558
12807
|
// Sound: resolve "${input.text}" to a spec WITHOUT playing it
|
|
@@ -11712,7 +12961,9 @@ ${guard('createSound')}
|
|
|
11712
12961
|
for (let i = 0; i < cues.length; i++) {
|
|
11713
12962
|
const c = cues[i];
|
|
11714
12963
|
try {
|
|
11715
|
-
|
|
12964
|
+
// The engine reads spec.instrument and never preset, so a preset cue
|
|
12965
|
+
// played a plain sine (gate sweep D).
|
|
12966
|
+
const spec = c.spec || (c.preset ? { instrument: c.preset, note: c.note } : null);
|
|
11716
12967
|
if (!spec) { failed.push({ index: i, t: c.t, error: 'neither spec nor preset' }); continue; }
|
|
11717
12968
|
const item = app.createSound(spec, c.visual || {});
|
|
11718
12969
|
if (!item) { failed.push({ index: i, t: c.t, error: 'the cue produced no waveform path' }); continue; }
|
|
@@ -11801,10 +13052,20 @@ ${guard('timbreFromPath')}
|
|
|
11801
13052
|
generatePathOp(input) {
|
|
11802
13053
|
const S = (v) => JSON.stringify(v);
|
|
11803
13054
|
const guard = (fn) => ` if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool' }; }`;
|
|
13055
|
+
// Paper items in the engine's answer become their registry ids: spread
|
|
13056
|
+
// as-is they serialised to raw Paper dumps (1.6.19 gate: boolean,
|
|
13057
|
+
// outline_stroke and pattern).
|
|
11804
13058
|
const pass = (expr) => `
|
|
13059
|
+
${REG_ID_JS}
|
|
13060
|
+
const __isItem = function (v) { return v && typeof v === 'object' && typeof v.className === 'string' && 'bounds' in v; };
|
|
13061
|
+
const __clean = function (v) { return __isItem(v) ? (__ppRegId(v) || ('paper:' + v.id)) : (Array.isArray(v) && v.some(__isItem) ? v.map(__clean) : v); };
|
|
11805
13062
|
const r = ${expr};
|
|
11806
13063
|
if (!r || r.ok === false) { return { success: false, error: (r && r.reason) || 'the engine refused the operation' }; }
|
|
11807
|
-
|
|
13064
|
+
const out = { success: true };
|
|
13065
|
+
Object.keys(r).forEach(function (k) { out[k] = __clean(r[k]); });
|
|
13066
|
+
// itemId, as every other tool names it; the engine says id (gate run 3).
|
|
13067
|
+
if (out.itemId === undefined && typeof out.id === 'string') out.itemId = out.id;
|
|
13068
|
+
return out;`;
|
|
11808
13069
|
switch (input.action) {
|
|
11809
13070
|
case 'boolean':
|
|
11810
13071
|
return `
|
|
@@ -12146,7 +13407,14 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
12146
13407
|
return wrap('Rigging: bake animation to keyframes', ` const res = R.bakeAnimation(${S(input.skeletonId)}, ${S(input.options ?? {})});
|
|
12147
13408
|
if (res && app.historyManager) app.historyManager.saveState();
|
|
12148
13409
|
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
|
-
|
|
13410
|
+
// bakeAnimation returns a MAP (itemId -> keyframes), which serialised to {},
|
|
13411
|
+
// and its report is a non-enumerable bakeReport (gate run 3). Read both.
|
|
13412
|
+
const rep = res.bakeReport || {};
|
|
13413
|
+
const detached = rep.detached || 0;
|
|
13414
|
+
return { success: (rep.written || 0) > 0 || ${input.options && input.options.write === false ? 'true' : 'false'}, action: 'bake_animation',
|
|
13415
|
+
items: res.size, written: rep.written || 0, keyframes: rep.keyframes || 0, cleared: rep.cleared || 0, detached: detached,
|
|
13416
|
+
wrote: rep.wrote || [], ...(rep.skipped && rep.skipped.length ? { skipped: rep.skipped } : {}),
|
|
13417
|
+
...(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
13418
|
case 'list_shape_keys':
|
|
12151
13419
|
return wrap('Rigging: list shape keys', ` const keys = R.listShapeKeys(${S(input.skeletonId)});
|
|
12152
13420
|
return { success: true, action: 'list_shape_keys', shapeKeys: keys, count: keys.length };`);
|
|
@@ -12380,7 +13648,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
12380
13648
|
generateEvent(input) {
|
|
12381
13649
|
if (input.action === 'create') {
|
|
12382
13650
|
const nameJson = JSON.stringify(input.name);
|
|
12383
|
-
const optsJson = JSON.stringify({ payloadType: input.payloadType, x: input.x, y: input.y });
|
|
13651
|
+
const optsJson = JSON.stringify({ payloadType: input.payloadType, x: input.x, y: input.y, ...(input.at !== undefined ? { at: input.at } : {}) });
|
|
12384
13652
|
return `
|
|
12385
13653
|
// Create event: ${input.name}
|
|
12386
13654
|
(function() {
|
|
@@ -12390,7 +13658,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
12390
13658
|
const eventId = app.createEvent(${nameJson}, ${optsJson});
|
|
12391
13659
|
if (!eventId) { return { success: false, error: 'Event creation failed' }; }
|
|
12392
13660
|
if (app.historyManager) app.historyManager.saveState();
|
|
12393
|
-
return { success: true, action: 'create', eventId };
|
|
13661
|
+
return { success: true, action: 'create', eventId${input.at !== undefined ? `, at: ${JSON.stringify(input.at)}` : ''} };
|
|
12394
13662
|
})();`.trim();
|
|
12395
13663
|
}
|
|
12396
13664
|
// pulse — a runtime fire, not a scene edit (no history save)
|