Talk to Figma MCP
by sonnylazuardi
Verified
- cursor-talk-to-figma-mcp
- src
- cursor_mcp_plugin
// This is the main code file for the Cursor MCP Figma plugin
// It handles Figma API commands
// Plugin state
const state = {
serverPort: 3055, // Default port
};
// Show UI
figma.showUI(__html__, { width: 350, height: 450 });
// Plugin commands from UI
figma.ui.onmessage = async (msg) => {
switch (msg.type) {
case "update-settings":
updateSettings(msg);
break;
case "notify":
figma.notify(msg.message);
break;
case "close-plugin":
figma.closePlugin();
break;
case "execute-command":
// Execute commands received from UI (which gets them from WebSocket)
try {
const result = await handleCommand(msg.command, msg.params);
// Send result back to UI
figma.ui.postMessage({
type: "command-result",
id: msg.id,
result,
});
} catch (error) {
figma.ui.postMessage({
type: "command-error",
id: msg.id,
error: error.message || "Error executing command",
});
}
break;
}
};
// Listen for plugin commands from menu
figma.on("run", ({ command }) => {
figma.ui.postMessage({ type: "auto-connect" });
});
// Update plugin settings
function updateSettings(settings) {
if (settings.serverPort) {
state.serverPort = settings.serverPort;
}
figma.clientStorage.setAsync("settings", {
serverPort: state.serverPort,
});
}
// Handle commands from UI
async function handleCommand(command, params) {
switch (command) {
case "get_document_info":
return await getDocumentInfo();
case "get_selection":
return await getSelection();
case "get_node_info":
if (!params || !params.nodeId) {
throw new Error("Missing nodeId parameter");
}
return await getNodeInfo(params.nodeId);
case "get_nodes_info":
if (!params || !params.nodeIds || !Array.isArray(params.nodeIds)) {
throw new Error("Missing or invalid nodeIds parameter");
}
return await getNodesInfo(params.nodeIds);
case "create_rectangle":
return await createRectangle(params);
case "create_frame":
return await createFrame(params);
case "create_text":
return await createText(params);
case "set_fill_color":
return await setFillColor(params);
case "set_stroke_color":
return await setStrokeColor(params);
case "move_node":
return await moveNode(params);
case "resize_node":
return await resizeNode(params);
case "delete_node":
return await deleteNode(params);
case "get_styles":
return await getStyles();
case "get_local_components":
return await getLocalComponents();
// case "get_team_components":
// return await getTeamComponents();
case "create_component_instance":
return await createComponentInstance(params);
case "export_node_as_image":
return await exportNodeAsImage(params);
case "set_corner_radius":
return await setCornerRadius(params);
case "set_text_content":
return await setTextContent(params);
case "clone_node":
return await cloneNode(params);
default:
throw new Error(`Unknown command: ${command}`);
}
}
// Command implementations
async function getDocumentInfo() {
await figma.currentPage.loadAsync();
const page = figma.currentPage;
return {
name: page.name,
id: page.id,
type: page.type,
children: page.children.map((node) => ({
id: node.id,
name: node.name,
type: node.type,
})),
currentPage: {
id: page.id,
name: page.name,
childCount: page.children.length,
},
pages: [
{
id: page.id,
name: page.name,
childCount: page.children.length,
},
],
};
}
async function getSelection() {
return {
selectionCount: figma.currentPage.selection.length,
selection: figma.currentPage.selection.map((node) => ({
id: node.id,
name: node.name,
type: node.type,
visible: node.visible,
})),
};
}
async function getNodeInfo(nodeId) {
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
const response = await node.exportAsync({
format: "JSON_REST_V1",
});
return response.document;
}
async function getNodesInfo(nodeIds) {
try {
// Load all nodes in parallel
const nodes = await Promise.all(
nodeIds.map((id) => figma.getNodeByIdAsync(id))
);
// Filter out any null values (nodes that weren't found)
const validNodes = nodes.filter((node) => node !== null);
// Export all valid nodes in parallel
const responses = await Promise.all(
validNodes.map(async (node) => {
const response = await node.exportAsync({
format: "JSON_REST_V1",
});
return {
nodeId: node.id,
document: response.document,
};
})
);
return responses;
} catch (error) {
throw new Error(`Error getting nodes info: ${error.message}`);
}
}
async function createRectangle(params) {
const {
x = 0,
y = 0,
width = 100,
height = 100,
name = "Rectangle",
parentId,
} = params || {};
const rect = figma.createRectangle();
rect.x = x;
rect.y = y;
rect.resize(width, height);
rect.name = name;
// If parentId is provided, append to that node, otherwise append to current page
if (parentId) {
const parentNode = await figma.getNodeByIdAsync(parentId);
if (!parentNode) {
throw new Error(`Parent node not found with ID: ${parentId}`);
}
if (!("appendChild" in parentNode)) {
throw new Error(`Parent node does not support children: ${parentId}`);
}
parentNode.appendChild(rect);
} else {
figma.currentPage.appendChild(rect);
}
return {
id: rect.id,
name: rect.name,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
parentId: rect.parent ? rect.parent.id : undefined,
};
}
async function createFrame(params) {
const {
x = 0,
y = 0,
width = 100,
height = 100,
name = "Frame",
parentId,
fillColor,
strokeColor,
strokeWeight,
} = params || {};
const frame = figma.createFrame();
frame.x = x;
frame.y = y;
frame.resize(width, height);
frame.name = name;
// Set fill color if provided
if (fillColor) {
const paintStyle = {
type: "SOLID",
color: {
r: parseFloat(fillColor.r) || 0,
g: parseFloat(fillColor.g) || 0,
b: parseFloat(fillColor.b) || 0,
},
opacity: parseFloat(fillColor.a) || 1,
};
frame.fills = [paintStyle];
}
// Set stroke color and weight if provided
if (strokeColor) {
const strokeStyle = {
type: "SOLID",
color: {
r: parseFloat(strokeColor.r) || 0,
g: parseFloat(strokeColor.g) || 0,
b: parseFloat(strokeColor.b) || 0,
},
opacity: parseFloat(strokeColor.a) || 1,
};
frame.strokes = [strokeStyle];
}
// Set stroke weight if provided
if (strokeWeight !== undefined) {
frame.strokeWeight = strokeWeight;
}
// If parentId is provided, append to that node, otherwise append to current page
if (parentId) {
const parentNode = await figma.getNodeByIdAsync(parentId);
if (!parentNode) {
throw new Error(`Parent node not found with ID: ${parentId}`);
}
if (!("appendChild" in parentNode)) {
throw new Error(`Parent node does not support children: ${parentId}`);
}
parentNode.appendChild(frame);
} else {
figma.currentPage.appendChild(frame);
}
return {
id: frame.id,
name: frame.name,
x: frame.x,
y: frame.y,
width: frame.width,
height: frame.height,
fills: frame.fills,
strokes: frame.strokes,
strokeWeight: frame.strokeWeight,
parentId: frame.parent ? frame.parent.id : undefined,
};
}
async function createText(params) {
const {
x = 0,
y = 0,
text = "Text",
fontSize = 14,
fontWeight = 400,
fontColor = { r: 0, g: 0, b: 0, a: 1 }, // Default to black
name = "Text",
parentId,
} = params || {};
// Map common font weights to Figma font styles
const getFontStyle = (weight) => {
switch (weight) {
case 100:
return "Thin";
case 200:
return "Extra Light";
case 300:
return "Light";
case 400:
return "Regular";
case 500:
return "Medium";
case 600:
return "Semi Bold";
case 700:
return "Bold";
case 800:
return "Extra Bold";
case 900:
return "Black";
default:
return "Regular";
}
};
const textNode = figma.createText();
textNode.x = x;
textNode.y = y;
textNode.name = name;
try {
await figma.loadFontAsync({
family: "Inter",
style: getFontStyle(fontWeight),
});
textNode.fontName = { family: "Inter", style: getFontStyle(fontWeight) };
textNode.fontSize = parseInt(fontSize);
} catch (error) {
console.error("Error setting font size", error);
}
setCharacters(textNode, text);
// Set text color
const paintStyle = {
type: "SOLID",
color: {
r: parseFloat(fontColor.r) || 0,
g: parseFloat(fontColor.g) || 0,
b: parseFloat(fontColor.b) || 0,
},
opacity: parseFloat(fontColor.a) || 1,
};
textNode.fills = [paintStyle];
// If parentId is provided, append to that node, otherwise append to current page
if (parentId) {
const parentNode = await figma.getNodeByIdAsync(parentId);
if (!parentNode) {
throw new Error(`Parent node not found with ID: ${parentId}`);
}
if (!("appendChild" in parentNode)) {
throw new Error(`Parent node does not support children: ${parentId}`);
}
parentNode.appendChild(textNode);
} else {
figma.currentPage.appendChild(textNode);
}
return {
id: textNode.id,
name: textNode.name,
x: textNode.x,
y: textNode.y,
width: textNode.width,
height: textNode.height,
characters: textNode.characters,
fontSize: textNode.fontSize,
fontWeight: fontWeight,
fontColor: fontColor,
fontName: textNode.fontName,
fills: textNode.fills,
parentId: textNode.parent ? textNode.parent.id : undefined,
};
}
async function setFillColor(params) {
console.log("setFillColor", params);
const {
nodeId,
color: { r, g, b, a },
} = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("fills" in node)) {
throw new Error(`Node does not support fills: ${nodeId}`);
}
// Create RGBA color
const rgbColor = {
r: parseFloat(r) || 0,
g: parseFloat(g) || 0,
b: parseFloat(b) || 0,
a: parseFloat(a) || 1,
};
// Set fill
const paintStyle = {
type: "SOLID",
color: {
r: parseFloat(rgbColor.r),
g: parseFloat(rgbColor.g),
b: parseFloat(rgbColor.b),
},
opacity: parseFloat(rgbColor.a),
};
console.log("paintStyle", paintStyle);
node.fills = [paintStyle];
return {
id: node.id,
name: node.name,
fills: [paintStyle],
};
}
async function setStrokeColor(params) {
const {
nodeId,
color: { r, g, b, a },
weight = 1,
} = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("strokes" in node)) {
throw new Error(`Node does not support strokes: ${nodeId}`);
}
// Create RGBA color
const rgbColor = {
r: r !== undefined ? r : 0,
g: g !== undefined ? g : 0,
b: b !== undefined ? b : 0,
a: a !== undefined ? a : 1,
};
// Set stroke
const paintStyle = {
type: "SOLID",
color: {
r: rgbColor.r,
g: rgbColor.g,
b: rgbColor.b,
},
opacity: rgbColor.a,
};
node.strokes = [paintStyle];
// Set stroke weight if available
if ("strokeWeight" in node) {
node.strokeWeight = weight;
}
return {
id: node.id,
name: node.name,
strokes: node.strokes,
strokeWeight: "strokeWeight" in node ? node.strokeWeight : undefined,
};
}
async function moveNode(params) {
const { nodeId, x, y } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
if (x === undefined || y === undefined) {
throw new Error("Missing x or y parameters");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("x" in node) || !("y" in node)) {
throw new Error(`Node does not support position: ${nodeId}`);
}
node.x = x;
node.y = y;
return {
id: node.id,
name: node.name,
x: node.x,
y: node.y,
};
}
async function resizeNode(params) {
const { nodeId, width, height } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
if (width === undefined || height === undefined) {
throw new Error("Missing width or height parameters");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("resize" in node)) {
throw new Error(`Node does not support resizing: ${nodeId}`);
}
node.resize(width, height);
return {
id: node.id,
name: node.name,
width: node.width,
height: node.height,
};
}
async function deleteNode(params) {
const { nodeId } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
// Save node info before deleting
const nodeInfo = {
id: node.id,
name: node.name,
type: node.type,
};
node.remove();
return nodeInfo;
}
async function getStyles() {
const styles = {
colors: await figma.getLocalPaintStylesAsync(),
texts: await figma.getLocalTextStylesAsync(),
effects: await figma.getLocalEffectStylesAsync(),
grids: await figma.getLocalGridStylesAsync(),
};
return {
colors: styles.colors.map((style) => ({
id: style.id,
name: style.name,
key: style.key,
paint: style.paints[0],
})),
texts: styles.texts.map((style) => ({
id: style.id,
name: style.name,
key: style.key,
fontSize: style.fontSize,
fontName: style.fontName,
})),
effects: styles.effects.map((style) => ({
id: style.id,
name: style.name,
key: style.key,
})),
grids: styles.grids.map((style) => ({
id: style.id,
name: style.name,
key: style.key,
})),
};
}
async function getLocalComponents() {
await figma.loadAllPagesAsync();
const components = figma.root.findAllWithCriteria({
types: ["COMPONENT"],
});
return {
count: components.length,
components: components.map((component) => ({
id: component.id,
name: component.name,
key: "key" in component ? component.key : null,
})),
};
}
// async function getTeamComponents() {
// try {
// const teamComponents =
// await figma.teamLibrary.getAvailableComponentsAsync();
// return {
// count: teamComponents.length,
// components: teamComponents.map((component) => ({
// key: component.key,
// name: component.name,
// description: component.description,
// libraryName: component.libraryName,
// })),
// };
// } catch (error) {
// throw new Error(`Error getting team components: ${error.message}`);
// }
// }
async function createComponentInstance(params) {
const { componentKey, x = 0, y = 0 } = params || {};
if (!componentKey) {
throw new Error("Missing componentKey parameter");
}
try {
const component = await figma.importComponentByKeyAsync(componentKey);
const instance = component.createInstance();
instance.x = x;
instance.y = y;
figma.currentPage.appendChild(instance);
return {
id: instance.id,
name: instance.name,
x: instance.x,
y: instance.y,
width: instance.width,
height: instance.height,
componentId: instance.componentId,
};
} catch (error) {
throw new Error(`Error creating component instance: ${error.message}`);
}
}
async function exportNodeAsImage(params) {
const { nodeId, scale = 1 } = params || {};
const format = "PNG";
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (!("exportAsync" in node)) {
throw new Error(`Node does not support exporting: ${nodeId}`);
}
try {
const settings = {
format: format,
constraint: { type: "SCALE", value: scale },
};
const bytes = await node.exportAsync(settings);
let mimeType;
switch (format) {
case "PNG":
mimeType = "image/png";
break;
case "JPG":
mimeType = "image/jpeg";
break;
case "SVG":
mimeType = "image/svg+xml";
break;
case "PDF":
mimeType = "application/pdf";
break;
default:
mimeType = "application/octet-stream";
}
// Proper way to convert Uint8Array to base64
const base64 = customBase64Encode(bytes);
// const imageData = `data:${mimeType};base64,${base64}`;
return {
nodeId,
format,
scale,
mimeType,
imageData: base64,
};
} catch (error) {
throw new Error(`Error exporting node as image: ${error.message}`);
}
}
function customBase64Encode(bytes) {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let base64 = "";
const byteLength = bytes.byteLength;
const byteRemainder = byteLength % 3;
const mainLength = byteLength - byteRemainder;
let a, b, c, d;
let chunk;
// Main loop deals with bytes in chunks of 3
for (let i = 0; i < mainLength; i = i + 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = chunk & 63; // 63 = 2^6 - 1
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += chars[a] + chars[b] + chars[c] + chars[d];
}
// Deal with the remaining bytes and padding
if (byteRemainder === 1) {
chunk = bytes[mainLength];
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4; // 3 = 2^2 - 1
base64 += chars[a] + chars[b] + "==";
} else if (byteRemainder === 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2; // 15 = 2^4 - 1
base64 += chars[a] + chars[b] + chars[c] + "=";
}
return base64;
}
async function setCornerRadius(params) {
const { nodeId, radius, corners } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
if (radius === undefined) {
throw new Error("Missing radius parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
// Check if node supports corner radius
if (!("cornerRadius" in node)) {
throw new Error(`Node does not support corner radius: ${nodeId}`);
}
// If corners array is provided, set individual corner radii
if (corners && Array.isArray(corners) && corners.length === 4) {
if ("topLeftRadius" in node) {
// Node supports individual corner radii
if (corners[0]) node.topLeftRadius = radius;
if (corners[1]) node.topRightRadius = radius;
if (corners[2]) node.bottomRightRadius = radius;
if (corners[3]) node.bottomLeftRadius = radius;
} else {
// Node only supports uniform corner radius
node.cornerRadius = radius;
}
} else {
// Set uniform corner radius
node.cornerRadius = radius;
}
return {
id: node.id,
name: node.name,
cornerRadius: "cornerRadius" in node ? node.cornerRadius : undefined,
topLeftRadius: "topLeftRadius" in node ? node.topLeftRadius : undefined,
topRightRadius: "topRightRadius" in node ? node.topRightRadius : undefined,
bottomRightRadius:
"bottomRightRadius" in node ? node.bottomRightRadius : undefined,
bottomLeftRadius:
"bottomLeftRadius" in node ? node.bottomLeftRadius : undefined,
};
}
async function setTextContent(params) {
const { nodeId, text } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
if (text === undefined) {
throw new Error("Missing text parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
if (node.type !== "TEXT") {
throw new Error(`Node is not a text node: ${nodeId}`);
}
try {
await figma.loadFontAsync(node.fontName);
await setCharacters(node, text);
return {
id: node.id,
name: node.name,
characters: node.characters,
fontName: node.fontName,
};
} catch (error) {
throw new Error(`Error setting text content: ${error.message}`);
}
}
// Initialize settings on load
(async function initializePlugin() {
try {
const savedSettings = await figma.clientStorage.getAsync("settings");
if (savedSettings) {
if (savedSettings.serverPort) {
state.serverPort = savedSettings.serverPort;
}
}
// Send initial settings to UI
figma.ui.postMessage({
type: "init-settings",
settings: {
serverPort: state.serverPort,
},
});
} catch (error) {
console.error("Error loading settings:", error);
}
})();
function uniqBy(arr, predicate) {
const cb = typeof predicate === "function" ? predicate : (o) => o[predicate];
return [
...arr
.reduce((map, item) => {
const key = item === null || item === undefined ? item : cb(item);
map.has(key) || map.set(key, item);
return map;
}, new Map())
.values(),
];
}
const setCharacters = async (node, characters, options) => {
const fallbackFont = (options && options.fallbackFont) || {
family: "Inter",
style: "Regular",
};
try {
if (node.fontName === figma.mixed) {
if (options && options.smartStrategy === "prevail") {
const fontHashTree = {};
for (let i = 1; i < node.characters.length; i++) {
const charFont = node.getRangeFontName(i - 1, i);
const key = `${charFont.family}::${charFont.style}`;
fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1;
}
const prevailedTreeItem = Object.entries(fontHashTree).sort(
(a, b) => b[1] - a[1]
)[0];
const [family, style] = prevailedTreeItem[0].split("::");
const prevailedFont = {
family,
style,
};
await figma.loadFontAsync(prevailedFont);
node.fontName = prevailedFont;
} else if (options && options.smartStrategy === "strict") {
return setCharactersWithStrictMatchFont(node, characters, fallbackFont);
} else if (options && options.smartStrategy === "experimental") {
return setCharactersWithSmartMatchFont(node, characters, fallbackFont);
} else {
const firstCharFont = node.getRangeFontName(0, 1);
await figma.loadFontAsync(firstCharFont);
node.fontName = firstCharFont;
}
} else {
await figma.loadFontAsync({
family: node.fontName.family,
style: node.fontName.style,
});
}
} catch (err) {
console.warn(
`Failed to load "${node.fontName["family"]} ${node.fontName["style"]}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`,
err
);
await figma.loadFontAsync(fallbackFont);
node.fontName = fallbackFont;
}
try {
node.characters = characters;
return true;
} catch (err) {
console.warn(`Failed to set characters. Skipped.`, err);
return false;
}
};
const setCharactersWithStrictMatchFont = async (
node,
characters,
fallbackFont
) => {
const fontHashTree = {};
for (let i = 1; i < node.characters.length; i++) {
const startIdx = i - 1;
const startCharFont = node.getRangeFontName(startIdx, i);
const startCharFontVal = `${startCharFont.family}::${startCharFont.style}`;
while (i < node.characters.length) {
i++;
const charFont = node.getRangeFontName(i - 1, i);
if (startCharFontVal !== `${charFont.family}::${charFont.style}`) {
break;
}
}
fontHashTree[`${startIdx}_${i}`] = startCharFontVal;
}
await figma.loadFontAsync(fallbackFont);
node.fontName = fallbackFont;
node.characters = characters;
console.log(fontHashTree);
await Promise.all(
Object.keys(fontHashTree).map(async (range) => {
console.log(range, fontHashTree[range]);
const [start, end] = range.split("_");
const [family, style] = fontHashTree[range].split("::");
const matchedFont = {
family,
style,
};
await figma.loadFontAsync(matchedFont);
return node.setRangeFontName(Number(start), Number(end), matchedFont);
})
);
return true;
};
const getDelimiterPos = (str, delimiter, startIdx = 0, endIdx = str.length) => {
const indices = [];
let temp = startIdx;
for (let i = startIdx; i < endIdx; i++) {
if (
str[i] === delimiter &&
i + startIdx !== endIdx &&
temp !== i + startIdx
) {
indices.push([temp, i + startIdx]);
temp = i + startIdx + 1;
}
}
temp !== endIdx && indices.push([temp, endIdx]);
return indices.filter(Boolean);
};
const buildLinearOrder = (node) => {
const fontTree = [];
const newLinesPos = getDelimiterPos(node.characters, "\n");
newLinesPos.forEach(([newLinesRangeStart, newLinesRangeEnd], n) => {
const newLinesRangeFont = node.getRangeFontName(
newLinesRangeStart,
newLinesRangeEnd
);
if (newLinesRangeFont === figma.mixed) {
const spacesPos = getDelimiterPos(
node.characters,
" ",
newLinesRangeStart,
newLinesRangeEnd
);
spacesPos.forEach(([spacesRangeStart, spacesRangeEnd], s) => {
const spacesRangeFont = node.getRangeFontName(
spacesRangeStart,
spacesRangeEnd
);
if (spacesRangeFont === figma.mixed) {
const spacesRangeFont = node.getRangeFontName(
spacesRangeStart,
spacesRangeStart[0]
);
fontTree.push({
start: spacesRangeStart,
delimiter: " ",
family: spacesRangeFont.family,
style: spacesRangeFont.style,
});
} else {
fontTree.push({
start: spacesRangeStart,
delimiter: " ",
family: spacesRangeFont.family,
style: spacesRangeFont.style,
});
}
});
} else {
fontTree.push({
start: newLinesRangeStart,
delimiter: "\n",
family: newLinesRangeFont.family,
style: newLinesRangeFont.style,
});
}
});
return fontTree
.sort((a, b) => +a.start - +b.start)
.map(({ family, style, delimiter }) => ({ family, style, delimiter }));
};
const setCharactersWithSmartMatchFont = async (
node,
characters,
fallbackFont
) => {
const rangeTree = buildLinearOrder(node);
const fontsToLoad = uniqBy(
rangeTree,
({ family, style }) => `${family}::${style}`
).map(({ family, style }) => ({
family,
style,
}));
await Promise.all([...fontsToLoad, fallbackFont].map(figma.loadFontAsync));
node.fontName = fallbackFont;
node.characters = characters;
let prevPos = 0;
rangeTree.forEach(({ family, style, delimiter }) => {
if (prevPos < node.characters.length) {
const delimeterPos = node.characters.indexOf(delimiter, prevPos);
const endPos =
delimeterPos > prevPos ? delimeterPos : node.characters.length;
const matchedFont = {
family,
style,
};
node.setRangeFontName(prevPos, endPos, matchedFont);
prevPos = endPos + 1;
}
});
return true;
};
// Add the cloneNode function implementation
async function cloneNode(params) {
const { nodeId, x, y } = params || {};
if (!nodeId) {
throw new Error("Missing nodeId parameter");
}
const node = await figma.getNodeByIdAsync(nodeId);
if (!node) {
throw new Error(`Node not found with ID: ${nodeId}`);
}
// Clone the node
const clone = node.clone();
// If x and y are provided, move the clone to that position
if (x !== undefined && y !== undefined) {
if (!("x" in clone) || !("y" in clone)) {
throw new Error(`Cloned node does not support position: ${nodeId}`);
}
clone.x = x;
clone.y = y;
}
// Add the clone to the same parent as the original node
if (node.parent) {
node.parent.appendChild(clone);
} else {
figma.currentPage.appendChild(clone);
}
return {
id: clone.id,
name: clone.name,
x: "x" in clone ? clone.x : undefined,
y: "y" in clone ? clone.y : undefined,
width: "width" in clone ? clone.width : undefined,
height: "height" in clone ? clone.height : undefined,
};
}