figma-rust-mcp 2.8.5 → 2.9.6
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 +101 -52
- package/bin/figma-mcp.js +1 -1
- package/package.json +1 -1
- package/plugin/code.js +481 -103
- package/plugin/ui.html +1116 -378
- package/scripts/test-rust-mcp.sh +4 -6
package/plugin/code.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// figma-mcp — Figma Plugin main thread v1.2.1
|
|
2
2
|
// Handles both WRITE (draw UI) and READ (extract design) operations.
|
|
3
3
|
// AUTO-GENERATED by scripts/build-plugin.js — edit plugin-src/*.js instead
|
|
4
|
-
// Generated: 2026-08-
|
|
4
|
+
// Generated: 2026-08-27T07:20:35.052Z
|
|
5
5
|
|
|
6
6
|
// ── plugin-src/utils.js ──
|
|
7
7
|
// ─── UTILS ────────────────────────────────────────────────────────────────────
|
|
@@ -328,6 +328,13 @@ function nodeToInfo(node) {
|
|
|
328
328
|
return info;
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
// Yield execution back to Figma main thread loop so UI never freezes/blocks
|
|
332
|
+
function yieldToUI(delayMs) {
|
|
333
|
+
return new Promise(function(resolve) {
|
|
334
|
+
setTimeout(resolve, delayMs !== undefined ? delayMs : 0);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
331
338
|
|
|
332
339
|
// ── plugin-src/svg-path-helpers.js ──
|
|
333
340
|
// ─── SVG PATH NORMALIZATION ───────────────────────────────────────────────────
|
|
@@ -1238,7 +1245,7 @@ const handlers = {};
|
|
|
1238
1245
|
|
|
1239
1246
|
handlers.status = async () => ({
|
|
1240
1247
|
connected: true,
|
|
1241
|
-
version: "2.
|
|
1248
|
+
version: "2.9.6",
|
|
1242
1249
|
fileName: figma.root.name,
|
|
1243
1250
|
currentPage: figma.currentPage.name,
|
|
1244
1251
|
pageCount: figma.root.children.length,
|
|
@@ -2902,8 +2909,37 @@ handlers.screenshot = async function(params) {
|
|
|
2902
2909
|
return Promise.reject(new Error("[v1.9.1-export] " + exportErr.message + " type=" + node.type + " id=" + node.id));
|
|
2903
2910
|
}
|
|
2904
2911
|
|
|
2912
|
+
var annotations = [];
|
|
2913
|
+
if (params && (params.withAnnotations || params.annotated) && "children" in node && Array.isArray(node.children)) {
|
|
2914
|
+
var annCount = 0;
|
|
2915
|
+
for (var ci = 0; ci < node.children.length; ci++) {
|
|
2916
|
+
var child = node.children[ci];
|
|
2917
|
+
if (child.visible === false) continue;
|
|
2918
|
+
annCount++;
|
|
2919
|
+
annotations.push({
|
|
2920
|
+
index: annCount,
|
|
2921
|
+
id: child.id,
|
|
2922
|
+
name: child.name,
|
|
2923
|
+
type: child.type,
|
|
2924
|
+
x: Math.round(child.x),
|
|
2925
|
+
y: Math.round(child.y),
|
|
2926
|
+
width: Math.round(child.width),
|
|
2927
|
+
height: Math.round(child.height)
|
|
2928
|
+
});
|
|
2929
|
+
if (annCount >= 40) break;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2905
2933
|
try {
|
|
2906
|
-
|
|
2934
|
+
var resObj = {
|
|
2935
|
+
dataUrl: "data:image/png;base64," + uint8ArrayToBase64(bytes),
|
|
2936
|
+
nodeId: node.id,
|
|
2937
|
+
nodeName: node.name,
|
|
2938
|
+
width: node.width,
|
|
2939
|
+
height: node.height
|
|
2940
|
+
};
|
|
2941
|
+
if (annotations.length) resObj.annotations = annotations;
|
|
2942
|
+
return resObj;
|
|
2907
2943
|
} catch(encodeErr) {
|
|
2908
2944
|
return Promise.reject(new Error("[v1.9.1-encode] " + encodeErr.message));
|
|
2909
2945
|
}
|
|
@@ -3006,64 +3042,106 @@ handlers.export_image = async function(params) {
|
|
|
3006
3042
|
};
|
|
3007
3043
|
};
|
|
3008
3044
|
|
|
3009
|
-
// index_scan — aggregate page nodes, styles, variables, and components in one call for pre-indexing
|
|
3045
|
+
// index_scan — aggregate page nodes, styles, variables, and components in one call for pre-indexing
|
|
3010
3046
|
handlers.index_scan = async function() {
|
|
3011
|
-
|
|
3047
|
+
if (figma.ui) {
|
|
3012
3048
|
try {
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
var CHUNK_SIZE = 10;
|
|
3017
|
-
|
|
3018
|
-
for (var i = 0; i < topFrames.length; i += CHUNK_SIZE) {
|
|
3019
|
-
var chunk = topFrames.slice(i, i + CHUNK_SIZE);
|
|
3020
|
-
var chunkData = chunk.map(function(n) {
|
|
3021
|
-
return Object.assign(nodeToInfo(n), { childCount: "children" in n ? n.children.length : 0 });
|
|
3022
|
-
});
|
|
3023
|
-
collected = collected.concat(chunkData);
|
|
3049
|
+
figma.ui.postMessage({ type: "index-progress", stage: "nodes", percent: 5, label: "Scanning canvas layers…" });
|
|
3050
|
+
} catch(e) {}
|
|
3051
|
+
}
|
|
3024
3052
|
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3053
|
+
var pageNodes = [];
|
|
3054
|
+
try {
|
|
3055
|
+
var page = figma.currentPage;
|
|
3056
|
+
var topFrames = page.children || [];
|
|
3057
|
+
var CHUNK_SIZE = 10;
|
|
3058
|
+
var totalFrames = topFrames.length;
|
|
3059
|
+
|
|
3060
|
+
for (var i = 0; i < topFrames.length; i += CHUNK_SIZE) {
|
|
3061
|
+
var chunk = topFrames.slice(i, i + CHUNK_SIZE);
|
|
3062
|
+
var chunkData = chunk.map(function(n) {
|
|
3063
|
+
return Object.assign(nodeToInfo(n), { childCount: "children" in n ? n.children.length : 0 });
|
|
3064
|
+
});
|
|
3065
|
+
pageNodes = pageNodes.concat(chunkData);
|
|
3066
|
+
|
|
3067
|
+
var nodePct = totalFrames > 0 ? Math.min(40, Math.round(5 + (pageNodes.length / totalFrames) * 35)) : 35;
|
|
3068
|
+
if (figma.ui) {
|
|
3069
|
+
try {
|
|
3070
|
+
figma.ui.postMessage({
|
|
3071
|
+
type: "index-progress",
|
|
3072
|
+
stage: "nodes",
|
|
3073
|
+
percent: nodePct,
|
|
3074
|
+
label: "Layers: " + pageNodes.length + (totalFrames ? "/" + totalFrames : "")
|
|
3075
|
+
});
|
|
3076
|
+
} catch(e) {}
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
// Stream chunk progressively if there are multiple chunks
|
|
3080
|
+
if (topFrames.length > CHUNK_SIZE && figma.ui) {
|
|
3081
|
+
try {
|
|
3082
|
+
figma.ui.postMessage({ type: "index-chunk", nodes: chunkData });
|
|
3083
|
+
} catch(e3) {}
|
|
3031
3084
|
}
|
|
3032
|
-
return collected;
|
|
3033
|
-
} catch (e) {}
|
|
3034
|
-
return [];
|
|
3035
|
-
})();
|
|
3036
3085
|
|
|
3037
|
-
|
|
3086
|
+
// Yield after every chunk so canvas/UI remains completely smooth
|
|
3087
|
+
if (typeof yieldToUI === "function") {
|
|
3088
|
+
await yieldToUI(0);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
} catch (e) {}
|
|
3092
|
+
|
|
3093
|
+
if (typeof yieldToUI === "function") await yieldToUI(0);
|
|
3094
|
+
|
|
3095
|
+
if (figma.ui) {
|
|
3038
3096
|
try {
|
|
3039
|
-
|
|
3040
|
-
} catch
|
|
3041
|
-
|
|
3042
|
-
|
|
3097
|
+
figma.ui.postMessage({ type: "index-progress", stage: "styles", percent: 45, label: "Indexing color & text styles…" });
|
|
3098
|
+
} catch(e) {}
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
var styles = null;
|
|
3102
|
+
try {
|
|
3103
|
+
if (handlers.get_styles) styles = await handlers.get_styles({});
|
|
3104
|
+
} catch (e) {}
|
|
3105
|
+
|
|
3106
|
+
if (typeof yieldToUI === "function") await yieldToUI(0);
|
|
3043
3107
|
|
|
3044
|
-
|
|
3108
|
+
if (figma.ui) {
|
|
3045
3109
|
try {
|
|
3046
|
-
|
|
3047
|
-
} catch
|
|
3048
|
-
|
|
3049
|
-
|
|
3110
|
+
figma.ui.postMessage({ type: "index-progress", stage: "variables", percent: 65, label: "Indexing variables & tokens…" });
|
|
3111
|
+
} catch(e) {}
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
var variables = null;
|
|
3115
|
+
try {
|
|
3116
|
+
if (handlers.get_variables) variables = await handlers.get_variables({});
|
|
3117
|
+
} catch (e) {}
|
|
3050
3118
|
|
|
3051
|
-
|
|
3119
|
+
if (typeof yieldToUI === "function") await yieldToUI(0);
|
|
3120
|
+
|
|
3121
|
+
if (figma.ui) {
|
|
3052
3122
|
try {
|
|
3053
|
-
|
|
3054
|
-
} catch
|
|
3055
|
-
|
|
3056
|
-
})();
|
|
3123
|
+
figma.ui.postMessage({ type: "index-progress", stage: "components", percent: 85, label: "Indexing components & sets…" });
|
|
3124
|
+
} catch(e) {}
|
|
3125
|
+
}
|
|
3057
3126
|
|
|
3058
|
-
var
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3127
|
+
var components = null;
|
|
3128
|
+
try {
|
|
3129
|
+
if (handlers.get_local_components) components = await handlers.get_local_components({});
|
|
3130
|
+
} catch (e) {}
|
|
3131
|
+
|
|
3132
|
+
if (figma.ui) {
|
|
3133
|
+
try {
|
|
3134
|
+
figma.ui.postMessage({ type: "index-progress", stage: "done", percent: 100, label: "Index ready" });
|
|
3135
|
+
} catch(e) {}
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
var resolvedSid = figma.fileKey || (function() {
|
|
3139
|
+
try { return figma.root.getPluginData("mcp_session_id") || "_default"; } catch(e) { return "_default"; }
|
|
3140
|
+
})();
|
|
3063
3141
|
|
|
3064
3142
|
return {
|
|
3065
3143
|
fileName: figma.root ? figma.root.name : "unknown",
|
|
3066
|
-
sessionId:
|
|
3144
|
+
sessionId: resolvedSid,
|
|
3067
3145
|
pageNodes: pageNodes,
|
|
3068
3146
|
styles: styles,
|
|
3069
3147
|
variables: variables,
|
|
@@ -3749,46 +3827,66 @@ handlers.get_styles = async function() {
|
|
|
3749
3827
|
|
|
3750
3828
|
// get_local_components — enhanced component listing with descriptions and properties
|
|
3751
3829
|
handlers.get_local_components = async function() {
|
|
3752
|
-
|
|
3830
|
+
if (typeof figma.loadAllPagesAsync === "function") {
|
|
3831
|
+
try { await figma.loadAllPagesAsync(); } catch (e) {}
|
|
3832
|
+
}
|
|
3753
3833
|
var comps = figma.root.findAllWithCriteria({ types: ["COMPONENT"] });
|
|
3834
|
+
if (typeof yieldToUI === "function") await yieldToUI(0);
|
|
3754
3835
|
var sets = figma.root.findAllWithCriteria({ types: ["COMPONENT_SET"] });
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3836
|
+
if (typeof yieldToUI === "function") await yieldToUI(0);
|
|
3837
|
+
|
|
3838
|
+
var componentList = [];
|
|
3839
|
+
var COMP_BATCH_SIZE = 50;
|
|
3840
|
+
for (var ci = 0; ci < comps.length; ci++) {
|
|
3841
|
+
var c = comps[ci];
|
|
3842
|
+
var info = {
|
|
3843
|
+
id: c.id, name: c.name, key: c.key || null,
|
|
3844
|
+
description: c.description || "",
|
|
3845
|
+
setName: (c.parent && c.parent.type === "COMPONENT_SET") ? c.parent.name : null,
|
|
3846
|
+
width: Math.round(c.width), height: Math.round(c.height),
|
|
3847
|
+
page: c.parent ? (function findPage(n) {
|
|
3848
|
+
while (n && n.type !== "PAGE") n = n.parent;
|
|
3849
|
+
return n ? n.name : null;
|
|
3850
|
+
})(c) : null,
|
|
3851
|
+
};
|
|
3852
|
+
// Component properties (variant props)
|
|
3853
|
+
try {
|
|
3854
|
+
if (c.componentPropertyDefinitions) {
|
|
3855
|
+
var defs = c.componentPropertyDefinitions;
|
|
3856
|
+
var props = {};
|
|
3857
|
+
for (var key in defs) {
|
|
3858
|
+
if (Object.prototype.hasOwnProperty.call(defs, key)) {
|
|
3859
|
+
props[key] = { type: defs[key].type, defaultValue: defs[key].defaultValue };
|
|
3860
|
+
if (defs[key].variantOptions) props[key].options = defs[key].variantOptions;
|
|
3778
3861
|
}
|
|
3779
|
-
info.properties = props;
|
|
3780
3862
|
}
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
})
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3863
|
+
info.properties = props;
|
|
3864
|
+
}
|
|
3865
|
+
} catch(e) { /* skip properties */ }
|
|
3866
|
+
componentList.push(info);
|
|
3867
|
+
|
|
3868
|
+
if (ci > 0 && ci % COMP_BATCH_SIZE === 0 && typeof yieldToUI === "function") {
|
|
3869
|
+
await yieldToUI(0);
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
var componentSets = [];
|
|
3874
|
+
for (var si = 0; si < sets.length; si++) {
|
|
3875
|
+
var s = sets[si];
|
|
3876
|
+
componentSets.push({
|
|
3877
|
+
id: s.id, name: s.name, key: s.key || null,
|
|
3878
|
+
description: s.description || "",
|
|
3879
|
+
variantCount: s.children ? s.children.length : 0,
|
|
3880
|
+
});
|
|
3881
|
+
if (si > 0 && si % COMP_BATCH_SIZE === 0 && typeof yieldToUI === "function") {
|
|
3882
|
+
await yieldToUI(0);
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
return {
|
|
3887
|
+
components: componentList,
|
|
3888
|
+
componentSets: componentSets,
|
|
3889
|
+
total: componentList.length + componentSets.length,
|
|
3792
3890
|
};
|
|
3793
3891
|
};
|
|
3794
3892
|
|
|
@@ -3871,12 +3969,17 @@ handlers.get_variables = async function() {
|
|
|
3871
3969
|
values: values,
|
|
3872
3970
|
description: v.description || "",
|
|
3873
3971
|
});
|
|
3972
|
+
|
|
3973
|
+
if (vi > 0 && vi % 50 === 0 && typeof yieldToUI === "function") {
|
|
3974
|
+
await yieldToUI(0);
|
|
3975
|
+
}
|
|
3874
3976
|
}
|
|
3875
3977
|
collections.push({
|
|
3876
3978
|
id: col.id, name: col.name,
|
|
3877
3979
|
modes: col.modes.map(function(m) { return { id: m.modeId, name: m.name }; }),
|
|
3878
3980
|
variables: variables,
|
|
3879
3981
|
});
|
|
3982
|
+
if (typeof yieldToUI === "function") await yieldToUI(0);
|
|
3880
3983
|
}
|
|
3881
3984
|
} catch(e) {
|
|
3882
3985
|
return { error: "Variables API not available: " + e.message, collections: [] };
|
|
@@ -3884,6 +3987,131 @@ handlers.get_variables = async function() {
|
|
|
3884
3987
|
return { collections: collections };
|
|
3885
3988
|
};
|
|
3886
3989
|
|
|
3990
|
+
// ─── EXPORT ASSETS (Batch Icon & Image Extractor) ────────────────────────────
|
|
3991
|
+
handlers.export_assets = async function(params) {
|
|
3992
|
+
var id = params ? (params.id || params.nodeId) : null;
|
|
3993
|
+
var targetNode = null;
|
|
3994
|
+
if (id) targetNode = await findNodeByIdAsync(id);
|
|
3995
|
+
if (!targetNode) {
|
|
3996
|
+
var sel = figma.currentPage.selection;
|
|
3997
|
+
targetNode = sel && sel.length > 0 ? sel[0] : figma.currentPage;
|
|
3998
|
+
}
|
|
3999
|
+
if (!targetNode) throw new Error("No node found to export assets from");
|
|
4000
|
+
|
|
4001
|
+
var icons = [];
|
|
4002
|
+
var images = [];
|
|
4003
|
+
var seenNames = {};
|
|
4004
|
+
|
|
4005
|
+
function sanitizeAssetName(rawName, fallback) {
|
|
4006
|
+
var name = (rawName || fallback)
|
|
4007
|
+
.toLowerCase()
|
|
4008
|
+
.replace(/^icon[s\/_\-\s]+/, '')
|
|
4009
|
+
.replace(/^ic[_\-\s]+/, '')
|
|
4010
|
+
.replace(/[\/\s_\.]+/g, '-')
|
|
4011
|
+
.replace(/[^a-z0-9\-]/g, '')
|
|
4012
|
+
.replace(/^-+|-+$/g, '');
|
|
4013
|
+
if (!name) name = fallback;
|
|
4014
|
+
if (seenNames[name]) {
|
|
4015
|
+
seenNames[name]++;
|
|
4016
|
+
return name + "-" + seenNames[name];
|
|
4017
|
+
}
|
|
4018
|
+
seenNames[name] = 1;
|
|
4019
|
+
return name;
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
function isVectorIcon(nd) {
|
|
4023
|
+
if (!nd || nd.visible === false) return false;
|
|
4024
|
+
var t = nd.type;
|
|
4025
|
+
if (t === "VECTOR" || t === "BOOLEAN_OPERATION" || t === "STAR" || t === "POLYGON") return true;
|
|
4026
|
+
if ((t === "FRAME" || t === "COMPONENT" || t === "INSTANCE" || t === "GROUP") && nd.width <= 96 && nd.height <= 96) {
|
|
4027
|
+
var n = nd.name.toLowerCase();
|
|
4028
|
+
if (n.indexOf("icon") >= 0 || n.indexOf("ic_") >= 0 || n.indexOf("ic-") >= 0) return true;
|
|
4029
|
+
if (nd.children && nd.children.length > 0 && nd.children.length <= 8) {
|
|
4030
|
+
var allVectors = nd.children.every(function(c) {
|
|
4031
|
+
return c.type === "VECTOR" || c.type === "BOOLEAN_OPERATION" || c.type === "LINE" || c.type === "ELLIPSE" || c.type === "RECTANGLE";
|
|
4032
|
+
});
|
|
4033
|
+
if (allVectors) return true;
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
return false;
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
function hasImageFill(nd) {
|
|
4040
|
+
if (!nd || !nd.fills || !Array.isArray(nd.fills)) return false;
|
|
4041
|
+
return nd.fills.some(function(f) { return f && f.type === "IMAGE" && f.visible !== false; });
|
|
4042
|
+
}
|
|
4043
|
+
|
|
4044
|
+
var nodesToInspect = [];
|
|
4045
|
+
function collectAssetNodes(nd, depth) {
|
|
4046
|
+
if (!nd || nd.visible === false || depth > 20) return;
|
|
4047
|
+
if (isVectorIcon(nd)) {
|
|
4048
|
+
nodesToInspect.push({ node: nd, kind: "icon" });
|
|
4049
|
+
return;
|
|
4050
|
+
}
|
|
4051
|
+
if (hasImageFill(nd)) {
|
|
4052
|
+
nodesToInspect.push({ node: nd, kind: "image" });
|
|
4053
|
+
}
|
|
4054
|
+
if (nd.children && Array.isArray(nd.children)) {
|
|
4055
|
+
for (var ci = 0; ci < nd.children.length; ci++) {
|
|
4056
|
+
collectAssetNodes(nd.children[ci], depth + 1);
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
if (targetNode === figma.currentPage) {
|
|
4062
|
+
for (var pi = 0; pi < targetNode.children.length; pi++) {
|
|
4063
|
+
collectAssetNodes(targetNode.children[pi], 0);
|
|
4064
|
+
}
|
|
4065
|
+
} else {
|
|
4066
|
+
collectAssetNodes(targetNode, 0);
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
var exportLimit = Math.min(nodesToInspect.length, 60);
|
|
4070
|
+
for (var i = 0; i < exportLimit; i++) {
|
|
4071
|
+
var item = nodesToInspect[i];
|
|
4072
|
+
var nd = item.node;
|
|
4073
|
+
var baseName = sanitizeAssetName(nd.name, "asset-" + (i + 1));
|
|
4074
|
+
|
|
4075
|
+
if (item.kind === "icon") {
|
|
4076
|
+
try {
|
|
4077
|
+
var svgBytes = await nd.exportAsync({ format: "SVG" });
|
|
4078
|
+
var svgStr = String.fromCharCode.apply(null, Array.from(svgBytes));
|
|
4079
|
+
icons.push({
|
|
4080
|
+
id: nd.id,
|
|
4081
|
+
name: baseName,
|
|
4082
|
+
fileName: baseName + ".svg",
|
|
4083
|
+
width: Math.round(nd.width),
|
|
4084
|
+
height: Math.round(nd.height),
|
|
4085
|
+
svg: svgStr
|
|
4086
|
+
});
|
|
4087
|
+
} catch (e) {}
|
|
4088
|
+
} else if (item.kind === "image") {
|
|
4089
|
+
try {
|
|
4090
|
+
var pngBytes = await nd.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: 2 } });
|
|
4091
|
+
var b64 = uint8ArrayToBase64(pngBytes);
|
|
4092
|
+
images.push({
|
|
4093
|
+
id: nd.id,
|
|
4094
|
+
name: baseName,
|
|
4095
|
+
fileName: baseName + ".png",
|
|
4096
|
+
width: Math.round(nd.width),
|
|
4097
|
+
height: Math.round(nd.height),
|
|
4098
|
+
dataUrl: "data:image/png;base64," + b64
|
|
4099
|
+
});
|
|
4100
|
+
} catch (e) {}
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
return {
|
|
4105
|
+
success: true,
|
|
4106
|
+
sourceNodeId: targetNode.id,
|
|
4107
|
+
sourceNodeName: targetNode.name,
|
|
4108
|
+
totalIcons: icons.length,
|
|
4109
|
+
totalImages: images.length,
|
|
4110
|
+
icons: icons,
|
|
4111
|
+
images: images
|
|
4112
|
+
};
|
|
4113
|
+
};
|
|
4114
|
+
|
|
3887
4115
|
|
|
3888
4116
|
// ── plugin-src/handlers-tokens.js ──
|
|
3889
4117
|
// ─── DESIGN TOKEN OPERATIONS (v1.7.0) ───────────────────────────────────────
|
|
@@ -5148,12 +5376,51 @@ figma.clientStorage.getAsync("mcp_window_size").then(function(saved) {
|
|
|
5148
5376
|
}
|
|
5149
5377
|
}).catch(function() {});
|
|
5150
5378
|
|
|
5379
|
+
// ─── UNIQUE SESSION IDENTIFIER (Multi-tab Support) ───────────────────────────
|
|
5380
|
+
// In Figma, figma.root.id is always "0:0" across all files. To support running
|
|
5381
|
+
// multiple plugins concurrently across multiple tabs/files, generate and persist
|
|
5382
|
+
// a unique document session ID in document pluginData.
|
|
5383
|
+
function getSavedFileKey() {
|
|
5384
|
+
if (figma.fileKey) return figma.fileKey;
|
|
5385
|
+
try {
|
|
5386
|
+
var saved = figma.root.getPluginData("figma_file_key");
|
|
5387
|
+
if (saved && saved.length > 0) return saved;
|
|
5388
|
+
} catch(e) {}
|
|
5389
|
+
return null;
|
|
5390
|
+
}
|
|
5391
|
+
|
|
5392
|
+
function getOrCreateSessionId() {
|
|
5393
|
+
var fk = getSavedFileKey();
|
|
5394
|
+
if (fk) {
|
|
5395
|
+
return fk;
|
|
5396
|
+
}
|
|
5397
|
+
try {
|
|
5398
|
+
var stored = figma.root.getPluginData("mcp_session_id");
|
|
5399
|
+
if (stored && stored.length > 0) {
|
|
5400
|
+
return stored;
|
|
5401
|
+
}
|
|
5402
|
+
} catch(e) {}
|
|
5403
|
+
|
|
5404
|
+
var rand = Math.random().toString(36).substring(2, 10);
|
|
5405
|
+
var ts = Date.now().toString(36);
|
|
5406
|
+
var newId = "doc_" + rand + "_" + ts;
|
|
5407
|
+
try {
|
|
5408
|
+
figma.root.setPluginData("mcp_session_id", newId);
|
|
5409
|
+
} catch(e) {}
|
|
5410
|
+
return newId;
|
|
5411
|
+
}
|
|
5412
|
+
|
|
5413
|
+
var currentSessionId = getOrCreateSessionId();
|
|
5414
|
+
var currentFileName = figma.root ? figma.root.name : "Untitled";
|
|
5415
|
+
var currentFileKey = getSavedFileKey();
|
|
5416
|
+
|
|
5151
5417
|
// Broadcast active file / session metadata on startup
|
|
5152
5418
|
try {
|
|
5153
5419
|
figma.ui.postMessage({
|
|
5154
5420
|
type: "session-info",
|
|
5155
|
-
sessionId:
|
|
5156
|
-
fileName:
|
|
5421
|
+
sessionId: currentSessionId,
|
|
5422
|
+
fileName: currentFileName,
|
|
5423
|
+
fileKey: currentFileKey
|
|
5157
5424
|
});
|
|
5158
5425
|
} catch (e) {}
|
|
5159
5426
|
|
|
@@ -5162,13 +5429,33 @@ figma.on("selectionchange", function() {
|
|
|
5162
5429
|
try {
|
|
5163
5430
|
var sel = figma.currentPage.selection;
|
|
5164
5431
|
var summary = [];
|
|
5432
|
+
var fullNode = null;
|
|
5165
5433
|
for (var i = 0; i < Math.min(sel.length, 5); i++) {
|
|
5166
|
-
|
|
5434
|
+
var n = sel[i];
|
|
5435
|
+
// If node is an internal instance sub-layer (starts with I...), find its main component instance or top frame
|
|
5436
|
+
var mainId = n.id;
|
|
5437
|
+
if (mainId.startsWith("I") && mainId.indexOf(";") !== -1) {
|
|
5438
|
+
var parts = mainId.split(";");
|
|
5439
|
+
mainId = parts[parts.length - 1]; // Use real canonical node id
|
|
5440
|
+
}
|
|
5441
|
+
summary.push({
|
|
5442
|
+
id: n.id,
|
|
5443
|
+
mainId: mainId,
|
|
5444
|
+
name: n.name,
|
|
5445
|
+
type: n.type,
|
|
5446
|
+
width: typeof n.width === "number" ? Math.round(n.width) : undefined,
|
|
5447
|
+
height: typeof n.height === "number" ? Math.round(n.height) : undefined
|
|
5448
|
+
});
|
|
5449
|
+
}
|
|
5450
|
+
if (sel.length === 1 && typeof nodeToInfo === "function") {
|
|
5451
|
+
try { fullNode = nodeToInfo(sel[0]); } catch(e0) {}
|
|
5167
5452
|
}
|
|
5168
5453
|
figma.ui.postMessage({
|
|
5169
5454
|
type: "selection-change",
|
|
5170
5455
|
count: sel.length,
|
|
5171
|
-
|
|
5456
|
+
pageName: figma.currentPage ? figma.currentPage.name : undefined,
|
|
5457
|
+
selection: summary,
|
|
5458
|
+
fullNode: fullNode
|
|
5172
5459
|
});
|
|
5173
5460
|
} catch (e) {}
|
|
5174
5461
|
});
|
|
@@ -5190,6 +5477,28 @@ function onDocChange(event) {
|
|
|
5190
5477
|
var ids = Array.from(pendingChangedNodeIds);
|
|
5191
5478
|
pendingChangedNodeIds.clear();
|
|
5192
5479
|
|
|
5480
|
+
if (ids.length === 1) {
|
|
5481
|
+
try {
|
|
5482
|
+
var singleNode = await findNodeByIdAsync(ids[0]);
|
|
5483
|
+
if (singleNode) {
|
|
5484
|
+
figma.ui.postMessage({
|
|
5485
|
+
type: "delta-diff",
|
|
5486
|
+
id: singleNode.id,
|
|
5487
|
+
diff: {
|
|
5488
|
+
name: singleNode.name,
|
|
5489
|
+
characters: "characters" in singleNode ? singleNode.characters : undefined,
|
|
5490
|
+
visible: singleNode.visible,
|
|
5491
|
+
width: singleNode.width,
|
|
5492
|
+
height: singleNode.height,
|
|
5493
|
+
x: singleNode.x,
|
|
5494
|
+
y: singleNode.y
|
|
5495
|
+
}
|
|
5496
|
+
});
|
|
5497
|
+
return;
|
|
5498
|
+
}
|
|
5499
|
+
} catch(e1) {}
|
|
5500
|
+
}
|
|
5501
|
+
|
|
5193
5502
|
if (ids.length > 0 && ids.length <= 15) {
|
|
5194
5503
|
// Incremental micro-diff update for fast real-time editing
|
|
5195
5504
|
var diffNodes = [];
|
|
@@ -5209,24 +5518,27 @@ function onDocChange(event) {
|
|
|
5209
5518
|
|
|
5210
5519
|
// Fallback for large batch changes: mark dirty or trigger background sync
|
|
5211
5520
|
figma.ui.postMessage({ type: "document-change" });
|
|
5212
|
-
},
|
|
5521
|
+
}, 500);
|
|
5213
5522
|
} catch (e) {}
|
|
5214
5523
|
}
|
|
5215
5524
|
|
|
5216
|
-
// In dynamic-page mode, Figma requires figma.loadAllPagesAsync() before registering documentchange
|
|
5217
|
-
|
|
5218
|
-
|
|
5525
|
+
// In dynamic-page mode, Figma requires figma.loadAllPagesAsync() before registering documentchange.
|
|
5526
|
+
// Delay loading all pages slightly so plugin UI opens and paints immediately without UI freeze.
|
|
5527
|
+
setTimeout(function() {
|
|
5528
|
+
if (typeof figma.loadAllPagesAsync === "function") {
|
|
5529
|
+
figma.loadAllPagesAsync().then(function() {
|
|
5530
|
+
try {
|
|
5531
|
+
figma.on("documentchange", onDocChange);
|
|
5532
|
+
} catch (e) {}
|
|
5533
|
+
}).catch(function() {});
|
|
5534
|
+
} else {
|
|
5219
5535
|
try {
|
|
5220
5536
|
figma.on("documentchange", onDocChange);
|
|
5221
5537
|
} catch (e) {}
|
|
5222
|
-
}
|
|
5223
|
-
}
|
|
5224
|
-
try {
|
|
5225
|
-
figma.on("documentchange", onDocChange);
|
|
5226
|
-
} catch (e) {}
|
|
5227
|
-
}
|
|
5538
|
+
}
|
|
5539
|
+
}, 800);
|
|
5228
5540
|
|
|
5229
|
-
// Background initial scan after 1.
|
|
5541
|
+
// Background initial scan after 1.8s startup delay (ensures UI is fully rendered and WebSocket connected)
|
|
5230
5542
|
setTimeout(async function() {
|
|
5231
5543
|
try {
|
|
5232
5544
|
if (handlers.index_scan) {
|
|
@@ -5235,13 +5547,13 @@ setTimeout(async function() {
|
|
|
5235
5547
|
figma.ui.postMessage({
|
|
5236
5548
|
type: "index-update",
|
|
5237
5549
|
data: scanResult,
|
|
5238
|
-
fileName:
|
|
5239
|
-
sessionId:
|
|
5550
|
+
fileName: currentFileName,
|
|
5551
|
+
sessionId: currentSessionId,
|
|
5240
5552
|
startMs: startMs
|
|
5241
5553
|
});
|
|
5242
5554
|
}
|
|
5243
5555
|
} catch (e) {}
|
|
5244
|
-
},
|
|
5556
|
+
}, 1800);
|
|
5245
5557
|
|
|
5246
5558
|
// ─── DISPATCHER ───────────────────────────────────────────────────────────────
|
|
5247
5559
|
|
|
@@ -5288,6 +5600,72 @@ figma.ui.onmessage = async (request) => {
|
|
|
5288
5600
|
return;
|
|
5289
5601
|
}
|
|
5290
5602
|
|
|
5603
|
+
// Set and persist Figma File Key
|
|
5604
|
+
if (request.type === "set-file-key") {
|
|
5605
|
+
try {
|
|
5606
|
+
var newFileKey = (request.fileKey || "").trim();
|
|
5607
|
+
if (newFileKey) {
|
|
5608
|
+
figma.root.setPluginData("figma_file_key", newFileKey);
|
|
5609
|
+
currentFileKey = newFileKey;
|
|
5610
|
+
figma.ui.postMessage({
|
|
5611
|
+
type: "session-info",
|
|
5612
|
+
sessionId: currentSessionId,
|
|
5613
|
+
fileName: currentFileName,
|
|
5614
|
+
fileKey: newFileKey
|
|
5615
|
+
});
|
|
5616
|
+
figma.notify("Figma File Key saved!", { timeout: 1500 });
|
|
5617
|
+
}
|
|
5618
|
+
} catch(e) {}
|
|
5619
|
+
return;
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5622
|
+
// Toast notification from UI
|
|
5623
|
+
if (request.type === "notify") {
|
|
5624
|
+
try {
|
|
5625
|
+
if (request.message) {
|
|
5626
|
+
figma.notify(request.message, { timeout: request.timeout || 1500, error: request.error || false });
|
|
5627
|
+
}
|
|
5628
|
+
} catch(e) {}
|
|
5629
|
+
return;
|
|
5630
|
+
}
|
|
5631
|
+
|
|
5632
|
+
// Zoom to selection when clicked on selection bar
|
|
5633
|
+
if (request.type === "zoom-to-selection") {
|
|
5634
|
+
try {
|
|
5635
|
+
if (figma.currentPage.selection.length > 0) {
|
|
5636
|
+
figma.viewport.scrollAndZoomIntoView(figma.currentPage.selection);
|
|
5637
|
+
figma.notify("Focused on selection", { timeout: 1000 });
|
|
5638
|
+
}
|
|
5639
|
+
} catch(e) {}
|
|
5640
|
+
return;
|
|
5641
|
+
}
|
|
5642
|
+
|
|
5643
|
+
// Export selection image as PNG for UI clipboard/download
|
|
5644
|
+
if (request.type === "export-selection-image") {
|
|
5645
|
+
try {
|
|
5646
|
+
var sel = figma.currentPage.selection;
|
|
5647
|
+
if (!sel || sel.length === 0) {
|
|
5648
|
+
figma.notify("Please select a layer to capture", { timeout: 1500 });
|
|
5649
|
+
return;
|
|
5650
|
+
}
|
|
5651
|
+
var targetNode = sel[0];
|
|
5652
|
+
var scale = request.scale || 2;
|
|
5653
|
+
var bytes = await targetNode.exportAsync({
|
|
5654
|
+
format: "PNG",
|
|
5655
|
+
constraint: { type: "SCALE", value: scale }
|
|
5656
|
+
});
|
|
5657
|
+
figma.ui.postMessage({
|
|
5658
|
+
type: "selection-image-exported",
|
|
5659
|
+
nodeId: targetNode.id,
|
|
5660
|
+
nodeName: targetNode.name,
|
|
5661
|
+
bytes: Array.from(bytes)
|
|
5662
|
+
});
|
|
5663
|
+
} catch (err) {
|
|
5664
|
+
figma.notify("Export failed: " + (err && err.message ? err.message : String(err)), { error: true });
|
|
5665
|
+
}
|
|
5666
|
+
return;
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5291
5669
|
// Handle manual reindex request from UI
|
|
5292
5670
|
if (request.type === "manual-reindex") {
|
|
5293
5671
|
try {
|
|
@@ -5297,8 +5675,8 @@ figma.ui.onmessage = async (request) => {
|
|
|
5297
5675
|
figma.ui.postMessage({
|
|
5298
5676
|
type: "index-update",
|
|
5299
5677
|
data: scanResult,
|
|
5300
|
-
fileName:
|
|
5301
|
-
sessionId:
|
|
5678
|
+
fileName: currentFileName,
|
|
5679
|
+
sessionId: currentSessionId,
|
|
5302
5680
|
startMs: startMs
|
|
5303
5681
|
});
|
|
5304
5682
|
}
|