@pinepaper.studio/mcp-server 1.6.11 → 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 +57 -0
- package/dist/tools/compact-descriptions.js +1 -1
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +139 -47
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +3 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +20 -5
- package/dist/tools/engine-surface.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.map +1 -1
- package/dist/types/code-generator.js +419 -50
- 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-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
|
|
@@ -475,8 +498,62 @@ app.applyEffect(item, '${effectType}', ${JSON.stringify(params, null, 2)});
|
|
|
475
498
|
/**
|
|
476
499
|
* Template for getting items
|
|
477
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
|
+
}
|
|
478
538
|
function generateGetItemsCode(filter) {
|
|
479
|
-
|
|
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) {
|
|
480
557
|
return `
|
|
481
558
|
// Get all items
|
|
482
559
|
const entries = app.itemRegistry.getAll();
|
|
@@ -491,19 +568,6 @@ const items = entries.map(entry => ({
|
|
|
491
568
|
({ items, count: items.length });
|
|
492
569
|
`.trim();
|
|
493
570
|
}
|
|
494
|
-
const conditions = [];
|
|
495
|
-
if (filter.type) {
|
|
496
|
-
conditions.push(`entry.type === '${filter.type}'`);
|
|
497
|
-
}
|
|
498
|
-
if (filter.source) {
|
|
499
|
-
conditions.push(`entry.source === '${filter.source}'`);
|
|
500
|
-
}
|
|
501
|
-
if (filter.hasAnimation !== undefined) {
|
|
502
|
-
conditions.push(`!!entry.item.data?.animationType === ${filter.hasAnimation}`);
|
|
503
|
-
}
|
|
504
|
-
if (filter.hasRelation !== undefined) {
|
|
505
|
-
conditions.push(`(app.getRelations(entry.id || entry.itemId).length > 0) === ${filter.hasRelation}`);
|
|
506
|
-
}
|
|
507
571
|
return `
|
|
508
572
|
// Get items with filter
|
|
509
573
|
const entries = app.itemRegistry.getAll();
|
|
@@ -801,15 +865,51 @@ const stats = app.getRelationStats();
|
|
|
801
865
|
function generateListGeneratorsCode() {
|
|
802
866
|
return `
|
|
803
867
|
// List available generators
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
+
}));
|
|
811
903
|
|
|
812
|
-
|
|
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
|
+
})();
|
|
813
913
|
`.trim();
|
|
814
914
|
}
|
|
815
915
|
/**
|
|
@@ -1041,6 +1141,64 @@ const ENSURE_EXPORT_ENGINE = `
|
|
|
1041
1141
|
if (!app.exportEngine && typeof app.ensureHeavyModules === 'function') {
|
|
1042
1142
|
try { await app.ensureHeavyModules(); } catch (_) { /* the guards below still speak */ }
|
|
1043
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
|
+
`;
|
|
1044
1202
|
export class PinePaperCodeGenerator {
|
|
1045
1203
|
/**
|
|
1046
1204
|
* Generate code for creating an item
|
|
@@ -1265,7 +1423,10 @@ export class PinePaperCodeGenerator {
|
|
|
1265
1423
|
if (!response.ok) {
|
|
1266
1424
|
return { success: false, error: 'the server refused ${url} — HTTP ' + response.status + ' ' + (response.statusText || ''), status: response.status };
|
|
1267
1425
|
}
|
|
1268
|
-
|
|
1426
|
+
let svgText = await response.text();
|
|
1427
|
+
${INLINE_REMOTE_IMAGES}
|
|
1428
|
+
const _inl = await inlineRemoteImages(svgText);
|
|
1429
|
+
svgText = _inl.svg;
|
|
1269
1430
|
const _r = ${importExpr('svgText')};
|
|
1270
1431
|
const imported = _r.item;
|
|
1271
1432
|
if (!imported) {
|
|
@@ -1275,7 +1436,8 @@ export class PinePaperCodeGenerator {
|
|
|
1275
1436
|
imported.scale(${scale});
|
|
1276
1437
|
const itemId = app.registerItem(imported, 'svg-import', { source: 'mcp' });
|
|
1277
1438
|
app.historyManager.saveState();
|
|
1278
|
-
return { success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} }
|
|
1439
|
+
return { success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} },
|
|
1440
|
+
...(_inl.notes.length ? { imageWarnings: _inl.notes } : {}) };
|
|
1279
1441
|
})();
|
|
1280
1442
|
`.trim();
|
|
1281
1443
|
}
|
|
@@ -1284,18 +1446,23 @@ export class PinePaperCodeGenerator {
|
|
|
1284
1446
|
const escapedSvg = svgString.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
1285
1447
|
return `
|
|
1286
1448
|
// Import SVG string${source === 'figma' ? ' (Figma-normalised)' : ''}
|
|
1287
|
-
|
|
1449
|
+
(async function() {
|
|
1450
|
+
let svgString = \`${escapedSvg}\`;
|
|
1451
|
+
${INLINE_REMOTE_IMAGES}
|
|
1452
|
+
const _inl = await inlineRemoteImages(svgString);
|
|
1453
|
+
svgString = _inl.svg;
|
|
1288
1454
|
const _r = ${importExpr('svgString')};
|
|
1289
1455
|
const imported = _r.item;
|
|
1290
|
-
if (imported) {
|
|
1291
|
-
imported.position = new paper.Point(${position.x}, ${position.y});
|
|
1292
|
-
imported.scale(${scale});
|
|
1293
|
-
const itemId = app.registerItem(imported, 'svg-import', { source: 'mcp' });
|
|
1294
|
-
app.historyManager.saveState();
|
|
1295
|
-
({ success: true, itemId, changes: _r.changes, position: { x: ${position.x}, y: ${position.y} } });
|
|
1296
|
-
} else {
|
|
1456
|
+
if (!imported) {
|
|
1297
1457
|
throw new Error('Failed to import SVG');
|
|
1298
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
|
+
})();
|
|
1299
1466
|
`.trim();
|
|
1300
1467
|
}
|
|
1301
1468
|
return `
|
|
@@ -2217,11 +2384,28 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2217
2384
|
hasImages: false,
|
|
2218
2385
|
};
|
|
2219
2386
|
|
|
2220
|
-
//
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
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
|
+
}
|
|
2225
2409
|
}
|
|
2226
2410
|
|
|
2227
2411
|
// Count items and analyze
|
|
@@ -2468,7 +2652,7 @@ throw new Error('Unknown diagram mode action: ${action}');
|
|
|
2468
2652
|
switch (op.type) {
|
|
2469
2653
|
case 'create': {
|
|
2470
2654
|
const pos = op.position || { x: 400, y: 300 };
|
|
2471
|
-
const createProps = (op.properties || {});
|
|
2655
|
+
const createProps = withRadiusAxes((op.properties || {}));
|
|
2472
2656
|
const props = JSON.stringify(createProps);
|
|
2473
2657
|
let createCode = `
|
|
2474
2658
|
const item = app.create('${op.itemType}', { position: { x: ${pos.x}, y: ${pos.y} }, ...${props} });
|
|
@@ -2796,6 +2980,19 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2796
2980
|
const scaled = scale !== undefined && scale !== 1
|
|
2797
2981
|
? { width: even(preset.width * scale), height: even(preset.height * scale) }
|
|
2798
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];
|
|
2799
2996
|
// Resolve "auto" format to the platform's recommended format
|
|
2800
2997
|
const exportFormat = (!format || format === 'auto') ? preset.staticFormat : format;
|
|
2801
2998
|
// AUDIO-ONLY EXPORT, off the visual path entirely.
|
|
@@ -2880,7 +3077,35 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2880
3077
|
// doubled the frame count and the render time — a surprise the schema used
|
|
2881
3078
|
// to hide because there was no way to ask for one without the other.
|
|
2882
3079
|
const settings = ${JSON.stringify(fps !== undefined ? { ...qualitySettings, fps } : qualitySettings)};
|
|
2883
|
-
|
|
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
|
+
}
|
|
2884
3109
|
|
|
2885
3110
|
// Preflight: same resolved settings as the real export, but render nothing.
|
|
2886
3111
|
// Branching HERE rather than in a separate tool is deliberate — the estimate
|
|
@@ -2906,6 +3131,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
2906
3131
|
fps: settings.fps,
|
|
2907
3132
|
dimensions: dimensions.width + 'x' + dimensions.height,
|
|
2908
3133
|
estimate: est,
|
|
3134
|
+
...fidelity(format),
|
|
2909
3135
|
};
|
|
2910
3136
|
}
|
|
2911
3137
|
|
|
@@ -3135,6 +3361,11 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3135
3361
|
result = { success: false, error: e.message, platform, format };
|
|
3136
3362
|
}
|
|
3137
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
|
+
|
|
3138
3369
|
return result;
|
|
3139
3370
|
})();
|
|
3140
3371
|
`.trim();
|
|
@@ -3161,11 +3392,28 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3161
3392
|
hasImages: false,
|
|
3162
3393
|
};
|
|
3163
3394
|
|
|
3164
|
-
//
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
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
|
+
}
|
|
3169
3417
|
}
|
|
3170
3418
|
|
|
3171
3419
|
// Count items and analyze types
|
|
@@ -3372,8 +3620,51 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3372
3620
|
*/
|
|
3373
3621
|
generateLoadMap(input) {
|
|
3374
3622
|
const validated = LoadMapInputSchema.parse(input);
|
|
3375
|
-
const { mapId, ...
|
|
3376
|
-
|
|
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
|
+
: '';
|
|
3377
3668
|
return `
|
|
3378
3669
|
// Load geographic map
|
|
3379
3670
|
(async function() {
|
|
@@ -3388,7 +3679,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
3388
3679
|
mapId: result.mapId || '${mapId}',
|
|
3389
3680
|
regions: result.regions?.length || 0,
|
|
3390
3681
|
bounds: result.bounds,
|
|
3391
|
-
center: result.center
|
|
3682
|
+
center: result.center,${warn}
|
|
3392
3683
|
};
|
|
3393
3684
|
} catch (error) {
|
|
3394
3685
|
return { success: false, error: error.message };
|
|
@@ -4641,7 +4932,51 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4641
4932
|
}
|
|
4642
4933
|
try {
|
|
4643
4934
|
await app.templateManager.loadTemplate('${templateId}', true);
|
|
4644
|
-
|
|
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 };
|
|
4645
4980
|
} catch (e) {
|
|
4646
4981
|
return { error: 'Failed to apply template: ' + e.message };
|
|
4647
4982
|
}
|
|
@@ -4687,7 +5022,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
|
|
|
4687
5022
|
res = await fetch(src);
|
|
4688
5023
|
} catch (netErr) {
|
|
4689
5024
|
return { success: false, error: 'could not reach ' + src + ' — ' + (netErr && netErr.message ? netErr.message : 'network request failed')
|
|
4690
|
-
+ '.
|
|
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.' };
|
|
4691
5026
|
}
|
|
4692
5027
|
if (!res.ok) {
|
|
4693
5028
|
return { success: false, error: 'the server refused ' + src + ' — HTTP ' + res.status + ' ' + (res.statusText || ''), status: res.status };
|
|
@@ -7935,6 +8270,40 @@ ${needWorld}
|
|
|
7935
8270
|
for (const [k, v] of Object.entries(rest))
|
|
7936
8271
|
if (v !== undefined)
|
|
7937
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
|
+
}
|
|
7938
8307
|
const fn = action === 'figure' ? 'stickFigure' : 'stickSet';
|
|
7939
8308
|
return `
|
|
7940
8309
|
// Stick: ${action}
|