@pinepaper.studio/mcp-server 1.6.19 → 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 +86 -4
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/build-info.json +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.js +2 -2
- 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.js +1 -1
- package/dist/tools/definitions.d.ts +10 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +284 -25
- 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 +6 -6
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +11 -9
- 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 +8 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +188 -10
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/export.d.ts.map +1 -1
- package/dist/tools/handlers/export.js +8 -1
- package/dist/tools/handlers/export.js.map +1 -1
- package/dist/tools/handlers.d.ts +23 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +176 -5
- 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 +9 -3
- 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 +24 -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 +6 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +35 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +538 -24
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +1617 -273
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +164 -2
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +25 -1
- 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
|
|
@@ -1955,6 +1956,108 @@ export function planKeyframeMerges(operations) {
|
|
|
1955
1956
|
* Such a snippet's value is its last top-level (-led statement; the
|
|
1956
1957
|
* font-loading form is an async IIFE, which is awaited instead.
|
|
1957
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
|
+
}
|
|
1958
2061
|
function asReturningBody(snippet) {
|
|
1959
2062
|
const single = snippet.trim();
|
|
1960
2063
|
if (single.startsWith('(async function()'))
|
|
@@ -2398,17 +2501,27 @@ export class PinePaperCodeGenerator {
|
|
|
2398
2501
|
properties.keyframes = validated.keyframes;
|
|
2399
2502
|
if (validated.anchor !== undefined && properties.anchor === undefined && properties.origin === undefined)
|
|
2400
2503
|
properties.anchor = validated.anchor;
|
|
2401
|
-
|
|
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');
|
|
2402
2508
|
}
|
|
2403
2509
|
/**
|
|
2404
2510
|
* Generate code for modifying an item
|
|
2405
2511
|
*/
|
|
2406
2512
|
generateModifyItem(input) {
|
|
2407
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);
|
|
2408
2517
|
if (validated.atTime !== undefined) {
|
|
2409
|
-
return generateAutoKeyCode(validated.itemId,
|
|
2518
|
+
return withItemSetters(generateAutoKeyCode(validated.itemId, props, validated.atTime, validated.easing), setters, idExpr);
|
|
2519
|
+
}
|
|
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);
|
|
2410
2523
|
}
|
|
2411
|
-
return generateModifyItemCode(validated.itemId,
|
|
2524
|
+
return withItemSetters(generateModifyItemCode(validated.itemId, props, validated.data), setters, idExpr);
|
|
2412
2525
|
}
|
|
2413
2526
|
/**
|
|
2414
2527
|
* Generate code for deleting an item
|
|
@@ -2452,7 +2565,15 @@ export class PinePaperCodeGenerator {
|
|
|
2452
2565
|
if (validated.relationType === 'moves_along_path') {
|
|
2453
2566
|
return generateMovesAlongPathCode(validated.sourceId, validated.targetId ?? undefined, (validated.params ?? {}));
|
|
2454
2567
|
}
|
|
2455
|
-
|
|
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).`);
|
|
2456
2577
|
}
|
|
2457
2578
|
/**
|
|
2458
2579
|
* Generate code for removing a relation
|
|
@@ -2478,7 +2599,19 @@ export class PinePaperCodeGenerator {
|
|
|
2478
2599
|
*/
|
|
2479
2600
|
generateKeyframeAnimate(input) {
|
|
2480
2601
|
const validated = KeyframeAnimateInputSchema.parse(input);
|
|
2481
|
-
|
|
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
|
+
})();`;
|
|
2482
2615
|
}
|
|
2483
2616
|
/**
|
|
2484
2617
|
* Generate code for importing / retargeting a motion-capture clip
|
|
@@ -2726,6 +2859,15 @@ throw new Error('Either svgString or url must be provided');
|
|
|
2726
2859
|
success: !!result.success,
|
|
2727
2860
|
nodeCount: nodes.length,
|
|
2728
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 } : {}),
|
|
2729
2871
|
nodes,
|
|
2730
2872
|
edges,
|
|
2731
2873
|
errors: result.errors || [],
|
|
@@ -3596,6 +3738,8 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3596
3738
|
let code = `
|
|
3597
3739
|
// End agent job
|
|
3598
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 */ }
|
|
3599
3743
|
const result = {
|
|
3600
3744
|
success: true,
|
|
3601
3745
|
jobEnded: true
|
|
@@ -3900,12 +4044,19 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
3900
4044
|
// the exact emitter create_item uses, adapted to return its result
|
|
3901
4045
|
// inside this op's function.
|
|
3902
4046
|
const pos = op.position || { x: 400, y: 300 };
|
|
3903
|
-
const
|
|
3904
|
-
const
|
|
3905
|
-
|
|
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)}` : ''}
|
|
3906
4054
|
const __created = await (async function() {
|
|
3907
4055
|
${returning}
|
|
3908
|
-
})()
|
|
4056
|
+
})();${hasItemSetters(opSetters) ? `
|
|
4057
|
+
if (__created && __created.success !== false && __created.itemId) {
|
|
4058
|
+
${itemSettersApplyJs(opSetters, '__created.itemId', '__created')}
|
|
4059
|
+
}` : ''}`;
|
|
3909
4060
|
// Coordinate-built items (path from segments/pathData, line/arc from
|
|
3910
4061
|
// from/through/to) derive their geometry from those coordinates and
|
|
3911
4062
|
// IGNORE params.position — so `create` at a point silently produced an
|
|
@@ -5481,6 +5632,34 @@ ${(() => {
|
|
|
5481
5632
|
})()}
|
|
5482
5633
|
${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) { /* the still is already rendered */ }
|
|
5483
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 */ }
|
|
5484
5663
|
// RELIGHT (FxTool D5): applied to png / pdf / mp4 / webm, not gif / apng
|
|
5485
5664
|
// or the vector formats; and it is what makes a video export slow.
|
|
5486
5665
|
if (result && result.success && typeof app.getRelight === 'function') {
|
|
@@ -6792,7 +6971,11 @@ ${stillTime !== undefined ? ` try { app.setPlaybackTime(__prevT); } catch (_) {
|
|
|
6792
6971
|
if (typeof app.captureProjectDocument !== 'function') { return { success: false, error: 'app.captureProjectDocument unavailable — update FxTool' }; }
|
|
6793
6972
|
${SCENE_COUNTS_JS}
|
|
6794
6973
|
const doc = app.captureProjectDocument(${JSON.stringify(name ? { name } : {})});
|
|
6795
|
-
|
|
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 };
|
|
6796
6979
|
})();`.trim();
|
|
6797
6980
|
}
|
|
6798
6981
|
/** Load a staged project document (window.__ppStage.projectDoc), replacing the scene. */
|
|
@@ -7709,13 +7892,22 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
7709
7892
|
opts.push(`defaultTransition: ${JSON.stringify(input.defaultTransition)}`);
|
|
7710
7893
|
if (input.transitionDuration !== undefined)
|
|
7711
7894
|
opts.push(`transitionDuration: ${input.transitionDuration * 1000}`);
|
|
7895
|
+
if (input.cutSound !== undefined)
|
|
7896
|
+
opts.push(`cutSound: ${JSON.stringify(input.cutSound)}`);
|
|
7712
7897
|
const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : '';
|
|
7713
7898
|
return `
|
|
7714
7899
|
// Create scene chain for sequential playback
|
|
7715
7900
|
(function() {
|
|
7716
7901
|
${guard}
|
|
7717
|
-
app.sceneManager.createChain(${JSON.stringify(input.sceneIds)}${optsStr});
|
|
7718
|
-
|
|
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;
|
|
7719
7911
|
})();
|
|
7720
7912
|
`.trim();
|
|
7721
7913
|
}
|
|
@@ -8143,6 +8335,32 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
|
|
|
8143
8335
|
})();`.trim();
|
|
8144
8336
|
}
|
|
8145
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
|
+
}
|
|
8146
8364
|
const opts = JSON.stringify({ ...(input.selectionOnly ? { selectionOnly: true } : {}) });
|
|
8147
8365
|
const method = input.action === 'plan' ? 'planBrandKit' : 'applyBrandKit';
|
|
8148
8366
|
const facade = this._facadeCall(method, `${JSON.stringify(input.kit)}, ${opts}`, `Brand kit: ${input.action}`);
|
|
@@ -8667,8 +8885,8 @@ ${resolveSource}
|
|
|
8667
8885
|
const resolve = ` const item = (typeof app._resolveRaster === 'function')
|
|
8668
8886
|
? app._resolveRaster(${JSON.stringify(input.itemId)})
|
|
8669
8887
|
: (app.itemRegistry ? app.itemRegistry.getItem(${JSON.stringify(input.itemId)}) : null);
|
|
8670
|
-
if (!item) return { error: 'Item not found or not a raster: ' + ${JSON.stringify(input.itemId)} };
|
|
8671
|
-
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' };`;
|
|
8672
8890
|
switch (input.action) {
|
|
8673
8891
|
case 'apply':
|
|
8674
8892
|
return `
|
|
@@ -8685,7 +8903,7 @@ ${resolve}
|
|
|
8685
8903
|
}
|
|
8686
8904
|
return { success: true, action: 'apply', itemId: ${JSON.stringify(input.itemId)}, filter: ${JSON.stringify(input.filterName || '')} };
|
|
8687
8905
|
} catch (e) {
|
|
8688
|
-
return { error: 'Failed to apply filter: ' + e.message };
|
|
8906
|
+
return { success: false, action: 'apply', error: 'Failed to apply filter: ' + e.message };
|
|
8689
8907
|
}
|
|
8690
8908
|
})();`.trim();
|
|
8691
8909
|
case 'chain':
|
|
@@ -8698,7 +8916,7 @@ ${resolve}
|
|
|
8698
8916
|
await app.applyImageFilterChain(item, filters);
|
|
8699
8917
|
return { success: true, action: 'chain', itemId: ${JSON.stringify(input.itemId)}, filterCount: filters.length };
|
|
8700
8918
|
} catch (e) {
|
|
8701
|
-
return { error: 'Failed to apply filter chain: ' + e.message };
|
|
8919
|
+
return { success: false, action: 'chain', error: 'Failed to apply filter chain: ' + e.message };
|
|
8702
8920
|
}
|
|
8703
8921
|
})();`.trim();
|
|
8704
8922
|
default:
|
|
@@ -11006,6 +11224,293 @@ ${guard}
|
|
|
11006
11224
|
* costs ~0.42 s per 1080p frame on the CPU, so a set says what an export
|
|
11007
11225
|
* will pay.
|
|
11008
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
|
+
}
|
|
11009
11514
|
generateRelight(input) {
|
|
11010
11515
|
const S = (v) => JSON.stringify(v);
|
|
11011
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.' }; }`;
|
|
@@ -11151,7 +11656,7 @@ ${body}
|
|
|
11151
11656
|
}
|
|
11152
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 } : {}) };
|
|
11153
11658
|
}
|
|
11154
|
-
return { success: true, action: 'apply', medium: r.medium || ${S(input.medium)}, fidelity: r.fidelity, itemId: r.id, ...(r.note ? { note: r.note } : {}) };
|
|
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 } : {}) };
|
|
11155
11660
|
})();`.trim();
|
|
11156
11661
|
case 'sdf_stroke': {
|
|
11157
11662
|
// FxTool D5: an SDF capsule stroke rendered to a bitmap and created as a
|
|
@@ -12080,7 +12585,14 @@ ${needWorld}
|
|
|
12080
12585
|
*/
|
|
12081
12586
|
generateStory(input) {
|
|
12082
12587
|
const S = (v) => JSON.stringify(v);
|
|
12083
|
-
|
|
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
|
+
: '';
|
|
12084
12596
|
switch (input.action) {
|
|
12085
12597
|
case 'distill':
|
|
12086
12598
|
return `
|
|
@@ -12106,16 +12618,18 @@ ${needWorld}
|
|
|
12106
12618
|
// fine in-page. Not a cycle; JSON.stringify succeeded on this the whole time.
|
|
12107
12619
|
// The groups have to stay for in-page callers, so the wrapper takes what
|
|
12108
12620
|
// travels: the spec, the ids, the count.
|
|
12109
|
-
|
|
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;
|
|
12110
12623
|
})();`.trim();
|
|
12111
12624
|
case 'apply_spec':
|
|
12112
12625
|
return `
|
|
12113
12626
|
// Story: assemble a spec you already have
|
|
12114
12627
|
(async function() {
|
|
12115
12628
|
if (typeof app.applyStorySpec !== 'function') { return { success: false, error: 'app.applyStorySpec unavailable — update FxTool' }; }
|
|
12116
|
-
const r = await app.applyStorySpec(${S(
|
|
12629
|
+
const r = await app.applyStorySpec(${S(spec)}, ${opts});
|
|
12117
12630
|
if (!r || r.ok === false) { return { success: false, error: (r && (r.reason || r.error)) || 'the spec did not assemble' }; }
|
|
12118
|
-
|
|
12631
|
+
const __out = { success: true, ...r };${cuts}
|
|
12632
|
+
return __out;
|
|
12119
12633
|
})();`.trim();
|
|
12120
12634
|
case 'plan_book':
|
|
12121
12635
|
return `
|
|
@@ -13134,7 +13648,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
13134
13648
|
generateEvent(input) {
|
|
13135
13649
|
if (input.action === 'create') {
|
|
13136
13650
|
const nameJson = JSON.stringify(input.name);
|
|
13137
|
-
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 } : {}) });
|
|
13138
13652
|
return `
|
|
13139
13653
|
// Create event: ${input.name}
|
|
13140
13654
|
(function() {
|
|
@@ -13144,7 +13658,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
13144
13658
|
const eventId = app.createEvent(${nameJson}, ${optsJson});
|
|
13145
13659
|
if (!eventId) { return { success: false, error: 'Event creation failed' }; }
|
|
13146
13660
|
if (app.historyManager) app.historyManager.saveState();
|
|
13147
|
-
return { success: true, action: 'create', eventId };
|
|
13661
|
+
return { success: true, action: 'create', eventId${input.at !== undefined ? `, at: ${JSON.stringify(input.at)}` : ''} };
|
|
13148
13662
|
})();`.trim();
|
|
13149
13663
|
}
|
|
13150
13664
|
// pulse — a runtime fire, not a scene edit (no history save)
|