@pinepaper.studio/mcp-server 1.6.8 → 1.6.10
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 +207 -7
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- 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/index.d.ts +1 -1
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/puppeteer-controller.d.ts +86 -2
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +271 -63
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -1
- package/dist/design/design-systems.d.ts +105 -7
- package/dist/design/design-systems.d.ts.map +1 -1
- package/dist/design/design-systems.js +210 -16
- package/dist/design/design-systems.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +94 -6
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +2 -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 +1188 -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 +329 -12
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +2 -1
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +1 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +10 -0
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +437 -87
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +119 -74
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +62 -6
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +5 -1
- package/package.json +5 -2
|
@@ -886,19 +886,36 @@ function generateBatchModifyCode(modifications) {
|
|
|
886
886
|
return `
|
|
887
887
|
// Batch modify ${modifications.length} items
|
|
888
888
|
const modifications = ${modsJson};
|
|
889
|
+
// TWO BUGS, ONE CALL, AND BOTH FAILED LOUDLY IN THE WRONG PLACE.
|
|
890
|
+
//
|
|
891
|
+
// 1. The engine takes { item, params } with a LIVE ITEM, or { itemId, params }
|
|
892
|
+
// with a registry id. This put the id STRING into the item field, which is
|
|
893
|
+
// truthy — so the engine's own id lookup never ran, the string was treated
|
|
894
|
+
// as an item, and Object.assign got a string: "Cannot convert undefined or
|
|
895
|
+
// null to object", from a call that named the id correctly.
|
|
896
|
+
// 2. batchModify returns a COUNT, not an array. Calling .map on a number
|
|
897
|
+
// is the second reported error, and it fires even when the modify worked.
|
|
889
898
|
const results = app.batchModify(modifications.map(mod => ({
|
|
890
|
-
|
|
899
|
+
itemId: mod.itemId,
|
|
891
900
|
params: mod.params
|
|
892
901
|
})));
|
|
893
902
|
|
|
894
|
-
//
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
903
|
+
// The engine records what it could not do rather than throwing — read it,
|
|
904
|
+
// instead of reporting a clean success over a half-applied batch.
|
|
905
|
+
//
|
|
906
|
+
// Read defensively (|| []): the property arrived with the engine-side
|
|
907
|
+
// batchModify fix, so an older studio reports a count and no skip list rather
|
|
908
|
+
// than failing. The batch still works; only the explanation is thinner.
|
|
909
|
+
const skipped = app.lastBatchModifySkipped || [];
|
|
910
|
+
const count = typeof results === 'number' ? results : (Array.isArray(results) ? results.length : 0);
|
|
900
911
|
|
|
901
|
-
({
|
|
912
|
+
({
|
|
913
|
+
success: skipped.length === 0 && count === modifications.length,
|
|
914
|
+
count,
|
|
915
|
+
requested: modifications.length,
|
|
916
|
+
skipped: skipped.map(s => ({ itemId: s.itemId, reason: s.reason })),
|
|
917
|
+
...(skipped.length ? { error: 'batch_modify changed ' + count + ' of ' + modifications.length + ' items; skipped: ' + skipped.map(s => s.itemId + ' (' + s.reason + ')').join(', ') } : {}),
|
|
918
|
+
});
|
|
902
919
|
`.trim();
|
|
903
920
|
}
|
|
904
921
|
/**
|
|
@@ -972,6 +989,28 @@ export function canvasPresetFor(platform) {
|
|
|
972
989
|
* inside the encode.
|
|
973
990
|
*/
|
|
974
991
|
export const INLINE_MAX_BYTES = 96 * 1024 * 1024;
|
|
992
|
+
/**
|
|
993
|
+
* Wait for the export subsystem before reading it.
|
|
994
|
+
*
|
|
995
|
+
* `app.exportEngine` is a LAZY getter over a code-split chunk: it returns
|
|
996
|
+
* undefined until `ensureHeavyModules()` has run, which the engine idle-prefetches
|
|
997
|
+
* roughly 1.2s after boot. An agent connecting and exporting immediately —
|
|
998
|
+
* which is the whole shape of an automated session — beats that prefetch, so
|
|
999
|
+
* every `app.exportEngine.*` read below found undefined and the generated code
|
|
1000
|
+
* reported the studio as too old to export. The build was fine; the chunk had
|
|
1001
|
+
* not landed yet. Awaiting it is the difference between a check and a race.
|
|
1002
|
+
*
|
|
1003
|
+
* The guards that follow this are left exactly as they are: after the await,
|
|
1004
|
+
* an absent exportEngine really does mean a studio that cannot export.
|
|
1005
|
+
*
|
|
1006
|
+
* Emitted only into the two doors an agent can reach cold. `readExport` and
|
|
1007
|
+
* `releaseExport` are only reachable after an export has already succeeded, by
|
|
1008
|
+
* which point the chunk is resident by construction.
|
|
1009
|
+
*/
|
|
1010
|
+
const ENSURE_EXPORT_ENGINE = `
|
|
1011
|
+
if (!app.exportEngine && typeof app.ensureHeavyModules === 'function') {
|
|
1012
|
+
try { await app.ensureHeavyModules(); } catch (_) { /* the guards below still speak */ }
|
|
1013
|
+
}`;
|
|
975
1014
|
export class PinePaperCodeGenerator {
|
|
976
1015
|
/**
|
|
977
1016
|
* Generate code for creating an item
|
|
@@ -1233,12 +1272,41 @@ throw new Error('Either svgString or url must be provided');
|
|
|
1233
1272
|
if (!app.importMermaid) return { success: false, error: 'importMermaid not available — diagramSystem missing' };
|
|
1234
1273
|
const mermaidText = \`${escaped}\`;
|
|
1235
1274
|
const result = app.importMermaid(mermaidText, ${optsJson});
|
|
1275
|
+
|
|
1276
|
+
// NEVER RETURN THE RAW NODES. They are Paper.js items, and a Paper item
|
|
1277
|
+
// refers to its project, which refers back — JSON.stringify dies on the cycle
|
|
1278
|
+
// with "property '_scope' closes the circle", and the tool reported "Failed
|
|
1279
|
+
// to execute code in browser" for an import that had worked. Summarised to
|
|
1280
|
+
// ids, labels and bounds, which is what a caller can act on anyway.
|
|
1281
|
+
const safeNode = (n) => {
|
|
1282
|
+
const item = n && (n.item || n);
|
|
1283
|
+
const b = item && item.bounds;
|
|
1284
|
+
return {
|
|
1285
|
+
id: (item && item.data && item.data.registryId) || (n && n.id) || null,
|
|
1286
|
+
label: (n && (n.label ?? n.text)) ?? null,
|
|
1287
|
+
bounds: b ? { x: Math.round(b.x), y: Math.round(b.y), width: Math.round(b.width), height: Math.round(b.height) } : null,
|
|
1288
|
+
};
|
|
1289
|
+
};
|
|
1290
|
+
const nodes = (result.nodes || []).map(safeNode);
|
|
1291
|
+
const edges = (result.edges || []).map((e) => ({ from: e?.from ?? e?.source ?? null, to: e?.to ?? e?.target ?? null, label: e?.label ?? null }));
|
|
1292
|
+
|
|
1293
|
+
// A zero-item import is NOT a success. The engine reported success:true with
|
|
1294
|
+
// itemsCreated:0 and the tool passed it on, so a diagram that drew nothing
|
|
1295
|
+
// read as a diagram that drew.
|
|
1296
|
+
if (!nodes.length) {
|
|
1297
|
+
return {
|
|
1298
|
+
success: false,
|
|
1299
|
+
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.',
|
|
1300
|
+
nodeCount: 0, edgeCount: edges.length, errors: result.errors || [],
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1236
1304
|
return {
|
|
1237
1305
|
success: !!result.success,
|
|
1238
|
-
nodeCount:
|
|
1239
|
-
edgeCount:
|
|
1240
|
-
nodes
|
|
1241
|
-
edges
|
|
1306
|
+
nodeCount: nodes.length,
|
|
1307
|
+
edgeCount: edges.length,
|
|
1308
|
+
nodes,
|
|
1309
|
+
edges,
|
|
1242
1310
|
errors: result.errors || [],
|
|
1243
1311
|
};
|
|
1244
1312
|
})();
|
|
@@ -1553,8 +1621,11 @@ bg.sendToBack();
|
|
|
1553
1621
|
if (backgroundGenerator) {
|
|
1554
1622
|
codeParts.push(`
|
|
1555
1623
|
// Execute background generator
|
|
1556
|
-
|
|
1557
|
-
|
|
1624
|
+
const _gen = app.generatorRegistry?.get('${backgroundGenerator}');
|
|
1625
|
+
if (typeof _gen === 'function') {
|
|
1626
|
+
_gen();
|
|
1627
|
+
} else if (_gen && typeof _gen.generate === 'function') {
|
|
1628
|
+
_gen.generate();
|
|
1558
1629
|
}
|
|
1559
1630
|
`);
|
|
1560
1631
|
}
|
|
@@ -1691,8 +1762,9 @@ results;
|
|
|
1691
1762
|
const labelStr = label ? JSON.stringify(label) : 'null';
|
|
1692
1763
|
return `
|
|
1693
1764
|
// Create diagram shape: ${shapeType}
|
|
1765
|
+
// POSITIONAL: createShape(shapeType, config). The type was being passed
|
|
1766
|
+
// INSIDE the config object, where nothing reads it.
|
|
1694
1767
|
const shapeConfig = {
|
|
1695
|
-
shapeType: '${shapeType}',
|
|
1696
1768
|
position: { x: ${position.x}, y: ${position.y} },
|
|
1697
1769
|
width: ${widthVal},
|
|
1698
1770
|
height: ${heightVal},
|
|
@@ -1700,7 +1772,8 @@ const shapeConfig = {
|
|
|
1700
1772
|
style: ${styleStr}
|
|
1701
1773
|
};
|
|
1702
1774
|
|
|
1703
|
-
const shape = app.
|
|
1775
|
+
const shape = app.diagramSystem.createShape('${shapeType}', shapeConfig);
|
|
1776
|
+
if (!shape) throw new Error('createShape returned nothing for shapeType "${shapeType}" — call pinepaper_get_diagram_shapes for the registered types.');
|
|
1704
1777
|
const itemId = shape.data?.registryId || shape.id;
|
|
1705
1778
|
app.historyManager.saveState();
|
|
1706
1779
|
|
|
@@ -1744,7 +1817,7 @@ if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}');
|
|
|
1744
1817
|
if (!targetItem) throw new Error('Target item not found: ${targetItemId}');
|
|
1745
1818
|
|
|
1746
1819
|
const config = ${JSON.stringify(config, null, 2)};
|
|
1747
|
-
const connector = app.
|
|
1820
|
+
const connector = app.diagramSystem.connect(sourceItem, targetItem, config);
|
|
1748
1821
|
const connectorId = connector.data?.registryId || connector.id;
|
|
1749
1822
|
app.historyManager.saveState();
|
|
1750
1823
|
|
|
@@ -1772,7 +1845,7 @@ if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}');
|
|
|
1772
1845
|
if (!targetItem) throw new Error('Target item not found: ${targetItemId}');
|
|
1773
1846
|
|
|
1774
1847
|
const config = ${configStr};
|
|
1775
|
-
const connector = app.
|
|
1848
|
+
const connector = app.diagramSystem.connectPorts(
|
|
1776
1849
|
sourceItem, '${sourcePort}',
|
|
1777
1850
|
targetItem, '${targetPort}',
|
|
1778
1851
|
config
|
|
@@ -1789,21 +1862,31 @@ app.historyManager.saveState();
|
|
|
1789
1862
|
generateAddPorts(input) {
|
|
1790
1863
|
const validated = AddPortsInputSchema.parse(input);
|
|
1791
1864
|
const { itemId, portType, ports, count } = validated;
|
|
1792
|
-
|
|
1793
|
-
|
|
1865
|
+
// THE SCHEMA PROMISES MORE THAN THE ENGINE HAS. `addPorts(item, type)` takes
|
|
1866
|
+
// a type string and lays out that type's port set; there is no per-port
|
|
1867
|
+
// placement and no count. The old emitter passed {portType, ports, count}
|
|
1868
|
+
// as the second argument, which the engine tried to iterate — "t.forEach is
|
|
1869
|
+
// not a function" — so every call failed and the two extra fields looked
|
|
1870
|
+
// like they might have worked. Refused by name instead of dropped.
|
|
1871
|
+
if (ports !== undefined || count !== undefined) {
|
|
1872
|
+
const named = [ports !== undefined && 'ports', count !== undefined && 'count'].filter(Boolean).join(' and ');
|
|
1873
|
+
throw new Error(`add_ports cannot honour ${named}: this engine lays out a whole port SET by type `
|
|
1874
|
+
+ `(portType '${portType}'), with no per-port placement or count. Drop ${named}, or place `
|
|
1875
|
+
+ 'individual connection points as items and join them with pinepaper_connect.');
|
|
1876
|
+
}
|
|
1794
1877
|
return `
|
|
1795
1878
|
// Add ports to item: ${itemId}
|
|
1796
1879
|
const item = app.getItemById('${itemId}');
|
|
1797
1880
|
if (!item) throw new Error('Item not found: ${itemId}');
|
|
1798
1881
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
count: ${countVal}
|
|
1803
|
-
});
|
|
1882
|
+
// The second argument is a TYPE STRING, not a config object. Passing an
|
|
1883
|
+
// object made the engine iterate it as a list — "t.forEach is not a function".
|
|
1884
|
+
const result = app.diagramSystem.addPorts(item, '${portType}');
|
|
1804
1885
|
app.historyManager.saveState();
|
|
1805
1886
|
|
|
1806
|
-
|
|
1887
|
+
// addPorts returns the ports it made; older builds returned a count object.
|
|
1888
|
+
const _added = Array.isArray(result) ? result.length : (result?.portsAdded ?? 0);
|
|
1889
|
+
({ itemId: '${itemId}', portsAdded: _added, portType: '${portType}' });
|
|
1807
1890
|
`.trim();
|
|
1808
1891
|
}
|
|
1809
1892
|
/**
|
|
@@ -1816,13 +1899,26 @@ app.historyManager.saveState();
|
|
|
1816
1899
|
const optionsStr = options ? JSON.stringify(options, null, 2) : '{}';
|
|
1817
1900
|
return `
|
|
1818
1901
|
// Apply auto-layout: ${layoutType}
|
|
1902
|
+
// Wrapped: applyLayout is async, and a bare top-level await only works on the
|
|
1903
|
+
// governed path. A paren-led IIFE also keeps the trailing-expression capture.
|
|
1904
|
+
(async function() {
|
|
1819
1905
|
const itemIds = ${itemIdsStr};
|
|
1820
1906
|
const options = ${optionsStr};
|
|
1821
1907
|
|
|
1822
|
-
|
|
1908
|
+
// applyLayout(items, type, options) — ITEMS first, and it takes live items
|
|
1909
|
+
// rather than ids. It is async, and it resolves to undefined: reading
|
|
1910
|
+
// result.itemsAffected off that throws, which is how a layout that worked
|
|
1911
|
+
// reported as a crash.
|
|
1912
|
+
const items = itemIds
|
|
1913
|
+
? itemIds.map((id) => app.getItemById(id)).filter(Boolean)
|
|
1914
|
+
: (app.itemRegistry?.getAll?.() ?? []);
|
|
1915
|
+
if (!items.length) throw new Error('auto_layout found no items to lay out' + (itemIds ? ' for the given itemIds' : ' on the canvas'));
|
|
1916
|
+
|
|
1917
|
+
await app.diagramSystem.applyLayout(items, '${layoutType}', options);
|
|
1823
1918
|
app.historyManager.saveState();
|
|
1824
1919
|
|
|
1825
|
-
|
|
1920
|
+
return { layoutType: '${layoutType}', itemsAffected: items.length, success: true };
|
|
1921
|
+
})();
|
|
1826
1922
|
`.trim();
|
|
1827
1923
|
}
|
|
1828
1924
|
/**
|
|
@@ -1835,7 +1931,10 @@ app.historyManager.saveState();
|
|
|
1835
1931
|
return `
|
|
1836
1932
|
// Get available diagram shapes
|
|
1837
1933
|
const category = ${categoryFilter};
|
|
1838
|
-
|
|
1934
|
+
// The library is the registry; diagramSystem has no getAvailableShapes.
|
|
1935
|
+
const _lib = app.diagramSystem?.shapeLibrary;
|
|
1936
|
+
if (!_lib) throw new Error('app.diagramSystem.shapeLibrary unavailable — update FxTool');
|
|
1937
|
+
const shapes = category ? _lib.getByCategory(category) : _lib.getAll();
|
|
1839
1938
|
|
|
1840
1939
|
({ shapes, count: shapes.length, category: ${categoryFilter} || 'all' });
|
|
1841
1940
|
`.trim();
|
|
@@ -1863,10 +1962,44 @@ const connector = app.getItemById('${connectorId}');
|
|
|
1863
1962
|
if (!connector) throw new Error('Connector not found: ${connectorId}');
|
|
1864
1963
|
|
|
1865
1964
|
const updates = ${updatesStr};
|
|
1866
|
-
app.diagramManager.updateConnector(connector, updates);
|
|
1867
|
-
app.historyManager.saveState();
|
|
1868
1965
|
|
|
1869
|
-
|
|
1966
|
+
// THERE IS NO updateConnector. ConnectorManager publishes removeConnector,
|
|
1967
|
+
// findConnector and updateSelectedStyle — nothing that edits one connector's
|
|
1968
|
+
// properties in place. The old emitter called app.diagramSystem.updateConnector
|
|
1969
|
+
// and reported updated:true, so a caller changing a connector's colour got a
|
|
1970
|
+
// success and an unchanged connector.
|
|
1971
|
+
//
|
|
1972
|
+
// Applied directly where the property is a plain Paper.js one, and refused by
|
|
1973
|
+
// name where it is not, rather than claiming a change that did not happen.
|
|
1974
|
+
// The engine gained getConnector(id) / updateConnector(id, style), which is
|
|
1975
|
+
// the call this tool always wanted: addressable by the id a connector is
|
|
1976
|
+
// created and serialised with. Guarded, because a studio predating that still
|
|
1977
|
+
// has to do something sensible rather than throw.
|
|
1978
|
+
if (typeof app.diagramSystem?.updateConnector === 'function') {
|
|
1979
|
+
const res = app.diagramSystem.updateConnector('${connectorId}', updates);
|
|
1980
|
+
if (res && res.ok === false) { return { success: false, error: res.reason || 'the connector could not be updated' }; }
|
|
1981
|
+
app.historyManager.saveState();
|
|
1982
|
+
return { connectorId: '${connectorId}', updated: true, applied: Object.keys(updates) };
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
const _applied = [];
|
|
1986
|
+
const _unsupported = [];
|
|
1987
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
1988
|
+
if (k === 'lineColor' && connector.strokeColor !== undefined) { connector.strokeColor = v; _applied.push(k); }
|
|
1989
|
+
else if (k === 'lineWidth' && connector.strokeWidth !== undefined) { connector.strokeWidth = v; _applied.push(k); }
|
|
1990
|
+
else _unsupported.push(k);
|
|
1991
|
+
}
|
|
1992
|
+
if (_applied.length) { app.historyManager.saveState(); app._scheduleRepaint?.(); }
|
|
1993
|
+
if (_unsupported.length && !_applied.length) {
|
|
1994
|
+
throw new Error(
|
|
1995
|
+
'this build cannot update ' + _unsupported.join(', ') + ' on an existing connector — '
|
|
1996
|
+
+ 'this build has no per-connector update — app.diagramSystem.updateConnector arrived later. '
|
|
1997
|
+
+ 'Update the studio for it. Removing and recreating the connector works, but mints a NEW id '
|
|
1998
|
+
+ 'and orphans anything holding the old one, so it is a last resort rather than the fix.'
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
({ connectorId: '${connectorId}', updated: _applied.length > 0, applied: _applied, unsupported: _unsupported });
|
|
1870
2003
|
`.trim();
|
|
1871
2004
|
}
|
|
1872
2005
|
/**
|
|
@@ -1880,7 +2013,7 @@ app.historyManager.saveState();
|
|
|
1880
2013
|
const connector = app.getItemById('${connectorId}');
|
|
1881
2014
|
if (!connector) throw new Error('Connector not found: ${connectorId}');
|
|
1882
2015
|
|
|
1883
|
-
app.
|
|
2016
|
+
app.diagramSystem.connectorManager.removeConnector(connector);
|
|
1884
2017
|
app.historyManager.saveState();
|
|
1885
2018
|
|
|
1886
2019
|
({ connectorId: '${connectorId}', removed: true });
|
|
@@ -1896,19 +2029,19 @@ app.historyManager.saveState();
|
|
|
1896
2029
|
case 'activate':
|
|
1897
2030
|
return `
|
|
1898
2031
|
// Activate diagram mode
|
|
1899
|
-
app.
|
|
2032
|
+
app.diagramSystem.activate();
|
|
1900
2033
|
({ action: 'activate', active: true });
|
|
1901
2034
|
`.trim();
|
|
1902
2035
|
case 'deactivate':
|
|
1903
2036
|
return `
|
|
1904
2037
|
// Deactivate diagram mode
|
|
1905
|
-
app.
|
|
2038
|
+
app.diagramSystem.deactivate();
|
|
1906
2039
|
({ action: 'deactivate', active: false });
|
|
1907
2040
|
`.trim();
|
|
1908
2041
|
case 'toggle':
|
|
1909
2042
|
return `
|
|
1910
2043
|
// Toggle diagram mode
|
|
1911
|
-
const isActive = app.
|
|
2044
|
+
const isActive = app.diagramSystem.toggle();
|
|
1912
2045
|
({ action: 'toggle', active: isActive });
|
|
1913
2046
|
`.trim();
|
|
1914
2047
|
case 'setMode':
|
|
@@ -1916,7 +2049,7 @@ const isActive = app.diagramManager.toggle();
|
|
|
1916
2049
|
const shapeStr = shapeType ? `, '${shapeType}'` : '';
|
|
1917
2050
|
return `
|
|
1918
2051
|
// Set diagram tool mode
|
|
1919
|
-
app.
|
|
2052
|
+
app.diagramSystem.setMode(${modeStr}${shapeStr});
|
|
1920
2053
|
({ action: 'setMode', mode: ${modeStr}${shapeType ? `, shapeType: '${shapeType}'` : ''} });
|
|
1921
2054
|
`.trim();
|
|
1922
2055
|
default:
|
|
@@ -2075,7 +2208,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2075
2208
|
|
|
2076
2209
|
// FxTool has no animationManager — animations are detected via item.data.animationType walk above
|
|
2077
2210
|
// Check timeline for active animations
|
|
2078
|
-
if (app.
|
|
2211
|
+
if (app.timelineState?.isPlaying || app.animatedItems?.size > 0) {
|
|
2079
2212
|
analysis.hasAnimations = true;
|
|
2080
2213
|
animationSet.add('timeline');
|
|
2081
2214
|
}
|
|
@@ -2204,6 +2337,17 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2204
2337
|
let code = `
|
|
2205
2338
|
// Batch execute ${operations.length} operations
|
|
2206
2339
|
(async function() {
|
|
2340
|
+
// An op's own verdict. Deliberately narrow, matching the server-side rule:
|
|
2341
|
+
// only an explicit success:false or ok:false is a failure, because most ops
|
|
2342
|
+
// return a bare value (an id, a count) and treating a missing field as
|
|
2343
|
+
// refusal would fail every one of them.
|
|
2344
|
+
const _opFailure = (v) => {
|
|
2345
|
+
if (!v || typeof v !== 'object') return null;
|
|
2346
|
+
if (v.success !== false && v.ok !== false) return null;
|
|
2347
|
+
return (typeof v.error === 'string' && v.error)
|
|
2348
|
+
|| (typeof v.reason === 'string' && v.reason)
|
|
2349
|
+
|| 'the operation reported failure without naming a reason';
|
|
2350
|
+
};
|
|
2207
2351
|
const results = [];
|
|
2208
2352
|
const itemIds = [];
|
|
2209
2353
|
let success = true;
|
|
@@ -2228,10 +2372,24 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2228
2372
|
const result${index} = await (async () => {
|
|
2229
2373
|
${opCode}
|
|
2230
2374
|
})();
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2375
|
+
// NOT THROWING IS NOT SUCCEEDING. An op that RETURNS { success: false }
|
|
2376
|
+
// — a guard that found a facade missing, a preset the engine does not
|
|
2377
|
+
// have, a precondition unmet — raises nothing, so this recorded it as a
|
|
2378
|
+
// success and the batch reported success over it. Measured on
|
|
2379
|
+
// applyAnimatedMask(item, 'star'): the per-op error was perfectly good
|
|
2380
|
+
// and the batch still said everything worked.
|
|
2381
|
+
const failed${index} = _opFailure(result${index});
|
|
2382
|
+
if (failed${index}) {
|
|
2383
|
+
// ATOMIC: throw WITHOUT recording, and let the catch below record it
|
|
2384
|
+
// once. Doing both put two entries in for one failed op.
|
|
2385
|
+
${isAtomic ? `throw new Error(failed${index});` : `results.push({ index: ${index}, success: false, error: failed${index}, result: result${index} });
|
|
2386
|
+
success = false;`}
|
|
2387
|
+
} else {
|
|
2388
|
+
results.push({ index: ${index}, success: true, result: result${index} });
|
|
2389
|
+
${claimsItemSlot ? `if (result${index} && result${index}.itemId) {
|
|
2390
|
+
itemIds.push(result${index}.itemId);
|
|
2391
|
+
}` : ''}
|
|
2392
|
+
}
|
|
2235
2393
|
} catch (opError) {
|
|
2236
2394
|
results.push({ index: ${index}, success: false, error: opError.message });
|
|
2237
2395
|
${isAtomic ? 'throw opError;' : 'success = false;'}
|
|
@@ -2243,7 +2401,14 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2243
2401
|
if (app.historyManager) app.historyManager.saveState();
|
|
2244
2402
|
|
|
2245
2403
|
const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
2246
|
-
|
|
2404
|
+
// Name WHICH ops failed on the result itself. A caller reading a failed
|
|
2405
|
+
// 60-op batch should not have to scan sixty entries to learn that op 41
|
|
2406
|
+
// could not find a preset.
|
|
2407
|
+
const _failed = results.filter((r) => !r.success);
|
|
2408
|
+
return { success, itemIds, results, operationCount: ${operations.length},
|
|
2409
|
+
failedCount: _failed.length,
|
|
2410
|
+
...(_failed.length ? { error: _failed.map((r) => 'op ' + r.index + ': ' + r.error).join('; ') } : {}),
|
|
2411
|
+
canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
|
|
2247
2412
|
} catch (e) {
|
|
2248
2413
|
const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
2249
2414
|
return { success: false, error: e.message, itemIds, results, operationCount: ${operations.length}, canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
|
|
@@ -2435,7 +2600,15 @@ return { success: !!maskedGroup, itemId: targetId, preset: '${preset}' };
|
|
|
2435
2600
|
const targetId = ${effItemRef};
|
|
2436
2601
|
const item = app.getItemById(targetId);
|
|
2437
2602
|
if (!item) throw new Error('Item not found: ' + targetId);
|
|
2438
|
-
|
|
2603
|
+
// PROPAGATE the engine's verdict. This discarded it and returned its own
|
|
2604
|
+
// shape, so an effect the engine refused — an unknown preset, a facade that
|
|
2605
|
+
// is not loaded — was recorded as applied. The batch cannot report what the
|
|
2606
|
+
// op never tells it.
|
|
2607
|
+
const _eff = app.applyEffect(item, '${op.effectType || 'sparkle'}', ${effParams});
|
|
2608
|
+
if (_eff && (_eff.success === false || _eff.ok === false)) {
|
|
2609
|
+
return { success: false, itemId: targetId, effectType: '${op.effectType || 'sparkle'}',
|
|
2610
|
+
error: _eff.error || _eff.reason || 'the effect was refused without a reason' };
|
|
2611
|
+
}
|
|
2439
2612
|
return { itemId: targetId, effectType: '${op.effectType || 'sparkle'}' };
|
|
2440
2613
|
`;
|
|
2441
2614
|
}
|
|
@@ -2490,6 +2663,25 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2490
2663
|
return { ok: false, reason: 'app.exportEngine.readExport unavailable — update FxTool' };
|
|
2491
2664
|
}
|
|
2492
2665
|
return await app.exportEngine.readExport(${JSON.stringify(exportId)}, ${JSON.stringify(range)});
|
|
2666
|
+
})();`.trim();
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* What the studio is still holding.
|
|
2670
|
+
*
|
|
2671
|
+
* `id` IS the OPFS filename and OPFS is origin-persistent, so this survives a
|
|
2672
|
+
* page reload: an export whose id was lost — to a dropped tool result, a
|
|
2673
|
+
* crashed session, a reload — is recoverable from here rather than only by
|
|
2674
|
+
* rendering it again. Newest first, which is the order a caller looking for
|
|
2675
|
+
* "the one I just made" wants.
|
|
2676
|
+
*/
|
|
2677
|
+
generateListExports() {
|
|
2678
|
+
return `
|
|
2679
|
+
// What the export store is holding
|
|
2680
|
+
(async function() {${ENSURE_EXPORT_ENGINE}
|
|
2681
|
+
if (!app.exportEngine || typeof app.exportEngine.listExports !== 'function') {
|
|
2682
|
+
return { ok: false, reason: 'app.exportEngine.listExports unavailable — update FxTool' };
|
|
2683
|
+
}
|
|
2684
|
+
return { ok: true, exports: await app.exportEngine.listExports() };
|
|
2493
2685
|
})();`.trim();
|
|
2494
2686
|
}
|
|
2495
2687
|
/**
|
|
@@ -2510,7 +2702,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2510
2702
|
}
|
|
2511
2703
|
generateAgentExport(input) {
|
|
2512
2704
|
const validated = AgentExportInputSchema.parse(input);
|
|
2513
|
-
const { platform, format, quality, framing, duration, estimateOnly } = validated;
|
|
2705
|
+
const { platform, format, quality, framing, duration, estimateOnly, scale, fps } = validated;
|
|
2514
2706
|
const qualityLevel = quality || 'standard';
|
|
2515
2707
|
const videoDuration = duration ?? 5;
|
|
2516
2708
|
// Quality settings (bitrate in bps for VideoEncoder)
|
|
@@ -2533,6 +2725,22 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2533
2725
|
'print-letter': { width: 2550, height: 3300, staticFormat: 'pdf', animatedFormat: 'pdf' },
|
|
2534
2726
|
};
|
|
2535
2727
|
const preset = platformPresets[platform] || platformPresets['web'];
|
|
2728
|
+
// SCALE is the only honest size control this surface has.
|
|
2729
|
+
//
|
|
2730
|
+
// A caller who wants a smaller file reaches for a bitrate, and there is
|
|
2731
|
+
// nowhere to put one: VideoExporter computes its own target from
|
|
2732
|
+
// resolution, quality and fps via _calculateBitrate and never reads a
|
|
2733
|
+
// caller-supplied value — passing one would be a knob that silently does
|
|
2734
|
+
// nothing. Resolution is an INPUT to that calculation, so halving the
|
|
2735
|
+
// frame genuinely halves the encode target, and it is also what makes a
|
|
2736
|
+
// preview render fast. One number, both jobs, and it is real.
|
|
2737
|
+
//
|
|
2738
|
+
// Rounded to EVEN: H.264 chroma subsampling is 4:2:0, and an odd dimension
|
|
2739
|
+
// is rejected by the encoder rather than rounded for you.
|
|
2740
|
+
const even = (n) => Math.max(2, Math.round(n / 2) * 2);
|
|
2741
|
+
const scaled = scale !== undefined && scale !== 1
|
|
2742
|
+
? { width: even(preset.width * scale), height: even(preset.height * scale) }
|
|
2743
|
+
: { width: preset.width, height: preset.height };
|
|
2536
2744
|
// Resolve "auto" format to the platform's recommended format
|
|
2537
2745
|
const exportFormat = (!format || format === 'auto') ? preset.staticFormat : format;
|
|
2538
2746
|
// AUDIO-ONLY EXPORT, off the visual path entirely.
|
|
@@ -2547,13 +2755,13 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2547
2755
|
// No platform preset resolves to wav, so reaching here means it was asked
|
|
2548
2756
|
// for by name. Dimensions, framing, fps and bitrate are all absent rather
|
|
2549
2757
|
// than passed and ignored.
|
|
2550
|
-
// @engine-methods renderSoundtrackWav exportEngine.sceneHasAudio
|
|
2758
|
+
// @engine-methods renderSoundtrackWav exportEngine.sceneHasAudio ensureHeavyModules
|
|
2551
2759
|
if (exportFormat === 'wav') {
|
|
2552
2760
|
const sr = validated.sampleRate ?? 48000;
|
|
2553
2761
|
const bd = validated.bitDepth ?? 16;
|
|
2554
2762
|
return `
|
|
2555
2763
|
// Export: the soundtrack on its own, rendered offline
|
|
2556
|
-
(function() {
|
|
2764
|
+
(async function() {${ENSURE_EXPORT_ENGINE}
|
|
2557
2765
|
if (typeof app.renderSoundtrackWav !== 'function') {
|
|
2558
2766
|
return { success: false, format: 'wav', error: 'app.renderSoundtrackWav unavailable — update FxTool. This build cannot export audio on its own; mp4/webm still mux the soundtrack into the video.' };
|
|
2559
2767
|
}
|
|
@@ -2607,13 +2815,17 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2607
2815
|
}
|
|
2608
2816
|
return `
|
|
2609
2817
|
// Smart export for ${platform}
|
|
2610
|
-
(async function() {
|
|
2818
|
+
(async function() {${ENSURE_EXPORT_ENGINE}
|
|
2611
2819
|
const platform = '${platform}';
|
|
2612
2820
|
const format = '${exportFormat}';
|
|
2613
2821
|
const quality = '${qualityLevel}';
|
|
2614
2822
|
const framing = '${framing}';
|
|
2615
|
-
|
|
2616
|
-
|
|
2823
|
+
// An explicit fps wins over the tier's. The tiers bundle frame rate with
|
|
2824
|
+
// compression (draft 15 / standard 30 / high 60), so "higher quality" also
|
|
2825
|
+
// doubled the frame count and the render time — a surprise the schema used
|
|
2826
|
+
// to hide because there was no way to ask for one without the other.
|
|
2827
|
+
const settings = ${JSON.stringify(fps !== undefined ? { ...qualitySettings, fps } : qualitySettings)};
|
|
2828
|
+
const dimensions = ${JSON.stringify(scaled)};
|
|
2617
2829
|
|
|
2618
2830
|
// Preflight: same resolved settings as the real export, but render nothing.
|
|
2619
2831
|
// Branching HERE rather than in a separate tool is deliberate — the estimate
|
|
@@ -2679,7 +2891,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2679
2891
|
try {
|
|
2680
2892
|
switch (format) {
|
|
2681
2893
|
case 'svg':
|
|
2682
|
-
const svgString = app.exportAnimatedSVG ? app.exportAnimatedSVG() : app.
|
|
2894
|
+
const svgString = app.exportAnimatedSVG ? app.exportAnimatedSVG() : app.exportSVGWithCSS();
|
|
2683
2895
|
result = {
|
|
2684
2896
|
success: true,
|
|
2685
2897
|
platform,
|
|
@@ -2728,7 +2940,10 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2728
2940
|
// ExportEngine._quickExportVideo overrides quality with undefined for
|
|
2729
2941
|
// mp4/webm, _calculateBitrate yields NaN, and VideoEncoder.configure
|
|
2730
2942
|
// rejects with 'malformed bitrate value'.
|
|
2731
|
-
|
|
2943
|
+
// width/height ride along ONLY when a scale was asked for. The buffered
|
|
2944
|
+
// fallback paths otherwise export at canvas size, and quietly changing
|
|
2945
|
+
// that for every existing caller is not what a new optional knob does.
|
|
2946
|
+
const baseVideoSettings = { format, fps: settings.fps, quality: settings.compression, duration: ${videoDuration}${scale !== undefined && scale !== 1 ? ', width: dimensions.width, height: dimensions.height' : ''} };
|
|
2732
2947
|
const blobToDataUrl = (b) => new Promise(resolve => {
|
|
2733
2948
|
const r = new FileReader();
|
|
2734
2949
|
r.onloadend = () => resolve(r.result);
|
|
@@ -2931,7 +3146,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2931
3146
|
|
|
2932
3147
|
// FxTool has no animationManager — animations are detected via item.data.animationType walk above
|
|
2933
3148
|
// Check timeline for active animations
|
|
2934
|
-
if (app.
|
|
3149
|
+
if (app.timelineState?.isPlaying || app.animatedItems?.size > 0) {
|
|
2935
3150
|
analysis.hasAnimations = true;
|
|
2936
3151
|
animationSet.add('timeline');
|
|
2937
3152
|
}
|
|
@@ -3077,8 +3292,15 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3077
3292
|
return { success: false, error: 'Letter collage animation not available' };
|
|
3078
3293
|
}
|
|
3079
3294
|
|
|
3080
|
-
|
|
3081
|
-
|
|
3295
|
+
// POSITIONAL, and it was not: the engine signature is
|
|
3296
|
+
// animateLetterCollage(collageId, animationType, options), in PinePaper.js.
|
|
3297
|
+
// Passing one object put it in the animationType slot, so the type the caller
|
|
3298
|
+
// asked for was discarded on EVERY call and the collage animated as whatever
|
|
3299
|
+
// the engine falls back to. Silent: the call still returned, still reported
|
|
3300
|
+
// success, and the collage still moved a little, which is why it read as
|
|
3301
|
+
// "the flagship text effect doesn't survive export" rather than as a
|
|
3302
|
+
// dropped argument.
|
|
3303
|
+
const result = app.animateLetterCollage(collageId, animationType, {
|
|
3082
3304
|
staggerDelay,
|
|
3083
3305
|
animationSpeed
|
|
3084
3306
|
});
|
|
@@ -3458,7 +3680,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3458
3680
|
}
|
|
3459
3681
|
|
|
3460
3682
|
try {
|
|
3461
|
-
app.mapSystem.
|
|
3683
|
+
app.mapSystem.stopRegionAnimations({
|
|
3462
3684
|
regions: ${regionsStr},
|
|
3463
3685
|
resetColors: ${validated.resetColors !== false}
|
|
3464
3686
|
});
|
|
@@ -3502,7 +3724,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3502
3724
|
}
|
|
3503
3725
|
|
|
3504
3726
|
try {
|
|
3505
|
-
const result = app.mapSystem.
|
|
3727
|
+
const result = app.mapSystem.exportRegionDataCSV({
|
|
3506
3728
|
includeHighlighted: ${validated.includeHighlighted !== false},
|
|
3507
3729
|
includeSelected: ${validated.includeSelected !== false},
|
|
3508
3730
|
includeColors: ${validated.includeColors !== false},
|
|
@@ -3531,8 +3753,9 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3531
3753
|
}
|
|
3532
3754
|
|
|
3533
3755
|
try {
|
|
3534
|
-
|
|
3535
|
-
|
|
3756
|
+
// importRegionDataCSV(csvText, options) — the TEXT is positional, and it
|
|
3757
|
+
// was being passed inside the options bag where nothing read it.
|
|
3758
|
+
const result = app.mapSystem.importRegionDataCSV(\`${escapedCsv}\`, {
|
|
3536
3759
|
applyColors: ${validated.applyColors !== false},
|
|
3537
3760
|
applyHighlight: ${validated.applyHighlight !== false},
|
|
3538
3761
|
applySelection: ${validated.applySelection !== false}
|
|
@@ -3841,7 +4064,15 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3841
4064
|
if (item.strokeColor) itemData.properties.strokeColor = item.strokeColor.toCSS ? item.strokeColor.toCSS() : item.strokeColor;
|
|
3842
4065
|
if (item.strokeWidth) itemData.properties.strokeWidth = item.strokeWidth;
|
|
3843
4066
|
if (item.opacity !== undefined) itemData.properties.opacity = item.opacity;
|
|
3844
|
-
|
|
4067
|
+
// TRUE ROTATION, not the property. create() BAKES an angle into the path
|
|
4068
|
+
// segments under Paper's applyMatrix default, so item.rotation reads back 0 on
|
|
4069
|
+
// an item that is visibly rotated. Reporting that 0 is why "I could not rotate
|
|
4070
|
+
// a rectangle" was filed against an engine that rotates correctly. The engine
|
|
4071
|
+
// records the baked offset at item.data._bakedRotation and sums the two itself
|
|
4072
|
+
// (PinePaper.js does exactly this); read the same sum rather than the half of
|
|
4073
|
+
// it that happens to be zero.
|
|
4074
|
+
const _rot = (item.data?._bakedRotation || 0) + (item.rotation || 0);
|
|
4075
|
+
if (_rot) itemData.properties.rotation = _rot;
|
|
3845
4076
|
if (item.data?.content) itemData.properties.content = item.data.content;
|
|
3846
4077
|
if (item.visible === false) itemData.properties.visible = false;
|
|
3847
4078
|
|
|
@@ -3895,7 +4126,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3895
4126
|
}
|
|
3896
4127
|
|
|
3897
4128
|
// Get background color and canvas size
|
|
3898
|
-
const backgroundColor = app.
|
|
4129
|
+
const backgroundColor = app.canvasEl?.style?.backgroundColor || null;
|
|
3899
4130
|
const canvasSize = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
3900
4131
|
|
|
3901
4132
|
return {
|
|
@@ -3959,10 +4190,15 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3959
4190
|
}
|
|
3960
4191
|
|
|
3961
4192
|
try {
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
}
|
|
4193
|
+
// Two methods, not one with a flag: getOriginalGeoJSON() returns the data,
|
|
4194
|
+
// downloadOriginalGeoJSON(filename) hands it to the browser.
|
|
4195
|
+
// exportOriginalGeoJSON has never existed.
|
|
4196
|
+
const result = ${download}
|
|
4197
|
+
? app.mapSystem.downloadOriginalGeoJSON(${JSON.stringify(filename)})
|
|
4198
|
+
: app.mapSystem.getOriginalGeoJSON();
|
|
4199
|
+
if (result === null || result === undefined) {
|
|
4200
|
+
return { success: false, error: 'no map is loaded, so there is no source GeoJSON to return' };
|
|
4201
|
+
}
|
|
3966
4202
|
return result;
|
|
3967
4203
|
} catch (error) {
|
|
3968
4204
|
return { success: false, error: error.message };
|
|
@@ -3982,7 +4218,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3982
4218
|
}
|
|
3983
4219
|
|
|
3984
4220
|
try {
|
|
3985
|
-
const result = app.mapSystem.
|
|
4221
|
+
const result = app.mapSystem.getMapSourceInfo();
|
|
3986
4222
|
return result;
|
|
3987
4223
|
} catch (error) {
|
|
3988
4224
|
return { success: false, error: error.message };
|
|
@@ -4079,9 +4315,25 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4079
4315
|
generateExecuteCustomCode(input) {
|
|
4080
4316
|
const validated = ExecuteCustomCodeInputSchema.parse(input);
|
|
4081
4317
|
const description = validated.description || 'Execute custom code';
|
|
4318
|
+
// ASYNC, because the code inside is not ours.
|
|
4319
|
+
//
|
|
4320
|
+
// This wrapped the caller's snippet in a SYNC IIFE, which does two things
|
|
4321
|
+
// to async work. A top-level `await` is a syntax error inside a
|
|
4322
|
+
// non-async function, so a snippet that awaits anything fails to parse at
|
|
4323
|
+
// all. And async work started without await returns a promise nobody
|
|
4324
|
+
// holds: the IIFE returns immediately, the tool reports success, and the
|
|
4325
|
+
// next call reads a canvas where the work has not happened yet.
|
|
4326
|
+
//
|
|
4327
|
+
// That is the whole async half of the engine — export, image import,
|
|
4328
|
+
// rigging bake, layout — unreachable from the one tool whose entire
|
|
4329
|
+
// purpose is reaching things the tool surface does not cover.
|
|
4330
|
+
//
|
|
4331
|
+
// An async IIFE is still a `(`-led expression statement, so the governor
|
|
4332
|
+
// captures its value, and executeCode's eval fallback already resolves a
|
|
4333
|
+
// returned promise. Both paths await it; neither needed changing.
|
|
4082
4334
|
return `
|
|
4083
4335
|
// ${description}
|
|
4084
|
-
(function() {
|
|
4336
|
+
(async function() {
|
|
4085
4337
|
try {
|
|
4086
4338
|
${validated.code}
|
|
4087
4339
|
} catch (error) {
|
|
@@ -4335,7 +4587,41 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4335
4587
|
return { error: 'Image tools not available. Make sure PinePaper Studio is loaded.' };
|
|
4336
4588
|
}
|
|
4337
4589
|
try {
|
|
4338
|
-
|
|
4590
|
+
// FETCH IT OURSELVES FIRST, to find out WHY when it fails.
|
|
4591
|
+
//
|
|
4592
|
+
// uploadFromURL sets img.src and rejects with a bare "Failed to load image
|
|
4593
|
+
// from URL" — an <img> onerror genuinely carries no reason, so that message
|
|
4594
|
+
// is the browser's limit rather than the engine being unhelpful. fetch()
|
|
4595
|
+
// has the reason: a status code, a CORS rejection, a refused connection, a
|
|
4596
|
+
// DNS failure. A caller cannot debug what the tool will not show them.
|
|
4597
|
+
//
|
|
4598
|
+
// The bytes then go in as a DATA URL, which img.src loads without touching
|
|
4599
|
+
// the network again — so a host that allows fetch but blocks the image
|
|
4600
|
+
// load (CORS on <img>, a tainted-canvas refusal) now works too.
|
|
4601
|
+
let src = '${url}';
|
|
4602
|
+
if (!src.startsWith('data:')) {
|
|
4603
|
+
let res;
|
|
4604
|
+
try {
|
|
4605
|
+
res = await fetch(src);
|
|
4606
|
+
} catch (netErr) {
|
|
4607
|
+
return { success: false, error: 'could not reach ' + src + ' — ' + (netErr && netErr.message ? netErr.message : 'network request failed')
|
|
4608
|
+
+ '. 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.' };
|
|
4609
|
+
}
|
|
4610
|
+
if (!res.ok) {
|
|
4611
|
+
return { success: false, error: 'the server refused ' + src + ' — HTTP ' + res.status + ' ' + (res.statusText || ''), status: res.status };
|
|
4612
|
+
}
|
|
4613
|
+
const blob = await res.blob();
|
|
4614
|
+
if (!blob.type.startsWith('image/')) {
|
|
4615
|
+
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 };
|
|
4616
|
+
}
|
|
4617
|
+
src = await new Promise((resolve, reject) => {
|
|
4618
|
+
const fr = new FileReader();
|
|
4619
|
+
fr.onloadend = () => resolve(fr.result);
|
|
4620
|
+
fr.onerror = () => reject(new Error('could not read the downloaded image'));
|
|
4621
|
+
fr.readAsDataURL(blob);
|
|
4622
|
+
});
|
|
4623
|
+
}
|
|
4624
|
+
const entry = await app.imageTools.uploadFromURL(src);
|
|
4339
4625
|
const opts = ${optsLiteral};
|
|
4340
4626
|
const raster = await app.imageTools.placeImage(entry.id, Object.keys(opts).length > 0 ? opts : undefined);
|
|
4341
4627
|
${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} // The REGISTRY id is \`data.id\`. \`data.itemId\` has never existed, so this
|
|
@@ -5313,8 +5599,23 @@ ${resolve}
|
|
|
5313
5599
|
(function() {
|
|
5314
5600
|
const itemIds = ${JSON.stringify(input.itemIds || [])};
|
|
5315
5601
|
const opts = ${JSON.stringify(opts)};
|
|
5316
|
-
|
|
5317
|
-
|
|
5602
|
+
|
|
5603
|
+
// createPrecomp wants LIVE ITEMS. Handing it registry ids put strings into
|
|
5604
|
+
// Paper.js's insertChildren, which fails as "l._remove is not a function" —
|
|
5605
|
+
// a minified internal, from a schema that promises ids work. Resolved here,
|
|
5606
|
+
// and a missing one is named rather than silently dropped from the group.
|
|
5607
|
+
const items = [];
|
|
5608
|
+
const missing = [];
|
|
5609
|
+
for (const id of itemIds) {
|
|
5610
|
+
const item = app.getItemById(id);
|
|
5611
|
+
if (item) items.push(item); else missing.push(id);
|
|
5612
|
+
}
|
|
5613
|
+
if (missing.length) return { success: false, error: 'precomp: no such item(s): ' + missing.join(', ') };
|
|
5614
|
+
if (!items.length) return { success: false, error: 'precomp needs at least one item' };
|
|
5615
|
+
|
|
5616
|
+
const precomp = app.createPrecomp(items, opts);
|
|
5617
|
+
if (!precomp) return { success: false, error: 'createPrecomp returned nothing' };
|
|
5618
|
+
return { success: true, action: 'create', precompId: precomp?.id || precomp, itemCount: items.length, name: ${JSON.stringify(input.name || '')} };
|
|
5318
5619
|
})();`.trim();
|
|
5319
5620
|
}
|
|
5320
5621
|
case 'add':
|
|
@@ -5406,7 +5707,7 @@ ${resolve}
|
|
|
5406
5707
|
bounds: item.bounds ? { x: item.bounds.x, y: item.bounds.y, width: item.bounds.width, height: item.bounds.height } : null,
|
|
5407
5708
|
visible: item.visible,
|
|
5408
5709
|
opacity: item.opacity,
|
|
5409
|
-
rotation: item.rotation,
|
|
5710
|
+
rotation: (item.data?._bakedRotation || 0) + (item.rotation || 0),
|
|
5410
5711
|
selected: item.selected,
|
|
5411
5712
|
};
|
|
5412
5713
|
})();`.trim();
|
|
@@ -5742,7 +6043,11 @@ ${resolve}
|
|
|
5742
6043
|
// SPRITE SHEETS
|
|
5743
6044
|
// ===========================================================================
|
|
5744
6045
|
generateSpriteSheet(input) {
|
|
5745
|
-
|
|
6046
|
+
// spriteSystem is _defineLazyHeavy: undefined until ensureHeavyModules()
|
|
6047
|
+
// lands, so the bare guard reported "not available" on a fresh session for
|
|
6048
|
+
// a studio that has it. Same bug as app.exportEngine in 1.6.9.
|
|
6049
|
+
const guard = `if (!app.spriteSystem && typeof app.ensureHeavyModules === 'function') { try { await app.ensureHeavyModules(); } catch (_) {} }
|
|
6050
|
+
if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
5746
6051
|
switch (input.action) {
|
|
5747
6052
|
case 'generate': {
|
|
5748
6053
|
const opts = {};
|
|
@@ -5762,7 +6067,7 @@ ${resolve}
|
|
|
5762
6067
|
// Generate sprite sheet from skeleton
|
|
5763
6068
|
(async function() {
|
|
5764
6069
|
${guard}
|
|
5765
|
-
const sheet = await app.
|
|
6070
|
+
const sheet = await app.generateSpriteSheet(${JSON.stringify(input.skeletonId || '')}, ${JSON.stringify(opts)});
|
|
5766
6071
|
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 };
|
|
5767
6072
|
})();`.trim();
|
|
5768
6073
|
}
|
|
@@ -5782,7 +6087,7 @@ ${resolve}
|
|
|
5782
6087
|
// Play sprite sheet animation
|
|
5783
6088
|
(async function() {
|
|
5784
6089
|
${guard}
|
|
5785
|
-
const player = await app.
|
|
6090
|
+
const player = await app.playSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, ${JSON.stringify(opts)});
|
|
5786
6091
|
return { success: true, action: 'play', playerId: player.id };
|
|
5787
6092
|
})();`.trim();
|
|
5788
6093
|
}
|
|
@@ -5791,7 +6096,7 @@ ${resolve}
|
|
|
5791
6096
|
// Export sprite sheet
|
|
5792
6097
|
(async function() {
|
|
5793
6098
|
${guard}
|
|
5794
|
-
const result = await app.
|
|
6099
|
+
const result = await app.exportSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, { format: '${input.format || 'png'}', download: true, includeMetadata: true });
|
|
5795
6100
|
return { success: true, action: 'export', format: '${input.format || 'png'}' };
|
|
5796
6101
|
})();`.trim();
|
|
5797
6102
|
default:
|
|
@@ -6149,7 +6454,8 @@ ${resolve}
|
|
|
6149
6454
|
// Toggle rulers
|
|
6150
6455
|
(function() {
|
|
6151
6456
|
${guard}
|
|
6152
|
-
|
|
6457
|
+
// showRulers()/hideRulers(), not a boolean setter — setRulersVisible never existed.
|
|
6458
|
+
app.measurementSystem[${enabled} ? 'showRulers' : 'hideRulers']();
|
|
6153
6459
|
return { success: true, action: 'set_rulers', enabled: ${enabled} };
|
|
6154
6460
|
})();`.trim();
|
|
6155
6461
|
}
|
|
@@ -6159,7 +6465,8 @@ ${resolve}
|
|
|
6159
6465
|
// Toggle grid
|
|
6160
6466
|
(function() {
|
|
6161
6467
|
${guard}
|
|
6162
|
-
|
|
6468
|
+
// showGrid()/hideGrid(), not a boolean setter — setGridVisible never existed.
|
|
6469
|
+
app.measurementSystem[${enabled} ? 'showGrid' : 'hideGrid']();
|
|
6163
6470
|
return { success: true, action: 'set_grid', enabled: ${enabled} };
|
|
6164
6471
|
})();`.trim();
|
|
6165
6472
|
}
|
|
@@ -6174,17 +6481,21 @@ ${resolve}
|
|
|
6174
6481
|
const entry = app.itemRegistry.get(${itemIdStr});
|
|
6175
6482
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
|
|
6176
6483
|
const b = entry.item.bounds;
|
|
6177
|
-
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 };
|
|
6484
|
+
return { success: true, action: 'get_dimensions', itemId: ${itemIdStr}, x: b.x, y: b.y, width: b.width, height: b.height, rotation: (entry.item.data?._bakedRotation || 0) + (entry.item.rotation || 0) };
|
|
6178
6485
|
})();`.trim();
|
|
6179
6486
|
}
|
|
6180
6487
|
case 'set_snap': {
|
|
6181
|
-
|
|
6488
|
+
// `enabled` is deliberately unread: there is no mode to set either way.
|
|
6182
6489
|
return `
|
|
6183
|
-
//
|
|
6490
|
+
// Snap to grid — refused, see below
|
|
6184
6491
|
(function() {
|
|
6185
6492
|
${guard}
|
|
6186
|
-
|
|
6187
|
-
|
|
6493
|
+
// NO EQUIVALENT. MeasurementSystem publishes snapCoordinate(), which snaps a
|
|
6494
|
+
// coordinate you hand it, and nothing that turns snapping on as a mode —
|
|
6495
|
+
// setSnapToUnitEnabled has never existed, so this reported success over a
|
|
6496
|
+
// setting that was never changed. Refused by name, with the thing that works.
|
|
6497
|
+
return { success: false, action: 'set_snap',
|
|
6498
|
+
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.' };
|
|
6188
6499
|
})();`.trim();
|
|
6189
6500
|
}
|
|
6190
6501
|
default:
|
|
@@ -6696,8 +7007,19 @@ ${guard}
|
|
|
6696
7007
|
const r = app.setFontAxes(${S(input.itemId)}, ${S(input.axes)});
|
|
6697
7008
|
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'axes failed' }; }
|
|
6698
7009
|
// rejected is the interesting half: an axis silently ignored is the failure
|
|
6699
|
-
// mode this surface exists to prevent.
|
|
6700
|
-
|
|
7010
|
+
// mode this surface exists to prevent. And NOTHING applied while something
|
|
7011
|
+
// was rejected is not a success — it is the call doing nothing under a
|
|
7012
|
+
// success, which is the same shape as an empty batch reporting ok.
|
|
7013
|
+
const _applied = r.applied || {};
|
|
7014
|
+
const _rejected = r.rejected || [];
|
|
7015
|
+
const _appliedCount = Array.isArray(_applied) ? _applied.length : Object.keys(_applied).length;
|
|
7016
|
+
if (_appliedCount === 0 && _rejected.length) {
|
|
7017
|
+
return { success: false, action: 'set_font_axes', applied: _applied, rejected: _rejected,
|
|
7018
|
+
error: 'no axis was applied. The font does not have: ' + _rejected.join(', ')
|
|
7019
|
+
+ (r.note ? '. ' + r.note : '') };
|
|
7020
|
+
}
|
|
7021
|
+
return { success: true, action: 'set_font_axes', applied: _applied, rejected: _rejected,
|
|
7022
|
+
...(r.note ? { note: r.note } : {}) };
|
|
6701
7023
|
})();`.trim();
|
|
6702
7024
|
}
|
|
6703
7025
|
case 'list_styles': {
|
|
@@ -7540,7 +7862,15 @@ ${needWorld}
|
|
|
7540
7862
|
if (typeof app.storyFromText !== 'function') { return { success: false, error: 'app.storyFromText unavailable — update FxTool' }; }
|
|
7541
7863
|
const r = await app.storyFromText(${S(input.text)}, ${opts});
|
|
7542
7864
|
if (!r || r.ok === false) { return { success: false, error: (r && (r.reason || r.error)) || 'the story did not assemble' }; }
|
|
7543
|
-
|
|
7865
|
+
// NAMED FIELDS, not a spread. assembleStory returns its parts as live Paper
|
|
7866
|
+
// Groups, and a Paper item's graph reaches the editor's EVENT HANDLERS.
|
|
7867
|
+
// Structured clone — which is what the bridge uses — refuses to carry a
|
|
7868
|
+
// function, so the whole result threw DataCloneError and the message did not
|
|
7869
|
+
// survive the wrapping: an empty error string for a story that assembled
|
|
7870
|
+
// fine in-page. Not a cycle; JSON.stringify succeeded on this the whole time.
|
|
7871
|
+
// The groups have to stay for in-page callers, so the wrapper takes what
|
|
7872
|
+
// travels: the spec, the ids, the count.
|
|
7873
|
+
return { success: true, spec: r.spec, partIds: r.partIds, partCount: r.partCount, beats: r.beats };
|
|
7544
7874
|
})();`.trim();
|
|
7545
7875
|
case 'apply_spec':
|
|
7546
7876
|
return `
|
|
@@ -7964,11 +8294,23 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
7964
8294
|
return { success: true, action: 'create_skeleton', skeletonId: skeletonId };`);
|
|
7965
8295
|
}
|
|
7966
8296
|
case 'add_bone': {
|
|
8297
|
+
// DEGREES IN, RADIANS OUT. The schema says degrees and every other
|
|
8298
|
+
// angle on this surface is degrees, but RiggingSystem stores what it is
|
|
8299
|
+
// given and _solveFKRecursive feeds it straight to Math.cos/Math.sin.
|
|
8300
|
+
// So a documented 90 was read as 90 radians and the character exploded
|
|
8301
|
+
// on the first pose — silently, because a wrong pose is not an error.
|
|
8302
|
+
// Converted here so the tool keeps the vocabulary it advertises.
|
|
8303
|
+
// BOTH SPELLINGS, deliberately. The engine's _angleRad prefers
|
|
8304
|
+
// `angleDegrees` and converts it, so the new build reads the caller's
|
|
8305
|
+
// intent verbatim instead of trusting arithmetic done out here. `angle`
|
|
8306
|
+
// carries the same value already in radians, which is what an older
|
|
8307
|
+
// build reads — it has no angleDegrees and would see 0. Identical
|
|
8308
|
+
// result on both, and the units are stated rather than implied.
|
|
7967
8309
|
const config = S({
|
|
7968
8310
|
...(input.name !== undefined ? { name: input.name } : {}),
|
|
7969
8311
|
...(input.parentBoneId !== undefined ? { parentBoneId: input.parentBoneId } : {}),
|
|
7970
8312
|
...(input.length !== undefined ? { length: input.length } : {}),
|
|
7971
|
-
...(input.angle !== undefined ? { angle: input.angle } : {}),
|
|
8313
|
+
...(input.angle !== undefined ? { angleDegrees: input.angle, angle: (input.angle * Math.PI) / 180 } : {}),
|
|
7972
8314
|
...(input.flexibility !== undefined ? { flexibility: input.flexibility } : {}),
|
|
7973
8315
|
...(input.segments !== undefined ? { segments: input.segments } : {}),
|
|
7974
8316
|
});
|
|
@@ -8008,7 +8350,15 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
8008
8350
|
...(input.movingHold !== undefined ? { movingHold: input.movingHold } : {}),
|
|
8009
8351
|
...(input.holdDrift !== undefined ? { holdDrift: input.holdDrift } : {}),
|
|
8010
8352
|
});
|
|
8011
|
-
|
|
8353
|
+
// Same unit conversion as add_bone: an inline pose is { boneId: angleDeg }
|
|
8354
|
+
// by the schema, and the solver reads radians. A SAVED pose is a string
|
|
8355
|
+
// id and is passed through untouched — those angles are already in the
|
|
8356
|
+
// engine's own units.
|
|
8357
|
+
const poseArg = (input.pose !== null && typeof input.pose === 'object')
|
|
8358
|
+
? S(Object.fromEntries(Object.entries(input.pose)
|
|
8359
|
+
.map(([bone, deg]) => [bone, (deg * Math.PI) / 180])))
|
|
8360
|
+
: S(input.pose);
|
|
8361
|
+
return wrap('Rigging: add pose keyframe', ` const ok = R.addPoseKeyframe(${S(input.skeletonId)}, ${input.time}, ${poseArg}, ${S(input.easing ?? 'linear')}, ${opts});
|
|
8012
8362
|
if (app.historyManager) app.historyManager.saveState();
|
|
8013
8363
|
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" }; }
|
|
8014
8364
|
return { success: true, action: 'add_pose_keyframe', time: ${input.time} };`);
|