Skip to main content
Glama

Figma MCP Server

by 1yhy
spatial-projection.ts12.5 kB
import { SimplifiedNode } from "~/services/simplify-node-response.js"; /** * 表示一个矩形区域 */ export interface Rect { left: number; top: number; width: number; height: number; } /** * 节点之间的空间关系类型 */ export enum NodeRelationship { /** 包含关系 - 一个节点完全包含另一个节点 */ CONTAINS = 'contains', /** 相交关系 - 两个节点有部分重叠 */ INTERSECTS = 'intersects', /** 相离关系 - 两个节点没有重叠 */ SEPARATE = 'separate' } /** * 投影线条,用于表示空间划分 */ export interface ProjectionLine { /** 起始位置 */ position: number; /** 投影方向: 'horizontal'(水平线) 或 'vertical'(垂直线) */ direction: 'horizontal' | 'vertical'; /** 覆盖的节点索引 */ nodeIndices: number[]; } /** * 安全地解析CSS数值,处理所有可能的无效输入 * @param value CSS值字符串 * @param defaultValue 解析失败时的默认值 */ function safeParseFloat(value: string | undefined | null, defaultValue: number = 0): number { if (!value) return defaultValue; const parsed = parseFloat(value); return isNaN(parsed) ? defaultValue : parsed; } /** * 安全地获取节点的坐标值 */ function getNodePosition(node: SimplifiedNode): { left: number; top: number } { let left = 0; let top = 0; // 优先使用绝对坐标 if (typeof node._absoluteX === 'number') { left = node._absoluteX; } else if (node.cssStyles?.left) { left = safeParseFloat(node.cssStyles.left); } if (typeof node._absoluteY === 'number') { top = node._absoluteY; } else if (node.cssStyles?.top) { top = safeParseFloat(node.cssStyles.top); } return { left, top }; } /** * 矩形相关的工具函数 */ export class RectUtils { /** * 从SimplifiedNode创建Rect对象 */ static fromNode(node: SimplifiedNode): Rect | null { if (!node.cssStyles || !node.cssStyles.width || !node.cssStyles.height) { return null; } // 解析宽高,确保有默认值 const width = safeParseFloat(node.cssStyles.width, 0); const height = safeParseFloat(node.cssStyles.height, 0); // 如果宽高为0,则不是有效矩形 if (width <= 0 || height <= 0) { return null; } // 获取位置 const { left, top } = getNodePosition(node); return { left, top, width, height }; } /** * 判断一个矩形是否包含另一个矩形 */ static contains(a: Rect, b: Rect): boolean { return ( a.left <= b.left && a.top <= b.top && a.left + a.width >= b.left + b.width && a.top + a.height >= b.top + b.height ); } /** * 判断两个矩形是否相交 */ static intersects(a: Rect, b: Rect): boolean { return !( a.left + a.width <= b.left || b.left + b.width <= a.left || a.top + a.height <= b.top || b.top + b.height <= a.top ); } /** * 计算两个矩形的交集 */ static intersection(a: Rect, b: Rect): Rect | null { if (!RectUtils.intersects(a, b)) { return null; } const left = Math.max(a.left, b.left); const top = Math.max(a.top, b.top); const right = Math.min(a.left + a.width, b.left + b.width); const bottom = Math.min(a.top + a.height, b.top + b.height); return { left, top, width: right - left, height: bottom - top }; } /** * 分析两个节点之间的空间关系 */ static analyzeRelationship(a: Rect, b: Rect): NodeRelationship { if (RectUtils.contains(a, b)) { return NodeRelationship.CONTAINS; } else if (RectUtils.contains(b, a)) { return NodeRelationship.CONTAINS; } else if (RectUtils.intersects(a, b)) { return NodeRelationship.INTERSECTS; } else { return NodeRelationship.SEPARATE; } } } /** * 二维空间投影分析器 */ export class SpatialProjectionAnalyzer { /** * 将简化节点列表转换为矩形列表 */ static nodesToRects(nodes: SimplifiedNode[]): Rect[] { return nodes .map(node => RectUtils.fromNode(node)) .filter((rect): rect is Rect => rect !== null); } /** * 生成水平投影线(用于确定行) * @param rects 矩形列表 * @param tolerance 坐标容差(像素) */ static generateHorizontalProjectionLines(rects: Rect[], tolerance: number = 1): ProjectionLine[] { if (rects.length === 0) return []; // 收集所有节点的顶部和底部坐标 const yCoordinates: number[] = []; rects.forEach(rect => { yCoordinates.push(rect.top); yCoordinates.push(rect.top + rect.height); }); // 排序并过滤相近的坐标 yCoordinates.sort((a, b) => a - b); const uniqueYCoordinates: number[] = []; for (let i = 0; i < yCoordinates.length; i++) { if (i === 0 || Math.abs(yCoordinates[i] - yCoordinates[i-1]) > tolerance) { uniqueYCoordinates.push(yCoordinates[i]); } } // 为每一个Y坐标创建投影线 return uniqueYCoordinates.map(y => { const line: ProjectionLine = { position: y, direction: 'horizontal', nodeIndices: [] }; // 找出与这条线相交的所有节点 for (let i = 0; i < rects.length; i++) { const rect = rects[i]; if (y >= rect.top && y <= rect.top + rect.height) { line.nodeIndices.push(i); } } return line; }); } /** * 生成垂直投影线(用于确定列) * @param rects 矩形列表 * @param tolerance 坐标容差(像素) */ static generateVerticalProjectionLines(rects: Rect[], tolerance: number = 1): ProjectionLine[] { if (rects.length === 0) return []; // 收集所有节点的左侧和右侧坐标 const xCoordinates: number[] = []; rects.forEach(rect => { xCoordinates.push(rect.left); xCoordinates.push(rect.left + rect.width); }); // 排序并过滤相近的坐标 xCoordinates.sort((a, b) => a - b); const uniqueXCoordinates: number[] = []; for (let i = 0; i < xCoordinates.length; i++) { if (i === 0 || Math.abs(xCoordinates[i] - xCoordinates[i-1]) > tolerance) { uniqueXCoordinates.push(xCoordinates[i]); } } // 为每一个X坐标创建投影线 return uniqueXCoordinates.map(x => { const line: ProjectionLine = { position: x, direction: 'vertical', nodeIndices: [] }; // 找出与这条线相交的所有节点 for (let i = 0; i < rects.length; i++) { const rect = rects[i]; if (x >= rect.left && x <= rect.left + rect.width) { line.nodeIndices.push(i); } } return line; }); } /** * 将节点按行分组 * @param nodes 节点列表 * @param tolerance 容差(像素) */ static groupNodesByRows(nodes: SimplifiedNode[], tolerance: number = 1): SimplifiedNode[][] { const rects = this.nodesToRects(nodes); if (rects.length === 0) return [nodes]; // 获取节点的垂直投影(水平线) const projectionLines = this.generateHorizontalProjectionLines(rects, tolerance); // 找到行的分割点 const rows: SimplifiedNode[][] = []; for (let i = 0; i < projectionLines.length - 1; i++) { const currentLine = projectionLines[i]; const nextLine = projectionLines[i + 1]; // 检查这两条线之间是否有节点 const nodesBetweenLines = new Set<number>(); // 找出完全位于这两条线之间的节点 for (let j = 0; j < rects.length; j++) { const rect = rects[j]; if (rect.top >= currentLine.position && rect.top + rect.height <= nextLine.position) { nodesBetweenLines.add(j); } } // 如果有节点,创建一个新行 if (nodesBetweenLines.size > 0) { // 把这些节点按照从左到右排序 const rowNodes = Array.from(nodesBetweenLines) .map(index => nodes[index]) .sort((a, b) => { const { left: aLeft } = getNodePosition(a); const { left: bLeft } = getNodePosition(b); return aLeft - bLeft; }); rows.push(rowNodes); } } // 处理没有分组的节点 if (rows.length === 0) { // 如果没有找到行,则将所有节点作为一行 rows.push([...nodes]); } return rows; } /** * 将一行节点按列分组 * @param rowNodes 一行中的节点 * @param tolerance 容差(像素) */ static groupRowNodesByColumns(rowNodes: SimplifiedNode[], tolerance: number = 1): SimplifiedNode[][] { const rects = this.nodesToRects(rowNodes); if (rects.length === 0) return [rowNodes]; // 获取节点的水平投影(垂直线) const projectionLines = this.generateVerticalProjectionLines(rects, tolerance); // 找到列的分割点 const columns: SimplifiedNode[][] = []; for (let i = 0; i < projectionLines.length - 1; i++) { const currentLine = projectionLines[i]; const nextLine = projectionLines[i + 1]; // 检查这两条线之间是否有节点 const nodesBetweenLines = new Set<number>(); // 找出完全位于这两条线之间的节点 for (let j = 0; j < rects.length; j++) { const rect = rects[j]; if (rect.left >= currentLine.position && rect.left + rect.width <= nextLine.position) { nodesBetweenLines.add(j); } } // 如果有节点,创建一个新列 if (nodesBetweenLines.size > 0) { const colNodes = Array.from(nodesBetweenLines).map(index => rowNodes[index]); columns.push(colNodes); } } // 处理没有分组的节点 if (columns.length === 0) { // 如果没有找到列,则将所有节点作为一列 columns.push([...rowNodes]); } return columns; } /** * 处理节点空间关系,构建包含关系 * @param nodes 节点列表 */ static processNodeRelationships(nodes: SimplifiedNode[]): SimplifiedNode[] { if (nodes.length <= 1) return [...nodes]; const rects = this.nodesToRects(nodes); if (rects.length !== nodes.length) { return nodes; // 无法处理所有节点,直接返回 } // 找出所有的包含关系 const containsRelations: [number, number][] = []; for (let i = 0; i < rects.length; i++) { for (let j = 0; j < rects.length; j++) { if (i !== j && RectUtils.contains(rects[i], rects[j])) { containsRelations.push([i, j]); // 节点i包含节点j } } } // 建立包含关系图 const childrenMap = new Map<number, Set<number>>(); const parentMap = new Map<number, number | null>(); // 初始化所有节点都没有父节点 for (let i = 0; i < nodes.length; i++) { parentMap.set(i, null); childrenMap.set(i, new Set<number>()); } // 处理包含关系 for (const [parent, child] of containsRelations) { childrenMap.get(parent)?.add(child); parentMap.set(child, parent); } // 修正多级包含关系,确保每个节点只有最直接的父节点 for (const [child, parent] of parentMap.entries()) { if (parent === null) continue; // 检查父节点是否也有父节点 let currentParent = parent; let grandParent = parentMap.get(currentParent); while (grandParent !== null) { // 如果祖父节点也直接包含当前子节点,则删除父节点到子节点的直接关系 if (childrenMap.get(grandParent)?.has(child)) { childrenMap.get(currentParent)?.delete(child); } currentParent = grandParent; grandParent = parentMap.get(currentParent); } } // 构建新的节点树结构 const rootIndices = Array.from(parentMap.entries()) .filter(([_, parent]) => parent === null) .map(([index]) => index); const result: SimplifiedNode[] = []; // 递归构建节点树 const buildNodeTree = (nodeIndex: number): SimplifiedNode => { const node = { ...nodes[nodeIndex] }; const childIndices = Array.from(childrenMap.get(nodeIndex) || []); if (childIndices.length > 0) { node.children = childIndices.map(buildNodeTree); } return node; }; // 从所有根节点开始构建 for (const rootIndex of rootIndices) { result.push(buildNodeTree(rootIndex)); } return result; } }

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