@pinepaper.studio/mcp-server 1.6.9 → 1.6.11
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.md +155 -4
- package/dist/agent/export-manager.d.ts.map +1 -1
- package/dist/agent/export-manager.js +5 -3
- package/dist/agent/export-manager.js.map +1 -1
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +128 -3
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/design/design-systems.d.ts +12 -3
- package/dist/design/design-systems.d.ts.map +1 -1
- package/dist/design/design-systems.js +12 -11
- package/dist/design/design-systems.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +80 -10
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +4 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +30 -0
- package/dist/tools/engine-surface.d.ts.map +1 -0
- package/dist/tools/engine-surface.js +1192 -0
- package/dist/tools/engine-surface.js.map +1 -0
- package/dist/tools/handlers/font.d.ts.map +1 -1
- package/dist/tools/handlers/font.js +77 -10
- package/dist/tools/handlers/font.js.map +1 -1
- package/dist/tools/handlers.d.ts +16 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +126 -4
- package/dist/tools/handlers.js.map +1 -1
- package/dist/types/code-generator.d.ts +15 -0
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +499 -105
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +44 -39
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +19 -3
- package/dist/types/schemas.js.map +1 -1
- package/dist/vendor/design/design-style.d.ts +3 -1
- package/dist/vendor/design/design-style.d.ts.map +1 -1
- package/dist/vendor/design/design-style.js +37 -2
- package/dist/vendor/design/design-style.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +5 -2
|
@@ -181,12 +181,27 @@ app.historyManager.saveState();
|
|
|
181
181
|
*/
|
|
182
182
|
function generateModifyItemCode(itemId, properties) {
|
|
183
183
|
const { contentType, contentFormat, countdownTarget, countdownEndText, ...restProperties } = properties;
|
|
184
|
+
// modifyItem(id, changes), NOT select() + modify().
|
|
185
|
+
//
|
|
186
|
+
// modify() operates on the SELECTION, so this selected an id and then edited
|
|
187
|
+
// whatever was selected — on an id that does not resolve, select changes
|
|
188
|
+
// nothing and modify edits the PREVIOUS selection instead. modifyItem takes
|
|
189
|
+
// the reference and returns false when it cannot resolve it, which is the
|
|
190
|
+
// answer this emitter needs and never asked for.
|
|
191
|
+
//
|
|
192
|
+
// The warning the engine logs on that path is console.warn, which production
|
|
193
|
+
// strips, so the failure was invisible twice over.
|
|
184
194
|
let code = `
|
|
185
195
|
// Modify item ${itemId}
|
|
186
|
-
|
|
187
|
-
// Pass non-dynamic-content properties to app.modify
|
|
196
|
+
let _modified = true;`;
|
|
188
197
|
if (Object.keys(restProperties).length > 0) {
|
|
189
|
-
code +=
|
|
198
|
+
code += `
|
|
199
|
+
_modified = app.modifyItem('${itemId}', ${JSON.stringify(restProperties, null, 2)}) !== false;
|
|
200
|
+
if (!_modified) { return { success: false, error: 'no item ${itemId} — nothing was modified' }; }`;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
code += `
|
|
204
|
+
if (!app.getItemById('${itemId}')) { return { success: false, error: 'no item ${itemId}' }; }`;
|
|
190
205
|
}
|
|
191
206
|
// Handle dynamic content type changes
|
|
192
207
|
if (contentType !== undefined) {
|
|
@@ -212,10 +227,10 @@ if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.it
|
|
|
212
227
|
}
|
|
213
228
|
code += `
|
|
214
229
|
app.historyManager.saveState();
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
return code.trim()
|
|
230
|
+
return { success: true, itemId: '${itemId}' };`;
|
|
231
|
+
// Wrapped, because the body now RETURNS early on a miss rather than falling
|
|
232
|
+
// through to an unconditional success.
|
|
233
|
+
return `(function() {\n${code.trim()}\n})();`;
|
|
219
234
|
}
|
|
220
235
|
/**
|
|
221
236
|
* Template for deleting items
|
|
@@ -675,9 +690,24 @@ function generateSetCanvasSizeCode(width, height, preset) {
|
|
|
675
690
|
: `{ width: ${width}, height: ${height} }`;
|
|
676
691
|
return `
|
|
677
692
|
// Set canvas size
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
693
|
+
(function() {
|
|
694
|
+
// READ THE VERDICT. This called setCanvasSize and then declared success with
|
|
695
|
+
// the dimensions it had ASKED for — so an unknown preset, or a width of -5,
|
|
696
|
+
// came back as a successful resize while the canvas was left exactly as it
|
|
697
|
+
// was. The engine now refuses by name; older builds return undefined, which
|
|
698
|
+
// is not a refusal and must not be read as one.
|
|
699
|
+
const r = app.setCanvasSize(${sizeArg});
|
|
700
|
+
if (r && r.ok === false) {
|
|
701
|
+
return { success: false, error: r.reason || 'the canvas size was refused',
|
|
702
|
+
...(r.known ? { knownPresets: r.known } : {}) };
|
|
703
|
+
}
|
|
704
|
+
app.historyManager.saveState();
|
|
705
|
+
// Report what the canvas ACTUALLY took where the engine says so, rather than
|
|
706
|
+
// echoing the request back.
|
|
707
|
+
return { success: true, width: (r && r.width) || ${width}, height: (r && r.height) || ${height},
|
|
708
|
+
...(r && r.preset ? { preset: r.preset } : {}),
|
|
709
|
+
...(r && r.unbounded !== undefined ? { unbounded: r.unbounded } : {}) };
|
|
710
|
+
})();
|
|
681
711
|
`.trim();
|
|
682
712
|
}
|
|
683
713
|
/**
|
|
@@ -886,19 +916,36 @@ function generateBatchModifyCode(modifications) {
|
|
|
886
916
|
return `
|
|
887
917
|
// Batch modify ${modifications.length} items
|
|
888
918
|
const modifications = ${modsJson};
|
|
919
|
+
// TWO BUGS, ONE CALL, AND BOTH FAILED LOUDLY IN THE WRONG PLACE.
|
|
920
|
+
//
|
|
921
|
+
// 1. The engine takes { item, params } with a LIVE ITEM, or { itemId, params }
|
|
922
|
+
// with a registry id. This put the id STRING into the item field, which is
|
|
923
|
+
// truthy — so the engine's own id lookup never ran, the string was treated
|
|
924
|
+
// as an item, and Object.assign got a string: "Cannot convert undefined or
|
|
925
|
+
// null to object", from a call that named the id correctly.
|
|
926
|
+
// 2. batchModify returns a COUNT, not an array. Calling .map on a number
|
|
927
|
+
// is the second reported error, and it fires even when the modify worked.
|
|
889
928
|
const results = app.batchModify(modifications.map(mod => ({
|
|
890
|
-
|
|
929
|
+
itemId: mod.itemId,
|
|
891
930
|
params: mod.params
|
|
892
931
|
})));
|
|
893
932
|
|
|
894
|
-
//
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
933
|
+
// The engine records what it could not do rather than throwing — read it,
|
|
934
|
+
// instead of reporting a clean success over a half-applied batch.
|
|
935
|
+
//
|
|
936
|
+
// Read defensively (|| []): the property arrived with the engine-side
|
|
937
|
+
// batchModify fix, so an older studio reports a count and no skip list rather
|
|
938
|
+
// than failing. The batch still works; only the explanation is thinner.
|
|
939
|
+
const skipped = app.lastBatchModifySkipped || [];
|
|
940
|
+
const count = typeof results === 'number' ? results : (Array.isArray(results) ? results.length : 0);
|
|
900
941
|
|
|
901
|
-
({
|
|
942
|
+
({
|
|
943
|
+
success: skipped.length === 0 && count === modifications.length,
|
|
944
|
+
count,
|
|
945
|
+
requested: modifications.length,
|
|
946
|
+
skipped: skipped.map(s => ({ itemId: s.itemId, reason: s.reason })),
|
|
947
|
+
...(skipped.length ? { error: 'batch_modify changed ' + count + ' of ' + modifications.length + ' items; skipped: ' + skipped.map(s => s.itemId + ' (' + s.reason + ')').join(', ') } : {}),
|
|
948
|
+
});
|
|
902
949
|
`.trim();
|
|
903
950
|
}
|
|
904
951
|
/**
|
|
@@ -1203,19 +1250,33 @@ export class PinePaperCodeGenerator {
|
|
|
1203
1250
|
if (url) {
|
|
1204
1251
|
return `
|
|
1205
1252
|
// Import SVG from URL${source === 'figma' ? ' (Figma-normalised)' : ''}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1253
|
+
// WRAPPED, because these were bare top-level statements with an await. That
|
|
1254
|
+
// parses under the governor, which builds an AsyncFunction, and is a SYNTAX
|
|
1255
|
+
// ERROR on the eval fallback — so importing an SVG by URL never ran at all on
|
|
1256
|
+
// a studio without runGenerated. A paren-led async IIFE parses on both and
|
|
1257
|
+
// still gives the governor a trailing expression to capture.
|
|
1258
|
+
(async function() {
|
|
1259
|
+
let response;
|
|
1260
|
+
try {
|
|
1261
|
+
response = await fetch('${url}');
|
|
1262
|
+
} catch (netErr) {
|
|
1263
|
+
return { success: false, error: 'could not reach ${url} — ' + ((netErr && netErr.message) || 'network request failed') };
|
|
1264
|
+
}
|
|
1265
|
+
if (!response.ok) {
|
|
1266
|
+
return { success: false, error: 'the server refused ${url} — HTTP ' + response.status + ' ' + (response.statusText || ''), status: response.status };
|
|
1267
|
+
}
|
|
1268
|
+
const svgText = await response.text();
|
|
1269
|
+
const _r = ${importExpr('svgText')};
|
|
1270
|
+
const imported = _r.item;
|
|
1271
|
+
if (!imported) {
|
|
1272
|
+
return { success: false, error: 'the response from ${url} did not parse as SVG. Check the URL serves the file itself rather than a page around it.' };
|
|
1273
|
+
}
|
|
1211
1274
|
imported.position = new paper.Point(${position.x}, ${position.y});
|
|
1212
1275
|
imported.scale(${scale});
|
|
1213
1276
|
const itemId = app.registerItem(imported, 'svg-import', { source: 'mcp' });
|
|
1214
1277
|
app.historyManager.saveState();
|
|
1215
|
-
|
|
1216
|
-
}
|
|
1217
|
-
throw new Error('Failed to import SVG from URL');
|
|
1218
|
-
}
|
|
1278
|
+
return { success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} } };
|
|
1279
|
+
})();
|
|
1219
1280
|
`.trim();
|
|
1220
1281
|
}
|
|
1221
1282
|
if (svgString) {
|
|
@@ -1255,12 +1316,41 @@ throw new Error('Either svgString or url must be provided');
|
|
|
1255
1316
|
if (!app.importMermaid) return { success: false, error: 'importMermaid not available — diagramSystem missing' };
|
|
1256
1317
|
const mermaidText = \`${escaped}\`;
|
|
1257
1318
|
const result = app.importMermaid(mermaidText, ${optsJson});
|
|
1319
|
+
|
|
1320
|
+
// NEVER RETURN THE RAW NODES. They are Paper.js items, and a Paper item
|
|
1321
|
+
// refers to its project, which refers back — JSON.stringify dies on the cycle
|
|
1322
|
+
// with "property '_scope' closes the circle", and the tool reported "Failed
|
|
1323
|
+
// to execute code in browser" for an import that had worked. Summarised to
|
|
1324
|
+
// ids, labels and bounds, which is what a caller can act on anyway.
|
|
1325
|
+
const safeNode = (n) => {
|
|
1326
|
+
const item = n && (n.item || n);
|
|
1327
|
+
const b = item && item.bounds;
|
|
1328
|
+
return {
|
|
1329
|
+
id: (item && item.data && item.data.registryId) || (n && n.id) || null,
|
|
1330
|
+
label: (n && (n.label ?? n.text)) ?? null,
|
|
1331
|
+
bounds: b ? { x: Math.round(b.x), y: Math.round(b.y), width: Math.round(b.width), height: Math.round(b.height) } : null,
|
|
1332
|
+
};
|
|
1333
|
+
};
|
|
1334
|
+
const nodes = (result.nodes || []).map(safeNode);
|
|
1335
|
+
const edges = (result.edges || []).map((e) => ({ from: e?.from ?? e?.source ?? null, to: e?.to ?? e?.target ?? null, label: e?.label ?? null }));
|
|
1336
|
+
|
|
1337
|
+
// A zero-item import is NOT a success. The engine reported success:true with
|
|
1338
|
+
// itemsCreated:0 and the tool passed it on, so a diagram that drew nothing
|
|
1339
|
+
// read as a diagram that drew.
|
|
1340
|
+
if (!nodes.length) {
|
|
1341
|
+
return {
|
|
1342
|
+
success: false,
|
|
1343
|
+
error: 'the mermaid import created no items. The text parsed but produced nothing to draw — check the diagram type is one this build supports, and that the body is not empty.',
|
|
1344
|
+
nodeCount: 0, edgeCount: edges.length, errors: result.errors || [],
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1258
1348
|
return {
|
|
1259
1349
|
success: !!result.success,
|
|
1260
|
-
nodeCount:
|
|
1261
|
-
edgeCount:
|
|
1262
|
-
nodes
|
|
1263
|
-
edges
|
|
1350
|
+
nodeCount: nodes.length,
|
|
1351
|
+
edgeCount: edges.length,
|
|
1352
|
+
nodes,
|
|
1353
|
+
edges,
|
|
1264
1354
|
errors: result.errors || [],
|
|
1265
1355
|
};
|
|
1266
1356
|
})();
|
|
@@ -1575,8 +1665,11 @@ bg.sendToBack();
|
|
|
1575
1665
|
if (backgroundGenerator) {
|
|
1576
1666
|
codeParts.push(`
|
|
1577
1667
|
// Execute background generator
|
|
1578
|
-
|
|
1579
|
-
|
|
1668
|
+
const _gen = app.generatorRegistry?.get('${backgroundGenerator}');
|
|
1669
|
+
if (typeof _gen === 'function') {
|
|
1670
|
+
_gen();
|
|
1671
|
+
} else if (_gen && typeof _gen.generate === 'function') {
|
|
1672
|
+
_gen.generate();
|
|
1580
1673
|
}
|
|
1581
1674
|
`);
|
|
1582
1675
|
}
|
|
@@ -1713,8 +1806,9 @@ results;
|
|
|
1713
1806
|
const labelStr = label ? JSON.stringify(label) : 'null';
|
|
1714
1807
|
return `
|
|
1715
1808
|
// Create diagram shape: ${shapeType}
|
|
1809
|
+
// POSITIONAL: createShape(shapeType, config). The type was being passed
|
|
1810
|
+
// INSIDE the config object, where nothing reads it.
|
|
1716
1811
|
const shapeConfig = {
|
|
1717
|
-
shapeType: '${shapeType}',
|
|
1718
1812
|
position: { x: ${position.x}, y: ${position.y} },
|
|
1719
1813
|
width: ${widthVal},
|
|
1720
1814
|
height: ${heightVal},
|
|
@@ -1722,7 +1816,8 @@ const shapeConfig = {
|
|
|
1722
1816
|
style: ${styleStr}
|
|
1723
1817
|
};
|
|
1724
1818
|
|
|
1725
|
-
const shape = app.
|
|
1819
|
+
const shape = app.diagramSystem.createShape('${shapeType}', shapeConfig);
|
|
1820
|
+
if (!shape) throw new Error('createShape returned nothing for shapeType "${shapeType}" — call pinepaper_get_diagram_shapes for the registered types.');
|
|
1726
1821
|
const itemId = shape.data?.registryId || shape.id;
|
|
1727
1822
|
app.historyManager.saveState();
|
|
1728
1823
|
|
|
@@ -1766,7 +1861,7 @@ if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}');
|
|
|
1766
1861
|
if (!targetItem) throw new Error('Target item not found: ${targetItemId}');
|
|
1767
1862
|
|
|
1768
1863
|
const config = ${JSON.stringify(config, null, 2)};
|
|
1769
|
-
const connector = app.
|
|
1864
|
+
const connector = app.diagramSystem.connect(sourceItem, targetItem, config);
|
|
1770
1865
|
const connectorId = connector.data?.registryId || connector.id;
|
|
1771
1866
|
app.historyManager.saveState();
|
|
1772
1867
|
|
|
@@ -1794,7 +1889,7 @@ if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}');
|
|
|
1794
1889
|
if (!targetItem) throw new Error('Target item not found: ${targetItemId}');
|
|
1795
1890
|
|
|
1796
1891
|
const config = ${configStr};
|
|
1797
|
-
const connector = app.
|
|
1892
|
+
const connector = app.diagramSystem.connectPorts(
|
|
1798
1893
|
sourceItem, '${sourcePort}',
|
|
1799
1894
|
targetItem, '${targetPort}',
|
|
1800
1895
|
config
|
|
@@ -1811,21 +1906,31 @@ app.historyManager.saveState();
|
|
|
1811
1906
|
generateAddPorts(input) {
|
|
1812
1907
|
const validated = AddPortsInputSchema.parse(input);
|
|
1813
1908
|
const { itemId, portType, ports, count } = validated;
|
|
1814
|
-
|
|
1815
|
-
|
|
1909
|
+
// THE SCHEMA PROMISES MORE THAN THE ENGINE HAS. `addPorts(item, type)` takes
|
|
1910
|
+
// a type string and lays out that type's port set; there is no per-port
|
|
1911
|
+
// placement and no count. The old emitter passed {portType, ports, count}
|
|
1912
|
+
// as the second argument, which the engine tried to iterate — "t.forEach is
|
|
1913
|
+
// not a function" — so every call failed and the two extra fields looked
|
|
1914
|
+
// like they might have worked. Refused by name instead of dropped.
|
|
1915
|
+
if (ports !== undefined || count !== undefined) {
|
|
1916
|
+
const named = [ports !== undefined && 'ports', count !== undefined && 'count'].filter(Boolean).join(' and ');
|
|
1917
|
+
throw new Error(`add_ports cannot honour ${named}: this engine lays out a whole port SET by type `
|
|
1918
|
+
+ `(portType '${portType}'), with no per-port placement or count. Drop ${named}, or place `
|
|
1919
|
+
+ 'individual connection points as items and join them with pinepaper_connect.');
|
|
1920
|
+
}
|
|
1816
1921
|
return `
|
|
1817
1922
|
// Add ports to item: ${itemId}
|
|
1818
1923
|
const item = app.getItemById('${itemId}');
|
|
1819
1924
|
if (!item) throw new Error('Item not found: ${itemId}');
|
|
1820
1925
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
count: ${countVal}
|
|
1825
|
-
});
|
|
1926
|
+
// The second argument is a TYPE STRING, not a config object. Passing an
|
|
1927
|
+
// object made the engine iterate it as a list — "t.forEach is not a function".
|
|
1928
|
+
const result = app.diagramSystem.addPorts(item, '${portType}');
|
|
1826
1929
|
app.historyManager.saveState();
|
|
1827
1930
|
|
|
1828
|
-
|
|
1931
|
+
// addPorts returns the ports it made; older builds returned a count object.
|
|
1932
|
+
const _added = Array.isArray(result) ? result.length : (result?.portsAdded ?? 0);
|
|
1933
|
+
({ itemId: '${itemId}', portsAdded: _added, portType: '${portType}' });
|
|
1829
1934
|
`.trim();
|
|
1830
1935
|
}
|
|
1831
1936
|
/**
|
|
@@ -1838,13 +1943,26 @@ app.historyManager.saveState();
|
|
|
1838
1943
|
const optionsStr = options ? JSON.stringify(options, null, 2) : '{}';
|
|
1839
1944
|
return `
|
|
1840
1945
|
// Apply auto-layout: ${layoutType}
|
|
1946
|
+
// Wrapped: applyLayout is async, and a bare top-level await only works on the
|
|
1947
|
+
// governed path. A paren-led IIFE also keeps the trailing-expression capture.
|
|
1948
|
+
(async function() {
|
|
1841
1949
|
const itemIds = ${itemIdsStr};
|
|
1842
1950
|
const options = ${optionsStr};
|
|
1843
1951
|
|
|
1844
|
-
|
|
1952
|
+
// applyLayout(items, type, options) — ITEMS first, and it takes live items
|
|
1953
|
+
// rather than ids. It is async, and it resolves to undefined: reading
|
|
1954
|
+
// result.itemsAffected off that throws, which is how a layout that worked
|
|
1955
|
+
// reported as a crash.
|
|
1956
|
+
const items = itemIds
|
|
1957
|
+
? itemIds.map((id) => app.getItemById(id)).filter(Boolean)
|
|
1958
|
+
: (app.itemRegistry?.getAll?.() ?? []);
|
|
1959
|
+
if (!items.length) throw new Error('auto_layout found no items to lay out' + (itemIds ? ' for the given itemIds' : ' on the canvas'));
|
|
1960
|
+
|
|
1961
|
+
await app.diagramSystem.applyLayout(items, '${layoutType}', options);
|
|
1845
1962
|
app.historyManager.saveState();
|
|
1846
1963
|
|
|
1847
|
-
|
|
1964
|
+
return { layoutType: '${layoutType}', itemsAffected: items.length, success: true };
|
|
1965
|
+
})();
|
|
1848
1966
|
`.trim();
|
|
1849
1967
|
}
|
|
1850
1968
|
/**
|
|
@@ -1857,7 +1975,10 @@ app.historyManager.saveState();
|
|
|
1857
1975
|
return `
|
|
1858
1976
|
// Get available diagram shapes
|
|
1859
1977
|
const category = ${categoryFilter};
|
|
1860
|
-
|
|
1978
|
+
// The library is the registry; diagramSystem has no getAvailableShapes.
|
|
1979
|
+
const _lib = app.diagramSystem?.shapeLibrary;
|
|
1980
|
+
if (!_lib) throw new Error('app.diagramSystem.shapeLibrary unavailable — update FxTool');
|
|
1981
|
+
const shapes = category ? _lib.getByCategory(category) : _lib.getAll();
|
|
1861
1982
|
|
|
1862
1983
|
({ shapes, count: shapes.length, category: ${categoryFilter} || 'all' });
|
|
1863
1984
|
`.trim();
|
|
@@ -1885,10 +2006,44 @@ const connector = app.getItemById('${connectorId}');
|
|
|
1885
2006
|
if (!connector) throw new Error('Connector not found: ${connectorId}');
|
|
1886
2007
|
|
|
1887
2008
|
const updates = ${updatesStr};
|
|
1888
|
-
app.diagramManager.updateConnector(connector, updates);
|
|
1889
|
-
app.historyManager.saveState();
|
|
1890
2009
|
|
|
1891
|
-
|
|
2010
|
+
// THERE IS NO updateConnector. ConnectorManager publishes removeConnector,
|
|
2011
|
+
// findConnector and updateSelectedStyle — nothing that edits one connector's
|
|
2012
|
+
// properties in place. The old emitter called app.diagramSystem.updateConnector
|
|
2013
|
+
// and reported updated:true, so a caller changing a connector's colour got a
|
|
2014
|
+
// success and an unchanged connector.
|
|
2015
|
+
//
|
|
2016
|
+
// Applied directly where the property is a plain Paper.js one, and refused by
|
|
2017
|
+
// name where it is not, rather than claiming a change that did not happen.
|
|
2018
|
+
// The engine gained getConnector(id) / updateConnector(id, style), which is
|
|
2019
|
+
// the call this tool always wanted: addressable by the id a connector is
|
|
2020
|
+
// created and serialised with. Guarded, because a studio predating that still
|
|
2021
|
+
// has to do something sensible rather than throw.
|
|
2022
|
+
if (typeof app.diagramSystem?.updateConnector === 'function') {
|
|
2023
|
+
const res = app.diagramSystem.updateConnector('${connectorId}', updates);
|
|
2024
|
+
if (res && res.ok === false) { return { success: false, error: res.reason || 'the connector could not be updated' }; }
|
|
2025
|
+
app.historyManager.saveState();
|
|
2026
|
+
return { connectorId: '${connectorId}', updated: true, applied: Object.keys(updates) };
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
const _applied = [];
|
|
2030
|
+
const _unsupported = [];
|
|
2031
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
2032
|
+
if (k === 'lineColor' && connector.strokeColor !== undefined) { connector.strokeColor = v; _applied.push(k); }
|
|
2033
|
+
else if (k === 'lineWidth' && connector.strokeWidth !== undefined) { connector.strokeWidth = v; _applied.push(k); }
|
|
2034
|
+
else _unsupported.push(k);
|
|
2035
|
+
}
|
|
2036
|
+
if (_applied.length) { app.historyManager.saveState(); app._scheduleRepaint?.(); }
|
|
2037
|
+
if (_unsupported.length && !_applied.length) {
|
|
2038
|
+
throw new Error(
|
|
2039
|
+
'this build cannot update ' + _unsupported.join(', ') + ' on an existing connector — '
|
|
2040
|
+
+ 'this build has no per-connector update — app.diagramSystem.updateConnector arrived later. '
|
|
2041
|
+
+ 'Update the studio for it. Removing and recreating the connector works, but mints a NEW id '
|
|
2042
|
+
+ 'and orphans anything holding the old one, so it is a last resort rather than the fix.'
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
({ connectorId: '${connectorId}', updated: _applied.length > 0, applied: _applied, unsupported: _unsupported });
|
|
1892
2047
|
`.trim();
|
|
1893
2048
|
}
|
|
1894
2049
|
/**
|
|
@@ -1902,7 +2057,7 @@ app.historyManager.saveState();
|
|
|
1902
2057
|
const connector = app.getItemById('${connectorId}');
|
|
1903
2058
|
if (!connector) throw new Error('Connector not found: ${connectorId}');
|
|
1904
2059
|
|
|
1905
|
-
app.
|
|
2060
|
+
app.diagramSystem.connectorManager.removeConnector(connector);
|
|
1906
2061
|
app.historyManager.saveState();
|
|
1907
2062
|
|
|
1908
2063
|
({ connectorId: '${connectorId}', removed: true });
|
|
@@ -1918,19 +2073,19 @@ app.historyManager.saveState();
|
|
|
1918
2073
|
case 'activate':
|
|
1919
2074
|
return `
|
|
1920
2075
|
// Activate diagram mode
|
|
1921
|
-
app.
|
|
2076
|
+
app.diagramSystem.activate();
|
|
1922
2077
|
({ action: 'activate', active: true });
|
|
1923
2078
|
`.trim();
|
|
1924
2079
|
case 'deactivate':
|
|
1925
2080
|
return `
|
|
1926
2081
|
// Deactivate diagram mode
|
|
1927
|
-
app.
|
|
2082
|
+
app.diagramSystem.deactivate();
|
|
1928
2083
|
({ action: 'deactivate', active: false });
|
|
1929
2084
|
`.trim();
|
|
1930
2085
|
case 'toggle':
|
|
1931
2086
|
return `
|
|
1932
2087
|
// Toggle diagram mode
|
|
1933
|
-
const isActive = app.
|
|
2088
|
+
const isActive = app.diagramSystem.toggle();
|
|
1934
2089
|
({ action: 'toggle', active: isActive });
|
|
1935
2090
|
`.trim();
|
|
1936
2091
|
case 'setMode':
|
|
@@ -1938,7 +2093,7 @@ const isActive = app.diagramManager.toggle();
|
|
|
1938
2093
|
const shapeStr = shapeType ? `, '${shapeType}'` : '';
|
|
1939
2094
|
return `
|
|
1940
2095
|
// Set diagram tool mode
|
|
1941
|
-
app.
|
|
2096
|
+
app.diagramSystem.setMode(${modeStr}${shapeStr});
|
|
1942
2097
|
({ action: 'setMode', mode: ${modeStr}${shapeType ? `, shapeType: '${shapeType}'` : ''} });
|
|
1943
2098
|
`.trim();
|
|
1944
2099
|
default:
|
|
@@ -2097,7 +2252,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2097
2252
|
|
|
2098
2253
|
// FxTool has no animationManager — animations are detected via item.data.animationType walk above
|
|
2099
2254
|
// Check timeline for active animations
|
|
2100
|
-
if (app.
|
|
2255
|
+
if (app.timelineState?.isPlaying || app.animatedItems?.size > 0) {
|
|
2101
2256
|
analysis.hasAnimations = true;
|
|
2102
2257
|
animationSet.add('timeline');
|
|
2103
2258
|
}
|
|
@@ -2226,6 +2381,17 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2226
2381
|
let code = `
|
|
2227
2382
|
// Batch execute ${operations.length} operations
|
|
2228
2383
|
(async function() {
|
|
2384
|
+
// An op's own verdict. Deliberately narrow, matching the server-side rule:
|
|
2385
|
+
// only an explicit success:false or ok:false is a failure, because most ops
|
|
2386
|
+
// return a bare value (an id, a count) and treating a missing field as
|
|
2387
|
+
// refusal would fail every one of them.
|
|
2388
|
+
const _opFailure = (v) => {
|
|
2389
|
+
if (!v || typeof v !== 'object') return null;
|
|
2390
|
+
if (v.success !== false && v.ok !== false) return null;
|
|
2391
|
+
return (typeof v.error === 'string' && v.error)
|
|
2392
|
+
|| (typeof v.reason === 'string' && v.reason)
|
|
2393
|
+
|| 'the operation reported failure without naming a reason';
|
|
2394
|
+
};
|
|
2229
2395
|
const results = [];
|
|
2230
2396
|
const itemIds = [];
|
|
2231
2397
|
let success = true;
|
|
@@ -2250,10 +2416,24 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2250
2416
|
const result${index} = await (async () => {
|
|
2251
2417
|
${opCode}
|
|
2252
2418
|
})();
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2419
|
+
// NOT THROWING IS NOT SUCCEEDING. An op that RETURNS { success: false }
|
|
2420
|
+
// — a guard that found a facade missing, a preset the engine does not
|
|
2421
|
+
// have, a precondition unmet — raises nothing, so this recorded it as a
|
|
2422
|
+
// success and the batch reported success over it. Measured on
|
|
2423
|
+
// applyAnimatedMask(item, 'star'): the per-op error was perfectly good
|
|
2424
|
+
// and the batch still said everything worked.
|
|
2425
|
+
const failed${index} = _opFailure(result${index});
|
|
2426
|
+
if (failed${index}) {
|
|
2427
|
+
// ATOMIC: throw WITHOUT recording, and let the catch below record it
|
|
2428
|
+
// once. Doing both put two entries in for one failed op.
|
|
2429
|
+
${isAtomic ? `throw new Error(failed${index});` : `results.push({ index: ${index}, success: false, error: failed${index}, result: result${index} });
|
|
2430
|
+
success = false;`}
|
|
2431
|
+
} else {
|
|
2432
|
+
results.push({ index: ${index}, success: true, result: result${index} });
|
|
2433
|
+
${claimsItemSlot ? `if (result${index} && result${index}.itemId) {
|
|
2434
|
+
itemIds.push(result${index}.itemId);
|
|
2435
|
+
}` : ''}
|
|
2436
|
+
}
|
|
2257
2437
|
} catch (opError) {
|
|
2258
2438
|
results.push({ index: ${index}, success: false, error: opError.message });
|
|
2259
2439
|
${isAtomic ? 'throw opError;' : 'success = false;'}
|
|
@@ -2265,7 +2445,14 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2265
2445
|
if (app.historyManager) app.historyManager.saveState();
|
|
2266
2446
|
|
|
2267
2447
|
const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
2268
|
-
|
|
2448
|
+
// Name WHICH ops failed on the result itself. A caller reading a failed
|
|
2449
|
+
// 60-op batch should not have to scan sixty entries to learn that op 41
|
|
2450
|
+
// could not find a preset.
|
|
2451
|
+
const _failed = results.filter((r) => !r.success);
|
|
2452
|
+
return { success, itemIds, results, operationCount: ${operations.length},
|
|
2453
|
+
failedCount: _failed.length,
|
|
2454
|
+
...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; ') } : {}),
|
|
2455
|
+
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
|
|
2269
2456
|
} catch (e) {
|
|
2270
2457
|
const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
2271
2458
|
return { success: false, error: e.message, itemIds, results, operationCount: ${operations.length}, canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
|
|
@@ -2322,8 +2509,12 @@ return { itemId };
|
|
|
2322
2509
|
const modifyProps = JSON.stringify(op.properties || {});
|
|
2323
2510
|
return `
|
|
2324
2511
|
const targetId = ${itemRef};
|
|
2325
|
-
|
|
2326
|
-
|
|
2512
|
+
// modifyItem(id, changes), not select() + modify(). modify() edits the
|
|
2513
|
+
// SELECTION, so an id that does not resolve left the PREVIOUS selection to be
|
|
2514
|
+
// edited instead — and this then reported modified:true either way.
|
|
2515
|
+
if (app.modifyItem(targetId, ${modifyProps}) === false) {
|
|
2516
|
+
return { success: false, itemId: targetId, error: 'no item ' + targetId + ' — nothing was modified' };
|
|
2517
|
+
}
|
|
2327
2518
|
return { itemId: targetId, modified: true };
|
|
2328
2519
|
`;
|
|
2329
2520
|
case 'animate':
|
|
@@ -2398,9 +2589,16 @@ return { success: true, generator: '${genName}'${op.generatorRegion ? ', region:
|
|
|
2398
2589
|
const h = op.height || 1080;
|
|
2399
2590
|
const sizeArg = op.preset ? `'${op.preset}'` : `{ width: ${w}, height: ${h} }`;
|
|
2400
2591
|
return `
|
|
2401
|
-
|
|
2592
|
+
// Read the verdict, as the standalone tool now does: an unknown preset or an
|
|
2593
|
+
// out-of-range dimension is refused by name, and reporting the size we ASKED
|
|
2594
|
+
// for left the batch claiming a resize that never happened.
|
|
2595
|
+
const _cs = app.setCanvasSize(${sizeArg});
|
|
2596
|
+
if (_cs && _cs.ok === false) {
|
|
2597
|
+
return { success: false, error: _cs.reason || 'the canvas size was refused',
|
|
2598
|
+
...(_cs.known ? { knownPresets: _cs.known } : {}) };
|
|
2599
|
+
}
|
|
2402
2600
|
await new Promise(r => setTimeout(r, 50));
|
|
2403
|
-
return { success: true, width: ${w}, height: ${h} };
|
|
2601
|
+
return { success: true, width: (_cs && _cs.width) || ${w}, height: (_cs && _cs.height) || ${h} };
|
|
2404
2602
|
`;
|
|
2405
2603
|
}
|
|
2406
2604
|
case 'keyframe_animate': {
|
|
@@ -2457,7 +2655,15 @@ return { success: !!maskedGroup, itemId: targetId, preset: '${preset}' };
|
|
|
2457
2655
|
const targetId = ${effItemRef};
|
|
2458
2656
|
const item = app.getItemById(targetId);
|
|
2459
2657
|
if (!item) throw new Error('Item not found: ' + targetId);
|
|
2460
|
-
|
|
2658
|
+
// PROPAGATE the engine's verdict. This discarded it and returned its own
|
|
2659
|
+
// shape, so an effect the engine refused — an unknown preset, a facade that
|
|
2660
|
+
// is not loaded — was recorded as applied. The batch cannot report what the
|
|
2661
|
+
// op never tells it.
|
|
2662
|
+
const _eff = app.applyEffect(item, '${op.effectType || 'sparkle'}', ${effParams});
|
|
2663
|
+
if (_eff && (_eff.success === false || _eff.ok === false)) {
|
|
2664
|
+
return { success: false, itemId: targetId, effectType: '${op.effectType || 'sparkle'}',
|
|
2665
|
+
error: _eff.error || _eff.reason || 'the effect was refused without a reason' };
|
|
2666
|
+
}
|
|
2461
2667
|
return { itemId: targetId, effectType: '${op.effectType || 'sparkle'}' };
|
|
2462
2668
|
`;
|
|
2463
2669
|
}
|
|
@@ -2551,7 +2757,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2551
2757
|
}
|
|
2552
2758
|
generateAgentExport(input) {
|
|
2553
2759
|
const validated = AgentExportInputSchema.parse(input);
|
|
2554
|
-
const { platform, format, quality, framing, duration, estimateOnly, scale } = validated;
|
|
2760
|
+
const { platform, format, quality, framing, duration, estimateOnly, scale, fps } = validated;
|
|
2555
2761
|
const qualityLevel = quality || 'standard';
|
|
2556
2762
|
const videoDuration = duration ?? 5;
|
|
2557
2763
|
// Quality settings (bitrate in bps for VideoEncoder)
|
|
@@ -2669,7 +2875,11 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2669
2875
|
const format = '${exportFormat}';
|
|
2670
2876
|
const quality = '${qualityLevel}';
|
|
2671
2877
|
const framing = '${framing}';
|
|
2672
|
-
|
|
2878
|
+
// An explicit fps wins over the tier's. The tiers bundle frame rate with
|
|
2879
|
+
// compression (draft 15 / standard 30 / high 60), so "higher quality" also
|
|
2880
|
+
// doubled the frame count and the render time — a surprise the schema used
|
|
2881
|
+
// to hide because there was no way to ask for one without the other.
|
|
2882
|
+
const settings = ${JSON.stringify(fps !== undefined ? { ...qualitySettings, fps } : qualitySettings)};
|
|
2673
2883
|
const dimensions = ${JSON.stringify(scaled)};
|
|
2674
2884
|
|
|
2675
2885
|
// Preflight: same resolved settings as the real export, but render nothing.
|
|
@@ -2736,7 +2946,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2736
2946
|
try {
|
|
2737
2947
|
switch (format) {
|
|
2738
2948
|
case 'svg':
|
|
2739
|
-
const svgString = app.exportAnimatedSVG ? app.exportAnimatedSVG() : app.
|
|
2949
|
+
const svgString = app.exportAnimatedSVG ? app.exportAnimatedSVG() : app.exportSVGWithCSS();
|
|
2740
2950
|
result = {
|
|
2741
2951
|
success: true,
|
|
2742
2952
|
platform,
|
|
@@ -2991,7 +3201,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2991
3201
|
|
|
2992
3202
|
// FxTool has no animationManager — animations are detected via item.data.animationType walk above
|
|
2993
3203
|
// Check timeline for active animations
|
|
2994
|
-
if (app.
|
|
3204
|
+
if (app.timelineState?.isPlaying || app.animatedItems?.size > 0) {
|
|
2995
3205
|
analysis.hasAnimations = true;
|
|
2996
3206
|
animationSet.add('timeline');
|
|
2997
3207
|
}
|
|
@@ -3525,7 +3735,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3525
3735
|
}
|
|
3526
3736
|
|
|
3527
3737
|
try {
|
|
3528
|
-
app.mapSystem.
|
|
3738
|
+
app.mapSystem.stopRegionAnimations({
|
|
3529
3739
|
regions: ${regionsStr},
|
|
3530
3740
|
resetColors: ${validated.resetColors !== false}
|
|
3531
3741
|
});
|
|
@@ -3569,7 +3779,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3569
3779
|
}
|
|
3570
3780
|
|
|
3571
3781
|
try {
|
|
3572
|
-
const result = app.mapSystem.
|
|
3782
|
+
const result = app.mapSystem.exportRegionDataCSV({
|
|
3573
3783
|
includeHighlighted: ${validated.includeHighlighted !== false},
|
|
3574
3784
|
includeSelected: ${validated.includeSelected !== false},
|
|
3575
3785
|
includeColors: ${validated.includeColors !== false},
|
|
@@ -3598,8 +3808,9 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3598
3808
|
}
|
|
3599
3809
|
|
|
3600
3810
|
try {
|
|
3601
|
-
|
|
3602
|
-
|
|
3811
|
+
// importRegionDataCSV(csvText, options) — the TEXT is positional, and it
|
|
3812
|
+
// was being passed inside the options bag where nothing read it.
|
|
3813
|
+
const result = app.mapSystem.importRegionDataCSV(\`${escapedCsv}\`, {
|
|
3603
3814
|
applyColors: ${validated.applyColors !== false},
|
|
3604
3815
|
applyHighlight: ${validated.applyHighlight !== false},
|
|
3605
3816
|
applySelection: ${validated.applySelection !== false}
|
|
@@ -3908,7 +4119,15 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3908
4119
|
if (item.strokeColor) itemData.properties.strokeColor = item.strokeColor.toCSS ? item.strokeColor.toCSS() : item.strokeColor;
|
|
3909
4120
|
if (item.strokeWidth) itemData.properties.strokeWidth = item.strokeWidth;
|
|
3910
4121
|
if (item.opacity !== undefined) itemData.properties.opacity = item.opacity;
|
|
3911
|
-
|
|
4122
|
+
// TRUE ROTATION, not the property. create() BAKES an angle into the path
|
|
4123
|
+
// segments under Paper's applyMatrix default, so item.rotation reads back 0 on
|
|
4124
|
+
// an item that is visibly rotated. Reporting that 0 is why "I could not rotate
|
|
4125
|
+
// a rectangle" was filed against an engine that rotates correctly. The engine
|
|
4126
|
+
// records the baked offset at item.data._bakedRotation and sums the two itself
|
|
4127
|
+
// (PinePaper.js does exactly this); read the same sum rather than the half of
|
|
4128
|
+
// it that happens to be zero.
|
|
4129
|
+
const _rot = typeof app.authoredRotation === 'function' ? app.authoredRotation(item) : ((item.data?._bakedRotation || 0) + (item.rotation || 0));
|
|
4130
|
+
if (_rot) itemData.properties.rotation = _rot;
|
|
3912
4131
|
if (item.data?.content) itemData.properties.content = item.data.content;
|
|
3913
4132
|
if (item.visible === false) itemData.properties.visible = false;
|
|
3914
4133
|
|
|
@@ -3962,7 +4181,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3962
4181
|
}
|
|
3963
4182
|
|
|
3964
4183
|
// Get background color and canvas size
|
|
3965
|
-
const backgroundColor = app.
|
|
4184
|
+
const backgroundColor = app.canvasEl?.style?.backgroundColor || null;
|
|
3966
4185
|
const canvasSize = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
3967
4186
|
|
|
3968
4187
|
return {
|
|
@@ -4026,10 +4245,15 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4026
4245
|
}
|
|
4027
4246
|
|
|
4028
4247
|
try {
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
}
|
|
4248
|
+
// Two methods, not one with a flag: getOriginalGeoJSON() returns the data,
|
|
4249
|
+
// downloadOriginalGeoJSON(filename) hands it to the browser.
|
|
4250
|
+
// exportOriginalGeoJSON has never existed.
|
|
4251
|
+
const result = ${download}
|
|
4252
|
+
? app.mapSystem.downloadOriginalGeoJSON(${JSON.stringify(filename)})
|
|
4253
|
+
: app.mapSystem.getOriginalGeoJSON();
|
|
4254
|
+
if (result === null || result === undefined) {
|
|
4255
|
+
return { success: false, error: 'no map is loaded, so there is no source GeoJSON to return' };
|
|
4256
|
+
}
|
|
4033
4257
|
return result;
|
|
4034
4258
|
} catch (error) {
|
|
4035
4259
|
return { success: false, error: error.message };
|
|
@@ -4049,7 +4273,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4049
4273
|
}
|
|
4050
4274
|
|
|
4051
4275
|
try {
|
|
4052
|
-
const result = app.mapSystem.
|
|
4276
|
+
const result = app.mapSystem.getMapSourceInfo();
|
|
4053
4277
|
return result;
|
|
4054
4278
|
} catch (error) {
|
|
4055
4279
|
return { success: false, error: error.message };
|
|
@@ -4146,9 +4370,25 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4146
4370
|
generateExecuteCustomCode(input) {
|
|
4147
4371
|
const validated = ExecuteCustomCodeInputSchema.parse(input);
|
|
4148
4372
|
const description = validated.description || 'Execute custom code';
|
|
4373
|
+
// ASYNC, because the code inside is not ours.
|
|
4374
|
+
//
|
|
4375
|
+
// This wrapped the caller's snippet in a SYNC IIFE, which does two things
|
|
4376
|
+
// to async work. A top-level `await` is a syntax error inside a
|
|
4377
|
+
// non-async function, so a snippet that awaits anything fails to parse at
|
|
4378
|
+
// all. And async work started without await returns a promise nobody
|
|
4379
|
+
// holds: the IIFE returns immediately, the tool reports success, and the
|
|
4380
|
+
// next call reads a canvas where the work has not happened yet.
|
|
4381
|
+
//
|
|
4382
|
+
// That is the whole async half of the engine — export, image import,
|
|
4383
|
+
// rigging bake, layout — unreachable from the one tool whose entire
|
|
4384
|
+
// purpose is reaching things the tool surface does not cover.
|
|
4385
|
+
//
|
|
4386
|
+
// An async IIFE is still a `(`-led expression statement, so the governor
|
|
4387
|
+
// captures its value, and executeCode's eval fallback already resolves a
|
|
4388
|
+
// returned promise. Both paths await it; neither needed changing.
|
|
4149
4389
|
return `
|
|
4150
4390
|
// ${description}
|
|
4151
|
-
(function() {
|
|
4391
|
+
(async function() {
|
|
4152
4392
|
try {
|
|
4153
4393
|
${validated.code}
|
|
4154
4394
|
} catch (error) {
|
|
@@ -4248,6 +4488,33 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4248
4488
|
}
|
|
4249
4489
|
})();
|
|
4250
4490
|
`.trim();
|
|
4491
|
+
}
|
|
4492
|
+
/**
|
|
4493
|
+
* What THIS item can animate, from the engine's own table.
|
|
4494
|
+
*
|
|
4495
|
+
* The table is derived from the applier rather than written beside it, so it
|
|
4496
|
+
* cannot drift from what actually gets applied — and it is filtered by item
|
|
4497
|
+
* type, because the trim/dash four only exist on a path and a rectangle has
|
|
4498
|
+
* no fontSize.
|
|
4499
|
+
*
|
|
4500
|
+
* `interpolates: false` is passed through VERBATIM and never flattened away.
|
|
4501
|
+
* `content` and `blendMode` are DISCRETE — they hold their value until the
|
|
4502
|
+
* next keyframe rather than blending toward it — and treating content as
|
|
4503
|
+
* tweenable is what made every word in a cycler appear half a beat early.
|
|
4504
|
+
* A list that says only "animatable" rebuilds that bug in the next agent.
|
|
4505
|
+
*/
|
|
4506
|
+
generateListItemAnimatableProperties(itemId) {
|
|
4507
|
+
return `
|
|
4508
|
+
// Animatable properties of ${JSON.stringify(itemId)}
|
|
4509
|
+
(function() {
|
|
4510
|
+
if (typeof app.listAnimatableProperties !== 'function') {
|
|
4511
|
+
return { success: false, error: 'app.listAnimatableProperties unavailable — update the studio. The keyframe tool lists the properties it accepts in its own description meanwhile.' };
|
|
4512
|
+
}
|
|
4513
|
+
const item = app.getItemById(${JSON.stringify(itemId)});
|
|
4514
|
+
if (!item) { return { success: false, error: 'no item ${itemId}' }; }
|
|
4515
|
+
const r = app.listAnimatableProperties(item);
|
|
4516
|
+
return { success: true, ...r };
|
|
4517
|
+
})();`.trim();
|
|
4251
4518
|
}
|
|
4252
4519
|
generateGetAnimatableProperties() {
|
|
4253
4520
|
return `
|
|
@@ -4402,7 +4669,41 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4402
4669
|
return { error: 'Image tools not available. Make sure PinePaper Studio is loaded.' };
|
|
4403
4670
|
}
|
|
4404
4671
|
try {
|
|
4405
|
-
|
|
4672
|
+
// FETCH IT OURSELVES FIRST, to find out WHY when it fails.
|
|
4673
|
+
//
|
|
4674
|
+
// uploadFromURL sets img.src and rejects with a bare "Failed to load image
|
|
4675
|
+
// from URL" — an <img> onerror genuinely carries no reason, so that message
|
|
4676
|
+
// is the browser's limit rather than the engine being unhelpful. fetch()
|
|
4677
|
+
// has the reason: a status code, a CORS rejection, a refused connection, a
|
|
4678
|
+
// DNS failure. A caller cannot debug what the tool will not show them.
|
|
4679
|
+
//
|
|
4680
|
+
// The bytes then go in as a DATA URL, which img.src loads without touching
|
|
4681
|
+
// the network again — so a host that allows fetch but blocks the image
|
|
4682
|
+
// load (CORS on <img>, a tainted-canvas refusal) now works too.
|
|
4683
|
+
let src = '${url}';
|
|
4684
|
+
if (!src.startsWith('data:')) {
|
|
4685
|
+
let res;
|
|
4686
|
+
try {
|
|
4687
|
+
res = await fetch(src);
|
|
4688
|
+
} catch (netErr) {
|
|
4689
|
+
return { success: false, error: 'could not reach ' + src + ' — ' + (netErr && netErr.message ? netErr.message : 'network request failed')
|
|
4690
|
+
+ '. This is the browser\'s network stack: check the host resolves, the port is open, and that any proxy passes plain HTTP as well as HTTPS CONNECT.' };
|
|
4691
|
+
}
|
|
4692
|
+
if (!res.ok) {
|
|
4693
|
+
return { success: false, error: 'the server refused ' + src + ' — HTTP ' + res.status + ' ' + (res.statusText || ''), status: res.status };
|
|
4694
|
+
}
|
|
4695
|
+
const blob = await res.blob();
|
|
4696
|
+
if (!blob.type.startsWith('image/')) {
|
|
4697
|
+
return { success: false, error: src + ' returned ' + (blob.type || 'an unknown content type') + ', not an image. Check the URL serves the file itself rather than a web page around it.', contentType: blob.type };
|
|
4698
|
+
}
|
|
4699
|
+
src = await new Promise((resolve, reject) => {
|
|
4700
|
+
const fr = new FileReader();
|
|
4701
|
+
fr.onloadend = () => resolve(fr.result);
|
|
4702
|
+
fr.onerror = () => reject(new Error('could not read the downloaded image'));
|
|
4703
|
+
fr.readAsDataURL(blob);
|
|
4704
|
+
});
|
|
4705
|
+
}
|
|
4706
|
+
const entry = await app.imageTools.uploadFromURL(src);
|
|
4406
4707
|
const opts = ${optsLiteral};
|
|
4407
4708
|
const raster = await app.imageTools.placeImage(entry.id, Object.keys(opts).length > 0 ? opts : undefined);
|
|
4408
4709
|
${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} // The REGISTRY id is \`data.id\`. \`data.itemId\` has never existed, so this
|
|
@@ -4610,7 +4911,21 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} // The RE
|
|
|
4610
4911
|
// Toggle loop mode
|
|
4611
4912
|
(function() {
|
|
4612
4913
|
${guard}
|
|
4613
|
-
|
|
4914
|
+
// chainLoop is a PROPERTY, set by createChain({loop}) and read by the chain
|
|
4915
|
+
// advance. setLoop has never existed, so this reported a toggle it had not
|
|
4916
|
+
// performed — and a chain built without loop went on not looping.
|
|
4917
|
+
if (typeof app.sceneManager.setLoop === 'function') {
|
|
4918
|
+
app.sceneManager.setLoop(${input.enabled ?? true});
|
|
4919
|
+
} else if ('chainLoop' in app.sceneManager) {
|
|
4920
|
+
app.sceneManager.chainLoop = ${input.enabled ?? true};
|
|
4921
|
+
} else {
|
|
4922
|
+
return { success: false, action: 'toggle_loop',
|
|
4923
|
+
error: 'this build has no chain-loop control to set. Pass loop:true to create_chain instead, which is where the setting is read from.' };
|
|
4924
|
+
}
|
|
4925
|
+
if (!app.sceneManager.sceneChain || !app.sceneManager.sceneChain.length) {
|
|
4926
|
+
return { success: true, action: 'toggle_loop', enabled: ${input.enabled ?? true},
|
|
4927
|
+
note: 'no chain exists yet — this setting applies to the next one created, and create_chain takes loop directly.' };
|
|
4928
|
+
}
|
|
4614
4929
|
return { success: true, action: 'toggle_loop', enabled: ${input.enabled ?? true} };
|
|
4615
4930
|
})();
|
|
4616
4931
|
`.trim();
|
|
@@ -5380,8 +5695,23 @@ ${resolve}
|
|
|
5380
5695
|
(function() {
|
|
5381
5696
|
const itemIds = ${JSON.stringify(input.itemIds || [])};
|
|
5382
5697
|
const opts = ${JSON.stringify(opts)};
|
|
5383
|
-
|
|
5384
|
-
|
|
5698
|
+
|
|
5699
|
+
// createPrecomp wants LIVE ITEMS. Handing it registry ids put strings into
|
|
5700
|
+
// Paper.js's insertChildren, which fails as "l._remove is not a function" —
|
|
5701
|
+
// a minified internal, from a schema that promises ids work. Resolved here,
|
|
5702
|
+
// and a missing one is named rather than silently dropped from the group.
|
|
5703
|
+
const items = [];
|
|
5704
|
+
const missing = [];
|
|
5705
|
+
for (const id of itemIds) {
|
|
5706
|
+
const item = app.getItemById(id);
|
|
5707
|
+
if (item) items.push(item); else missing.push(id);
|
|
5708
|
+
}
|
|
5709
|
+
if (missing.length) return { success: false, error: 'precomp: no such item(s): ' + missing.join(', ') };
|
|
5710
|
+
if (!items.length) return { success: false, error: 'precomp needs at least one item' };
|
|
5711
|
+
|
|
5712
|
+
const precomp = app.createPrecomp(items, opts);
|
|
5713
|
+
if (!precomp) return { success: false, error: 'createPrecomp returned nothing' };
|
|
5714
|
+
return { success: true, action: 'create', precompId: precomp?.id || precomp, itemCount: items.length, name: ${JSON.stringify(input.name || '')} };
|
|
5385
5715
|
})();`.trim();
|
|
5386
5716
|
}
|
|
5387
5717
|
case 'add':
|
|
@@ -5473,7 +5803,7 @@ ${resolve}
|
|
|
5473
5803
|
bounds: item.bounds ? { x: item.bounds.x, y: item.bounds.y, width: item.bounds.width, height: item.bounds.height } : null,
|
|
5474
5804
|
visible: item.visible,
|
|
5475
5805
|
opacity: item.opacity,
|
|
5476
|
-
rotation: item.rotation,
|
|
5806
|
+
rotation: typeof app.authoredRotation === 'function' ? app.authoredRotation(item) : ((item.data?._bakedRotation || 0) + (item.rotation || 0)),
|
|
5477
5807
|
selected: item.selected,
|
|
5478
5808
|
};
|
|
5479
5809
|
})();`.trim();
|
|
@@ -5809,7 +6139,11 @@ ${resolve}
|
|
|
5809
6139
|
// SPRITE SHEETS
|
|
5810
6140
|
// ===========================================================================
|
|
5811
6141
|
generateSpriteSheet(input) {
|
|
5812
|
-
|
|
6142
|
+
// spriteSystem is _defineLazyHeavy: undefined until ensureHeavyModules()
|
|
6143
|
+
// lands, so the bare guard reported "not available" on a fresh session for
|
|
6144
|
+
// a studio that has it. Same bug as app.exportEngine in 1.6.9.
|
|
6145
|
+
const guard = `if (!app.spriteSystem && typeof app.ensureHeavyModules === 'function') { try { await app.ensureHeavyModules(); } catch (_) {} }
|
|
6146
|
+
if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
5813
6147
|
switch (input.action) {
|
|
5814
6148
|
case 'generate': {
|
|
5815
6149
|
const opts = {};
|
|
@@ -5829,7 +6163,7 @@ ${resolve}
|
|
|
5829
6163
|
// Generate sprite sheet from skeleton
|
|
5830
6164
|
(async function() {
|
|
5831
6165
|
${guard}
|
|
5832
|
-
const sheet = await app.
|
|
6166
|
+
const sheet = await app.generateSpriteSheet(${JSON.stringify(input.skeletonId || '')}, ${JSON.stringify(opts)});
|
|
5833
6167
|
return { success: true, action: 'generate', spriteSheetId: sheet.id, name: sheet.name, width: sheet.atlasWidth, height: sheet.atlasHeight, frameCount: sheet.frames?.size ?? sheet.frames?.length ?? 0 };
|
|
5834
6168
|
})();`.trim();
|
|
5835
6169
|
}
|
|
@@ -5849,7 +6183,7 @@ ${resolve}
|
|
|
5849
6183
|
// Play sprite sheet animation
|
|
5850
6184
|
(async function() {
|
|
5851
6185
|
${guard}
|
|
5852
|
-
const player = await app.
|
|
6186
|
+
const player = await app.playSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, ${JSON.stringify(opts)});
|
|
5853
6187
|
return { success: true, action: 'play', playerId: player.id };
|
|
5854
6188
|
})();`.trim();
|
|
5855
6189
|
}
|
|
@@ -5858,7 +6192,7 @@ ${resolve}
|
|
|
5858
6192
|
// Export sprite sheet
|
|
5859
6193
|
(async function() {
|
|
5860
6194
|
${guard}
|
|
5861
|
-
const result = await app.
|
|
6195
|
+
const result = await app.exportSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, { format: '${input.format || 'png'}', download: true, includeMetadata: true });
|
|
5862
6196
|
return { success: true, action: 'export', format: '${input.format || 'png'}' };
|
|
5863
6197
|
})();`.trim();
|
|
5864
6198
|
default:
|
|
@@ -6216,7 +6550,8 @@ ${resolve}
|
|
|
6216
6550
|
// Toggle rulers
|
|
6217
6551
|
(function() {
|
|
6218
6552
|
${guard}
|
|
6219
|
-
|
|
6553
|
+
// showRulers()/hideRulers(), not a boolean setter — setRulersVisible never existed.
|
|
6554
|
+
app.measurementSystem[${enabled} ? 'showRulers' : 'hideRulers']();
|
|
6220
6555
|
return { success: true, action: 'set_rulers', enabled: ${enabled} };
|
|
6221
6556
|
})();`.trim();
|
|
6222
6557
|
}
|
|
@@ -6226,7 +6561,8 @@ ${resolve}
|
|
|
6226
6561
|
// Toggle grid
|
|
6227
6562
|
(function() {
|
|
6228
6563
|
${guard}
|
|
6229
|
-
|
|
6564
|
+
// showGrid()/hideGrid(), not a boolean setter — setGridVisible never existed.
|
|
6565
|
+
app.measurementSystem[${enabled} ? 'showGrid' : 'hideGrid']();
|
|
6230
6566
|
return { success: true, action: 'set_grid', enabled: ${enabled} };
|
|
6231
6567
|
})();`.trim();
|
|
6232
6568
|
}
|
|
@@ -6241,17 +6577,21 @@ ${resolve}
|
|
|
6241
6577
|
const entry = app.itemRegistry.get(${itemIdStr});
|
|
6242
6578
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
|
|
6243
6579
|
const b = entry.item.bounds;
|
|
6244
|
-
return { success: true, action: 'get_dimensions', itemId: ${itemIdStr}, x: b.x, y: b.y, width: b.width, height: b.height, rotation: entry.item.rotation || 0 };
|
|
6580
|
+
return { success: true, action: 'get_dimensions', itemId: ${itemIdStr}, x: b.x, y: b.y, width: b.width, height: b.height, rotation: typeof app.authoredRotation === 'function' ? app.authoredRotation(entry.item) : ((entry.item.data?._bakedRotation || 0) + (entry.item.rotation || 0)) };
|
|
6245
6581
|
})();`.trim();
|
|
6246
6582
|
}
|
|
6247
6583
|
case 'set_snap': {
|
|
6248
|
-
|
|
6584
|
+
// `enabled` is deliberately unread: there is no mode to set either way.
|
|
6249
6585
|
return `
|
|
6250
|
-
//
|
|
6586
|
+
// Snap to grid — refused, see below
|
|
6251
6587
|
(function() {
|
|
6252
6588
|
${guard}
|
|
6253
|
-
|
|
6254
|
-
|
|
6589
|
+
// NO EQUIVALENT. MeasurementSystem publishes snapCoordinate(), which snaps a
|
|
6590
|
+
// coordinate you hand it, and nothing that turns snapping on as a mode —
|
|
6591
|
+
// setSnapToUnitEnabled has never existed, so this reported success over a
|
|
6592
|
+
// setting that was never changed. Refused by name, with the thing that works.
|
|
6593
|
+
return { success: false, action: 'set_snap',
|
|
6594
|
+
error: 'this build has no snap-to-unit MODE to switch: the measurement system snaps a coordinate on request rather than holding a setting. Snap positions yourself before creating or moving items, or set a grid with the grid action and place on its multiples.' };
|
|
6255
6595
|
})();`.trim();
|
|
6256
6596
|
}
|
|
6257
6597
|
default:
|
|
@@ -6746,10 +7086,25 @@ ${guard}
|
|
|
6746
7086
|
});
|
|
6747
7087
|
return `
|
|
6748
7088
|
// Display text style: ${input.styleKey} — stacked-layer title, id adopted from the text
|
|
6749
|
-
(function() {
|
|
7089
|
+
(async function() {
|
|
6750
7090
|
if (typeof app.applyTextStyle !== 'function') { return { success: false, error: 'app.applyTextStyle unavailable — update FxTool to a text-styles build' }; }
|
|
7091
|
+
// LOAD THE FACES FIRST. The decorative stylesheet is fetched on pointerenter
|
|
7092
|
+
// over the style grid — a gesture no agent ever makes — so every decorative
|
|
7093
|
+
// family was missing in production and a style asking for one silently drew
|
|
7094
|
+
// the fallback and reported success. ensureFontsLoaded is the fix; guarded,
|
|
7095
|
+
// because a studio without it behaves as it always did.
|
|
7096
|
+
if (typeof app.ensureFontsLoaded === 'function') {
|
|
7097
|
+
try { await app.ensureFontsLoaded('decorative'); } catch (_) { /* the pending flag below still reports it */ }
|
|
7098
|
+
}
|
|
6751
7099
|
const r = app.applyTextStyle(${S(input.itemId)}, ${S(input.styleKey)}, ${opts});
|
|
6752
7100
|
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'style failed' }; }
|
|
7101
|
+
// fontPending: the face is not there YET, so this drew in the fallback. Said
|
|
7102
|
+
// out loud rather than left to be discovered in the export.
|
|
7103
|
+
if (r.fontPending) {
|
|
7104
|
+
return { success: true, action: 'apply_style', id: r.id, style: r.style, layers: r.layers,
|
|
7105
|
+
palette: r.palette, bounds: r.bounds, fontPending: true,
|
|
7106
|
+
warning: 'the face for this style had not loaded, so it drew in a FALLBACK font. Re-apply the style, or call pinepaper_font check with the family and your text to see what is really being used.' };
|
|
7107
|
+
}
|
|
6753
7108
|
// The styled group ADOPTS the text item's registry id — r.id is the SAME id
|
|
6754
7109
|
// the caller passed, so their handle keeps working. Say so in the result.
|
|
6755
7110
|
return { success: true, action: 'apply_style', id: r.id, style: r.style, layers: r.layers, palette: r.palette, bounds: r.bounds };
|
|
@@ -6763,8 +7118,19 @@ ${guard}
|
|
|
6763
7118
|
const r = app.setFontAxes(${S(input.itemId)}, ${S(input.axes)});
|
|
6764
7119
|
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'axes failed' }; }
|
|
6765
7120
|
// rejected is the interesting half: an axis silently ignored is the failure
|
|
6766
|
-
// mode this surface exists to prevent.
|
|
6767
|
-
|
|
7121
|
+
// mode this surface exists to prevent. And NOTHING applied while something
|
|
7122
|
+
// was rejected is not a success — it is the call doing nothing under a
|
|
7123
|
+
// success, which is the same shape as an empty batch reporting ok.
|
|
7124
|
+
const _applied = r.applied || {};
|
|
7125
|
+
const _rejected = r.rejected || [];
|
|
7126
|
+
const _appliedCount = Array.isArray(_applied) ? _applied.length : Object.keys(_applied).length;
|
|
7127
|
+
if (_appliedCount === 0 && _rejected.length) {
|
|
7128
|
+
return { success: false, action: 'set_font_axes', applied: _applied, rejected: _rejected,
|
|
7129
|
+
error: 'no axis was applied. The font does not have: ' + _rejected.join(', ')
|
|
7130
|
+
+ (r.note ? '. ' + r.note : '') };
|
|
7131
|
+
}
|
|
7132
|
+
return { success: true, action: 'set_font_axes', applied: _applied, rejected: _rejected,
|
|
7133
|
+
...(r.note ? { note: r.note } : {}) };
|
|
6768
7134
|
})();`.trim();
|
|
6769
7135
|
}
|
|
6770
7136
|
case 'list_styles': {
|
|
@@ -7607,7 +7973,15 @@ ${needWorld}
|
|
|
7607
7973
|
if (typeof app.storyFromText !== 'function') { return { success: false, error: 'app.storyFromText unavailable — update FxTool' }; }
|
|
7608
7974
|
const r = await app.storyFromText(${S(input.text)}, ${opts});
|
|
7609
7975
|
if (!r || r.ok === false) { return { success: false, error: (r && (r.reason || r.error)) || 'the story did not assemble' }; }
|
|
7610
|
-
|
|
7976
|
+
// NAMED FIELDS, not a spread. assembleStory returns its parts as live Paper
|
|
7977
|
+
// Groups, and a Paper item's graph reaches the editor's EVENT HANDLERS.
|
|
7978
|
+
// Structured clone — which is what the bridge uses — refuses to carry a
|
|
7979
|
+
// function, so the whole result threw DataCloneError and the message did not
|
|
7980
|
+
// survive the wrapping: an empty error string for a story that assembled
|
|
7981
|
+
// fine in-page. Not a cycle; JSON.stringify succeeded on this the whole time.
|
|
7982
|
+
// The groups have to stay for in-page callers, so the wrapper takes what
|
|
7983
|
+
// travels: the spec, the ids, the count.
|
|
7984
|
+
return { success: true, spec: r.spec, partIds: r.partIds, partCount: r.partCount, beats: r.beats };
|
|
7611
7985
|
})();`.trim();
|
|
7612
7986
|
case 'apply_spec':
|
|
7613
7987
|
return `
|
|
@@ -8031,11 +8405,23 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
8031
8405
|
return { success: true, action: 'create_skeleton', skeletonId: skeletonId };`);
|
|
8032
8406
|
}
|
|
8033
8407
|
case 'add_bone': {
|
|
8408
|
+
// DEGREES IN, RADIANS OUT. The schema says degrees and every other
|
|
8409
|
+
// angle on this surface is degrees, but RiggingSystem stores what it is
|
|
8410
|
+
// given and _solveFKRecursive feeds it straight to Math.cos/Math.sin.
|
|
8411
|
+
// So a documented 90 was read as 90 radians and the character exploded
|
|
8412
|
+
// on the first pose — silently, because a wrong pose is not an error.
|
|
8413
|
+
// Converted here so the tool keeps the vocabulary it advertises.
|
|
8414
|
+
// BOTH SPELLINGS, deliberately. The engine's _angleRad prefers
|
|
8415
|
+
// `angleDegrees` and converts it, so the new build reads the caller's
|
|
8416
|
+
// intent verbatim instead of trusting arithmetic done out here. `angle`
|
|
8417
|
+
// carries the same value already in radians, which is what an older
|
|
8418
|
+
// build reads — it has no angleDegrees and would see 0. Identical
|
|
8419
|
+
// result on both, and the units are stated rather than implied.
|
|
8034
8420
|
const config = S({
|
|
8035
8421
|
...(input.name !== undefined ? { name: input.name } : {}),
|
|
8036
8422
|
...(input.parentBoneId !== undefined ? { parentBoneId: input.parentBoneId } : {}),
|
|
8037
8423
|
...(input.length !== undefined ? { length: input.length } : {}),
|
|
8038
|
-
...(input.angle !== undefined ? { angle: input.angle } : {}),
|
|
8424
|
+
...(input.angle !== undefined ? { angleDegrees: input.angle, angle: (input.angle * Math.PI) / 180 } : {}),
|
|
8039
8425
|
...(input.flexibility !== undefined ? { flexibility: input.flexibility } : {}),
|
|
8040
8426
|
...(input.segments !== undefined ? { segments: input.segments } : {}),
|
|
8041
8427
|
});
|
|
@@ -8075,7 +8461,15 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
8075
8461
|
...(input.movingHold !== undefined ? { movingHold: input.movingHold } : {}),
|
|
8076
8462
|
...(input.holdDrift !== undefined ? { holdDrift: input.holdDrift } : {}),
|
|
8077
8463
|
});
|
|
8078
|
-
|
|
8464
|
+
// Same unit conversion as add_bone: an inline pose is { boneId: angleDeg }
|
|
8465
|
+
// by the schema, and the solver reads radians. A SAVED pose is a string
|
|
8466
|
+
// id and is passed through untouched — those angles are already in the
|
|
8467
|
+
// engine's own units.
|
|
8468
|
+
const poseArg = (input.pose !== null && typeof input.pose === 'object')
|
|
8469
|
+
? S(Object.fromEntries(Object.entries(input.pose)
|
|
8470
|
+
.map(([bone, deg]) => [bone, (deg * Math.PI) / 180])))
|
|
8471
|
+
: S(input.pose);
|
|
8472
|
+
return wrap('Rigging: add pose keyframe', ` const ok = R.addPoseKeyframe(${S(input.skeletonId)}, ${input.time}, ${poseArg}, ${S(input.easing ?? 'linear')}, ${opts});
|
|
8079
8473
|
if (app.historyManager) app.historyManager.saveState();
|
|
8080
8474
|
if (!ok) { return { success: false, action: 'add_pose_keyframe', error: "no such skeleton, or 'pose' named a saved pose that does not exist. Pass an inline { boneId: angleDeg } map instead, or list_poses for the saved ones" }; }
|
|
8081
8475
|
return { success: true, action: 'add_pose_keyframe', time: ${input.time} };`);
|