@pinepaper.studio/mcp-server 1.6.9 → 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.md +118 -3
- 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/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 +31 -3
- package/dist/tools/definitions.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 +117 -4
- package/dist/tools/handlers.js.map +1 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +363 -80
- 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/manifest.json +1 -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
|
/**
|
|
@@ -1255,12 +1272,41 @@ throw new Error('Either svgString or url must be provided');
|
|
|
1255
1272
|
if (!app.importMermaid) return { success: false, error: 'importMermaid not available — diagramSystem missing' };
|
|
1256
1273
|
const mermaidText = \`${escaped}\`;
|
|
1257
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
|
+
|
|
1258
1304
|
return {
|
|
1259
1305
|
success: !!result.success,
|
|
1260
|
-
nodeCount:
|
|
1261
|
-
edgeCount:
|
|
1262
|
-
nodes
|
|
1263
|
-
edges
|
|
1306
|
+
nodeCount: nodes.length,
|
|
1307
|
+
edgeCount: edges.length,
|
|
1308
|
+
nodes,
|
|
1309
|
+
edges,
|
|
1264
1310
|
errors: result.errors || [],
|
|
1265
1311
|
};
|
|
1266
1312
|
})();
|
|
@@ -1575,8 +1621,11 @@ bg.sendToBack();
|
|
|
1575
1621
|
if (backgroundGenerator) {
|
|
1576
1622
|
codeParts.push(`
|
|
1577
1623
|
// Execute background generator
|
|
1578
|
-
|
|
1579
|
-
|
|
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();
|
|
1580
1629
|
}
|
|
1581
1630
|
`);
|
|
1582
1631
|
}
|
|
@@ -1713,8 +1762,9 @@ results;
|
|
|
1713
1762
|
const labelStr = label ? JSON.stringify(label) : 'null';
|
|
1714
1763
|
return `
|
|
1715
1764
|
// Create diagram shape: ${shapeType}
|
|
1765
|
+
// POSITIONAL: createShape(shapeType, config). The type was being passed
|
|
1766
|
+
// INSIDE the config object, where nothing reads it.
|
|
1716
1767
|
const shapeConfig = {
|
|
1717
|
-
shapeType: '${shapeType}',
|
|
1718
1768
|
position: { x: ${position.x}, y: ${position.y} },
|
|
1719
1769
|
width: ${widthVal},
|
|
1720
1770
|
height: ${heightVal},
|
|
@@ -1722,7 +1772,8 @@ const shapeConfig = {
|
|
|
1722
1772
|
style: ${styleStr}
|
|
1723
1773
|
};
|
|
1724
1774
|
|
|
1725
|
-
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.');
|
|
1726
1777
|
const itemId = shape.data?.registryId || shape.id;
|
|
1727
1778
|
app.historyManager.saveState();
|
|
1728
1779
|
|
|
@@ -1766,7 +1817,7 @@ if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}');
|
|
|
1766
1817
|
if (!targetItem) throw new Error('Target item not found: ${targetItemId}');
|
|
1767
1818
|
|
|
1768
1819
|
const config = ${JSON.stringify(config, null, 2)};
|
|
1769
|
-
const connector = app.
|
|
1820
|
+
const connector = app.diagramSystem.connect(sourceItem, targetItem, config);
|
|
1770
1821
|
const connectorId = connector.data?.registryId || connector.id;
|
|
1771
1822
|
app.historyManager.saveState();
|
|
1772
1823
|
|
|
@@ -1794,7 +1845,7 @@ if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}');
|
|
|
1794
1845
|
if (!targetItem) throw new Error('Target item not found: ${targetItemId}');
|
|
1795
1846
|
|
|
1796
1847
|
const config = ${configStr};
|
|
1797
|
-
const connector = app.
|
|
1848
|
+
const connector = app.diagramSystem.connectPorts(
|
|
1798
1849
|
sourceItem, '${sourcePort}',
|
|
1799
1850
|
targetItem, '${targetPort}',
|
|
1800
1851
|
config
|
|
@@ -1811,21 +1862,31 @@ app.historyManager.saveState();
|
|
|
1811
1862
|
generateAddPorts(input) {
|
|
1812
1863
|
const validated = AddPortsInputSchema.parse(input);
|
|
1813
1864
|
const { itemId, portType, ports, count } = validated;
|
|
1814
|
-
|
|
1815
|
-
|
|
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
|
+
}
|
|
1816
1877
|
return `
|
|
1817
1878
|
// Add ports to item: ${itemId}
|
|
1818
1879
|
const item = app.getItemById('${itemId}');
|
|
1819
1880
|
if (!item) throw new Error('Item not found: ${itemId}');
|
|
1820
1881
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
count: ${countVal}
|
|
1825
|
-
});
|
|
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}');
|
|
1826
1885
|
app.historyManager.saveState();
|
|
1827
1886
|
|
|
1828
|
-
|
|
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}' });
|
|
1829
1890
|
`.trim();
|
|
1830
1891
|
}
|
|
1831
1892
|
/**
|
|
@@ -1838,13 +1899,26 @@ app.historyManager.saveState();
|
|
|
1838
1899
|
const optionsStr = options ? JSON.stringify(options, null, 2) : '{}';
|
|
1839
1900
|
return `
|
|
1840
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() {
|
|
1841
1905
|
const itemIds = ${itemIdsStr};
|
|
1842
1906
|
const options = ${optionsStr};
|
|
1843
1907
|
|
|
1844
|
-
|
|
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);
|
|
1845
1918
|
app.historyManager.saveState();
|
|
1846
1919
|
|
|
1847
|
-
|
|
1920
|
+
return { layoutType: '${layoutType}', itemsAffected: items.length, success: true };
|
|
1921
|
+
})();
|
|
1848
1922
|
`.trim();
|
|
1849
1923
|
}
|
|
1850
1924
|
/**
|
|
@@ -1857,7 +1931,10 @@ app.historyManager.saveState();
|
|
|
1857
1931
|
return `
|
|
1858
1932
|
// Get available diagram shapes
|
|
1859
1933
|
const category = ${categoryFilter};
|
|
1860
|
-
|
|
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();
|
|
1861
1938
|
|
|
1862
1939
|
({ shapes, count: shapes.length, category: ${categoryFilter} || 'all' });
|
|
1863
1940
|
`.trim();
|
|
@@ -1885,10 +1962,44 @@ const connector = app.getItemById('${connectorId}');
|
|
|
1885
1962
|
if (!connector) throw new Error('Connector not found: ${connectorId}');
|
|
1886
1963
|
|
|
1887
1964
|
const updates = ${updatesStr};
|
|
1888
|
-
app.diagramManager.updateConnector(connector, updates);
|
|
1889
|
-
app.historyManager.saveState();
|
|
1890
1965
|
|
|
1891
|
-
|
|
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 });
|
|
1892
2003
|
`.trim();
|
|
1893
2004
|
}
|
|
1894
2005
|
/**
|
|
@@ -1902,7 +2013,7 @@ app.historyManager.saveState();
|
|
|
1902
2013
|
const connector = app.getItemById('${connectorId}');
|
|
1903
2014
|
if (!connector) throw new Error('Connector not found: ${connectorId}');
|
|
1904
2015
|
|
|
1905
|
-
app.
|
|
2016
|
+
app.diagramSystem.connectorManager.removeConnector(connector);
|
|
1906
2017
|
app.historyManager.saveState();
|
|
1907
2018
|
|
|
1908
2019
|
({ connectorId: '${connectorId}', removed: true });
|
|
@@ -1918,19 +2029,19 @@ app.historyManager.saveState();
|
|
|
1918
2029
|
case 'activate':
|
|
1919
2030
|
return `
|
|
1920
2031
|
// Activate diagram mode
|
|
1921
|
-
app.
|
|
2032
|
+
app.diagramSystem.activate();
|
|
1922
2033
|
({ action: 'activate', active: true });
|
|
1923
2034
|
`.trim();
|
|
1924
2035
|
case 'deactivate':
|
|
1925
2036
|
return `
|
|
1926
2037
|
// Deactivate diagram mode
|
|
1927
|
-
app.
|
|
2038
|
+
app.diagramSystem.deactivate();
|
|
1928
2039
|
({ action: 'deactivate', active: false });
|
|
1929
2040
|
`.trim();
|
|
1930
2041
|
case 'toggle':
|
|
1931
2042
|
return `
|
|
1932
2043
|
// Toggle diagram mode
|
|
1933
|
-
const isActive = app.
|
|
2044
|
+
const isActive = app.diagramSystem.toggle();
|
|
1934
2045
|
({ action: 'toggle', active: isActive });
|
|
1935
2046
|
`.trim();
|
|
1936
2047
|
case 'setMode':
|
|
@@ -1938,7 +2049,7 @@ const isActive = app.diagramManager.toggle();
|
|
|
1938
2049
|
const shapeStr = shapeType ? `, '${shapeType}'` : '';
|
|
1939
2050
|
return `
|
|
1940
2051
|
// Set diagram tool mode
|
|
1941
|
-
app.
|
|
2052
|
+
app.diagramSystem.setMode(${modeStr}${shapeStr});
|
|
1942
2053
|
({ action: 'setMode', mode: ${modeStr}${shapeType ? `, shapeType: '${shapeType}'` : ''} });
|
|
1943
2054
|
`.trim();
|
|
1944
2055
|
default:
|
|
@@ -2097,7 +2208,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2097
2208
|
|
|
2098
2209
|
// FxTool has no animationManager — animations are detected via item.data.animationType walk above
|
|
2099
2210
|
// Check timeline for active animations
|
|
2100
|
-
if (app.
|
|
2211
|
+
if (app.timelineState?.isPlaying || app.animatedItems?.size > 0) {
|
|
2101
2212
|
analysis.hasAnimations = true;
|
|
2102
2213
|
animationSet.add('timeline');
|
|
2103
2214
|
}
|
|
@@ -2226,6 +2337,17 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2226
2337
|
let code = `
|
|
2227
2338
|
// Batch execute ${operations.length} operations
|
|
2228
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
|
+
};
|
|
2229
2351
|
const results = [];
|
|
2230
2352
|
const itemIds = [];
|
|
2231
2353
|
let success = true;
|
|
@@ -2250,10 +2372,24 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2250
2372
|
const result${index} = await (async () => {
|
|
2251
2373
|
${opCode}
|
|
2252
2374
|
})();
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
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
|
+
}
|
|
2257
2393
|
} catch (opError) {
|
|
2258
2394
|
results.push({ index: ${index}, success: false, error: opError.message });
|
|
2259
2395
|
${isAtomic ? 'throw opError;' : 'success = false;'}
|
|
@@ -2265,7 +2401,14 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2265
2401
|
if (app.historyManager) app.historyManager.saveState();
|
|
2266
2402
|
|
|
2267
2403
|
const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
2268
|
-
|
|
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 } };
|
|
2269
2412
|
} catch (e) {
|
|
2270
2413
|
const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
2271
2414
|
return { success: false, error: e.message, itemIds, results, operationCount: ${operations.length}, canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
|
|
@@ -2457,7 +2600,15 @@ return { success: !!maskedGroup, itemId: targetId, preset: '${preset}' };
|
|
|
2457
2600
|
const targetId = ${effItemRef};
|
|
2458
2601
|
const item = app.getItemById(targetId);
|
|
2459
2602
|
if (!item) throw new Error('Item not found: ' + targetId);
|
|
2460
|
-
|
|
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
|
+
}
|
|
2461
2612
|
return { itemId: targetId, effectType: '${op.effectType || 'sparkle'}' };
|
|
2462
2613
|
`;
|
|
2463
2614
|
}
|
|
@@ -2551,7 +2702,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2551
2702
|
}
|
|
2552
2703
|
generateAgentExport(input) {
|
|
2553
2704
|
const validated = AgentExportInputSchema.parse(input);
|
|
2554
|
-
const { platform, format, quality, framing, duration, estimateOnly, scale } = validated;
|
|
2705
|
+
const { platform, format, quality, framing, duration, estimateOnly, scale, fps } = validated;
|
|
2555
2706
|
const qualityLevel = quality || 'standard';
|
|
2556
2707
|
const videoDuration = duration ?? 5;
|
|
2557
2708
|
// Quality settings (bitrate in bps for VideoEncoder)
|
|
@@ -2669,7 +2820,11 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2669
2820
|
const format = '${exportFormat}';
|
|
2670
2821
|
const quality = '${qualityLevel}';
|
|
2671
2822
|
const framing = '${framing}';
|
|
2672
|
-
|
|
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)};
|
|
2673
2828
|
const dimensions = ${JSON.stringify(scaled)};
|
|
2674
2829
|
|
|
2675
2830
|
// Preflight: same resolved settings as the real export, but render nothing.
|
|
@@ -2736,7 +2891,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2736
2891
|
try {
|
|
2737
2892
|
switch (format) {
|
|
2738
2893
|
case 'svg':
|
|
2739
|
-
const svgString = app.exportAnimatedSVG ? app.exportAnimatedSVG() : app.
|
|
2894
|
+
const svgString = app.exportAnimatedSVG ? app.exportAnimatedSVG() : app.exportSVGWithCSS();
|
|
2740
2895
|
result = {
|
|
2741
2896
|
success: true,
|
|
2742
2897
|
platform,
|
|
@@ -2991,7 +3146,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2991
3146
|
|
|
2992
3147
|
// FxTool has no animationManager — animations are detected via item.data.animationType walk above
|
|
2993
3148
|
// Check timeline for active animations
|
|
2994
|
-
if (app.
|
|
3149
|
+
if (app.timelineState?.isPlaying || app.animatedItems?.size > 0) {
|
|
2995
3150
|
analysis.hasAnimations = true;
|
|
2996
3151
|
animationSet.add('timeline');
|
|
2997
3152
|
}
|
|
@@ -3525,7 +3680,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3525
3680
|
}
|
|
3526
3681
|
|
|
3527
3682
|
try {
|
|
3528
|
-
app.mapSystem.
|
|
3683
|
+
app.mapSystem.stopRegionAnimations({
|
|
3529
3684
|
regions: ${regionsStr},
|
|
3530
3685
|
resetColors: ${validated.resetColors !== false}
|
|
3531
3686
|
});
|
|
@@ -3569,7 +3724,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3569
3724
|
}
|
|
3570
3725
|
|
|
3571
3726
|
try {
|
|
3572
|
-
const result = app.mapSystem.
|
|
3727
|
+
const result = app.mapSystem.exportRegionDataCSV({
|
|
3573
3728
|
includeHighlighted: ${validated.includeHighlighted !== false},
|
|
3574
3729
|
includeSelected: ${validated.includeSelected !== false},
|
|
3575
3730
|
includeColors: ${validated.includeColors !== false},
|
|
@@ -3598,8 +3753,9 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3598
3753
|
}
|
|
3599
3754
|
|
|
3600
3755
|
try {
|
|
3601
|
-
|
|
3602
|
-
|
|
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}\`, {
|
|
3603
3759
|
applyColors: ${validated.applyColors !== false},
|
|
3604
3760
|
applyHighlight: ${validated.applyHighlight !== false},
|
|
3605
3761
|
applySelection: ${validated.applySelection !== false}
|
|
@@ -3908,7 +4064,15 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3908
4064
|
if (item.strokeColor) itemData.properties.strokeColor = item.strokeColor.toCSS ? item.strokeColor.toCSS() : item.strokeColor;
|
|
3909
4065
|
if (item.strokeWidth) itemData.properties.strokeWidth = item.strokeWidth;
|
|
3910
4066
|
if (item.opacity !== undefined) itemData.properties.opacity = item.opacity;
|
|
3911
|
-
|
|
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;
|
|
3912
4076
|
if (item.data?.content) itemData.properties.content = item.data.content;
|
|
3913
4077
|
if (item.visible === false) itemData.properties.visible = false;
|
|
3914
4078
|
|
|
@@ -3962,7 +4126,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3962
4126
|
}
|
|
3963
4127
|
|
|
3964
4128
|
// Get background color and canvas size
|
|
3965
|
-
const backgroundColor = app.
|
|
4129
|
+
const backgroundColor = app.canvasEl?.style?.backgroundColor || null;
|
|
3966
4130
|
const canvasSize = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
|
|
3967
4131
|
|
|
3968
4132
|
return {
|
|
@@ -4026,10 +4190,15 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4026
4190
|
}
|
|
4027
4191
|
|
|
4028
4192
|
try {
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
}
|
|
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
|
+
}
|
|
4033
4202
|
return result;
|
|
4034
4203
|
} catch (error) {
|
|
4035
4204
|
return { success: false, error: error.message };
|
|
@@ -4049,7 +4218,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4049
4218
|
}
|
|
4050
4219
|
|
|
4051
4220
|
try {
|
|
4052
|
-
const result = app.mapSystem.
|
|
4221
|
+
const result = app.mapSystem.getMapSourceInfo();
|
|
4053
4222
|
return result;
|
|
4054
4223
|
} catch (error) {
|
|
4055
4224
|
return { success: false, error: error.message };
|
|
@@ -4146,9 +4315,25 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4146
4315
|
generateExecuteCustomCode(input) {
|
|
4147
4316
|
const validated = ExecuteCustomCodeInputSchema.parse(input);
|
|
4148
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.
|
|
4149
4334
|
return `
|
|
4150
4335
|
// ${description}
|
|
4151
|
-
(function() {
|
|
4336
|
+
(async function() {
|
|
4152
4337
|
try {
|
|
4153
4338
|
${validated.code}
|
|
4154
4339
|
} catch (error) {
|
|
@@ -4402,7 +4587,41 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4402
4587
|
return { error: 'Image tools not available. Make sure PinePaper Studio is loaded.' };
|
|
4403
4588
|
}
|
|
4404
4589
|
try {
|
|
4405
|
-
|
|
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);
|
|
4406
4625
|
const opts = ${optsLiteral};
|
|
4407
4626
|
const raster = await app.imageTools.placeImage(entry.id, Object.keys(opts).length > 0 ? opts : undefined);
|
|
4408
4627
|
${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} // The REGISTRY id is \`data.id\`. \`data.itemId\` has never existed, so this
|
|
@@ -5380,8 +5599,23 @@ ${resolve}
|
|
|
5380
5599
|
(function() {
|
|
5381
5600
|
const itemIds = ${JSON.stringify(input.itemIds || [])};
|
|
5382
5601
|
const opts = ${JSON.stringify(opts)};
|
|
5383
|
-
|
|
5384
|
-
|
|
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 || '')} };
|
|
5385
5619
|
})();`.trim();
|
|
5386
5620
|
}
|
|
5387
5621
|
case 'add':
|
|
@@ -5473,7 +5707,7 @@ ${resolve}
|
|
|
5473
5707
|
bounds: item.bounds ? { x: item.bounds.x, y: item.bounds.y, width: item.bounds.width, height: item.bounds.height } : null,
|
|
5474
5708
|
visible: item.visible,
|
|
5475
5709
|
opacity: item.opacity,
|
|
5476
|
-
rotation: item.rotation,
|
|
5710
|
+
rotation: (item.data?._bakedRotation || 0) + (item.rotation || 0),
|
|
5477
5711
|
selected: item.selected,
|
|
5478
5712
|
};
|
|
5479
5713
|
})();`.trim();
|
|
@@ -5809,7 +6043,11 @@ ${resolve}
|
|
|
5809
6043
|
// SPRITE SHEETS
|
|
5810
6044
|
// ===========================================================================
|
|
5811
6045
|
generateSpriteSheet(input) {
|
|
5812
|
-
|
|
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' };`;
|
|
5813
6051
|
switch (input.action) {
|
|
5814
6052
|
case 'generate': {
|
|
5815
6053
|
const opts = {};
|
|
@@ -5829,7 +6067,7 @@ ${resolve}
|
|
|
5829
6067
|
// Generate sprite sheet from skeleton
|
|
5830
6068
|
(async function() {
|
|
5831
6069
|
${guard}
|
|
5832
|
-
const sheet = await app.
|
|
6070
|
+
const sheet = await app.generateSpriteSheet(${JSON.stringify(input.skeletonId || '')}, ${JSON.stringify(opts)});
|
|
5833
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 };
|
|
5834
6072
|
})();`.trim();
|
|
5835
6073
|
}
|
|
@@ -5849,7 +6087,7 @@ ${resolve}
|
|
|
5849
6087
|
// Play sprite sheet animation
|
|
5850
6088
|
(async function() {
|
|
5851
6089
|
${guard}
|
|
5852
|
-
const player = await app.
|
|
6090
|
+
const player = await app.playSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, ${JSON.stringify(opts)});
|
|
5853
6091
|
return { success: true, action: 'play', playerId: player.id };
|
|
5854
6092
|
})();`.trim();
|
|
5855
6093
|
}
|
|
@@ -5858,7 +6096,7 @@ ${resolve}
|
|
|
5858
6096
|
// Export sprite sheet
|
|
5859
6097
|
(async function() {
|
|
5860
6098
|
${guard}
|
|
5861
|
-
const result = await app.
|
|
6099
|
+
const result = await app.exportSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, { format: '${input.format || 'png'}', download: true, includeMetadata: true });
|
|
5862
6100
|
return { success: true, action: 'export', format: '${input.format || 'png'}' };
|
|
5863
6101
|
})();`.trim();
|
|
5864
6102
|
default:
|
|
@@ -6216,7 +6454,8 @@ ${resolve}
|
|
|
6216
6454
|
// Toggle rulers
|
|
6217
6455
|
(function() {
|
|
6218
6456
|
${guard}
|
|
6219
|
-
|
|
6457
|
+
// showRulers()/hideRulers(), not a boolean setter — setRulersVisible never existed.
|
|
6458
|
+
app.measurementSystem[${enabled} ? 'showRulers' : 'hideRulers']();
|
|
6220
6459
|
return { success: true, action: 'set_rulers', enabled: ${enabled} };
|
|
6221
6460
|
})();`.trim();
|
|
6222
6461
|
}
|
|
@@ -6226,7 +6465,8 @@ ${resolve}
|
|
|
6226
6465
|
// Toggle grid
|
|
6227
6466
|
(function() {
|
|
6228
6467
|
${guard}
|
|
6229
|
-
|
|
6468
|
+
// showGrid()/hideGrid(), not a boolean setter — setGridVisible never existed.
|
|
6469
|
+
app.measurementSystem[${enabled} ? 'showGrid' : 'hideGrid']();
|
|
6230
6470
|
return { success: true, action: 'set_grid', enabled: ${enabled} };
|
|
6231
6471
|
})();`.trim();
|
|
6232
6472
|
}
|
|
@@ -6241,17 +6481,21 @@ ${resolve}
|
|
|
6241
6481
|
const entry = app.itemRegistry.get(${itemIdStr});
|
|
6242
6482
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
|
|
6243
6483
|
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 };
|
|
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) };
|
|
6245
6485
|
})();`.trim();
|
|
6246
6486
|
}
|
|
6247
6487
|
case 'set_snap': {
|
|
6248
|
-
|
|
6488
|
+
// `enabled` is deliberately unread: there is no mode to set either way.
|
|
6249
6489
|
return `
|
|
6250
|
-
//
|
|
6490
|
+
// Snap to grid — refused, see below
|
|
6251
6491
|
(function() {
|
|
6252
6492
|
${guard}
|
|
6253
|
-
|
|
6254
|
-
|
|
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.' };
|
|
6255
6499
|
})();`.trim();
|
|
6256
6500
|
}
|
|
6257
6501
|
default:
|
|
@@ -6763,8 +7007,19 @@ ${guard}
|
|
|
6763
7007
|
const r = app.setFontAxes(${S(input.itemId)}, ${S(input.axes)});
|
|
6764
7008
|
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'axes failed' }; }
|
|
6765
7009
|
// rejected is the interesting half: an axis silently ignored is the failure
|
|
6766
|
-
// mode this surface exists to prevent.
|
|
6767
|
-
|
|
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 } : {}) };
|
|
6768
7023
|
})();`.trim();
|
|
6769
7024
|
}
|
|
6770
7025
|
case 'list_styles': {
|
|
@@ -7607,7 +7862,15 @@ ${needWorld}
|
|
|
7607
7862
|
if (typeof app.storyFromText !== 'function') { return { success: false, error: 'app.storyFromText unavailable — update FxTool' }; }
|
|
7608
7863
|
const r = await app.storyFromText(${S(input.text)}, ${opts});
|
|
7609
7864
|
if (!r || r.ok === false) { return { success: false, error: (r && (r.reason || r.error)) || 'the story did not assemble' }; }
|
|
7610
|
-
|
|
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 };
|
|
7611
7874
|
})();`.trim();
|
|
7612
7875
|
case 'apply_spec':
|
|
7613
7876
|
return `
|
|
@@ -8031,11 +8294,23 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
8031
8294
|
return { success: true, action: 'create_skeleton', skeletonId: skeletonId };`);
|
|
8032
8295
|
}
|
|
8033
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.
|
|
8034
8309
|
const config = S({
|
|
8035
8310
|
...(input.name !== undefined ? { name: input.name } : {}),
|
|
8036
8311
|
...(input.parentBoneId !== undefined ? { parentBoneId: input.parentBoneId } : {}),
|
|
8037
8312
|
...(input.length !== undefined ? { length: input.length } : {}),
|
|
8038
|
-
...(input.angle !== undefined ? { angle: input.angle } : {}),
|
|
8313
|
+
...(input.angle !== undefined ? { angleDegrees: input.angle, angle: (input.angle * Math.PI) / 180 } : {}),
|
|
8039
8314
|
...(input.flexibility !== undefined ? { flexibility: input.flexibility } : {}),
|
|
8040
8315
|
...(input.segments !== undefined ? { segments: input.segments } : {}),
|
|
8041
8316
|
});
|
|
@@ -8075,7 +8350,15 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
|
|
|
8075
8350
|
...(input.movingHold !== undefined ? { movingHold: input.movingHold } : {}),
|
|
8076
8351
|
...(input.holdDrift !== undefined ? { holdDrift: input.holdDrift } : {}),
|
|
8077
8352
|
});
|
|
8078
|
-
|
|
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});
|
|
8079
8362
|
if (app.historyManager) app.historyManager.saveState();
|
|
8080
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" }; }
|
|
8081
8364
|
return { success: true, action: 'add_pose_keyframe', time: ${input.time} };`);
|