Skip to main content
Glama

Figma MCP Server

by 1yhy
simplify-node-response.ts17.9 kB
import type { GetFileNodesResponse, Node as FigmaDocumentNode, GetFileResponse, Paint, } from "@figma/rest-api-spec"; import { isVisible, parsePaint, convertColor, formatRGBAColor, generateCSSShorthand, isVisibleInParent } from "~/utils/common.js"; import { isRectangleCornerRadii, hasValue } from "~/utils/identity.js"; import { buildSimplifiedEffects } from "~/transformers/effects.js"; import { buildSimplifiedStrokes } from "~/transformers/style.js"; import { generateFileName, suggestExportFormat as suggestFormat } from "~/utils/file.js"; import { isSVGNode, processSVGNodesBottomUp } from "~/utils/svg.js"; import { hasImageFill, detectAndMarkImageGroup as markImageGroup, sortNodesByPosition, cleanupTemporaryProperties } from "~/transformers/node.js"; import { LayoutOptimizer } from "~/transformers/layout-optimizer.js"; // -------------------- SIMPLIFIED STRUCTURES -------------------- export type CSSHexColor = `#${string}`; export type CSSRGBAColor = `rgba(${number}, ${number}, ${number}, ${number})`; // 添加图片资源类型 export type ImageResource = { // 图片引用ID,下载图片的必要属性 imageRef: string; }; // 导出信息,包含需要的图片导出属性 export type ExportInfo = { // 导出类型 (单图片/图片组) type: 'IMAGE' | 'IMAGE_GROUP'; // 推荐的导出格式 format: 'PNG' | 'JPG' | 'SVG'; // 图片节点ID,用于API调用 nodeId?: string; // 建议的文件名 fileName?: string; }; export type TextStyle = Partial<{ fontFamily: string; fontWeight: number; fontSize: number; textAlignHorizontal: string; textAlignVertical: string; lineHeightPx: number; }>; // CSS样式对象,包含所有可能的CSS属性 export type CSSStyle = { // 文本样式 fontFamily?: string; fontSize?: string; fontWeight?: string | number; textAlign?: string; verticalAlign?: string; lineHeight?: string; // 颜色和背景 color?: string; backgroundColor?: string; background?: string; // 布局 width?: string; height?: string; margin?: string; padding?: string; position?: string; top?: string; right?: string; bottom?: string; left?: string; display?: string; flexDirection?: string; justifyContent?: string; alignItems?: string; gap?: string; // 边框和圆角 border?: string; borderRadius?: string; borderWidth?: string; borderStyle?: string; borderColor?: string; // 特效 boxShadow?: string; filter?: string; backdropFilter?: string; opacity?: string; // 添加任何其他需要的CSS属性 [key: string]: string | number | undefined; }; export interface SimplifiedDesign { name: string; lastModified: string; thumbnailUrl: string; nodes: SimplifiedNode[]; } export interface SimplifiedNode { id: string; name: string; type: string; // e.g. FRAME, TEXT, INSTANCE, RECTANGLE, etc. // text text?: string; // 旧的样式对象,保留向后兼容性 style?: TextStyle; // 新的CSS样式对象 cssStyles?: CSSStyle; // appearance fills?: SimplifiedFill[]; // 导出信息 exportInfo?: ExportInfo; // children children?: SimplifiedNode[]; // 内部使用的绝对坐标,用于计算子节点相对位置 _absoluteX?: number; _absoluteY?: number; } export type SimplifiedFill = { type: 'SOLID' | 'GRADIENT_LINEAR' | 'GRADIENT_RADIAL' | 'GRADIENT_ANGULAR' | 'GRADIENT_DIAMOND' | 'IMAGE'; // 颜色可以是十六进制表示 color?: string; // 或者是对象表示 rgba?: { r: number; g: number; b: number; a: number; }; opacity?: number; // 渐变属性 gradientHandlePositions?: Array<{x: number, y: number}>; gradientStops?: Array<{ position: number; color: string; }>; imageRef?: string; }; // 在文件顶部添加导出类型 export type FigmaNodeType = 'FRAME' | 'GROUP' | 'TEXT' | 'VECTOR' | 'RECTANGLE' | 'ELLIPSE' | 'INSTANCE' | 'COMPONENT' | 'DOCUMENT' | 'CANVAS' | string; // ---------------------- PARSING ---------------------- export function parseFigmaResponse(data: GetFileResponse | GetFileNodesResponse): SimplifiedDesign { // 提取基本信息 const { name, lastModified, thumbnailUrl } = data; // 处理节点 let nodes: FigmaDocumentNode[] = []; if ('document' in data) { // 如果是整个文件的响应 nodes = data.document.children; } else if ('nodes' in data) { // 如果是特定节点的响应 const nodeData = Object.values(data.nodes).filter( (node): node is { document: FigmaDocumentNode } => node !== null && typeof node === 'object' && 'document' in node ); nodes = nodeData.map(n => n.document); } // 提取节点并生成简化数据 const simplifiedNodes = extractNodes(nodes); // 清理临时属性 simplifiedNodes.forEach(cleanupTemporaryProperties); // 应用布局优化 const optimizedDesign = LayoutOptimizer.optimizeDesign({ name, lastModified, thumbnailUrl: thumbnailUrl || '', nodes: simplifiedNodes }); return optimizedDesign; } // 提取节点信息 function extractNodes(children: FigmaDocumentNode[], parentNode?: SimplifiedNode): SimplifiedNode[] { if (!Array.isArray(children)) return []; // 创建一个对应的原始父节点对象,用于可见性判断 const parentForVisibility = parentNode ? { clipsContent: (parentNode as any).clipsContent, absoluteBoundingBox: parentNode._absoluteX !== undefined && parentNode._absoluteY !== undefined ? { x: parentNode._absoluteX, y: parentNode._absoluteY, width: parseFloat(parentNode.cssStyles?.width || '0'), height: parseFloat(parentNode.cssStyles?.height || '0') } : undefined } : undefined; const visibilityFilter = (node: FigmaDocumentNode) => { // 使用类型保护确保只检查有必要属性的节点 const nodeForVisibility = { visible: (node as any).visible, opacity: (node as any).opacity, absoluteBoundingBox: (node as any).absoluteBoundingBox, absoluteRenderBounds: (node as any).absoluteRenderBounds }; // 如果没有父节点信息,只检查节点自身可见性 if (!parentForVisibility) { return isVisible(nodeForVisibility); } // 如果有父节点,同时考虑父节点的裁剪效果 return isVisibleInParent(nodeForVisibility, parentForVisibility); }; const nodes = children .filter(visibilityFilter) .map(node => extractNode(node, parentNode)) .filter((node): node is SimplifiedNode => node !== null); // 对同级元素按照top值排序(从上到下) return sortNodesByPosition(nodes); } /** * 提取单个节点信息 * 优化对SVG类型的处理 */ function extractNode(node: FigmaDocumentNode, parentNode?: SimplifiedNode): SimplifiedNode | null { if (!node) return null; const { id, name, type } = node; // 创建基本节点对象 const result: SimplifiedNode = { id, name, type }; // 设置CSS样式 result.cssStyles = {}; // 添加尺寸和位置的CSS转换逻辑 if (hasValue('absoluteBoundingBox', node) && node.absoluteBoundingBox) { // 添加到CSS样式 result.cssStyles.width = `${node.absoluteBoundingBox.width}px`; result.cssStyles.height = `${node.absoluteBoundingBox.height}px`; // 对非根节点添加定位信息 if ((node.type as string) !== 'DOCUMENT' && (node.type as string) !== 'CANVAS') { result.cssStyles.position = 'absolute'; // 存储原始坐标,供子节点计算相对位置使用 result._absoluteX = node.absoluteBoundingBox.x; result._absoluteY = node.absoluteBoundingBox.y; // 如果有父节点,计算相对位置 if (parentNode && parentNode._absoluteX !== undefined && parentNode._absoluteY !== undefined) { result.cssStyles.left = `${node.absoluteBoundingBox.x - parentNode._absoluteX}px`; result.cssStyles.top = `${node.absoluteBoundingBox.y - parentNode._absoluteY}px`; } else { // 否则使用绝对位置(顶层元素) result.cssStyles.left = `${node.absoluteBoundingBox.x}px`; result.cssStyles.top = `${node.absoluteBoundingBox.y}px`; } } } // 处理文本 - 保留原始文本内容 if (hasValue('characters', node) && typeof node.characters === 'string') { result.text = node.characters; // 对于文本节点,添加文本颜色样式 if (hasValue('fills', node) && Array.isArray(node.fills) && node.fills.length > 0) { const fill = node.fills[0]; if (fill.type === 'SOLID' && fill.color) { // 使用convertColor获取hex格式的颜色 const { hex, opacity } = convertColor(fill.color, fill.opacity ?? 1); // 如果透明度为1,使用hex格式,否则使用rgba格式 result.cssStyles.color = opacity === 1 ? hex : formatRGBAColor(fill.color, opacity); } } } // 提取图片信息 processImageResources(node, result); // 提取通用的属性处理逻辑 processNodeStyle(node, result); processFills(node, result); processStrokes(node, result); processEffects(node, result); processCornerRadius(node, result); // 递归处理子节点 if (hasValue('children', node) && Array.isArray(node.children) && node.children.length) { result.children = extractNodes(node.children, result); // 处理图片组 detectAndMarkImageGroup(result); processSVGNodesBottomUp(result, generateFileName); } return result; } /** * 检测并标记图片组 */ function detectAndMarkImageGroup(node: SimplifiedNode): void { markImageGroup(node, (n) => suggestExportFormat(n), generateFileName ); } /** * 提取节点中的图片资源 */ function processImageResources(node: FigmaDocumentNode, result: SimplifiedNode): void { // 检查fills和background中的图片资源 const imageResources: ImageResource[] = []; // 从fills中提取图片资源 if (hasValue('fills', node) && Array.isArray(node.fills)) { const fillImages = node.fills.filter(fill => fill.type === 'IMAGE' && fill.imageRef ).map(fill => ({ imageRef: fill.imageRef, })); imageResources.push(...fillImages); } // 从background中提取图片资源 if (hasValue('background', node) && Array.isArray(node.background)) { const bgImages = node.background.filter(bg => bg.type === 'IMAGE' && bg.imageRef ).map(bg => ({ imageRef: bg.imageRef, })); imageResources.push(...bgImages); } // 如果找到图片资源,保存并添加导出信息 if (imageResources.length > 0) { // 设置CSS背景图片属性 - 使用第一个图片 if (!result.cssStyles) { result.cssStyles = {}; } const primaryImage = imageResources[0]; result.cssStyles.backgroundImage = `url({{FIGMA_IMAGE:${primaryImage.imageRef}}})`; // 添加导出信息 const format = suggestExportFormat(result); result.exportInfo = { type: 'IMAGE', format, nodeId: result.id, fileName: generateFileName(result.name, format) }; } if (isSVGNode(node)) { result.exportInfo = { type: 'IMAGE', format: 'SVG', nodeId: result.id, } } } /** * 处理节点的样式属性 */ function processNodeStyle(node: FigmaDocumentNode, result: SimplifiedNode): void { if (!hasValue('style', node)) return; const style = node.style as any; // 转换文本样式 const textStyle: TextStyle = { fontFamily: style?.fontFamily, fontSize: style?.fontSize, fontWeight: style?.fontWeight, textAlignHorizontal: style?.textAlignHorizontal, textAlignVertical: style?.textAlignVertical }; // 处理行高 if (style?.lineHeightPx) { const cssStyle = textStyleToCss(textStyle); cssStyle.lineHeight = `${style.lineHeightPx}px`; Object.assign(result.cssStyles!, cssStyle); } else { Object.assign(result.cssStyles!, textStyleToCss(textStyle)); } } function processGradient(gradient: Paint): string { if (!gradient.gradientHandlePositions || !gradient.gradientStops) return ''; const stops = gradient.gradientStops.map(stop => { const color = convertColor(stop.color, stop.color.a); return `${color.hex} ${stop.position * 100}%`; }).join(', '); // 获取起点和终点 const [start, end] = gradient.gradientHandlePositions; // 计算角度 // 在Figma中,渐变方向是从起点到终点 // 而在CSS中,0度是从下到上,顺时针旋转 let angle = Math.atan2(end.y - start.y, end.x - start.x) * (180 / Math.PI); // 将Figma的角度转换为CSS角度 // 1. 首先将角度转为以上方为0度的系统 angle = angle - 90; // 2. 因为CSS中0度是从下到上,所以我们需要翻转角度 angle = 180 - angle; // 3. 确保角度在0-360度之间 angle = ((angle % 360) + 360) % 360; return `linear-gradient(${angle}deg, ${stops})`; } /** * 处理节点的填充属性 */ function processFills(node: FigmaDocumentNode, result: SimplifiedNode): void { if (!hasValue('fills', node) || !Array.isArray(node.fills) || node.fills.length === 0) return; // 跳过图片填充 if (hasImageFill(node)) { return; } const fills = node.fills.filter(isVisible); if (fills.length === 0) return; const fill = fills[0]; if (fill.type === 'SOLID' && fill.color) { const { hex, opacity } = convertColor(fill.color, fill.opacity ?? 1); const color = opacity === 1 ? hex : formatRGBAColor(fill.color, opacity); if (node.type === 'TEXT') { result.cssStyles!.color = color; } else { result.cssStyles!.backgroundColor = color; } } else if (fill.type === 'GRADIENT_LINEAR') { const gradient = processGradient(fill); if (node.type === 'TEXT') { result.cssStyles!.background = gradient; result.cssStyles!.webkitBackgroundClip = 'text'; result.cssStyles!.backgroundClip = 'text'; result.cssStyles!.webkitTextFillColor = 'transparent'; } else { result.cssStyles!.background = gradient; } } } /** * 处理节点的边框属性 */ function processStrokes(node: FigmaDocumentNode, result: SimplifiedNode): void { if ((node as any).type === 'TEXT') return; const strokes = buildSimplifiedStrokes(node); if (strokes.colors.length === 0) return; const stroke = strokes.colors[0]; if (typeof stroke === 'string') { result.cssStyles!.borderColor = stroke; if (strokes.strokeWeight) { result.cssStyles!.borderWidth = strokes.strokeWeight; } result.cssStyles!.borderStyle = 'solid'; } else if (typeof stroke === 'object' && 'type' in stroke) { if (stroke.type === 'SOLID' && stroke.color) { const { hex, opacity } = convertColor(stroke.color); result.cssStyles!.borderColor = opacity === 1 ? hex : formatRGBAColor(stroke.color); if (strokes.strokeWeight) { result.cssStyles!.borderWidth = strokes.strokeWeight; } result.cssStyles!.borderStyle = 'solid'; } else if (stroke.type === 'GRADIENT_LINEAR') { const gradient = processGradient(stroke); result.cssStyles!.borderImage = gradient; result.cssStyles!.borderImageSlice = '1'; if (strokes.strokeWeight) { result.cssStyles!.borderWidth = strokes.strokeWeight; } } } } /** * 处理节点的特效属性 */ function processEffects(node: FigmaDocumentNode, result: SimplifiedNode): void { const effects = buildSimplifiedEffects(node); if (effects.boxShadow) result.cssStyles!.boxShadow = effects.boxShadow; if (effects.filter) result.cssStyles!.filter = effects.filter; if (effects.backdropFilter) result.cssStyles!.backdropFilter = effects.backdropFilter; } /** * 处理节点的圆角属性 */ function processCornerRadius(node: FigmaDocumentNode, result: SimplifiedNode): void { if (!hasValue('cornerRadius', node)) return; if (typeof node.cornerRadius === 'number' && node.cornerRadius > 0) { // 处理均匀圆角 result.cssStyles!.borderRadius = `${node.cornerRadius}px`; } else if (node.cornerRadius === 'mixed' && hasValue('rectangleCornerRadii', node, isRectangleCornerRadii)) { // 处理不均匀圆角 (左上、右上、右下、左下) result.cssStyles!.borderRadius = generateCSSShorthand({ top: node.rectangleCornerRadii[0], right: node.rectangleCornerRadii[1], bottom: node.rectangleCornerRadii[2], left: node.rectangleCornerRadii[3] }) || '0'; } } /** * 将文本样式转换为CSS样式 * @param textStyle Figma文本样式 * @returns CSS样式对象 */ function textStyleToCss(textStyle: TextStyle): CSSStyle { const cssStyle: CSSStyle = {}; if (textStyle.fontFamily) cssStyle.fontFamily = textStyle.fontFamily; if (textStyle.fontSize) cssStyle.fontSize = `${textStyle.fontSize}px`; if (textStyle.fontWeight) cssStyle.fontWeight = textStyle.fontWeight; // 处理文本对齐 if (textStyle.textAlignHorizontal) { switch(textStyle.textAlignHorizontal) { case 'LEFT': cssStyle.textAlign = 'left'; break; case 'CENTER': cssStyle.textAlign = 'center'; break; case 'RIGHT': cssStyle.textAlign = 'right'; break; case 'JUSTIFIED': cssStyle.textAlign = 'justify'; break; } } // 处理垂直对齐 if (textStyle.textAlignVertical) { switch(textStyle.textAlignVertical) { case 'TOP': cssStyle.verticalAlign = 'top'; break; case 'CENTER': cssStyle.verticalAlign = 'middle'; break; case 'BOTTOM': cssStyle.verticalAlign = 'bottom'; break; } } return cssStyle; } /** * 根据节点特征选择导出格式 */ function suggestExportFormat(node: FigmaDocumentNode | SimplifiedNode): 'PNG' | 'JPG' | 'SVG' { return suggestFormat(node, isSVGNode); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/1yhy/Figma-Context-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server