Skip to main content
Glama

Figma MCP Server

by 1yhy
layout-optimizer.ts26.4 kB
import { SimplifiedNode, SimplifiedDesign } from "~/services/simplify-node-response.js"; import { SpatialProjectionAnalyzer, Rect } from "~/utils/spatial-projection.js"; /** * 布局优化器 - 优化UI设计的布局结构 */ export class LayoutOptimizer { private static containerIdCounter = 1; /** * 优化设计的布局结构 * * @param design 原始简化设计 * @returns 优化后的设计 */ static optimizeDesign(design: SimplifiedDesign): SimplifiedDesign { // 如果没有节点数据,直接返回 if (!design.nodes) { return design; } // 递归优化节点树 const optimizedNodes = design.nodes.map(node => this.optimizeNodeTree(node)); // 更新设计 return { ...design, nodes: optimizedNodes }; } /** * 递归优化节点树 * * @param node 节点 * @returns 优化后的节点 */ static optimizeNodeTree(node: SimplifiedNode): SimplifiedNode { // 如果没有子节点,直接返回 if (!node.children || node.children.length === 0) { return node; } // 递归处理每个子节点 const optimizedChildren = node.children.map(child => this.optimizeNodeTree(child)); // 对容器节点进行行列布局分析 return this.optimizeContainer({ ...node, children: optimizedChildren }); } /** * 优化容器布局 * * @param node 容器节点 * @returns 优化后的容器节点 */ static optimizeContainer(node: SimplifiedNode): SimplifiedNode { // 如果没有子节点或只有一个子节点,直接返回 if (!node.children || node.children.length <= 1) { return node; } // 分析是否是FRAME或GROUP类型的容器 const isContainer = node.type === 'FRAME' || node.type === 'GROUP'; // 分析子节点的空间关系,确定是行布局还是列布局 const { isRow, isColumn, rowGap, columnGap, justifyContent, alignItems } = this.analyzeLayoutDirection(node.children); // 如果是有效的行或列布局 if (isRow || isColumn) { // 如果是容器节点,直接添加flex样式而不创建新容器 if (isContainer) { const direction = isRow ? 'row' : 'column'; const gap = isRow ? rowGap : columnGap; // 在原容器上添加flex样式 return { ...node, cssStyles: { ...node.cssStyles, display: 'flex', flexDirection: direction, ...(gap > 0 ? { gap: `${Math.round(gap)}px` } : {}), ...(justifyContent ? { justifyContent } : {}), ...(alignItems ? { alignItems } : {}) }, children: node.children }; } // 不是容器节点但子节点有明确的布局关系,创建新的布局容器 else { // 分析是否需要对子节点进行分组 const groups = this.groupChildrenByLayout(node.children, isRow); // 如果分组后只有一个组且包含所有子节点,直接返回带flex样式的原节点 if (groups.length === 1 && groups[0].length === node.children.length) { const direction = isRow ? 'row' : 'column'; const gap = isRow ? rowGap : columnGap; return { ...node, cssStyles: { ...node.cssStyles, display: 'flex', flexDirection: direction, ...(gap > 0 ? { gap: `${Math.round(gap)}px` } : {}), ...(justifyContent ? { justifyContent } : {}), ...(alignItems ? { alignItems } : {}) }, children: node.children }; } // 需要分组的情况 const groupContainers = groups.map((group, index) => { // 如果组内只有一个元素,直接返回该元素 if (group.length === 1) { return group[0]; } // 为多元素组创建容器 const direction = isRow ? 'column' : 'row'; return this.createLayoutContainer(`group-${index}`, direction, group); }); // 返回包含分组容器的父节点 const direction = isRow ? 'row' : 'column'; return { ...node, cssStyles: { ...node.cssStyles, display: 'flex', flexDirection: direction, ...(justifyContent ? { justifyContent } : {}), ...(alignItems ? { alignItems } : {}) }, children: groupContainers }; } } // 如果没有明显的行列布局,则保持原样 return node; } /** * 分析节点的布局方向 */ static analyzeLayoutDirection(nodes: SimplifiedNode[]): { isRow: boolean; isColumn: boolean; rowGap: number; columnGap: number; justifyContent: string | null; alignItems: string | null; } { // 打印容器名称,帮助调试 const containerName = nodes[0]?.name || "未知容器"; const rects = nodes .map(node => { if (!node.cssStyles) return null; const left = parseFloat(node.cssStyles.left as string || '0'); const top = parseFloat(node.cssStyles.top as string || '0'); const width = parseFloat(node.cssStyles.width as string || '0'); const height = parseFloat(node.cssStyles.height as string || '0'); return { left, top, width, height }; }) .filter((rect): rect is { left: number; top: number; width: number; height: number } => rect !== null); if (rects.length < 2) { return { isRow: false, isColumn: false, rowGap: 0, columnGap: 0, justifyContent: null, alignItems: null }; } // 分析水平和垂直方向的对齐情况 const { horizontalAlignment, verticalAlignment, horizontalGap, verticalGap } = this.analyzeAlignment(rects); // 计算行和列的可信度分数 const rowScore = this.calculateRowScore(rects, horizontalAlignment, verticalAlignment); const columnScore = this.calculateColumnScore(rects, horizontalAlignment, verticalAlignment); // 降低识别阈值,更容易识别布局 const isRow = rowScore > columnScore && rowScore > 0.4; const isColumn = columnScore > rowScore && columnScore > 0.4; // 确定对齐方式 let justifyContent: string | null = null; let alignItems: string | null = null; if (isRow) { justifyContent = this.getJustifyContent(horizontalAlignment); alignItems = this.getAlignItems(verticalAlignment); } else if (isColumn) { justifyContent = this.getJustifyContent(verticalAlignment); alignItems = this.getAlignItems(horizontalAlignment); } return { isRow, isColumn, rowGap: horizontalGap, columnGap: verticalGap, justifyContent, alignItems }; } /** * 分析节点的对齐情况 */ static analyzeAlignment(rects: { left: number; top: number; width: number; height: number }[]): { horizontalAlignment: string; verticalAlignment: string; horizontalGap: number; verticalGap: number; } { // 计算水平方向的位置和间距 const lefts = rects.map(rect => rect.left); const rights = rects.map(rect => rect.left + rect.width); const minLeft = Math.min(...lefts); const maxRight = Math.max(...rights); // 计算垂直方向的位置和间距 const tops = rects.map(rect => rect.top); const bottoms = rects.map(rect => rect.top + rect.height); const minTop = Math.min(...tops); const maxBottom = Math.max(...bottoms); // 判断水平对齐情况 const leftAligned = this.areValuesAligned(lefts); const rightAligned = this.areValuesAligned(rights); const centerHAligned = this.areValuesAligned(rects.map(rect => rect.left + rect.width / 2)); // 判断垂直对齐情况 const topAligned = this.areValuesAligned(tops); const bottomAligned = this.areValuesAligned(bottoms); const centerVAligned = this.areValuesAligned(rects.map(rect => rect.top + rect.height / 2)); // 确定水平对齐方式 let horizontalAlignment = 'none'; if (leftAligned) horizontalAlignment = 'left'; else if (rightAligned) horizontalAlignment = 'right'; else if (centerHAligned) horizontalAlignment = 'center'; // 确定垂直对齐方式 let verticalAlignment = 'none'; if (topAligned) verticalAlignment = 'top'; else if (bottomAligned) verticalAlignment = 'bottom'; else if (centerVAligned) verticalAlignment = 'center'; // 计算平均间距 const horizontalGap = this.calculateAverageGap(rects, 'horizontal'); const verticalGap = this.calculateAverageGap(rects, 'vertical'); return { horizontalAlignment, verticalAlignment, horizontalGap, verticalGap }; } /** * 判断一组值是否对齐(在一定容差范围内) */ static areValuesAligned(values: number[], tolerance: number = 2): boolean { if (values.length < 2) return true; const firstValue = values[0]; return values.every(value => Math.abs(value - firstValue) <= tolerance); } /** * 计算平均间距 */ static calculateAverageGap( rects: { left: number; top: number; width: number; height: number }[], direction: 'horizontal' | 'vertical' ): number { if (rects.length < 2) return 0; // 排序节点 const sortedRects = [...rects].sort((a, b) => { if (direction === 'horizontal') { return a.left - b.left; } else { return a.top - b.top; } }); // 计算相邻节点间的间距 const gaps: number[] = []; for (let i = 0; i < sortedRects.length - 1; i++) { const current = sortedRects[i]; const next = sortedRects[i + 1]; if (direction === 'horizontal') { const gap = next.left - (current.left + current.width); if (gap > 0) gaps.push(gap); } else { const gap = next.top - (current.top + current.height); if (gap > 0) gaps.push(gap); } } // 计算平均间距 if (gaps.length === 0) return 0; return gaps.reduce((sum, gap) => sum + gap, 0) / gaps.length; } /** * 计算行布局的可信度分数 */ static calculateRowScore( rects: { left: number; top: number; width: number; height: number }[], horizontalAlignment: string, verticalAlignment: string ): number { if (rects.length < 2) return 0; // 排序节点 const sortedByLeft = [...rects].sort((a, b) => a.left - b.left); // 计算相邻节点间的水平间距 let consecutiveHorizontalGaps = 0; for (let i = 0; i < sortedByLeft.length - 1; i++) { const current = sortedByLeft[i]; const next = sortedByLeft[i + 1]; const gap = next.left - (current.left + current.width); if (gap >= 0 && gap <= 50) consecutiveHorizontalGaps++; } // 计算水平分布的均匀性 const horizontalDistribution = consecutiveHorizontalGaps / (sortedByLeft.length - 1); // 垂直对齐增加分数 const verticalAlignmentScore = (verticalAlignment !== 'none') ? 0.3 : 0; // 综合评分 return horizontalDistribution * 0.7 + verticalAlignmentScore; } /** * 计算列布局的可信度分数 */ static calculateColumnScore( rects: { left: number; top: number; width: number; height: number }[], horizontalAlignment: string, verticalAlignment: string ): number { if (rects.length < 2) return 0; // 排序节点 const sortedByTop = [...rects].sort((a, b) => a.top - b.top); // 计算相邻节点间的垂直间距 let consecutiveVerticalGaps = 0; for (let i = 0; i < sortedByTop.length - 1; i++) { const current = sortedByTop[i]; const next = sortedByTop[i + 1]; const gap = next.top - (current.top + current.height); if (gap >= 0 && gap <= 50) consecutiveVerticalGaps++; } // 计算垂直分布的均匀性 const verticalDistribution = consecutiveVerticalGaps / (sortedByTop.length - 1); // 水平对齐增加分数 const horizontalAlignmentScore = (horizontalAlignment !== 'none') ? 0.3 : 0; // 综合评分 return verticalDistribution * 0.7 + horizontalAlignmentScore; } /** * 将子节点按布局特征分组 */ static groupChildrenByLayout( nodes: SimplifiedNode[], isRow: boolean ): SimplifiedNode[][] { if (nodes.length <= 1) return [nodes]; // 提取节点的位置信息 const rects = nodes.map((node, index) => { if (!node.cssStyles) return null; const left = parseFloat(node.cssStyles.left as string || '0'); const top = parseFloat(node.cssStyles.top as string || '0'); const width = parseFloat(node.cssStyles.width as string || '0'); const height = parseFloat(node.cssStyles.height as string || '0'); return { index, left, top, width, height }; }).filter((rect): rect is { index: number; left: number; top: number; width: number; height: number } => rect !== null); // 根据布局方向排序 const sortedRects = [...rects].sort((a, b) => { if (isRow) { return a.left - b.left; } else { return a.top - b.top; } }); // 寻找可能的分组点 const groups: SimplifiedNode[][] = []; let currentGroup: SimplifiedNode[] = [nodes[sortedRects[0].index]]; for (let i = 1; i < sortedRects.length; i++) { const current = sortedRects[i - 1]; const next = sortedRects[i]; let shouldSplit = false; if (isRow) { // 在行布局中,检查垂直位置是否有明显变化 if (Math.abs(next.top - current.top) > 20) { shouldSplit = true; } } else { // 在列布局中,检查水平位置是否有明显变化 if (Math.abs(next.left - current.left) > 20) { shouldSplit = true; } } if (shouldSplit) { // 结束当前组并开始新组 groups.push(currentGroup); currentGroup = [nodes[next.index]]; } else { // 继续添加到当前组 currentGroup.push(nodes[next.index]); } } // 添加最后一组 if (currentGroup.length > 0) { groups.push(currentGroup); } return groups; } /** * 将justifyContent对齐方式转换为CSS值 */ static getJustifyContent(alignment: string): string | null { switch (alignment) { case 'left': case 'top': return 'flex-start'; case 'right': case 'bottom': return 'flex-end'; case 'center': return 'center'; default: return 'space-between'; } } /** * 将alignItems对齐方式转换为CSS值 */ static getAlignItems(alignment: string): string | null { switch (alignment) { case 'left': case 'top': return 'flex-start'; case 'right': case 'bottom': return 'flex-end'; case 'center': return 'center'; default: return null; } } /** * 创建布局容器节点 */ static createLayoutContainer( name: string, direction: 'row' | 'column', children: SimplifiedNode[] ): SimplifiedNode { // 计算容器的边界框 let minLeft = Infinity; let minTop = Infinity; let maxRight = -Infinity; let maxBottom = -Infinity; // 找出所有子节点的最小外接矩形 children.forEach(child => { if (!child.cssStyles) return; const left = parseFloat(child.cssStyles.left as string || '0'); const top = parseFloat(child.cssStyles.top as string || '0'); const width = parseFloat(child.cssStyles.width as string || '0'); const height = parseFloat(child.cssStyles.height as string || '0'); minLeft = Math.min(minLeft, left); minTop = Math.min(minTop, top); maxRight = Math.max(maxRight, left + width); maxBottom = Math.max(maxBottom, top + height); }); // 计算对齐方式 const { justifyContent, alignItems } = this.analyzeLayoutDirection(children); // 如果没有有效的子节点,返回空容器 if (minLeft === Infinity || minTop === Infinity || maxRight === -Infinity || maxBottom === -Infinity) { return { id: `container-${this.containerIdCounter++}-${name}`, name: `Layout Container ${name}`, type: 'FRAME', cssStyles: { display: 'flex', flexDirection: direction, width: '100%', height: 'auto' }, children }; } // 设置容器样式和位置 return { id: `container-${this.containerIdCounter++}-${name}`, name: `Layout Container ${name}`, type: 'FRAME', cssStyles: { display: 'flex', flexDirection: direction, position: 'absolute', left: `${minLeft}px`, top: `${minTop}px`, width: `${maxRight - minLeft}px`, height: `${maxBottom - minTop}px`, ...(justifyContent ? { justifyContent } : {}), ...(alignItems ? { alignItems } : {}) }, children }; } /** * 计算方差 */ static calculateVariance(values: number[]): number { if (values.length <= 1) return 0; const mean = values.reduce((sum, v) => sum + v, 0) / values.length; const squaredDiffs = values.map(v => Math.pow(v - mean, 2)); return squaredDiffs.reduce((sum, sq) => sum + sq, 0) / values.length; } // 以下是重新实现的analysisHorizontalLayout和analysisVerticalLayout方法 // 这些方法现在是从节点数组中直接提取位置信息,不再使用外部的extractElementRects /** * 辅助方法:提取元素位置信息 */ static extractElementRects(elements: SimplifiedNode[]): Array<{ index: number; left: number; top: number; right: number; bottom: number; width: number; height: number; centerX: number; centerY: number; }> { return elements .map((element, index) => { if (!element.cssStyles) return null; const left = parseFloat(element.cssStyles.left as string || '0'); const top = parseFloat(element.cssStyles.top as string || '0'); const width = parseFloat(element.cssStyles.width as string || '0'); const height = parseFloat(element.cssStyles.height as string || '0'); const right = left + width; const bottom = top + height; const centerX = left + width / 2; const centerY = top + height / 2; return { index, left, top, right, bottom, width, height, centerX, centerY }; }) .filter(rect => rect !== null) as Array<{ index: number; left: number; top: number; right: number; bottom: number; width: number; height: number; centerX: number; centerY: number; }>; } /** * 分析水平布局特征 */ static analyzeHorizontalLayout( rects: ReturnType<typeof LayoutOptimizer.extractElementRects>, bounds: { left: number; top: number; right: number; bottom: number; width: number; height: number } ): { distributionScore: number; alignmentScore: number; leftAligned: boolean; rightAligned: boolean; centerAligned: boolean; averageGap: number; gapConsistency: number; gaps: number[]; } { // 按左边界排序 const sortedByLeft = [...rects].sort((a, b) => a.left - b.left); // 计算水平间距 const gaps: number[] = []; let consecutiveGaps = 0; let totalGapWidth = 0; for (let i = 0; i < sortedByLeft.length - 1; i++) { const current = sortedByLeft[i]; const next = sortedByLeft[i + 1]; const gap = next.left - current.right; if (gap >= 0) { gaps.push(gap); totalGapWidth += gap; consecutiveGaps++; } } // 计算水平分布得分 const distributionScore = consecutiveGaps / (sortedByLeft.length - 1); // 分析水平对齐 const lefts = sortedByLeft.map(r => r.left); const rights = sortedByLeft.map(r => r.right); const centers = sortedByLeft.map(r => r.centerX); // 计算相对于容器宽度的对齐容差 const relativeTolerance = Math.max(5, bounds.width * 0.01); // 至少5px或容器宽度的1% const leftAligned = this.areValuesAligned(lefts, relativeTolerance); const rightAligned = this.areValuesAligned(rights, relativeTolerance); const centerAligned = this.areValuesAligned(centers, relativeTolerance); // 计算对齐得分 const alignmentScore = (leftAligned || rightAligned || centerAligned) ? 0.5 : 0; // 计算平均间距 const averageGap = gaps.length > 0 ? totalGapWidth / gaps.length : 0; // 计算间距一致性:间距方差越小,一致性越高 const gapConsistency = gaps.length > 1 ? 1 - this.calculateVariance(gaps) / (averageGap * averageGap + 0.1) : 0; return { distributionScore, alignmentScore, leftAligned, rightAligned, centerAligned, averageGap, gapConsistency, gaps }; } /** * 分析垂直布局特征 */ static analyzeVerticalLayout( rects: ReturnType<typeof LayoutOptimizer.extractElementRects>, bounds: { left: number; top: number; right: number; bottom: number; width: number; height: number } ): { distributionScore: number; alignmentScore: number; topAligned: boolean; bottomAligned: boolean; centerAligned: boolean; averageGap: number; gapConsistency: number; gaps: number[]; } { // 按顶边界排序 const sortedByTop = [...rects].sort((a, b) => a.top - b.top); // 计算垂直间距 const gaps: number[] = []; let consecutiveGaps = 0; let totalGapHeight = 0; for (let i = 0; i < sortedByTop.length - 1; i++) { const current = sortedByTop[i]; const next = sortedByTop[i + 1]; const gap = next.top - current.bottom; if (gap >= 0) { gaps.push(gap); totalGapHeight += gap; consecutiveGaps++; } } // 计算垂直分布得分 const distributionScore = consecutiveGaps / (sortedByTop.length - 1); // 分析垂直对齐 const tops = sortedByTop.map(r => r.top); const bottoms = sortedByTop.map(r => r.bottom); const centers = sortedByTop.map(r => r.centerY); // 计算相对于容器高度的对齐容差 const relativeTolerance = Math.max(5, bounds.height * 0.01); // 至少5px或容器高度的1% const topAligned = this.areValuesAligned(tops, relativeTolerance); const bottomAligned = this.areValuesAligned(bottoms, relativeTolerance); const centerAligned = this.areValuesAligned(centers, relativeTolerance); // 计算对齐得分 const alignmentScore = (topAligned || bottomAligned || centerAligned) ? 0.5 : 0; // 计算平均间距 const averageGap = gaps.length > 0 ? totalGapHeight / gaps.length : 0; // 计算间距一致性 const gapConsistency = gaps.length > 1 ? 1 - this.calculateVariance(gaps) / (averageGap * averageGap + 0.1) : 0; return { distributionScore, alignmentScore, topAligned, bottomAligned, centerAligned, averageGap, gapConsistency, gaps }; } /** * 计算边界 */ static calculateBounds(rects: ReturnType<typeof LayoutOptimizer.extractElementRects>) { const left = Math.min(...rects.map(r => r.left)); const top = Math.min(...rects.map(r => r.top)); const right = Math.max(...rects.map(r => r.right)); const bottom = Math.max(...rects.map(r => r.bottom)); return { left, top, right, bottom, width: right - left, height: bottom - top }; } /** * 根据布局特征生成flex属性 */ static generateFlexProperties( isRow: boolean, mainAxisInfo: ReturnType<typeof LayoutOptimizer.analyzeHorizontalLayout> | ReturnType<typeof LayoutOptimizer.analyzeVerticalLayout>, crossAxisInfo: ReturnType<typeof LayoutOptimizer.analyzeHorizontalLayout> | ReturnType<typeof LayoutOptimizer.analyzeVerticalLayout> ): Record<string, any> { const properties: Record<string, any> = { flexDirection: isRow ? 'row' : 'column' }; // 设置间距 if (mainAxisInfo.averageGap > 0) { properties.gap = `${Math.round(mainAxisInfo.averageGap)}px`; } // 设置主轴对齐方式 let justifyContent = 'flex-start'; if (isRow) { // 在水平布局中,处理水平方向的对齐 const horizontalInfo = mainAxisInfo as ReturnType<typeof LayoutOptimizer.analyzeHorizontalLayout>; if (horizontalInfo.rightAligned) { justifyContent = 'flex-end'; } else if (horizontalInfo.centerAligned) { justifyContent = 'center'; } else if (horizontalInfo.gaps.length > 0 && horizontalInfo.gapConsistency > 0.7) { justifyContent = 'space-between'; } } else { // 在垂直布局中,处理垂直方向的对齐 const verticalInfo = mainAxisInfo as ReturnType<typeof LayoutOptimizer.analyzeVerticalLayout>; if (verticalInfo.bottomAligned) { justifyContent = 'flex-end'; } else if (verticalInfo.centerAligned) { justifyContent = 'center'; } else if (verticalInfo.gaps.length > 0 && verticalInfo.gapConsistency > 0.7) { justifyContent = 'space-between'; } } properties.justifyContent = justifyContent; // 设置交叉轴对齐方式 let alignItems = 'flex-start'; if (isRow) { // 在水平布局中,处理垂直方向的对齐 const verticalInfo = crossAxisInfo as ReturnType<typeof LayoutOptimizer.analyzeVerticalLayout>; if (verticalInfo.bottomAligned) { alignItems = 'flex-end'; } else if (verticalInfo.centerAligned) { alignItems = 'center'; } } else { // 在垂直布局中,处理水平方向的对齐 const horizontalInfo = crossAxisInfo as ReturnType<typeof LayoutOptimizer.analyzeHorizontalLayout>; if (horizontalInfo.rightAligned) { alignItems = 'flex-end'; } else if (horizontalInfo.centerAligned) { alignItems = 'center'; } } properties.alignItems = alignItems; return properties; } }

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