@pinepaper.studio/mcp-server 1.6.10 → 1.6.12
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 +94 -1
- package/dist/design/design-systems.d.ts +12 -3
- package/dist/design/design-systems.d.ts.map +1 -1
- package/dist/design/design-systems.js +12 -11
- package/dist/design/design-systems.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/compact-descriptions.js +1 -1
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +189 -55
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +7 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +25 -6
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers.js +9 -0
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.js +1 -1
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/shape-params.d.ts +36 -0
- package/dist/tools/shape-params.d.ts.map +1 -0
- package/dist/tools/shape-params.js +159 -0
- package/dist/tools/shape-params.js.map +1 -0
- package/dist/tools/stick-vocabulary.d.ts +26 -0
- package/dist/tools/stick-vocabulary.d.ts.map +1 -0
- package/dist/tools/stick-vocabulary.js +102 -0
- package/dist/tools/stick-vocabulary.js.map +1 -0
- package/dist/types/code-generator.d.ts +15 -0
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +556 -76
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +97 -26
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +18 -11
- package/dist/types/schemas.js.map +1 -1
- package/dist/vendor/design/design-style.d.ts +3 -1
- package/dist/vendor/design/design-style.d.ts.map +1 -1
- package/dist/vendor/design/design-style.js +37 -2
- package/dist/vendor/design/design-style.js.map +1 -1
- package/dist/vendor/design/design-systems-dtcg.d.ts +9 -0
- package/dist/vendor/design/design-systems-dtcg.d.ts.map +1 -1
- package/dist/vendor/design/design-systems-dtcg.js +53 -1
- package/dist/vendor/design/design-systems-dtcg.js.map +1 -1
- package/dist/vendor/design/device-mockup-generator.d.ts +83 -0
- package/dist/vendor/design/device-mockup-generator.d.ts.map +1 -0
- package/dist/vendor/design/device-mockup-generator.js +634 -0
- package/dist/vendor/design/device-mockup-generator.js.map +1 -0
- package/dist/vendor/design/recursive-subdivision-generator.d.ts +79 -0
- package/dist/vendor/design/recursive-subdivision-generator.d.ts.map +1 -0
- package/dist/vendor/design/recursive-subdivision-generator.js +319 -0
- package/dist/vendor/design/recursive-subdivision-generator.js.map +1 -0
- package/dist/vendor/design/textile-weave-generator.d.ts +140 -0
- package/dist/vendor/design/textile-weave-generator.d.ts.map +1 -0
- package/dist/vendor/design/textile-weave-generator.js +568 -0
- package/dist/vendor/design/textile-weave-generator.js.map +1 -0
- package/manifest.json +1 -1
- package/package.json +8 -2
|
@@ -86,7 +86,7 @@ function generateCreateItemCode(itemType, position, properties) {
|
|
|
86
86
|
const params = {
|
|
87
87
|
x: position.x,
|
|
88
88
|
y: position.y,
|
|
89
|
-
...baseProperties,
|
|
89
|
+
...withRadiusAxes(baseProperties),
|
|
90
90
|
};
|
|
91
91
|
// Handle simple solid colors in params
|
|
92
92
|
if (color && !isGradient(color)) {
|
|
@@ -128,6 +128,29 @@ function generateCreateItemCode(itemType, position, properties) {
|
|
|
128
128
|
const item = app.create('rectangle', ${JSON.stringify(plate, null, 2)});
|
|
129
129
|
if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.data.renderParams = ${JSON.stringify(params)}; }`;
|
|
130
130
|
}
|
|
131
|
+
// AN UNKNOWN ANCHOR IS INVISIBLE OVER MCP.
|
|
132
|
+
//
|
|
133
|
+
// The engine names a bad anchor through console.warn and leaves (x, y) as
|
|
134
|
+
// the centre — good behaviour in the editor, and nothing at all here, since
|
|
135
|
+
// the production build strips console.warn. A typo would therefore displace
|
|
136
|
+
// the item by half its own size, which is the exact bug `anchor` exists to
|
|
137
|
+
// remove, arriving silently. Checked before it is sent instead.
|
|
138
|
+
const ANCHORS = ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'center', 'centre'];
|
|
139
|
+
const anchorGiven = params.anchor ?? params.origin;
|
|
140
|
+
if (anchorGiven !== undefined) {
|
|
141
|
+
const normalized = String(anchorGiven).toLowerCase().replace(/[\s_]+/g, '-');
|
|
142
|
+
if (!ANCHORS.includes(normalized)) {
|
|
143
|
+
return `
|
|
144
|
+
// Create ${itemType} — refused, rather than silently centred
|
|
145
|
+
({ success: false, error: ${JSON.stringify(`anchor ${JSON.stringify(String(anchorGiven))} is not one of ${ANCHORS.slice(0, 5).join(', ')}. `
|
|
146
|
+
+ 'The engine would leave x/y as the CENTRE and say so only in a console this tool cannot read, '
|
|
147
|
+
+ 'so the item would land half its own size away with nothing to explain it.')} });`.trim();
|
|
148
|
+
}
|
|
149
|
+
if (params.anchor !== undefined)
|
|
150
|
+
params.anchor = normalized;
|
|
151
|
+
if (params.origin !== undefined)
|
|
152
|
+
params.origin = normalized;
|
|
153
|
+
}
|
|
131
154
|
// Build the code
|
|
132
155
|
let code = `
|
|
133
156
|
// Create ${itemType} item
|
|
@@ -181,12 +204,27 @@ app.historyManager.saveState();
|
|
|
181
204
|
*/
|
|
182
205
|
function generateModifyItemCode(itemId, properties) {
|
|
183
206
|
const { contentType, contentFormat, countdownTarget, countdownEndText, ...restProperties } = properties;
|
|
207
|
+
// modifyItem(id, changes), NOT select() + modify().
|
|
208
|
+
//
|
|
209
|
+
// modify() operates on the SELECTION, so this selected an id and then edited
|
|
210
|
+
// whatever was selected — on an id that does not resolve, select changes
|
|
211
|
+
// nothing and modify edits the PREVIOUS selection instead. modifyItem takes
|
|
212
|
+
// the reference and returns false when it cannot resolve it, which is the
|
|
213
|
+
// answer this emitter needs and never asked for.
|
|
214
|
+
//
|
|
215
|
+
// The warning the engine logs on that path is console.warn, which production
|
|
216
|
+
// strips, so the failure was invisible twice over.
|
|
184
217
|
let code = `
|
|
185
218
|
// Modify item ${itemId}
|
|
186
|
-
|
|
187
|
-
// Pass non-dynamic-content properties to app.modify
|
|
219
|
+
let _modified = true;`;
|
|
188
220
|
if (Object.keys(restProperties).length > 0) {
|
|
189
|
-
code +=
|
|
221
|
+
code += `
|
|
222
|
+
_modified = app.modifyItem('${itemId}', ${JSON.stringify(restProperties, null, 2)}) !== false;
|
|
223
|
+
if (!_modified) { return { success: false, error: 'no item ${itemId} — nothing was modified' }; }`;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
code += `
|
|
227
|
+
if (!app.getItemById('${itemId}')) { return { success: false, error: 'no item ${itemId}' }; }`;
|
|
190
228
|
}
|
|
191
229
|
// Handle dynamic content type changes
|
|
192
230
|
if (contentType !== undefined) {
|
|
@@ -212,10 +250,10 @@ if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.it
|
|
|
212
250
|
}
|
|
213
251
|
code += `
|
|
214
252
|
app.historyManager.saveState();
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
return code.trim()
|
|
253
|
+
return { success: true, itemId: '${itemId}' };`;
|
|
254
|
+
// Wrapped, because the body now RETURNS early on a miss rather than falling
|
|
255
|
+
// through to an unconditional success.
|
|
256
|
+
return `(function() {\n${code.trim()}\n})();`;
|
|
219
257
|
}
|
|
220
258
|
/**
|
|
221
259
|
* Template for deleting items
|
|
@@ -460,8 +498,62 @@ app.applyEffect(item, '${effectType}', ${JSON.stringify(params, null, 2)});
|
|
|
460
498
|
/**
|
|
461
499
|
* Template for getting items
|
|
462
500
|
*/
|
|
501
|
+
/**
|
|
502
|
+
* radiusX / radiusY REACH NOTHING.
|
|
503
|
+
*
|
|
504
|
+
* The engine's create() feeds a shape's config from width, height and radius.
|
|
505
|
+
* radiusX and radiusY are not among them, so an ellipse asked for
|
|
506
|
+
* `{radiusX: 250, radiusY: 72}` was built at the DEFAULT 100x60 and reported
|
|
507
|
+
* success — measured from production as "about 90x50". This package documented
|
|
508
|
+
* that exact pair as the ellipse's properties and used it in two worked
|
|
509
|
+
* examples, so a model following the guide got the wrong size every time and
|
|
510
|
+
* nothing anywhere said why.
|
|
511
|
+
*
|
|
512
|
+
* Mapped rather than merely re-documented: the radii are the natural way to
|
|
513
|
+
* describe an ellipse, the conversion is unambiguous (a radius is half its
|
|
514
|
+
* axis), and a caller who already wrote radiusX should not have to rewrite it.
|
|
515
|
+
* An explicit width/height wins, so nothing that works today changes.
|
|
516
|
+
*
|
|
517
|
+
* A COMPATIBILITY SHIM WITH A REMOVAL CONDITION. FxTool aaaf98ae aliases the
|
|
518
|
+
* same pair in the engine's shared param normaliser, which is the better place
|
|
519
|
+
* — it fixes every door at once, not just this one. This stays until that
|
|
520
|
+
* reaches the studios this package is pointed at, which is not the same moment
|
|
521
|
+
* it reaches origin/main: mcp-server ships independently and drives whatever
|
|
522
|
+
* studio it is given. The two cannot fight, because this deletes radiusX
|
|
523
|
+
* before the call, so the engine's alias never sees it.
|
|
524
|
+
*
|
|
525
|
+
* Delete this when a studio without aaaf98ae is no longer worth supporting.
|
|
526
|
+
*/
|
|
527
|
+
function withRadiusAxes(props) {
|
|
528
|
+
const { radiusX, radiusY, ...rest } = props;
|
|
529
|
+
if (radiusX === undefined && radiusY === undefined)
|
|
530
|
+
return props;
|
|
531
|
+
const out = { ...rest };
|
|
532
|
+
if (typeof radiusX === 'number' && out.width === undefined)
|
|
533
|
+
out.width = radiusX * 2;
|
|
534
|
+
if (typeof radiusY === 'number' && out.height === undefined)
|
|
535
|
+
out.height = radiusY * 2;
|
|
536
|
+
return out;
|
|
537
|
+
}
|
|
463
538
|
function generateGetItemsCode(filter) {
|
|
464
|
-
|
|
539
|
+
// A filter OBJECT is not the same as a filter. `{}` is already handled, but
|
|
540
|
+
// so is `{ type: undefined }` and anything whose keys this function does not
|
|
541
|
+
// recognise: those have a non-zero key count and contribute no conditions,
|
|
542
|
+
// and the emitted `entries.filter(entry => )` is a SYNTAX ERROR rather than
|
|
543
|
+
// a wrong answer — the whole call dies before it runs. Decide on the
|
|
544
|
+
// conditions, not on the keys.
|
|
545
|
+
const conditions = [];
|
|
546
|
+
if (filter?.type)
|
|
547
|
+
conditions.push(`entry.type === '${filter.type}'`);
|
|
548
|
+
if (filter?.source)
|
|
549
|
+
conditions.push(`entry.source === '${filter.source}'`);
|
|
550
|
+
if (filter?.hasAnimation !== undefined) {
|
|
551
|
+
conditions.push(`!!entry.item.data?.animationType === ${filter.hasAnimation}`);
|
|
552
|
+
}
|
|
553
|
+
if (filter?.hasRelation !== undefined) {
|
|
554
|
+
conditions.push(`(app.getRelations(entry.id || entry.itemId).length > 0) === ${filter.hasRelation}`);
|
|
555
|
+
}
|
|
556
|
+
if (conditions.length === 0) {
|
|
465
557
|
return `
|
|
466
558
|
// Get all items
|
|
467
559
|
const entries = app.itemRegistry.getAll();
|
|
@@ -476,19 +568,6 @@ const items = entries.map(entry => ({
|
|
|
476
568
|
({ items, count: items.length });
|
|
477
569
|
`.trim();
|
|
478
570
|
}
|
|
479
|
-
const conditions = [];
|
|
480
|
-
if (filter.type) {
|
|
481
|
-
conditions.push(`entry.type === '${filter.type}'`);
|
|
482
|
-
}
|
|
483
|
-
if (filter.source) {
|
|
484
|
-
conditions.push(`entry.source === '${filter.source}'`);
|
|
485
|
-
}
|
|
486
|
-
if (filter.hasAnimation !== undefined) {
|
|
487
|
-
conditions.push(`!!entry.item.data?.animationType === ${filter.hasAnimation}`);
|
|
488
|
-
}
|
|
489
|
-
if (filter.hasRelation !== undefined) {
|
|
490
|
-
conditions.push(`(app.getRelations(entry.id || entry.itemId).length > 0) === ${filter.hasRelation}`);
|
|
491
|
-
}
|
|
492
571
|
return `
|
|
493
572
|
// Get items with filter
|
|
494
573
|
const entries = app.itemRegistry.getAll();
|
|
@@ -675,9 +754,24 @@ function generateSetCanvasSizeCode(width, height, preset) {
|
|
|
675
754
|
: `{ width: ${width}, height: ${height} }`;
|
|
676
755
|
return `
|
|
677
756
|
// Set canvas size
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
757
|
+
(function() {
|
|
758
|
+
// READ THE VERDICT. This called setCanvasSize and then declared success with
|
|
759
|
+
// the dimensions it had ASKED for — so an unknown preset, or a width of -5,
|
|
760
|
+
// came back as a successful resize while the canvas was left exactly as it
|
|
761
|
+
// was. The engine now refuses by name; older builds return undefined, which
|
|
762
|
+
// is not a refusal and must not be read as one.
|
|
763
|
+
const r = app.setCanvasSize(${sizeArg});
|
|
764
|
+
if (r && r.ok === false) {
|
|
765
|
+
return { success: false, error: r.reason || 'the canvas size was refused',
|
|
766
|
+
...(r.known ? { knownPresets: r.known } : {}) };
|
|
767
|
+
}
|
|
768
|
+
app.historyManager.saveState();
|
|
769
|
+
// Report what the canvas ACTUALLY took where the engine says so, rather than
|
|
770
|
+
// echoing the request back.
|
|
771
|
+
return { success: true, width: (r && r.width) || ${width}, height: (r && r.height) || ${height},
|
|
772
|
+
...(r && r.preset ? { preset: r.preset } : {}),
|
|
773
|
+
...(r && r.unbounded !== undefined ? { unbounded: r.unbounded } : {}) };
|
|
774
|
+
})();
|
|
681
775
|
`.trim();
|
|
682
776
|
}
|
|
683
777
|
/**
|
|
@@ -771,15 +865,51 @@ const stats = app.getRelationStats();
|
|
|
771
865
|
function generateListGeneratorsCode() {
|
|
772
866
|
return `
|
|
773
867
|
// List available generators
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
868
|
+
//
|
|
869
|
+
// THREE OF SEVENTY-FOUR. getAvailableBackgroundGenerators() returned a
|
|
870
|
+
// hand-written literal of three names while the registry held 74 — nothing was
|
|
871
|
+
// broken, the other seventy-one executed fine BY NAME, they were simply
|
|
872
|
+
// invisible to anyone asking what exists. listGenerators() merges the registry
|
|
873
|
+
// and awaits it; it is newer than some studios, so the old call stays as the
|
|
874
|
+
// fallback and the result says which answer you got.
|
|
875
|
+
//
|
|
876
|
+
// Wrapped in an async IIFE deliberately: a bare top-level await parses under
|
|
877
|
+
// the governor and is a syntax error on the eval path.
|
|
878
|
+
(async function() {
|
|
879
|
+
let generators;
|
|
880
|
+
let complete = true;
|
|
881
|
+
if (typeof app.listGenerators === 'function') {
|
|
882
|
+
generators = await app.listGenerators();
|
|
883
|
+
} else {
|
|
884
|
+
generators = app.getAvailableBackgroundGenerators();
|
|
885
|
+
// The merged array carries a non-enumerable pending flag when the registry
|
|
886
|
+
// chunk has not landed, so a cold page honestly answers three.
|
|
887
|
+
complete = !generators || generators.pending !== true;
|
|
888
|
+
}
|
|
889
|
+
// PARAMS ARE THE WHOLE POINT OF ASKING.
|
|
890
|
+
//
|
|
891
|
+
// The tool says it lists generators "with their parameters" and this mapper
|
|
892
|
+
// dropped the params key, so every answer was a name and a sentence. A
|
|
893
|
+
// caller who wanted to know what drawGradientMesh accepts had to guess, and
|
|
894
|
+
// an unknown generatorParams key is accepted in silence — so a guess that
|
|
895
|
+
// was wrong looked exactly like one that was right.
|
|
896
|
+
const formatted = (generators || []).map(g => ({
|
|
897
|
+
name: g.name,
|
|
898
|
+
displayName: g.displayName || g.name,
|
|
899
|
+
category: g.category || 'background',
|
|
900
|
+
description: g.description || '',
|
|
901
|
+
...(g.params ? { params: g.params } : {})
|
|
902
|
+
}));
|
|
781
903
|
|
|
782
|
-
|
|
904
|
+
return {
|
|
905
|
+
generators: formatted,
|
|
906
|
+
count: formatted.length,
|
|
907
|
+
...(complete ? {} : {
|
|
908
|
+
partial: true,
|
|
909
|
+
note: 'the generator registry had not finished loading, so this is a partial list — the built-ins only. Retry for the full set.',
|
|
910
|
+
}),
|
|
911
|
+
};
|
|
912
|
+
})();
|
|
783
913
|
`.trim();
|
|
784
914
|
}
|
|
785
915
|
/**
|
|
@@ -1011,6 +1141,64 @@ const ENSURE_EXPORT_ENGINE = `
|
|
|
1011
1141
|
if (!app.exportEngine && typeof app.ensureHeavyModules === 'function') {
|
|
1012
1142
|
try { await app.ensureHeavyModules(); } catch (_) { /* the guards below still speak */ }
|
|
1013
1143
|
}`;
|
|
1144
|
+
/**
|
|
1145
|
+
* A REMOTE <image> INSIDE AN SVG TAINTS THE CANVAS, PERMANENTLY.
|
|
1146
|
+
*
|
|
1147
|
+
* paper.importSVG draws a cross-origin raster straight onto the canvas. The
|
|
1148
|
+
* canvas then has an origin-dirty flag, and every export afterwards throws
|
|
1149
|
+
* "Tainted canvases may not be exported" — not just the one containing the
|
|
1150
|
+
* image. Nothing recovers it but a page reload, so ONE import poisons the
|
|
1151
|
+
* whole session, long after the call that did it returned success.
|
|
1152
|
+
*
|
|
1153
|
+
* Reported from production: import_svg with a picsum href answered
|
|
1154
|
+
* success/itemsCreated:0 and then broke every later export.
|
|
1155
|
+
*
|
|
1156
|
+
* So each remote href is fetched and inlined as a data: URL before the SVG
|
|
1157
|
+
* goes anywhere near the importer — the same thing import_image does, for the
|
|
1158
|
+
* same reason. An href that CANNOT be inlined has its element removed: a
|
|
1159
|
+
* missing picture is a visible, local, recoverable problem, and leaving it in
|
|
1160
|
+
* is an unrecoverable one that surfaces somewhere else entirely.
|
|
1161
|
+
*/
|
|
1162
|
+
const INLINE_REMOTE_IMAGES = `
|
|
1163
|
+
async function inlineRemoteImages(svgText) {
|
|
1164
|
+
const notes = [];
|
|
1165
|
+
const hrefs = [];
|
|
1166
|
+
const re = /(?:xlink:)?href\\s*=\\s*("|')(https?:\\/\\/[^"']+)\\1/gi;
|
|
1167
|
+
let m;
|
|
1168
|
+
while ((m = re.exec(svgText)) !== null) if (hrefs.indexOf(m[2]) === -1) hrefs.push(m[2]);
|
|
1169
|
+
if (hrefs.length === 0) return { svg: svgText, notes: notes };
|
|
1170
|
+
for (const href of hrefs) {
|
|
1171
|
+
let dataUrl = null;
|
|
1172
|
+
try {
|
|
1173
|
+
const r = await fetch(href, { mode: 'cors' });
|
|
1174
|
+
if (r.ok) {
|
|
1175
|
+
const blob = await r.blob();
|
|
1176
|
+
dataUrl = await new Promise(function(res, rej) {
|
|
1177
|
+
const fr = new FileReader();
|
|
1178
|
+
fr.onload = function() { res(fr.result); };
|
|
1179
|
+
fr.onerror = function() { rej(new Error('could not read ' + href)); };
|
|
1180
|
+
fr.readAsDataURL(blob);
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
} catch (e) { /* fall through to removal */ }
|
|
1184
|
+
if (dataUrl) {
|
|
1185
|
+
svgText = svgText.split(href).join(dataUrl);
|
|
1186
|
+
} else {
|
|
1187
|
+
// Drop the element rather than let it taint the canvas.
|
|
1188
|
+
const esc = href.replace(/[.*+?^\\\${}()|[\\]\\\\]/g, '\\\\$&');
|
|
1189
|
+
svgText = svgText.replace(new RegExp('<image\\\\b[^>]*' + esc + '[^>]*\\\\/?>', 'gi'), '');
|
|
1190
|
+
svgText = svgText.replace(new RegExp('<image\\\\b[^>]*' + esc + '[\\\\s\\\\S]*?<\\\\/image>', 'gi'), '');
|
|
1191
|
+
notes.push({
|
|
1192
|
+
url: href,
|
|
1193
|
+
action: 'removed',
|
|
1194
|
+
reason: 'unfetchable',
|
|
1195
|
+
message: 'removed an embedded image this page could not fetch (' + href + '). It was dropped rather than imported, because a cross-origin raster taints the canvas and every later export would have failed with "Tainted canvases may not be exported" until a reload.',
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return { svg: svgText, notes: notes };
|
|
1200
|
+
}
|
|
1201
|
+
`;
|
|
1014
1202
|
export class PinePaperCodeGenerator {
|
|
1015
1203
|
/**
|
|
1016
1204
|
* Generate code for creating an item
|
|
@@ -1220,19 +1408,37 @@ export class PinePaperCodeGenerator {
|
|
|
1220
1408
|
if (url) {
|
|
1221
1409
|
return `
|
|
1222
1410
|
// Import SVG from URL${source === 'figma' ? ' (Figma-normalised)' : ''}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1411
|
+
// WRAPPED, because these were bare top-level statements with an await. That
|
|
1412
|
+
// parses under the governor, which builds an AsyncFunction, and is a SYNTAX
|
|
1413
|
+
// ERROR on the eval fallback — so importing an SVG by URL never ran at all on
|
|
1414
|
+
// a studio without runGenerated. A paren-led async IIFE parses on both and
|
|
1415
|
+
// still gives the governor a trailing expression to capture.
|
|
1416
|
+
(async function() {
|
|
1417
|
+
let response;
|
|
1418
|
+
try {
|
|
1419
|
+
response = await fetch('${url}');
|
|
1420
|
+
} catch (netErr) {
|
|
1421
|
+
return { success: false, error: 'could not reach ${url} — ' + ((netErr && netErr.message) || 'network request failed') };
|
|
1422
|
+
}
|
|
1423
|
+
if (!response.ok) {
|
|
1424
|
+
return { success: false, error: 'the server refused ${url} — HTTP ' + response.status + ' ' + (response.statusText || ''), status: response.status };
|
|
1425
|
+
}
|
|
1426
|
+
let svgText = await response.text();
|
|
1427
|
+
${INLINE_REMOTE_IMAGES}
|
|
1428
|
+
const _inl = await inlineRemoteImages(svgText);
|
|
1429
|
+
svgText = _inl.svg;
|
|
1430
|
+
const _r = ${importExpr('svgText')};
|
|
1431
|
+
const imported = _r.item;
|
|
1432
|
+
if (!imported) {
|
|
1433
|
+
return { success: false, error: 'the response from ${url} did not parse as SVG. Check the URL serves the file itself rather than a page around it.' };
|
|
1434
|
+
}
|
|
1228
1435
|
imported.position = new paper.Point(${position.x}, ${position.y});
|
|
1229
1436
|
imported.scale(${scale});
|
|
1230
1437
|
const itemId = app.registerItem(imported, 'svg-import', { source: 'mcp' });
|
|
1231
1438
|
app.historyManager.saveState();
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1439
|
+
return { success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} },
|
|
1440
|
+
...(_inl.notes.length ? { imageWarnings: _inl.notes } : {}) };
|
|
1441
|
+
})();
|
|
1236
1442
|
`.trim();
|
|
1237
1443
|
}
|
|
1238
1444
|
if (svgString) {
|
|
@@ -1240,18 +1446,23 @@ if (imported) {
|
|
|
1240
1446
|
const escapedSvg = svgString.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
1241
1447
|
return `
|
|
1242
1448
|
// Import SVG string${source === 'figma' ? ' (Figma-normalised)' : ''}
|
|
1243
|
-
|
|
1449
|
+
(async function() {
|
|
1450
|
+
let svgString = \`${escapedSvg}\`;
|
|
1451
|
+
${INLINE_REMOTE_IMAGES}
|
|
1452
|
+
const _inl = await inlineRemoteImages(svgString);
|
|
1453
|
+
svgString = _inl.svg;
|
|
1244
1454
|
const _r = ${importExpr('svgString')};
|
|
1245
1455
|
const imported = _r.item;
|
|
1246
|
-
if (imported) {
|
|
1247
|
-
imported.position = new paper.Point(${position.x}, ${position.y});
|
|
1248
|
-
imported.scale(${scale});
|
|
1249
|
-
const itemId = app.registerItem(imported, 'svg-import', { source: 'mcp' });
|
|
1250
|
-
app.historyManager.saveState();
|
|
1251
|
-
({ success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} } });
|
|
1252
|
-
} else {
|
|
1456
|
+
if (!imported) {
|
|
1253
1457
|
throw new Error('Failed to import SVG');
|
|
1254
1458
|
}
|
|
1459
|
+
imported.position = new paper.Point(${position.x}, ${position.y});
|
|
1460
|
+
imported.scale(${scale});
|
|
1461
|
+
const itemId = app.registerItem(imported, 'svg-import', { source: 'mcp' });
|
|
1462
|
+
app.historyManager.saveState();
|
|
1463
|
+
return { success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} },
|
|
1464
|
+
...(_inl.notes.length ? { imageWarnings: _inl.notes } : {}) };
|
|
1465
|
+
})();
|
|
1255
1466
|
`.trim();
|
|
1256
1467
|
}
|
|
1257
1468
|
return `
|
|
@@ -2173,11 +2384,28 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2173
2384
|
hasImages: false,
|
|
2174
2385
|
};
|
|
2175
2386
|
|
|
2176
|
-
//
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2387
|
+
// THE ARTBOARD, NOT THE BACKING STORE.
|
|
2388
|
+
//
|
|
2389
|
+
// canvasEl.width and paper.view.size are both in device pixels, so a
|
|
2390
|
+
// 1920x1080 artboard was reported as 2233x1472 — measured from production.
|
|
2391
|
+
// That is not even the device pixel ratio (a 2233px store in a 924px box is
|
|
2392
|
+
// 2.4166), so a caller cannot divide it back out. Anything derived from it
|
|
2393
|
+
// is wrong by an unknown factor, and synthetic clicks computed from it miss
|
|
2394
|
+
// in silence. getCanvasSize() is the artboard the caller set.
|
|
2395
|
+
if (typeof app.getCanvasSize === 'function') {
|
|
2396
|
+
const cs = app.getCanvasSize();
|
|
2397
|
+
if (cs && cs.width) analysis.canvasSize = { width: cs.width, height: cs.height };
|
|
2398
|
+
}
|
|
2399
|
+
if (!analysis.canvasSize.width) {
|
|
2400
|
+
if (app.canvasSize && app.canvasSize.width) {
|
|
2401
|
+
analysis.canvasSize = { width: app.canvasSize.width, height: app.canvasSize.height };
|
|
2402
|
+
} else if (app.canvasEl) {
|
|
2403
|
+
// Last resort on a studio with neither: a backing-store size is better
|
|
2404
|
+
// than zero, and it is flagged so nobody treats it as the artboard.
|
|
2405
|
+
analysis.canvasSize = { width: app.canvasEl.width, height: app.canvasEl.height, backingStore: true };
|
|
2406
|
+
} else if (paper.view) {
|
|
2407
|
+
analysis.canvasSize = { width: paper.view.size.width, height: paper.view.size.height, backingStore: true };
|
|
2408
|
+
}
|
|
2181
2409
|
}
|
|
2182
2410
|
|
|
2183
2411
|
// Count items and analyze
|
|
@@ -2424,7 +2652,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2424
2652
|
switch (op.type) {
|
|
2425
2653
|
case 'create': {
|
|
2426
2654
|
const pos = op.position || { x: 400, y: 300 };
|
|
2427
|
-
const createProps = (op.properties || {});
|
|
2655
|
+
const createProps = withRadiusAxes((op.properties || {}));
|
|
2428
2656
|
const props = JSON.stringify(createProps);
|
|
2429
2657
|
let createCode = `
|
|
2430
2658
|
const item = app.create('${op.itemType}', { position: { x: ${pos.x}, y: ${pos.y} }, ...${props} });
|
|
@@ -2465,8 +2693,12 @@ return { itemId };
|
|
|
2465
2693
|
const modifyProps = JSON.stringify(op.properties || {});
|
|
2466
2694
|
return `
|
|
2467
2695
|
const targetId = ${itemRef};
|
|
2468
|
-
|
|
2469
|
-
|
|
2696
|
+
// modifyItem(id, changes), not select() + modify(). modify() edits the
|
|
2697
|
+
// SELECTION, so an id that does not resolve left the PREVIOUS selection to be
|
|
2698
|
+
// edited instead — and this then reported modified:true either way.
|
|
2699
|
+
if (app.modifyItem(targetId, ${modifyProps}) === false) {
|
|
2700
|
+
return { success: false, itemId: targetId, error: 'no item ' + targetId + ' — nothing was modified' };
|
|
2701
|
+
}
|
|
2470
2702
|
return { itemId: targetId, modified: true };
|
|
2471
2703
|
`;
|
|
2472
2704
|
case 'animate':
|
|
@@ -2541,9 +2773,16 @@ return { success: true, generator: '${genName}'${op.generatorRegion ? ', region:
|
|
|
2541
2773
|
const h = op.height || 1080;
|
|
2542
2774
|
const sizeArg = op.preset ? `'${op.preset}'` : `{ width: ${w}, height: ${h} }`;
|
|
2543
2775
|
return `
|
|
2544
|
-
|
|
2776
|
+
// Read the verdict, as the standalone tool now does: an unknown preset or an
|
|
2777
|
+
// out-of-range dimension is refused by name, and reporting the size we ASKED
|
|
2778
|
+
// for left the batch claiming a resize that never happened.
|
|
2779
|
+
const _cs = app.setCanvasSize(${sizeArg});
|
|
2780
|
+
if (_cs && _cs.ok === false) {
|
|
2781
|
+
return { success: false, error: _cs.reason || 'the canvas size was refused',
|
|
2782
|
+
...(_cs.known ? { knownPresets: _cs.known } : {}) };
|
|
2783
|
+
}
|
|
2545
2784
|
await new Promise(r => setTimeout(r, 50));
|
|
2546
|
-
return { success: true, width: ${w}, height: ${h} };
|
|
2785
|
+
return { success: true, width: (_cs && _cs.width) || ${w}, height: (_cs && _cs.height) || ${h} };
|
|
2547
2786
|
`;
|
|
2548
2787
|
}
|
|
2549
2788
|
case 'keyframe_animate': {
|
|
@@ -2741,6 +2980,19 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2741
2980
|
const scaled = scale !== undefined && scale !== 1
|
|
2742
2981
|
? { width: even(preset.width * scale), height: even(preset.height * scale) }
|
|
2743
2982
|
: { width: preset.width, height: preset.height };
|
|
2983
|
+
// 'auto' MEANS THE CANVAS, NOT A PRESET THAT HAPPENS TO BE FIRST.
|
|
2984
|
+
//
|
|
2985
|
+
// `platform` defaults to 'auto', 'auto' is not a key in platformPresets,
|
|
2986
|
+
// and the lookup falls through to `web` — so an export that named no
|
|
2987
|
+
// platform silently rendered 800x600. A beta probe measured exactly that:
|
|
2988
|
+
// three exports of a 960x540 canvas all came back 800x600, reported as a
|
|
2989
|
+
// dimension mismatch because from outside it is one. Nothing said the
|
|
2990
|
+
// canvas had been replaced by a preset.
|
|
2991
|
+
//
|
|
2992
|
+
// Resolved in the generated code rather than here, because the canvas size
|
|
2993
|
+
// is only known in the page. The preset stays the fallback for a studio
|
|
2994
|
+
// too old to report one, and `scale` still applies either way.
|
|
2995
|
+
const usesCanvasSize = !platformPresets[platform];
|
|
2744
2996
|
// Resolve "auto" format to the platform's recommended format
|
|
2745
2997
|
const exportFormat = (!format || format === 'auto') ? preset.staticFormat : format;
|
|
2746
2998
|
// AUDIO-ONLY EXPORT, off the visual path entirely.
|
|
@@ -2825,7 +3077,35 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2825
3077
|
// doubled the frame count and the render time — a surprise the schema used
|
|
2826
3078
|
// to hide because there was no way to ask for one without the other.
|
|
2827
3079
|
const settings = ${JSON.stringify(fps !== undefined ? { ...qualitySettings, fps } : qualitySettings)};
|
|
2828
|
-
|
|
3080
|
+
${usesCanvasSize ? ` // 'auto': the canvas's own size, with the preset only as a fallback for a
|
|
3081
|
+
// studio too old to report one. Named platforms emit a plain object literal
|
|
3082
|
+
// just below, so what they render stays readable in the code itself.
|
|
3083
|
+
const __presetDims = ${JSON.stringify(scaled)};
|
|
3084
|
+
const __canvasDims = app.canvasSize || (app.view && app.view.viewSize) || null;
|
|
3085
|
+
const __even = (n) => Math.max(2, Math.round((n * ${scale !== undefined ? scale : 1}) / 2) * 2);
|
|
3086
|
+
const dimensions = (__canvasDims && __canvasDims.width && __canvasDims.height)
|
|
3087
|
+
? { width: __even(__canvasDims.width), height: __even(__canvasDims.height) }
|
|
3088
|
+
: __presetDims;` : ` const dimensions = ${JSON.stringify(scaled)};`}
|
|
3089
|
+
|
|
3090
|
+
// WHAT THIS SCENE LOSES TO THIS FORMAT, asked at the moment you would care.
|
|
3091
|
+
//
|
|
3092
|
+
// exportFidelity walks the actual scene: a PNG of an animated scene is one
|
|
3093
|
+
// frame, a GIF with a soundtrack drops it, a wav of a silent scene is
|
|
3094
|
+
// silence. The engine derived it so that 117 tool descriptions would not
|
|
3095
|
+
// have to answer "does this survive export?" by hand and drift.
|
|
3096
|
+
//
|
|
3097
|
+
// Reported ONLY when there is something to report. An empty list would read
|
|
3098
|
+
// as "this format is lossless", and it does not mean that — it means this
|
|
3099
|
+
// scene has nothing THIS format will lose. Omitting it entirely is the one
|
|
3100
|
+
// shape that cannot be misread, and it keeps a clean export quiet.
|
|
3101
|
+
function fidelity(fmt) {
|
|
3102
|
+
try {
|
|
3103
|
+
if (!app.exportEngine || typeof app.exportEngine.exportFidelity !== 'function') return {};
|
|
3104
|
+
const r = app.exportEngine.exportFidelity(fmt);
|
|
3105
|
+
if (!r || !r.warnings || r.warnings.length === 0) return {};
|
|
3106
|
+
return { fidelity: { warnings: r.warnings, checked: r.checked } };
|
|
3107
|
+
} catch (e) { return {}; }
|
|
3108
|
+
}
|
|
2829
3109
|
|
|
2830
3110
|
// Preflight: same resolved settings as the real export, but render nothing.
|
|
2831
3111
|
// Branching HERE rather than in a separate tool is deliberate — the estimate
|
|
@@ -2851,6 +3131,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2851
3131
|
fps: settings.fps,
|
|
2852
3132
|
dimensions: dimensions.width + 'x' + dimensions.height,
|
|
2853
3133
|
estimate: est,
|
|
3134
|
+
...fidelity(format),
|
|
2854
3135
|
};
|
|
2855
3136
|
}
|
|
2856
3137
|
|
|
@@ -3080,6 +3361,11 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3080
3361
|
result = { success: false, error: e.message, platform, format };
|
|
3081
3362
|
}
|
|
3082
3363
|
|
|
3364
|
+
// One site, after every branch has set its result: a successful export says
|
|
3365
|
+
// what this scene lost to this format, rather than the caller finding out by
|
|
3366
|
+
// opening the file. Only when there is something to say — see fidelity().
|
|
3367
|
+
if (result && result.success) Object.assign(result, fidelity(format));
|
|
3368
|
+
|
|
3083
3369
|
return result;
|
|
3084
3370
|
})();
|
|
3085
3371
|
`.trim();
|
|
@@ -3106,11 +3392,28 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3106
3392
|
hasImages: false,
|
|
3107
3393
|
};
|
|
3108
3394
|
|
|
3109
|
-
//
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3395
|
+
// THE ARTBOARD, NOT THE BACKING STORE.
|
|
3396
|
+
//
|
|
3397
|
+
// canvasEl.width and paper.view.size are both in device pixels, so a
|
|
3398
|
+
// 1920x1080 artboard was reported as 2233x1472 — measured from production.
|
|
3399
|
+
// That is not even the device pixel ratio (a 2233px store in a 924px box is
|
|
3400
|
+
// 2.4166), so a caller cannot divide it back out. Anything derived from it
|
|
3401
|
+
// is wrong by an unknown factor, and synthetic clicks computed from it miss
|
|
3402
|
+
// in silence. getCanvasSize() is the artboard the caller set.
|
|
3403
|
+
if (typeof app.getCanvasSize === 'function') {
|
|
3404
|
+
const cs = app.getCanvasSize();
|
|
3405
|
+
if (cs && cs.width) analysis.canvasSize = { width: cs.width, height: cs.height };
|
|
3406
|
+
}
|
|
3407
|
+
if (!analysis.canvasSize.width) {
|
|
3408
|
+
if (app.canvasSize && app.canvasSize.width) {
|
|
3409
|
+
analysis.canvasSize = { width: app.canvasSize.width, height: app.canvasSize.height };
|
|
3410
|
+
} else if (app.canvasEl) {
|
|
3411
|
+
// Last resort on a studio with neither: a backing-store size is better
|
|
3412
|
+
// than zero, and it is flagged so nobody treats it as the artboard.
|
|
3413
|
+
analysis.canvasSize = { width: app.canvasEl.width, height: app.canvasEl.height, backingStore: true };
|
|
3414
|
+
} else if (paper.view) {
|
|
3415
|
+
analysis.canvasSize = { width: paper.view.size.width, height: paper.view.size.height, backingStore: true };
|
|
3416
|
+
}
|
|
3114
3417
|
}
|
|
3115
3418
|
|
|
3116
3419
|
// Count items and analyze types
|
|
@@ -3317,8 +3620,51 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3317
3620
|
*/
|
|
3318
3621
|
generateLoadMap(input) {
|
|
3319
3622
|
const validated = LoadMapInputSchema.parse(input);
|
|
3320
|
-
const { mapId, ...
|
|
3321
|
-
|
|
3623
|
+
const { mapId, ...raw } = validated;
|
|
3624
|
+
// THIS SCHEMA AND loadMap() SHARED ALMOST NO VOCABULARY.
|
|
3625
|
+
//
|
|
3626
|
+
// loadMap(source, options) destructures projection, center, scale, rotate,
|
|
3627
|
+
// fitBounds, parent, styles, style, interactive, selectable, showOcean,
|
|
3628
|
+
// oceanColor, quality, smoothPaths and simplifyTolerance. Of the twelve
|
|
3629
|
+
// options this tool offered, SEVEN were none of those: the styling keys
|
|
3630
|
+
// live nested under `styles` and are spelled differently there, and the two
|
|
3631
|
+
// interactivity flags have other names entirely. Every one was accepted,
|
|
3632
|
+
// JSON-stringified into the call and dropped — a caller who set fillColor
|
|
3633
|
+
// got the default grey with no indication why.
|
|
3634
|
+
const { fillColor, strokeColor, strokeWidth, hoverFill, hoverStroke, enableHover, enableClick, ...passthrough } = raw;
|
|
3635
|
+
// The engine's own style vocabulary — see stylePresets in MapSystem.js.
|
|
3636
|
+
const styles = {};
|
|
3637
|
+
if (fillColor !== undefined)
|
|
3638
|
+
styles.fill = fillColor;
|
|
3639
|
+
if (strokeColor !== undefined)
|
|
3640
|
+
styles.stroke = strokeColor;
|
|
3641
|
+
// strokeWidth only survives inside `styles`; the merge overwrites a
|
|
3642
|
+
// top-level one with the quality preset's value.
|
|
3643
|
+
if (strokeWidth !== undefined)
|
|
3644
|
+
styles.strokeWidth = strokeWidth;
|
|
3645
|
+
if (hoverFill !== undefined)
|
|
3646
|
+
styles.hoverFill = hoverFill;
|
|
3647
|
+
// hoverStroke reads from mergedStyles beside hoverFill, so it belongs in
|
|
3648
|
+
// the same place. It landed late — the two MCP specs documented it for a
|
|
3649
|
+
// long time before any source file did — and an older studio merges it,
|
|
3650
|
+
// finds nothing that reads it, and leaves the outline at the base stroke.
|
|
3651
|
+
// Mapped rather than stripped so it simply starts working on such a studio
|
|
3652
|
+
// without a change here, and reported below so a caller on an older one
|
|
3653
|
+
// learns why their outline did not move.
|
|
3654
|
+
if (hoverStroke !== undefined)
|
|
3655
|
+
styles.hoverStroke = hoverStroke;
|
|
3656
|
+
const engineOptions = { ...passthrough };
|
|
3657
|
+
if (Object.keys(styles).length > 0)
|
|
3658
|
+
engineOptions.styles = styles;
|
|
3659
|
+
if (enableHover !== undefined)
|
|
3660
|
+
engineOptions.interactive = enableHover;
|
|
3661
|
+
if (enableClick !== undefined)
|
|
3662
|
+
engineOptions.selectable = enableClick;
|
|
3663
|
+
const ignored = hoverStroke !== undefined ? ['hoverStroke'] : [];
|
|
3664
|
+
const optionsStr = Object.keys(engineOptions).length > 0 ? JSON.stringify(engineOptions) : '{}';
|
|
3665
|
+
const warn = ignored.length
|
|
3666
|
+
? `\n conditional: ${JSON.stringify(ignored)},\n note: 'hoverStroke needs a studio recent enough to read it; an older one ignores it and the hover outline stays the base stroke.',`
|
|
3667
|
+
: '';
|
|
3322
3668
|
return `
|
|
3323
3669
|
// Load geographic map
|
|
3324
3670
|
(async function() {
|
|
@@ -3333,7 +3679,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3333
3679
|
mapId: result.mapId || '${mapId}',
|
|
3334
3680
|
regions: result.regions?.length || 0,
|
|
3335
3681
|
bounds: result.bounds,
|
|
3336
|
-
center: result.center
|
|
3682
|
+
center: result.center,${warn}
|
|
3337
3683
|
};
|
|
3338
3684
|
} catch (error) {
|
|
3339
3685
|
return { success: false, error: error.message };
|
|
@@ -4071,7 +4417,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4071
4417
|
// records the baked offset at item.data._bakedRotation and sums the two itself
|
|
4072
4418
|
// (PinePaper.js does exactly this); read the same sum rather than the half of
|
|
4073
4419
|
// it that happens to be zero.
|
|
4074
|
-
const _rot = (item.data?._bakedRotation || 0) + (item.rotation || 0);
|
|
4420
|
+
const _rot = typeof app.authoredRotation === 'function' ? app.authoredRotation(item) : ((item.data?._bakedRotation || 0) + (item.rotation || 0));
|
|
4075
4421
|
if (_rot) itemData.properties.rotation = _rot;
|
|
4076
4422
|
if (item.data?.content) itemData.properties.content = item.data.content;
|
|
4077
4423
|
if (item.visible === false) itemData.properties.visible = false;
|
|
@@ -4433,6 +4779,33 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4433
4779
|
}
|
|
4434
4780
|
})();
|
|
4435
4781
|
`.trim();
|
|
4782
|
+
}
|
|
4783
|
+
/**
|
|
4784
|
+
* What THIS item can animate, from the engine's own table.
|
|
4785
|
+
*
|
|
4786
|
+
* The table is derived from the applier rather than written beside it, so it
|
|
4787
|
+
* cannot drift from what actually gets applied — and it is filtered by item
|
|
4788
|
+
* type, because the trim/dash four only exist on a path and a rectangle has
|
|
4789
|
+
* no fontSize.
|
|
4790
|
+
*
|
|
4791
|
+
* `interpolates: false` is passed through VERBATIM and never flattened away.
|
|
4792
|
+
* `content` and `blendMode` are DISCRETE — they hold their value until the
|
|
4793
|
+
* next keyframe rather than blending toward it — and treating content as
|
|
4794
|
+
* tweenable is what made every word in a cycler appear half a beat early.
|
|
4795
|
+
* A list that says only "animatable" rebuilds that bug in the next agent.
|
|
4796
|
+
*/
|
|
4797
|
+
generateListItemAnimatableProperties(itemId) {
|
|
4798
|
+
return `
|
|
4799
|
+
// Animatable properties of ${JSON.stringify(itemId)}
|
|
4800
|
+
(function() {
|
|
4801
|
+
if (typeof app.listAnimatableProperties !== 'function') {
|
|
4802
|
+
return { success: false, error: 'app.listAnimatableProperties unavailable — update the studio. The keyframe tool lists the properties it accepts in its own description meanwhile.' };
|
|
4803
|
+
}
|
|
4804
|
+
const item = app.getItemById(${JSON.stringify(itemId)});
|
|
4805
|
+
if (!item) { return { success: false, error: 'no item ${itemId}' }; }
|
|
4806
|
+
const r = app.listAnimatableProperties(item);
|
|
4807
|
+
return { success: true, ...r };
|
|
4808
|
+
})();`.trim();
|
|
4436
4809
|
}
|
|
4437
4810
|
generateGetAnimatableProperties() {
|
|
4438
4811
|
return `
|
|
@@ -4559,7 +4932,51 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4559
4932
|
}
|
|
4560
4933
|
try {
|
|
4561
4934
|
await app.templateManager.loadTemplate('${templateId}', true);
|
|
4562
|
-
|
|
4935
|
+
|
|
4936
|
+
// DID THE CLIPPED CHARACTER PARTS SURVIVE?
|
|
4937
|
+
//
|
|
4938
|
+
// A clipped character group is saved with its nested texture and mask in
|
|
4939
|
+
// paperJSON. A studio whose restore predates the reader for that key
|
|
4940
|
+
// rebuilds it through the generic group branch instead — an empty,
|
|
4941
|
+
// unclipped group — so the part is written to the template correctly and
|
|
4942
|
+
// dropped on the way back in, with nothing raised.
|
|
4943
|
+
//
|
|
4944
|
+
// Checked by BEHAVIOUR, not by version: the template says how many clipped
|
|
4945
|
+
// groups it carries, the canvas says how many came back. An engine that
|
|
4946
|
+
// restores them makes this condition unreachable, so it needs no removing
|
|
4947
|
+
// later; an older studio is exactly where it still matters, and this
|
|
4948
|
+
// package ships independently of the studio it drives.
|
|
4949
|
+
//
|
|
4950
|
+
// THE WARNING MATTERS MORE THAN THE LOAD. Loading is harmless and
|
|
4951
|
+
// reversible — the file on disk still holds the paperJSON, so the same
|
|
4952
|
+
// template reloads intact on a studio that can read it. Re-SAVING from
|
|
4953
|
+
// this canvas is not: the exporter would serialise the empty group it can
|
|
4954
|
+
// see, and the texture and mask would leave the file for good.
|
|
4955
|
+
let fidelity = {};
|
|
4956
|
+
try {
|
|
4957
|
+
const tpl = (app.templateManager.getAllTemplates() || [])
|
|
4958
|
+
.find(function(t) { return t && (t.id === '${templateId}' || t.templateId === '${templateId}'); });
|
|
4959
|
+
const items = tpl && tpl.data && Array.isArray(tpl.data.items) ? tpl.data.items : null;
|
|
4960
|
+
const declared = items ? items.filter(function(i) { return i && i.type === 'group' && i.paperJSON; }).length : 0;
|
|
4961
|
+
if (declared > 0 && app.itemRegistry && typeof app.itemRegistry.getAll === 'function') {
|
|
4962
|
+
const clipped = (app.itemRegistry.getAll() || []).filter(function(e) {
|
|
4963
|
+
const it = e && (e.item || e);
|
|
4964
|
+
return it && it.className === 'Group' && it.clipped === true;
|
|
4965
|
+
}).length;
|
|
4966
|
+
if (clipped === 0) {
|
|
4967
|
+
fidelity = {
|
|
4968
|
+
clippedPartsLost: declared,
|
|
4969
|
+
warning: 'This template carries ' + declared + ' clipped character part(s) with a nested texture and mask, '
|
|
4970
|
+
+ 'and this studio restored none of them — they are on the canvas as empty groups. '
|
|
4971
|
+
+ 'DO NOT SAVE THIS CANVAS BACK OVER THE TEMPLATE: the export would write the empty groups and the '
|
|
4972
|
+
+ 'texture would be lost from the file permanently. The template itself is still intact; update the '
|
|
4973
|
+
+ 'studio and load it again.',
|
|
4974
|
+
};
|
|
4975
|
+
}
|
|
4976
|
+
}
|
|
4977
|
+
} catch (e) { /* a check that cannot run must not fail the load */ }
|
|
4978
|
+
|
|
4979
|
+
return { success: true, templateId: '${templateId}', message: 'Template applied successfully. Canvas has been replaced with template content.', ...fidelity };
|
|
4563
4980
|
} catch (e) {
|
|
4564
4981
|
return { error: 'Failed to apply template: ' + e.message };
|
|
4565
4982
|
}
|
|
@@ -4605,7 +5022,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4605
5022
|
res = await fetch(src);
|
|
4606
5023
|
} catch (netErr) {
|
|
4607
5024
|
return { success: false, error: 'could not reach ' + src + ' — ' + (netErr && netErr.message ? netErr.message : 'network request failed')
|
|
4608
|
-
+ '.
|
|
5025
|
+
+ '. The failure is in the browser network stack: check the host resolves, the port is open, and that any proxy passes plain HTTP as well as HTTPS CONNECT.' };
|
|
4609
5026
|
}
|
|
4610
5027
|
if (!res.ok) {
|
|
4611
5028
|
return { success: false, error: 'the server refused ' + src + ' — HTTP ' + res.status + ' ' + (res.statusText || ''), status: res.status };
|
|
@@ -4829,7 +5246,21 @@ ${mask ? ` app.imageTools.applyMask(raster, '${mask}');\n` : ''} // The RE
|
|
|
4829
5246
|
// Toggle loop mode
|
|
4830
5247
|
(function() {
|
|
4831
5248
|
${guard}
|
|
4832
|
-
|
|
5249
|
+
// chainLoop is a PROPERTY, set by createChain({loop}) and read by the chain
|
|
5250
|
+
// advance. setLoop has never existed, so this reported a toggle it had not
|
|
5251
|
+
// performed — and a chain built without loop went on not looping.
|
|
5252
|
+
if (typeof app.sceneManager.setLoop === 'function') {
|
|
5253
|
+
app.sceneManager.setLoop(${input.enabled ?? true});
|
|
5254
|
+
} else if ('chainLoop' in app.sceneManager) {
|
|
5255
|
+
app.sceneManager.chainLoop = ${input.enabled ?? true};
|
|
5256
|
+
} else {
|
|
5257
|
+
return { success: false, action: 'toggle_loop',
|
|
5258
|
+
error: 'this build has no chain-loop control to set. Pass loop:true to create_chain instead, which is where the setting is read from.' };
|
|
5259
|
+
}
|
|
5260
|
+
if (!app.sceneManager.sceneChain || !app.sceneManager.sceneChain.length) {
|
|
5261
|
+
return { success: true, action: 'toggle_loop', enabled: ${input.enabled ?? true},
|
|
5262
|
+
note: 'no chain exists yet — this setting applies to the next one created, and create_chain takes loop directly.' };
|
|
5263
|
+
}
|
|
4833
5264
|
return { success: true, action: 'toggle_loop', enabled: ${input.enabled ?? true} };
|
|
4834
5265
|
})();
|
|
4835
5266
|
`.trim();
|
|
@@ -5707,7 +6138,7 @@ ${resolve}
|
|
|
5707
6138
|
bounds: item.bounds ? { x: item.bounds.x, y: item.bounds.y, width: item.bounds.width, height: item.bounds.height } : null,
|
|
5708
6139
|
visible: item.visible,
|
|
5709
6140
|
opacity: item.opacity,
|
|
5710
|
-
rotation: (item.data?._bakedRotation || 0) + (item.rotation || 0),
|
|
6141
|
+
rotation: typeof app.authoredRotation === 'function' ? app.authoredRotation(item) : ((item.data?._bakedRotation || 0) + (item.rotation || 0)),
|
|
5711
6142
|
selected: item.selected,
|
|
5712
6143
|
};
|
|
5713
6144
|
})();`.trim();
|
|
@@ -6481,7 +6912,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
|
|
|
6481
6912
|
const entry = app.itemRegistry.get(${itemIdStr});
|
|
6482
6913
|
if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
|
|
6483
6914
|
const b = entry.item.bounds;
|
|
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) };
|
|
6915
|
+
return { success: true, action: 'get_dimensions', itemId: ${itemIdStr}, x: b.x, y: b.y, width: b.width, height: b.height, rotation: typeof app.authoredRotation === 'function' ? app.authoredRotation(entry.item) : ((entry.item.data?._bakedRotation || 0) + (entry.item.rotation || 0)) };
|
|
6485
6916
|
})();`.trim();
|
|
6486
6917
|
}
|
|
6487
6918
|
case 'set_snap': {
|
|
@@ -6990,10 +7421,25 @@ ${guard}
|
|
|
6990
7421
|
});
|
|
6991
7422
|
return `
|
|
6992
7423
|
// Display text style: ${input.styleKey} — stacked-layer title, id adopted from the text
|
|
6993
|
-
(function() {
|
|
7424
|
+
(async function() {
|
|
6994
7425
|
if (typeof app.applyTextStyle !== 'function') { return { success: false, error: 'app.applyTextStyle unavailable — update FxTool to a text-styles build' }; }
|
|
7426
|
+
// LOAD THE FACES FIRST. The decorative stylesheet is fetched on pointerenter
|
|
7427
|
+
// over the style grid — a gesture no agent ever makes — so every decorative
|
|
7428
|
+
// family was missing in production and a style asking for one silently drew
|
|
7429
|
+
// the fallback and reported success. ensureFontsLoaded is the fix; guarded,
|
|
7430
|
+
// because a studio without it behaves as it always did.
|
|
7431
|
+
if (typeof app.ensureFontsLoaded === 'function') {
|
|
7432
|
+
try { await app.ensureFontsLoaded('decorative'); } catch (_) { /* the pending flag below still reports it */ }
|
|
7433
|
+
}
|
|
6995
7434
|
const r = app.applyTextStyle(${S(input.itemId)}, ${S(input.styleKey)}, ${opts});
|
|
6996
7435
|
if (!r || !r.ok) { return { success: false, error: (r && r.error) || 'style failed' }; }
|
|
7436
|
+
// fontPending: the face is not there YET, so this drew in the fallback. Said
|
|
7437
|
+
// out loud rather than left to be discovered in the export.
|
|
7438
|
+
if (r.fontPending) {
|
|
7439
|
+
return { success: true, action: 'apply_style', id: r.id, style: r.style, layers: r.layers,
|
|
7440
|
+
palette: r.palette, bounds: r.bounds, fontPending: true,
|
|
7441
|
+
warning: 'the face for this style had not loaded, so it drew in a FALLBACK font. Re-apply the style, or call pinepaper_font check with the family and your text to see what is really being used.' };
|
|
7442
|
+
}
|
|
6997
7443
|
// The styled group ADOPTS the text item's registry id — r.id is the SAME id
|
|
6998
7444
|
// the caller passed, so their handle keeps working. Say so in the result.
|
|
6999
7445
|
return { success: true, action: 'apply_style', id: r.id, style: r.style, layers: r.layers, palette: r.palette, bounds: r.bounds };
|
|
@@ -7824,6 +8270,40 @@ ${needWorld}
|
|
|
7824
8270
|
for (const [k, v] of Object.entries(rest))
|
|
7825
8271
|
if (v !== undefined)
|
|
7826
8272
|
opts[k] = v;
|
|
8273
|
+
// SPREAD-THROUGH IS NOT THE SAME AS AGREEING.
|
|
8274
|
+
//
|
|
8275
|
+
// Everything above this line reaches app.stickFigure verbatim, which made
|
|
8276
|
+
// the schema look consumed to every guard we have while four fields meant
|
|
8277
|
+
// something the engine does not. The engine compares propSide against
|
|
8278
|
+
// 'L' | 'R', so 'right' — which the schema declared and advertised —
|
|
8279
|
+
// matched neither arm of that test and every prop went to the LEFT hand.
|
|
8280
|
+
// Nothing failed; the figure just held it in the wrong hand, forever.
|
|
8281
|
+
if (action === 'figure') {
|
|
8282
|
+
const side = typeof opts.propSide === 'string' ? opts.propSide : undefined;
|
|
8283
|
+
if (side)
|
|
8284
|
+
opts.propSide = side.toUpperCase().startsWith('R') ? 'R' : 'L';
|
|
8285
|
+
// The engine iterates opts.walk, so `true` — the form the schema itself
|
|
8286
|
+
// advertised as "the default" — walked exactly nowhere.
|
|
8287
|
+
if (opts.walk === true)
|
|
8288
|
+
opts.walk = [{ seconds: 1.2, from: 0 }];
|
|
8289
|
+
else if (opts.walk === false)
|
|
8290
|
+
delete opts.walk;
|
|
8291
|
+
// `facing` is read for chairs and for nothing else. Passing it here
|
|
8292
|
+
// would be accepted and ignored, which is the thing being fixed, so it
|
|
8293
|
+
// is refused by name instead — the add_ports precedent.
|
|
8294
|
+
if (opts.facing !== undefined) {
|
|
8295
|
+
// Built through S() rather than interpolated: the message names two
|
|
8296
|
+
// quoted values, and hand-escaping them into a single-quoted emitted
|
|
8297
|
+
// string produced a syntax error the first time.
|
|
8298
|
+
const msg = `facing is a property of a chair, not of a figure — app.stickFigure `
|
|
8299
|
+
+ `never reads it, so ${String(opts.facing)} would have been accepted and ignored. `
|
|
8300
|
+
+ `A figure faces the way it travels, so give it travel, or a pose that turns it. `
|
|
8301
|
+
+ `facing belongs on action "set" with kind "chair".`;
|
|
8302
|
+
return `
|
|
8303
|
+
// Stick: figure — refused, rather than accepted and ignored
|
|
8304
|
+
({ success: false, error: ${S(msg)} });`.trim();
|
|
8305
|
+
}
|
|
8306
|
+
}
|
|
7827
8307
|
const fn = action === 'figure' ? 'stickFigure' : 'stickSet';
|
|
7828
8308
|
return `
|
|
7829
8309
|
// Stick: ${action}
|