@pinepaper.studio/mcp-server 1.6.29 → 1.6.31
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 +104 -1
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/SECURITY.md +15 -0
- package/dist/browser/puppeteer-controller.d.ts +40 -0
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +179 -27
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/i18n/i18n-manager.d.ts.map +1 -1
- package/dist/i18n/i18n-manager.js +57 -3
- package/dist/i18n/i18n-manager.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -9
- 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 +2 -0
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/tools/batch-op-names.d.ts +27 -0
- package/dist/tools/batch-op-names.d.ts.map +1 -0
- package/dist/tools/batch-op-names.js +102 -0
- package/dist/tools/batch-op-names.js.map +1 -0
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +31 -22
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/effect-check.d.ts +1 -1
- package/dist/tools/effect-check.d.ts.map +1 -1
- package/dist/tools/effect-check.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +6 -3
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +16 -3
- 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 +3 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +20 -4
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +2 -2
- package/dist/tools/handlers/generate.d.ts.map +1 -1
- package/dist/tools/handlers/generate.js +4 -4
- package/dist/tools/handlers/generate.js.map +1 -1
- package/dist/tools/handlers.d.ts +7 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +70 -3
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +8 -2
- package/dist/tools/lane.d.ts.map +1 -1
- package/dist/tools/lane.js +22 -3
- package/dist/tools/lane.js.map +1 -1
- package/dist/tools/p5-compat/p5-helpers.d.ts +6 -17
- package/dist/tools/p5-compat/p5-helpers.d.ts.map +1 -1
- package/dist/tools/p5-compat/p5-helpers.js +8 -1
- package/dist/tools/p5-compat/p5-helpers.js.map +1 -1
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.d.ts.map +1 -1
- package/dist/tools/shape-params.js +5 -2
- package/dist/tools/shape-params.js.map +1 -1
- package/dist/tools/stick-vocabulary.d.ts +1 -1
- package/dist/tools/stick-vocabulary.js +1 -1
- package/dist/types/code-generator.d.ts +17 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +187 -90
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +11 -0
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +5 -1
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/capabilities.d.ts.map +1 -1
- package/dist/utils/capabilities.js +3 -1
- package/dist/utils/capabilities.js.map +1 -1
- package/dist/utils/env.d.ts +47 -6
- package/dist/utils/env.d.ts.map +1 -1
- package/dist/utils/env.js +52 -6
- package/dist/utils/env.js.map +1 -1
- package/dist/utils/safe-fetch.d.ts +1 -0
- package/dist/utils/safe-fetch.d.ts.map +1 -1
- package/dist/utils/safe-fetch.js +3 -4
- package/dist/utils/safe-fetch.js.map +1 -1
- package/manifest.json +2 -2
- package/package.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { compileChoreography } from '../tools/choreograph.js';
|
|
8
8
|
import { compileBatchTimelines, emitTimelineOpBody } from '../tools/timeline.js';
|
|
9
9
|
import { normalizeSvgSize } from '../utils/svg-size.js';
|
|
10
|
-
import { WORLD3D_COLOR_PATHS, PATTERN_KINDS, EXPRESSION_FUNCTION_ARITY, EXPRESSION_CONSTANTS, EXPRESSION_REJECTED, RELATION_PARAM_KEYS } from '../tools/engine-lists.js';
|
|
10
|
+
import { WORLD3D_COLOR_PATHS, PATTERN_KINDS, EXPRESSION_FUNCTION_ARITY, EXPRESSION_CONSTANTS, EXPRESSION_REJECTED, RELATION_PARAM_KEYS, ANIMATION_OPTION_KEYS } from '../tools/engine-lists.js';
|
|
11
11
|
import { expressionGrammarText } from '../tools/expression-grammar.js';
|
|
12
12
|
import { REQUIRED_ENGINE_METHODS, OPTIONAL_ENGINE_METHODS } from '../tools/engine-methods.js';
|
|
13
13
|
import { NORMALIZE_PARAM_READS, MODIFY_CHANGE_READS } from '../tools/shape-params.js';
|
|
@@ -402,8 +402,13 @@ const item = app.create(${jsLiteral(itemType)}, ${JSON.stringify(params, null, 2
|
|
|
402
402
|
if (hasCreateScale) {
|
|
403
403
|
code += `
|
|
404
404
|
const __scale = (function(it) {
|
|
405
|
+
// A studio whose create() applies scale (FxTool #262) already has it: nothing to redo.
|
|
406
|
+
const __want = ${JSON.stringify(createScale)};
|
|
407
|
+
const __s0 = (it && it.scaling) || { x: 1, y: 1 };
|
|
408
|
+
const __near = function(a, b) { return b === undefined || Math.abs(a - b) < 1e-6; };
|
|
409
|
+
if (__near(__s0.x, __want.scaleX) && __near(__s0.y, __want.scaleY)) return { applied: true, x: __s0.x, y: __s0.y };
|
|
405
410
|
if (!it || !it.data || typeof app.modifyItem !== 'function') return { applied: false, note: 'this studio has no app.modifyItem, so the scale was not applied at create; set it with pinepaper_modify_item.' };
|
|
406
|
-
app.modifyItem(it.data.registryId,
|
|
411
|
+
app.modifyItem(it.data.registryId, __want);
|
|
407
412
|
const s = it.scaling || { x: 1, y: 1 };
|
|
408
413
|
return { applied: true, x: s.x, y: s.y };
|
|
409
414
|
})(item);`;
|
|
@@ -1253,14 +1258,33 @@ export function fireIfProblem(params) {
|
|
|
1253
1258
|
}
|
|
1254
1259
|
return null;
|
|
1255
1260
|
}
|
|
1261
|
+
/**
|
|
1262
|
+
* A TARGET-LESS on_event_step_active runs only on a studio that says so. An older studio runs an event reaction only
|
|
1263
|
+
* when its target exists (a sound alone was exempt), and step_active acts on its GROUP, not its target, so one bound
|
|
1264
|
+
* with no target reported success and never stepped (beta STEPACT30). FxTool #281 runs it target-less and lists it in
|
|
1265
|
+
* PinePaper.TARGETLESS_EVENT_RELATIONS, the set _bindOnEventRelation itself reads, so the marker cannot say yes while
|
|
1266
|
+
* the engine says no. The refusal is decided in the page (STEP_ACTIVE_TARGETLESS_JS); here, only whether to ask.
|
|
1267
|
+
*/
|
|
1268
|
+
export function targetlessStepProblem(relationType, target) {
|
|
1269
|
+
if (relationType !== 'on_event_step_active' || (target != null && target !== ''))
|
|
1270
|
+
return null;
|
|
1271
|
+
return 'on_event_step_active needs a targetId on this studio. It steps the group named in params.groupName, not its target, but this studio runs an event reaction only when its target exists: without one it binds, reports success and never runs. Point it at any item, for example one member of the group (a current studio runs it without one).';
|
|
1272
|
+
}
|
|
1273
|
+
/** True in the page when the studio runs on_event_step_active with no target. */
|
|
1274
|
+
export const STEP_ACTIVE_TARGETLESS_JS = "(typeof app !== 'undefined' && !!app && app.TARGETLESS_EVENT_RELATIONS?.has?.('on_event_step_active') === true)";
|
|
1256
1275
|
/**
|
|
1257
1276
|
* fireIfProblem for any relation a tool is about to add: an on_event_fire_if
|
|
1258
1277
|
* itself, or one an on_event_add_relation will add when its event fires
|
|
1259
1278
|
* (params.type / params.params). Every path that adds relations asks this, so
|
|
1260
1279
|
* the check does not depend on which tool built the relation.
|
|
1261
1280
|
*/
|
|
1262
|
-
export function relationSpecProblem(relationType, params) {
|
|
1281
|
+
export function relationSpecProblem(relationType, params, ...target) {
|
|
1263
1282
|
const p = (params && typeof params === 'object' ? params : {});
|
|
1283
|
+
// A target-less on_event_step_active is decided in the page, not here (targetlessStepProblem): it runs on a studio
|
|
1284
|
+
// that lists it in TARGETLESS_EVENT_RELATIONS and does nothing on an older one. Only a caller with no page to ask
|
|
1285
|
+
// (create_scene, which binds no relation without a target) refuses it outright.
|
|
1286
|
+
if (target.length > 1 && target[1] === 'no-page' && targetlessStepProblem(relationType, target[0]))
|
|
1287
|
+
return targetlessStepProblem(relationType, target[0]);
|
|
1264
1288
|
if (relationType === 'on_event_fire_if')
|
|
1265
1289
|
return fireIfProblem(p);
|
|
1266
1290
|
if (relationType === 'time_expression' && typeof p.expression === 'string')
|
|
@@ -1388,14 +1412,26 @@ function equationFlipNote(relationType, params) {
|
|
|
1388
1412
|
}
|
|
1389
1413
|
function unreadOptionWarning(relationType, params, injected) {
|
|
1390
1414
|
const keys = typeof relationType === 'string' ? RELATION_PARAM_KEYS[relationType] : undefined;
|
|
1391
|
-
|
|
1415
|
+
return keys ? unreadKeysWarning(String(relationType), keys, params, injected) : null;
|
|
1416
|
+
}
|
|
1417
|
+
/** The keys of `params` outside `keys`, named with a "did you mean" and the list that is read; null when all are read. */
|
|
1418
|
+
function unreadKeysWarning(reader, keys, params, injected = []) {
|
|
1419
|
+
if (!params || typeof params !== 'object')
|
|
1392
1420
|
return null;
|
|
1393
1421
|
const unknown = Object.keys(params).filter((k) => !keys.includes(k) && !injected.includes(k));
|
|
1394
1422
|
if (!unknown.length)
|
|
1395
1423
|
return null;
|
|
1396
1424
|
const norm = (x) => x.replace(/_/g, '').toLowerCase();
|
|
1397
1425
|
const hint = unknown.map((k) => keys.find((x) => norm(x) === norm(k)) ?? keys.find((x) => editDistance(norm(x), norm(k)) <= (k.length > 5 ? 2 : 1))).map((x, i) => (x ? `${unknown[i]} → ${x}?` : '')).filter(Boolean);
|
|
1398
|
-
return `${
|
|
1426
|
+
return `${reader} does not read ${unknown.join(', ')}, so ${unknown.length > 1 ? 'they were' : 'it was'} ignored${hint.length ? ` (${hint.join('; ')})` : ''}. ${reader} reads: ${keys.join(', ')}.`;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* An animationOptions key app.animate never reads (ANIMATION_OPTION_KEYS, generated from the engine). The batch hands
|
|
1430
|
+
* the options over as they are, so an unknown key was dropped with the op reported done. pinepaper_animate's own
|
|
1431
|
+
* names (intensity, speed …) are translated before this (batch-op-names.ts), so they never reach it.
|
|
1432
|
+
*/
|
|
1433
|
+
export function animationOptionWarning(options) {
|
|
1434
|
+
return unreadKeysWarning('animationOptions', ANIMATION_OPTION_KEYS, options);
|
|
1399
1435
|
}
|
|
1400
1436
|
const VISIBILITY_REACTION = /^on_(event|enter|exit)_set_visibility$/;
|
|
1401
1437
|
/**
|
|
@@ -1541,13 +1577,14 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
|
1541
1577
|
mismatch(`bursts[${i}]`, own.count ?? out.count, own.velocities ?? out.velocities);
|
|
1542
1578
|
});
|
|
1543
1579
|
// A RANGE WHERE THE STUDIO READS ONE NUMBER is not drawn as a range (1.6.24
|
|
1544
|
-
// probe:
|
|
1545
|
-
//
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1548
|
-
//
|
|
1549
|
-
//
|
|
1550
|
-
|
|
1580
|
+
// probe: count [a, b] → 0 on older studios, the middle on FxTool #228). Only
|
|
1581
|
+
// the keys that take one number on EVERY build are named here. gravity, drag,
|
|
1582
|
+
// friction, opacity, sizeEnd and jitter were too, until the studio began to
|
|
1583
|
+
// sample them per particle: the tester measured drag 101–206 against this text
|
|
1584
|
+
// saying one value. What a given studio does with a range on those is its own
|
|
1585
|
+
// to say (emitter-range-not-read, in engineWarnings), so it is true for
|
|
1586
|
+
// whichever studio the server is pointed at.
|
|
1587
|
+
const SCALAR_ONLY = ['fadeIn', 'fadeOut', 'strokeWidth', 'jitterHz', 'aspect', 'count'];
|
|
1551
1588
|
const SPEC_ONLY = ['rate', 'start', 'end', 'seed'];
|
|
1552
1589
|
const OUTCOME = {
|
|
1553
1590
|
count: 'an older studio reads a range here as 0 and draws no particles',
|
|
@@ -1556,7 +1593,7 @@ export function normalizeEmitterSpec(spec, assumeDefaultCount = true) {
|
|
|
1556
1593
|
for (const k of keys) {
|
|
1557
1594
|
const v = o[k];
|
|
1558
1595
|
if (Array.isArray(v) && v.length >= 2) {
|
|
1559
|
-
warnings.push(`${where}.${k} ${JSON.stringify(v)} is not drawn as a range: ${k} takes one number, so the studio draws one value for every particle (its default, or the middle of the range, depending on the build${OUTCOME[k] ? `; ${OUTCOME[k]}` : ''}). Pass the single value you want
|
|
1596
|
+
warnings.push(`${where}.${k} ${JSON.stringify(v)} is not drawn as a range: ${k} takes one number, so the studio draws one value for every particle (its default, or the middle of the range, depending on the build${OUTCOME[k] ? `; ${OUTCOME[k]}` : ''}). Pass the single value you want.`);
|
|
1560
1597
|
}
|
|
1561
1598
|
}
|
|
1562
1599
|
const f = o.fade;
|
|
@@ -1916,12 +1953,14 @@ const TEXT_KEY_ENGINE_NAMES = {
|
|
|
1916
1953
|
wordSpacing: ['wordSpacing'],
|
|
1917
1954
|
textDecoration: ['textDecoration'],
|
|
1918
1955
|
textTransform: ['textTransform'],
|
|
1919
|
-
// Not text, same failure (SHEARKEY, mcp-cloud #29):
|
|
1920
|
-
// channel
|
|
1921
|
-
//
|
|
1956
|
+
// Not text, same failure (SHEARKEY, mcp-cloud #29): a studio before FxTool
|
|
1957
|
+
// #263 has no skew channel, so skew keys were stored and drew nothing. The
|
|
1958
|
+
// current one animates skewX / skewY. `shear` is no channel on any studio:
|
|
1959
|
+
// keyed, it drew nothing with no word (measured on 56c79202), so it is named
|
|
1960
|
+
// unless a studio ever lists `shear` itself.
|
|
1922
1961
|
skewX: ['skewX'],
|
|
1923
1962
|
skewY: ['skewY'],
|
|
1924
|
-
shear: ['shear'
|
|
1963
|
+
shear: ['shear'],
|
|
1925
1964
|
};
|
|
1926
1965
|
/** A JS expression (run in the page) → the keys of `keys` this studio does not animate. '' when none are text keys. */
|
|
1927
1966
|
function unanimatedTextKeysJs(keys) {
|
|
@@ -1936,7 +1975,49 @@ function unanimatedTextKeysJs(keys) {
|
|
|
1936
1975
|
})()`;
|
|
1937
1976
|
}
|
|
1938
1977
|
/** The sentence for the keys unanimatedTextKeysJs found, built in the page. */
|
|
1939
|
-
const UNANIMATED_TEXT_KEYS_SENTENCE = `' is not an animatable property in this studio (app.listAnimatableProperties does not list it), so it had no effect: a key animates only where that list names it. Here, set a fixed value with pinepaper_modify_item, or animate fontSize or scaleX
|
|
1978
|
+
const UNANIMATED_TEXT_KEYS_SENTENCE = `' is not an animatable property in this studio (app.listAnimatableProperties does not list it), so it had no effect: a key animates only where that list names it. Here, set a fixed value with pinepaper_modify_item, or animate fontSize or scaleX. For a skew, key skewX / skewY (degrees) where the studio lists them, else a transform matrix track.'`;
|
|
1979
|
+
/**
|
|
1980
|
+
* The warnings a keyframe track's result carries, as object-literal fields (", key: value" …): keys that leave out a
|
|
1981
|
+
* property, loop in an export, x beside fontSize on an older studio, easing on the first key. One function for
|
|
1982
|
+
* pinepaper_keyframe_animate and the batch keyframe_animate op: the batch had its own copy of the emitter and none
|
|
1983
|
+
* of these (SIZEKEY-PAIR: the same keys warned through the tool and not through the batch).
|
|
1984
|
+
*/
|
|
1985
|
+
function keyframeResultNotes(keyframes, { loop = false, append = false, easingAt } = {}) {
|
|
1986
|
+
// A KEY THAT LEAVES A PROPERTY OUT (round 9 HH, engine 2.30): the engine
|
|
1987
|
+
// treats the gap as a break in that property's track rather than holding or
|
|
1988
|
+
// interpolating across it, silently. Named here — which keys lack which
|
|
1989
|
+
// properties — with the fix a caller can make now: give every key the full set.
|
|
1990
|
+
const propSets = keyframes.map((k) => Object.keys(k.properties ?? {}));
|
|
1991
|
+
const allProps = [...new Set(propSets.flat())];
|
|
1992
|
+
const gaps = keyframes
|
|
1993
|
+
.map((k, i) => ({ time: k.time, missing: allProps.filter((p) => !propSets[i].includes(p)) }))
|
|
1994
|
+
.filter((g) => g.missing.length);
|
|
1995
|
+
// With append the gaps that matter are the MERGED track's (an earlier key
|
|
1996
|
+
// with no scale beside a new key with one), so that check runs in the page
|
|
1997
|
+
// on the result instead (below).
|
|
1998
|
+
const gapNote = !append && gaps.length && keyframes.length > 1
|
|
1999
|
+
? `, propertyGaps: ${JSON.stringify(gaps.slice(0, 10))}, gapWarning: ${JSON.stringify(`${gaps.length} keyframe(s) leave out a property other keys animate (${[...new Set(gaps.flatMap((g) => g.missing))].join(', ')}). The engine can break that property's track at such a key; repeat the value on every key — hold it by repeating the previous value.`)}`
|
|
2000
|
+
: '';
|
|
2001
|
+
const first = [...keyframes].sort((a, b) => a.time - b.time)[0];
|
|
2002
|
+
// loop:true repeats in the editor; an export of the measured build played
|
|
2003
|
+
// the keyframes ONCE and held (round 7, 2.24). Said where it is set, with
|
|
2004
|
+
// the two ways that do export, until the engine's export clock wraps them.
|
|
2005
|
+
const loopNote = loop
|
|
2006
|
+
? `, loopNote: ${JSON.stringify('loop:true repeats in the editor, but has been measured playing ONCE in an exported video and then holding the last keyframe. For an export longer than one cycle, check a frame in the second cycle — if it holds, write the keyframes out per cycle, or use a loop preset (pinepaper_animate), which does cycle in export.')}`
|
|
2007
|
+
: '';
|
|
2008
|
+
// SIZEKEY-PAIR (mcp-cloud #28): on a studio before FxTool #262, x (or
|
|
2009
|
+
// position) keyed beside fontSize on one text item drifted in an export: the
|
|
2010
|
+
// size was applied after the position, and a text resizes around its anchor.
|
|
2011
|
+
// Said there, with the two ways that hold; a current studio is quiet.
|
|
2012
|
+
const sizekeyPairNote = (allProps.includes('x') || allProps.includes('position')) && allProps.includes('fontSize')
|
|
2013
|
+
// A studio that writes text size before position (FxTool #262, which also brought _applyScaleParams) holds x.
|
|
2014
|
+
? `, ...(typeof app._applyScaleParams !== 'function' ? { sizekeyPairWarning: ${JSON.stringify('x (or position) and fontSize keyed on the same text item drift off the keyed position in an export on this studio (SIZEKEY-PAIR). Animate scaleX / scaleY instead of fontSize, or move a group that holds the text; or update the studio.')} } : {})`
|
|
2015
|
+
: '';
|
|
2016
|
+
const easingNote = easingAt !== 'departure' && first?.easing && first.easing !== 'linear'
|
|
2017
|
+
? `, note: ${JSON.stringify(`easing '${first.easing}' is on the first keyframe, where it has no effect by default: easing shapes the segment ARRIVING at a keyframe. Put it on the keyframe you are moving to, or pass easingAt: 'departure' to read every easing as the move LEAVING its key (current studio).`)}`
|
|
2018
|
+
: '';
|
|
2019
|
+
return `${easingNote}${sizekeyPairNote}${loopNote}${gapNote}`;
|
|
2020
|
+
}
|
|
1940
2021
|
function generateKeyframeAnimateCode(itemId, keyframes, duration, loop = false, timeOffset, clipInPoint, clipOutPoint, timeUnits, append = false, easingAt) {
|
|
1941
2022
|
const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
|
|
1942
2023
|
const keyframesJson = JSON.stringify(normalizeKeyframePositions(keyframes), null, 2);
|
|
@@ -1988,38 +2069,7 @@ String(JSON.stringify(levelKeys)) +
|
|
|
1988
2069
|
// never applies — a Ken Burns eased only there rendered linear. Said in the
|
|
1989
2070
|
// result rather than silently moved: moving it would change what the other
|
|
1990
2071
|
// keys the caller wrote mean.
|
|
1991
|
-
|
|
1992
|
-
// treats the gap as a break in that property's track rather than holding or
|
|
1993
|
-
// interpolating across it, silently. Named here — which keys lack which
|
|
1994
|
-
// properties — with the fix a caller can make now: give every key the full set.
|
|
1995
|
-
const propSets = keyframes.map((k) => Object.keys(k.properties ?? {}));
|
|
1996
|
-
const allProps = [...new Set(propSets.flat())];
|
|
1997
|
-
const gaps = keyframes
|
|
1998
|
-
.map((k, i) => ({ time: k.time, missing: allProps.filter((p) => !propSets[i].includes(p)) }))
|
|
1999
|
-
.filter((g) => g.missing.length);
|
|
2000
|
-
// With append the gaps that matter are the MERGED track's (an earlier key
|
|
2001
|
-
// with no scale beside a new key with one), so that check runs in the page
|
|
2002
|
-
// on the result instead (below).
|
|
2003
|
-
const gapNote = !append && gaps.length && keyframes.length > 1
|
|
2004
|
-
? `, propertyGaps: ${JSON.stringify(gaps.slice(0, 10))}, gapWarning: ${JSON.stringify(`${gaps.length} keyframe(s) leave out a property other keys animate (${[...new Set(gaps.flatMap((g) => g.missing))].join(', ')}). The engine can break that property's track at such a key; repeat the value on every key — hold it by repeating the previous value.`)}`
|
|
2005
|
-
: '';
|
|
2006
|
-
const first = [...keyframes].sort((a, b) => a.time - b.time)[0];
|
|
2007
|
-
// loop:true repeats in the editor; an export of the measured build played
|
|
2008
|
-
// the keyframes ONCE and held (round 7, 2.24). Said where it is set, with
|
|
2009
|
-
// the two ways that do export, until the engine's export clock wraps them.
|
|
2010
|
-
const loopNote = loop
|
|
2011
|
-
? `, loopNote: ${JSON.stringify('loop:true repeats in the editor, but has been measured playing ONCE in an exported video and then holding the last keyframe. For an export longer than one cycle, check a frame in the second cycle — if it holds, write the keyframes out per cycle, or use a loop preset (pinepaper_animate), which does cycle in export.')}`
|
|
2012
|
-
: '';
|
|
2013
|
-
// SIZEKEY-PAIR (mcp-cloud #28): x (or position) keyed beside fontSize on one
|
|
2014
|
-
// text item has been measured drifting in an export: the size is applied
|
|
2015
|
-
// after the position, and a text resizes around its anchor, so the box leaves
|
|
2016
|
-
// the keyed x. Said, with the two ways that hold.
|
|
2017
|
-
const sizekeyPairNote = (allProps.includes('x') || allProps.includes('position')) && allProps.includes('fontSize')
|
|
2018
|
-
? `, sizekeyPairWarning: ${JSON.stringify('x (or position) and fontSize keyed on the same text item have been measured drifting off the keyed position in an export (SIZEKEY-PAIR). Animate scaleX / scaleY instead of fontSize, or move a group that holds the text.')}`
|
|
2019
|
-
: '';
|
|
2020
|
-
const easingNote = easingAt !== 'departure' && first?.easing && first.easing !== 'linear'
|
|
2021
|
-
? `, note: ${JSON.stringify(`easing '${first.easing}' is on the first keyframe, where it has no effect by default: easing shapes the segment ARRIVING at a keyframe. Put it on the keyframe you are moving to, or pass easingAt: 'departure' to read every easing as the move LEAVING its key (current studio).`)}`
|
|
2022
|
-
: '';
|
|
2072
|
+
const __notes = keyframeResultNotes(keyframes, { loop, append, easingAt });
|
|
2023
2073
|
const textKeysJs = unanimatedTextKeysJs(keyframes.flatMap((k) => Object.keys(k.properties ?? {})));
|
|
2024
2074
|
const textKeyCheck = textKeysJs ? `
|
|
2025
2075
|
const __ignored = ${textKeysJs};
|
|
@@ -2062,7 +2112,7 @@ String(calculatedDuration) +
|
|
|
2062
2112
|
String(loop) +
|
|
2063
2113
|
String(append ? ', append: true' : '') +
|
|
2064
2114
|
", ...(__track ? { track: __track } : {})" +
|
|
2065
|
-
String(append ? `, ...(__gaps.length ? { propertyGaps: __gaps.slice(0, 10), gapWarning: __gaps.length + ' key(s) of the merged track leave out a property other keys animate (' + __gaps.reduce(function(a, g) { g.missing.forEach(function(p) { if (a.indexOf(p) < 0) a.push(p); }); return a; }, []).join(', ') + '). The engine can break that property at such a key; give each key the full set — hold a value by repeating it.' } : {})` : '') + String(timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : '') + String(clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : '') + String(clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : '') + String(
|
|
2115
|
+
String(append ? `, ...(__gaps.length ? { propertyGaps: __gaps.slice(0, 10), gapWarning: __gaps.length + ' key(s) of the merged track leave out a property other keys animate (' + __gaps.reduce(function(a, g) { g.missing.forEach(function(p) { if (a.indexOf(p) < 0) a.push(p); }); return a; }, []).join(', ') + '). The engine can break that property at such a key; give each key the full set — hold a value by repeating it.' } : {})` : '') + String(timeOffset !== undefined ? `, timeOffset: ${timeOffset}` : '') + String(clipInPoint !== undefined ? `, clipInPoint: ${clipInPoint}` : '') + String(clipOutPoint !== undefined ? `, clipOutPoint: ${clipOutPoint}` : '') + String(__notes) + String(runtimeWarning) + " };\n})();\n").trim();
|
|
2066
2116
|
}
|
|
2067
2117
|
/**
|
|
2068
2118
|
* Template for executing generators
|
|
@@ -3377,14 +3427,22 @@ String(JSON.stringify(validated.presetId)) +
|
|
|
3377
3427
|
).trim();
|
|
3378
3428
|
}
|
|
3379
3429
|
{
|
|
3380
|
-
const problem = relationSpecProblem(validated.relationType, validated.params);
|
|
3430
|
+
const problem = relationSpecProblem(validated.relationType, validated.params, validated.targetId);
|
|
3381
3431
|
if (problem)
|
|
3382
3432
|
return `(function() { return { success: false, relationType: ${jsLiteral(validated.relationType)}, error: ${jsLiteral(problem)} }; })();`;
|
|
3383
3433
|
}
|
|
3384
3434
|
if (validated.relationType === 'moves_along_path') {
|
|
3385
3435
|
return generateMovesAlongPathCode(validated.sourceId, validated.targetId ?? undefined, (validated.params ?? {}));
|
|
3386
3436
|
}
|
|
3387
|
-
const
|
|
3437
|
+
const built = generateAddRelationCode(validated.sourceId, validated.targetId || validated.sourceId, validated.relationType, validated.params);
|
|
3438
|
+
// Target-less step_active: refused in the page, only on a studio that would drop it (targetlessStepProblem).
|
|
3439
|
+
const stepNoTarget = targetlessStepProblem(validated.relationType, validated.targetId);
|
|
3440
|
+
const code = stepNoTarget
|
|
3441
|
+
? `(function() {
|
|
3442
|
+
if (!${STEP_ACTIVE_TARGETLESS_JS}) return { success: false, relationType: 'on_event_step_active', error: ${jsLiteral(stepNoTarget)} };
|
|
3443
|
+
return ${built.trim().replace(/;$/, '')};
|
|
3444
|
+
})();`
|
|
3445
|
+
: built;
|
|
3388
3446
|
// NO TARGET (event reactions like on_event_play_sound, self-relations): the
|
|
3389
3447
|
// engine is handed the source as the target, but the reply must not say
|
|
3390
3448
|
// "item_2 → item_2" — a model reads that as the event wired to itself.
|
|
@@ -4065,7 +4123,7 @@ if (typeof _gen === 'function') {
|
|
|
4065
4123
|
for (const relation of relations) {
|
|
4066
4124
|
const { source, target, type, params = {} } = relation;
|
|
4067
4125
|
const paramsStr = JSON.stringify(params);
|
|
4068
|
-
const specProblem = relationSpecProblem(type, params);
|
|
4126
|
+
const specProblem = relationSpecProblem(type, params, target, 'no-page');
|
|
4069
4127
|
if (specProblem) {
|
|
4070
4128
|
codeParts.push(`
|
|
4071
4129
|
// Refused relation: ${oneLine(source)} ${oneLine(type)} ${oneLine(target)}
|
|
@@ -5062,13 +5120,16 @@ const targetId = ${animItemRef};
|
|
|
5062
5120
|
const item = app.getItemById(targetId);
|
|
5063
5121
|
if (!item) throw new Error('Item not found: ' + targetId);
|
|
5064
5122
|
app.animate(item, { animationType: ${JSON.stringify(String(op.animationType))}, ...${animOpts} });
|
|
5065
|
-
return { itemId: targetId, animationType: ${JSON.stringify(String(op.animationType))} };
|
|
5123
|
+
return { itemId: targetId, animationType: ${JSON.stringify(String(op.animationType))}${(() => { const w = animationOptionWarning(op.animationOptions); return w ? `, warning: ${jsLiteral(w)}` : ''; })()} };
|
|
5066
5124
|
`;
|
|
5067
5125
|
case 'relation':
|
|
5068
5126
|
const srcRef = batchRef(op.sourceId);
|
|
5069
5127
|
const tgtRef = batchRef(op.targetId);
|
|
5070
5128
|
const relOpts = JSON.stringify(op.relationOptions || {});
|
|
5071
|
-
const fireIf = relationSpecProblem(op.relationType, op.relationOptions);
|
|
5129
|
+
const fireIf = relationSpecProblem(op.relationType, op.relationOptions, op.targetId);
|
|
5130
|
+
// Target-less step_active: refused in the page, only on a studio that would drop it (targetlessStepProblem).
|
|
5131
|
+
const stepNoTarget = targetlessStepProblem(op.relationType, op.targetId);
|
|
5132
|
+
const stepGuard = stepNoTarget ? `if (!${STEP_ACTIVE_TARGETLESS_JS}) throw new Error(${jsLiteral('on_event_step_active: ' + stepNoTarget)});\n` : '';
|
|
5072
5133
|
if (fireIf)
|
|
5073
5134
|
return `
|
|
5074
5135
|
throw new Error(${JSON.stringify(String(op.relationType) + ': ' + fireIf)});
|
|
@@ -5092,6 +5153,7 @@ if (!__members.length) throw new Error('no item is named like ' + ${jsLiteral(op
|
|
|
5092
5153
|
: `const __members = [{ id: ${srcRef} }];
|
|
5093
5154
|
if (!app.getItemById(__members[0].id)) throw new Error('Source not found: ' + __members[0].id);`;
|
|
5094
5155
|
return ("\n" +
|
|
5156
|
+
String(stepGuard) +
|
|
5095
5157
|
String(members) +
|
|
5096
5158
|
"\nconst targetId = " +
|
|
5097
5159
|
String(noTarget ? 'null' : tgtRef) +
|
|
@@ -5119,10 +5181,11 @@ String(type) +
|
|
|
5119
5181
|
", sourceIds: __failed, error: " +
|
|
5120
5182
|
String(op.update === true ? '__why' : `'the studio refused the ' + ${type} + ' relation for ' + __failed.join(', ') + ' — nothing was bound' + (__why ? ': ' + __why : '.')`) +
|
|
5121
5183
|
" };\n}\nreturn { success: true, relationType: " +
|
|
5122
|
-
String(type) +
|
|
5123
|
-
", " + String(op.update === true ? 'updated' : 'bound') + ": __done, targetId" + String(glob ? ', count: __members.length' : '') + String((() => { const w = relationOptionWarning(op.relationType, op.relationOptions, ['index', 'count', 'pattern']); return w ? `, optionWarning: ${jsLiteral(w)}` : ''; })()) + ", ...(__failed.length ? { failed: __failed, warning: __failed.length + ' of ' + __members.length + ' were not " + String(op.update === true ? 'changed' : 'bound') + ": ' + __failed.join(', ') } : {}) };\n");
|
|
5184
|
+
String(type) + ", " + String(op.update === true ? 'updated' : 'bound') + ": __done, targetId" + String(glob ? ', count: __members.length' : '') + String((() => { const w = relationOptionWarning(op.relationType, op.relationOptions, ['index', 'count', 'pattern']); return w ? `, optionWarning: ${jsLiteral(w)}` : ''; })()) + ", ...(__failed.length ? { failed: __failed, warning: __failed.length + ' of ' + __members.length + ' were not " + String(op.update === true ? 'changed' : 'bound') + ": ' + __failed.join(', ') } : {}) };\n");
|
|
5124
5185
|
}
|
|
5125
|
-
return ("\
|
|
5186
|
+
return ("\n" +
|
|
5187
|
+
String(stepGuard) +
|
|
5188
|
+
"const sourceId = " +
|
|
5126
5189
|
String(srcRef) +
|
|
5127
5190
|
";\nconst targetId = " +
|
|
5128
5191
|
String(noTarget ? 'null' : tgtRef) +
|
|
@@ -5137,9 +5200,7 @@ String(relOpts) +
|
|
|
5137
5200
|
");\n// NOT BOUND IS NOT A SUCCESS (owner rule 1, B119). addRelation answers false\n// when it refuses — spring_follow \"not loaded yet\" — and this op returned\n// { relationId: false } with nothing marking it failed, so the batch said\n// success over a relation that does not exist.\nif (!relationId) {\n const __why = " +
|
|
5138
5201
|
String(relationRefusalReasonJs(jsLiteral(op.relationType))) +
|
|
5139
5202
|
";\n return { success: false, relationId: false, sourceId, targetId, relationType: " +
|
|
5140
|
-
String(JSON.stringify(op.relationType)) +
|
|
5141
|
-
",\n error: 'the studio refused the ' + " +
|
|
5142
|
-
String(JSON.stringify(op.relationType)) + " + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };\n}\nreturn { relationId, sourceId, targetId, relationType: " + String(JSON.stringify(String(op.relationType))) + String((() => { const w = relationOptionWarning(op.relationType, op.relationOptions); return w ? `, warning: ${jsLiteral(w)}` : ''; })()) + " };\n");
|
|
5203
|
+
String(JSON.stringify(op.relationType)) + ",\n error: 'the studio refused the ' + " + String(JSON.stringify(op.relationType)) + " + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };\n}\nreturn { relationId, sourceId, targetId, relationType: " + String(JSON.stringify(String(op.relationType))) + String((() => { const w = relationOptionWarning(op.relationType, op.relationOptions); return w ? `, warning: ${jsLiteral(w)}` : ''; })()) + " };\n");
|
|
5143
5204
|
case 'delete':
|
|
5144
5205
|
const delRef = batchRef(op.itemId);
|
|
5145
5206
|
return `
|
|
@@ -5240,7 +5301,7 @@ String(kfLoop) +
|
|
|
5240
5301
|
String(kfAppend ? ", mode: 'merge'" : '') +
|
|
5241
5302
|
String(kfExtraStr) +
|
|
5242
5303
|
" });" +
|
|
5243
|
-
String(kfTextCheck) + "\nreturn { success: true, itemId: targetId, duration: " + String(kfDuration) + ", loop: " + String(kfLoop) + String(kfAppend ? ', append: true' : '') + String(kfAppend ? '' : `, ...(_prevKeys ? { track: { replaced: true, previousKeys: _prevKeys, note: 'the item already had a keyframe track; this op replaced it. To add keys to it instead, pass append: true.' } } : {})`) + String(kfIgnored) + " };\n");
|
|
5304
|
+
String(kfTextCheck) + "\nreturn { success: true, itemId: targetId, duration: " + String(kfDuration) + ", loop: " + String(kfLoop) + String(kfAppend ? ', append: true' : '') + String(keyframeResultNotes(kfSource, { loop: kfLoop === true, append: kfAppend })) + String(kfAppend ? '' : `, ...(_prevKeys ? { track: { replaced: true, previousKeys: _prevKeys, note: 'the item already had a keyframe track; this op replaced it. To add keys to it instead, pass append: true.' } } : {})`) + String(kfIgnored) + " };\n");
|
|
5244
5305
|
}
|
|
5245
5306
|
case 'apply_mask': {
|
|
5246
5307
|
const maskItemRef = batchRef(op.itemId);
|
|
@@ -5643,11 +5704,12 @@ String(JSON.stringify(scaled)) +
|
|
|
5643
5704
|
String(format === 'webm' && transparent === true ? '' : ", 'webm'") +
|
|
5644
5705
|
String(format === 'apng' && transparent === false ? ", 'apng'" : '') +
|
|
5645
5706
|
"].indexOf(fmt) === -1) return [];\n const bg = app.canvasEl && app.canvasEl.style && app.canvasEl.style.backgroundColor;\n const hasColor = !!bg && !/^(transparent|rgba\\([^)]*,\\s*0\\))$/i.test(bg);\n const hasBgItems = !!(app.patternGroup && app.patternGroup.children && app.patternGroup.children.length);\n // A 3D world under the canvas IS the background: since FxTool #50\n // (747315e3) jpg/webp stills composite it, so nothing turns white/black.\n const hasWorld = !!app._world3d;\n // So does a styled scene: one raster sized to COVER the frame (gate run 3:\n // every export after styled_scene warned of black areas it does not have).\n const h" +
|
|
5646
|
-
"asStyledScene = !!(app.textItemGroup && app.textItemGroup.children\n && app.textItemGroup.children.some(function (it) { return it && it.data && it.data.styledScene && it.visible !== false; }));\n if (hasColor || hasBgItems || hasWorld || hasStyledScene) return [];\n
|
|
5647
|
-
"
|
|
5648
|
-
"(o.id || 'a text item') + ' (' + JSON.stringify(String(o.content || '').slice(0, 40)) + '): ' + (o.reason || 'overflows'), items: [o.id] };\n });\n } catch (e) { return []; }\n }\n function fidelity(fmt) {
|
|
5649
|
-
"
|
|
5650
|
-
"
|
|
5707
|
+
"asStyledScene = !!(app.textItemGroup && app.textItemGroup.children\n && app.textItemGroup.children.some(function (it) { return it && it.data && it.data.styledScene && it.visible !== false; }));\n if (hasColor || hasBgItems || hasWorld || hasStyledScene) return [];\n // The fill is the studio's page colour, not black: every opaque export\n // path asks the engine's _pageColor, which is white when no page colour\n // is set (an older engine fell back to black for video). An apng was\n // measured white (bitrate sweep, 11.02), so name the page colour, not a hue.\n return [{ code: 'alpha_dropped', message: fmt === 'apng'\n ? 'apng with transparent: f" +
|
|
5708
|
+
"alse and no background colour: the frames are filled with the studio page colour (white in current studios), not a colour this scene chose. Set a background colour, or drop transparent: false to keep the alpha.'\n : fmt === 'jpg'\n ? 'jpg has no transparency and this scene has no background, so transparent areas are filled white. Use png or webp to keep them.'\n : fmt + ' is exported without transparency and this scene has no background, so transparent areas are filled with the studio page colour (white in current studios), not a colour this scene chose. Set a background colour, or export apng (animated, full alpha) or png to keep it.' }];\n } catch (e) { return [];" +
|
|
5709
|
+
" }\n }\n // TEXT THAT DOES NOT FIT (FxTool 661224ef: textOverflowReport) — text past\n // its fit box at the minimum size, or off the frame. The pre-batch check for\n // certificates, speaker cards and localised variants, on every export.\n function textOverflow() {\n try {\n const rep = (typeof app.textOverflowReport === 'function' && app.textOverflowReport()) || [];\n return rep.slice(0, 10).map(function(o) {\n return { code: 'text_overflow', message: (o.id || 'a text item') + ' (' + JSON.stringify(String(o.content || '').slice(0, 40)) + '): ' + (o.reason || 'overflows'), items: [o.id] };\n });\n } catch (e) { return []; }\n }\n function fidelity(fmt) {" +
|
|
5710
|
+
"\n const own = alphaLoss(fmt).concat(textOverflow());\n try {\n if (!app.exportEngine || typeof app.exportEngine.exportFidelity !== 'function') {\n return { fidelity: { available: false, reason: 'this studio cannot check export fidelity — update PinePaper Studio.', ...(own.length ? { warnings: own } : {}) } };\n }\n const r = app.exportEngine.exportFidelity(fmt);\n if (!r) { return { fidelity: { available: false, reason: 'the fidelity check returned nothing.', ...(own.length ? { warnings: own } : {}) } }; }\n const warnings = (r.warnings || []).concat(own);\n return {\n fidelity: {\n available: true,\n warnings: warnings," +
|
|
5711
|
+
"\n checked: r.checked,\n ...(warnings.length === 0\n ? { note: 'this scene loses nothing to ' + fmt + '. That is a statement about THIS SCENE, not a claim that the format is lossless.' }\n : {}),\n },\n };\n } catch (e) {\n return { fidelity: { available: false, reason: (e && e.message) || 'the fidelity check threw.', ...(own.length ? { warnings: own } : {}) } };\n }\n }\n\n // Preflight: same resolved settings as the real export, but render nothing.\n // Branching HERE rather than in a separate tool is deliberate — the estimate\n // has to see the same platform dimensions, fps and quality the export would\n // use, and a " +
|
|
5712
|
+
"parallel tool would re-derive them and drift.\n if (" +
|
|
5651
5713
|
String(estimateOnly ? 'true' : 'false') +
|
|
5652
5714
|
") {\n if (typeof app.estimateExportSize !== 'function') {\n return { success: false, estimateOnly: true, error: 'This PinePaper build has no estimateExportSize() — update the editor to preflight export size.' };\n }\n const est = await app.estimateExportSize({\n format,\n duration: " +
|
|
5653
5715
|
String(videoDuration) +
|
|
@@ -7160,6 +7222,9 @@ String(jsLiteral(format)) +
|
|
|
7160
7222
|
|
|
7161
7223
|
|
|
7162
7224
|
|
|
7225
|
+
|
|
7226
|
+
|
|
7227
|
+
|
|
7163
7228
|
|
|
7164
7229
|
|
|
7165
7230
|
|
|
@@ -10569,7 +10634,7 @@ String(jsLiteral(input.filterName || '')) +
|
|
|
10569
10634
|
String(JSON.stringify(input.itemId)) +
|
|
10570
10635
|
", filter: " +
|
|
10571
10636
|
String(JSON.stringify(input.filterName || '')) +
|
|
10572
|
-
String(input.filterName === 'bloom' ? `, warning: 'bloom is drawn inside the image\\'s own pixels, so its halo stops at the image edge
|
|
10637
|
+
String(input.filterName === 'bloom' ? `, ...(typeof app._writeRasterPixels !== 'function' ? { warning: 'on this studio bloom is drawn inside the image\\'s own pixels, so its halo stops at the image edge (BLOOMCLIP). For a halo past the edge, use pinepaper_apply_effect glow or a shadow, or update the studio.' } : {})` : '') +
|
|
10573
10638
|
" };\n } catch (e) {\n return { success: false, action: 'apply', error: 'Failed to apply filter: ' + e.message };\n }\n})();"
|
|
10574
10639
|
).trim();
|
|
10575
10640
|
case 'chain':
|
|
@@ -12593,11 +12658,19 @@ String(sheet.format === 'png' ? '' : ', 0.82') +
|
|
|
12593
12658
|
// A fire_if the doc declares is checked before anything is built: refused, the whole document is, with every reason.
|
|
12594
12659
|
const specProblems = ops
|
|
12595
12660
|
.filter((o) => o.op === 'addRelation')
|
|
12596
|
-
.map((o) => { const r = o; const why = relationSpecProblem(r.relation, r.params); return why ? `${r.from} -> ${r.to} (${r.relation}): ${why}` : null; })
|
|
12661
|
+
.map((o) => { const r = o; const why = relationSpecProblem(r.relation, r.params, r.to); return why ? `${r.from} -> ${r.to} (${r.relation}): ${why}` : null; })
|
|
12597
12662
|
.filter((x) => !!x);
|
|
12598
12663
|
if (specProblems.length) {
|
|
12599
12664
|
return `(function() { return { success: false, error: ${jsLiteral('nothing was built: ' + specProblems.join(' | '))} }; })();`;
|
|
12600
12665
|
}
|
|
12666
|
+
// Target-less step_active relations refuse the whole document in the page, only on a studio that would drop them.
|
|
12667
|
+
const stepNoTarget = ops
|
|
12668
|
+
.filter((o) => o.op === 'addRelation')
|
|
12669
|
+
.map((o) => { const r = o; const why = targetlessStepProblem(r.relation, r.to); return why ? `${r.from} -> (no target) (${r.relation}): ${why}` : null; })
|
|
12670
|
+
.filter((x) => !!x);
|
|
12671
|
+
const stepDocGuard = stepNoTarget.length
|
|
12672
|
+
? `if (!${STEP_ACTIVE_TARGETLESS_JS}) return { success: false, error: ${jsLiteral('nothing was built: ' + stepNoTarget.join(' | '))} };\n`
|
|
12673
|
+
: '';
|
|
12601
12674
|
const opsJson = JSON.stringify(ops, null, 2);
|
|
12602
12675
|
const diagJson = JSON.stringify(diagnostics);
|
|
12603
12676
|
const docJson = JSON.stringify(input.doc);
|
|
@@ -12609,7 +12682,9 @@ String(sheet.format === 'png' ? '' : ', 0.82') +
|
|
|
12609
12682
|
String(oneLine(ops.filter((o) => o.op === 'create').length)) +
|
|
12610
12683
|
" items, " +
|
|
12611
12684
|
String(oneLine(ops.filter((o) => o.op === 'addRelation').length)) +
|
|
12612
|
-
" relations)\n(async function() {\n
|
|
12685
|
+
" relations)\n(async function() {\n " +
|
|
12686
|
+
String(stepDocGuard) +
|
|
12687
|
+
" // Engine door first: the studio's own OntologyCompiler reads every node facet\n // (geometry, paint, text, the keyframe track, masks) and builds through the\n // template loader's paths — the round trip the studio's pixel tests verify\n // (e2e/graph-instantiate-parity.spec.js). The server-side compile above is\n // kept for the diagnostics preview and for studios that predate the facade,\n // which get the box loop below and a note saying so.\n const doc = " +
|
|
12613
12688
|
String(docJson) +
|
|
12614
12689
|
";\n if (typeof app.instantiateOntology === 'function') {\n // THE LIVE CANVAS, unless one was given: the engine lays out for 1080x1080\n // otherwise, which put items at (540,540) on a 640x360 canvas (gate run 3).\n const __opts = " +
|
|
12615
12690
|
String(engineOptsJson) +
|
|
@@ -12667,8 +12742,6 @@ String(diagJson) +
|
|
|
12667
12742
|
|
|
12668
12743
|
|
|
12669
12744
|
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
12745
|
|
|
12673
12746
|
|
|
12674
12747
|
|
|
@@ -12688,12 +12761,32 @@ String(diagJson) +
|
|
|
12688
12761
|
String(optsJson) +
|
|
12689
12762
|
");\n // THE RELATIONS AS BOUND, so a later agent can find the one to edit without exporting the scene to read it:\n // from, to, type and params, capped. And the custom relation definitions registered on this page, as given.\n const __rels = [];\n let __relCount = 0;\n try {\n const __rr = app.relationRegistry;\n const __active = __rr && typeof __rr.queryActive === 'function' ? __rr.queryActive() : [];\n __active.forEach((e) => (e.associations || []).forEach((a) => {\n __relCount++;\n if (__rels.length >= " +
|
|
12690
12763
|
String(RELATIONS_READBACK_CAP) +
|
|
12691
|
-
") return;\n const __name = e.item && e.item.data && e.item.data.name;\n __rels.push({ from: e.itemId, ...(__name ? { fromName: __name } : {}), to: a.target === undefined ? null : a.target, type: a.relation, params: a.params || {} });\n }));\n } catch (e) { /* the relations are an addition; the lint stands without them */ }\n
|
|
12692
|
-
"
|
|
12693
|
-
|
|
12694
|
-
"
|
|
12695
|
-
"
|
|
12696
|
-
"
|
|
12764
|
+
") return;\n const __name = e.item && e.item.data && e.item.data.name;\n __rels.push({ from: e.itemId, ...(__name ? { fromName: __name } : {}), to: a.target === undefined ? null : a.target, type: a.relation, params: a.params || {} });\n }));\n } catch (e) { /* the relations are an addition; the lint stands without them */ }\n // CUSTOM RELATIONS THIS SCENE USES (#43): one registered before a clear_canvas stayed in the read-back of the scene\n // after it. Those in use are listed; the rest are named in registeredUnused. Their source (computeFunction /\n // applyFunction) only with withSource: true, since an edit of params never needs it.\n const __usedTypes = new Set();\n " +
|
|
12765
|
+
"try { const __rr2 = app.relationRegistry; (__rr2 && typeof __rr2.queryActive === 'function' ? __rr2.queryActive() : []).forEach((e) => (e.associations || []).forEach((a) => __usedTypes.add(a.relation))); } catch (e) { /* none known */ }\n const __allCustom = (typeof window !== 'undefined' && window.__ppCustomRelations) ? Object.values(window.__ppCustomRelations) : [];\n const __strip = (d) => { if (" +
|
|
12766
|
+
String(input.withSource === true ? 'true' : 'false') +
|
|
12767
|
+
") return d; const o = Object.assign({}, d); delete o.computeFunction; delete o.applyFunction; return o; };\n const __custom = __allCustom.filter((d) => __usedTypes.has(d.name)).map(__strip);\n const __unusedCustom = __allCustom.filter((d) => !__usedTypes.has(d.name)).map((d) => d.name);\n // HOW TO CHANGE IT (#43): an editing agent needs the op, not the schemas. Each relation comes with the batch op that\n // changes it; a group bound by a name pattern (\"dot-*\", recorded in each member's params) comes once, with the ops\n // that add and remove a member; a custom motion with the call that redefines it.\n const __edit = { hint: 'To change this scene, send the ops below (your values " +
|
|
12768
|
+
"in relationOptions) in ONE pinepaper_agent_batch_execute call; a custom motion changes by sending its pinepaper_register_custom_relation call again.', groups: [], relations: [], customRelations: [] };\n try {\n const __byPat = new Map();\n __rels.forEach((r) => {\n const pat = r.params && r.params.pattern;\n if (typeof pat === 'string') {\n const k = pat + '|' + r.type + '|' + r.to;\n if (!__byPat.has(k)) __byPat.set(k, { pattern: pat, type: r.type, to: r.to, members: [] });\n __byPat.get(k).members.push(r);\n return;\n }\n __edit.relations.push({ from: r.from, ...(r.fromName ? { fromName: r.fromName } : {}), to: r.to, relationType: r." +
|
|
12769
|
+
"type,\n change: { type: 'relation', sourceId: r.fromName || r.from, ...(r.to != null ? { targetId: r.to } : {}), relationType: r.type, update: true, relationOptions: r.params } });\n });\n __byPat.forEach((g) => {\n const base = Object.assign({}, g.members[0].params); delete base.index; delete base.count; delete base.pattern;\n const names = g.members.map((m) => m.fromName).filter(Boolean).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));\n const prefix = g.pattern.split('*')[0];\n const nums = names.map((n) => Number(n.slice(prefix.length))).filter((n) => Number.isFinite(n));\n const next = prefix + (nums.length ? Math.max.apply(null, " +
|
|
12770
|
+
"nums) + 1 : g.members.length + 1);\n const firstEntry = app.itemRegistry.get(g.members[0].from);\n const bind = { type: 'relation', sourceId: g.pattern, ...(g.to != null ? { targetId: g.to } : {}), relationType: g.type, relationOptions: base };\n __edit.groups.push({ pattern: g.pattern, relationType: g.type, members: g.members.length, ...(names.length ? { first: names[0], last: names[names.length - 1] } : {}), relationOptions: base,\n change: Object.assign({}, bind, { update: true }),\n addMember: [{ type: 'create', itemType: (firstEntry && firstEntry.type) || 'circle', position: [0, 0], properties: { name: next } }, bind],\n ...(names.length ? { removeM" +
|
|
12771
|
+
"ember: [{ type: 'delete', itemId: names[names.length - 1] }, bind] } : {}) });\n });\n __custom.forEach((d) => __edit.customRelations.push({ name: d.name, change: { tool: 'pinepaper_register_custom_relation', args: d } }));\n } catch (e) { /* the read-back stands without the edit ops */ }\n if (!" +
|
|
12772
|
+
String(input.withSource === true ? 'true' : 'false') +
|
|
12773
|
+
" && __custom.length) __edit.customHint = 'custom relation source is left out; pass withSource: true to read the current computeFunction / applyFunction before rewriting one.';\n // A GROUP IS LISTED ONCE (#43): members bound by a name pattern repeated the same relation once per item in\n // `relations`; they are in edit.groups, and counted here.\n const __loose = __rels.filter((r) => !(r.params && typeof r.params.pattern === 'string'));\n const __grouped = __rels.length - __loose.length;\n const __editOut = __edit.groups.length || __edit.relations.length || __edit.customRelations.length ? { edit: __edit } : {};\n if (" +
|
|
12774
|
+
String(input.only === 'edit' ? 'true' : 'false') +
|
|
12775
|
+
") {\n return Object.assign({ success: true }, __editOut.edit ? __editOut : { edit: { hint: 'nothing to change: this scene binds no relations.' } },\n __unusedCustom.length ? { registeredUnused: __unusedCustom } : {});\n }\n // warnings: what the engine's own rules found (TEXT_CLIPS_FRAME: a centre-anchored text running past the\n // frame edge). Passed through when the studio reports them; an older studio has none to pass.\n return { success: true, density: result.density, suggestions: result.suggestions,\n ...(Array.isArray(result.warnings) && result.warnings.length ? { warnings: result.warnings } : {}),\n relations: __loose, ...(__grouped ? { groupedRelations: __grouped" +
|
|
12776
|
+
", groupedNote: 'relations bound by a name pattern are listed once per group in edit.groups' } : {}),\n ...(__relCount > __rels.length ? { relationsShown: __rels.length, relationCount: __relCount } : {}),\n ...(__custom.length ? { customRelations: __custom } : {}),\n ...(__unusedCustom.length ? { registeredUnused: __unusedCustom } : {}),\n ...__editOut };\n})();"
|
|
12777
|
+
|
|
12778
|
+
|
|
12779
|
+
|
|
12780
|
+
|
|
12781
|
+
|
|
12782
|
+
|
|
12783
|
+
|
|
12784
|
+
|
|
12785
|
+
|
|
12786
|
+
|
|
12787
|
+
|
|
12788
|
+
|
|
12789
|
+
|
|
12697
12790
|
|
|
12698
12791
|
|
|
12699
12792
|
|
|
@@ -13699,7 +13792,7 @@ String(interval === null ? 'null' : interval) +
|
|
|
13699
13792
|
${guard}
|
|
13700
13793
|
const r = await app.removeLook(${target});
|
|
13701
13794
|
if (!r || r.ok === false) return { success: false, action: 'remove', error: (r && r.error) || 'nothing to remove' };
|
|
13702
|
-
return { success: true, action: 'remove', target: ${target}, removed: r.removed, scene: r.scene, note: '
|
|
13795
|
+
return { success: true, action: 'remove', target: ${target}, removed: r.removed, scene: r.scene, ...(typeof app._applySkewKeyframePose !== 'function' ? { note: 'this studio restores colours, strokes, shadows, fonts and pixels when a look is removed, not geometry; it has been measured leaving a multi-part character rig about 1 px off its pose (LOOKRESTORE). Check the pose if pixel-exact alignment matters, or update the studio.' } : {}) };
|
|
13703
13796
|
})();`.trim();
|
|
13704
13797
|
case 'grid':
|
|
13705
13798
|
return `
|
|
@@ -13933,16 +14026,18 @@ ${body}
|
|
|
13933
14026
|
return ("\n// Medium: apply " +
|
|
13934
14027
|
String(oneLine(input.medium)) +
|
|
13935
14028
|
"\n(async function() {\n if (typeof app.applyMedium !== 'function') { return { success: false, error: 'app.applyMedium unavailable — update FxTool' }; }\n" +
|
|
13936
|
-
String(input.medium === 'ink' && input.options && input.options.keepColor && !input.options.color ?
|
|
13937
|
-
|
|
13938
|
-
//
|
|
13939
|
-
|
|
13940
|
-
const
|
|
13941
|
-
|
|
13942
|
-
const __css = function(c) { try { return c && typeof c.toCSS === 'function' ? c.toCSS(true) : null; } catch (_) { return null; } }
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
14029
|
+
String(input.medium === 'ink' && input.options && input.options.keepColor && !input.options.color ? (" // INKCOLOR (mcp-cloud #30): keepColor keeps the item's colour, but with no\n // color the ink MARKS take the brush default (#16141c). They take the item's\n // own colour instead.\n const __mopts = " +
|
|
14030
|
+
String(S(input.options)) +
|
|
14031
|
+
";\n // A studio from FxTool #261 on (which also brought _writeRasterPixels) picks the item's colour itself, gradients\n // and text included, and says so in colorNote: leave it to the studio there.\n const __e0 = typeof app._writeRasterPixels === 'function' ? null : app.itemRegistry && app.itemRegistry.get(" +
|
|
14032
|
+
String(S(input.itemId)) +
|
|
14033
|
+
");\n const __it0 = typeof app._writeRasterPixels === 'function' ? null : ((__e0 && __e0.item) || (app.getItemById ? app.getItemById(" +
|
|
14034
|
+
String(S(input.itemId)) +
|
|
14035
|
+
") : null));\n const __css = function(c) { try { return c && typeof c.toCSS === 'function' ? c.toCSS(true) : null; } catch (_) { return null; } };\n const __c0 = __it0 && (__css(__it0.strokeColor) || __css(__it0.fillColor));\n if (__c0) __mopts.color = __c0;\n const r = await app.applyMedium(" +
|
|
14036
|
+
String(S(input.itemId)) +
|
|
14037
|
+
", " +
|
|
14038
|
+
String(S(input.medium)) +
|
|
14039
|
+
", __mopts);"
|
|
14040
|
+
) : ` const r = await app.applyMedium(${S(input.itemId)}, ${S(input.medium)}, ${S(input.options ?? {})});`) +
|
|
13946
14041
|
"\n if (!r || r.ok === false) {\n return { success: false, error: (r && (r.error || r.reason)) || 'the medium could not be applied', medium: " +
|
|
13947
14042
|
String(S(input.medium)) +
|
|
13948
14043
|
", ...(r && r.fidelity ? { fidelity: r.fidelity } : {}) };\n }\n // ink / charcoal restyle IN PLACE (FxTool D32): the same item, marked with\n // data.brushStyle on its paths and texts. Read the mark back, so an item\n // the engine passed over is a failure rather than a success (gate D32).\n if (" +
|
|
@@ -13975,6 +14070,8 @@ String(S(input.medium)) +
|
|
|
13975
14070
|
|
|
13976
14071
|
|
|
13977
14072
|
|
|
14073
|
+
|
|
14074
|
+
|
|
13978
14075
|
).trim();
|
|
13979
14076
|
case 'sdf_stroke': {
|
|
13980
14077
|
// FxTool D5: an SDF capsule stroke rendered to a bitmap and created as a
|